Skip to content

docs(openspec): Expose the CLI's commands as the MCP, on @sentry/api - #1669

Open
sentry-junior[bot] wants to merge 37 commits into
mainfrom
feat/unify-sentry-api-layer-proposal
Open

sentry-junior[bot] wants to merge 37 commits into
mainfrom
feat/unify-sentry-api-layer-proposal

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Two OpenSpec changes, docs only. Implementation lands in follow-up PRs.

cli-first-mcp (new)

The CLI becomes the only product surface, and the MCP exposes its commands.

  1. CLI parity. Every MCP catalog tool gets a CLI command. design.md has a parity table built from a file listing, by name and brief, not by behavior. About 20 of the 75 catalog tools have no command yet: team and project-access management, project edit, DSN create and edit, uptime monitors, monitor view, issue assign, comment, and activity, event attachments, profiles, and dropped events (feat(cli): add event analytics command for dropped events #1812). New capabilities become commands, not MCP tools.
  2. Search and execute, no sandbox. search_sentry finds commands, returns a command's JSON Schema, and completes parameter values. execute_sentry_read and execute_sentry_write take { command, params }; execute is split by permission class because MCP hosts allowlist and approve by tool name (execute_sentry_tool breaks host allowlists and approval UX #1254). It validates the params and checks the command's permission class and the session's constraints. Then it calls the createSentrySDK() method. It only dispatches to known commands, so there is no code to sandbox. An all-tools mode lists each exposed command as its own tool, for harnesses that search large tool lists themselves.
  3. Exposure is declared per command. A command is exposed, stdio-only (local files, cwd, terminal), or hidden (auth login/logout, cli upgrade, local, and monitor run, which runs a child process). A lint check fails when a command doesn't declare one.
  4. Published API routes come from sentry api and sentry schema. They are exposed commands, so no extra tool is needed.
  5. Completion for agents. The shell completers work over a data source: the SQLite cache on stdio, the live API on hosted. They back the complete mode and validation errors. They replace the embedded natural-language search agents and the OpenAI/Anthropic setup those agents need.
  6. The CLI SDK runs on the hosted MCP. No second, portable copy of each command:
    • Spike first: bundle the SDK for workerd and record what breaks and the bundle size.
    • No persistence: one SDK instance per request in the Hono app, with the grant's token. The CLI's SQLite holds stored accounts, defaults, cursor stacks, and caches, which a server doesn't need, so hosted runs with a null store and no config directory. No D1, R2, or Durable Objects. Pagination is stateless through nextCursor.
    • Filesystem: exposed commands don't need one, and local-file commands are stdio-only.
    • Per-request state: a Workers isolate shares module scope across concurrent requests, so module-level state on the exposed commands' import graph gets audited and scoped per invocation. A concurrent two-identity test is required before hosted serves anything.
    • Fallback: split into portable operations only for domains Workers can't run.
  7. Eval-gated cutover. The full eval suite runs on the catalog, search mode, and the all-tools mode, with more than one model. The command surface becomes the default once it matches. The catalog stays behind a flag for one release, then gets deleted.
  8. Code Mode (feat(mcp): add HTTP Code Mode with explicit API permissions #1670) is optional and comes later. If evals show multi-step tasks need it, it becomes a third tool over the same execute path.

unify-sentry-api-layer (re-scoped)

  • Stops staging in toolkit-core. The next step is to move toolkit-core/src/api-client/ into getsentry/sentry-api-schema and reconcile it with the package's existing errors, pagination, and browser client. Then toolkit switches over (8.5, 8.6).
  • The CLI's lib/api/ wrappers then move onto @sentry/api (8.7).
  • 8.8: get the endpoints still called by raw path into the published schema. A path grep found about 85 such call sites in the CLI and 55 in the MCP, with many duplicates.
  • Dropped:
    • the remaining MCP domain wrappers
    • merging wrappers that both products call
    • the compression dictionary (group 10)
    • porting each MCP tool (11.3)
  • Replaced by cli-first-mcp group 6: 11.1, 11.2, and 11.4.
  • Parked: the R2 cache tier (feat(mcp-cloudflare): Share allowlisted cache entries across isolates through R2 #1842).
  • 6.3 is ticked (feat(mcp): Add a fresh input to read tools that bypasses cached reads #1832).

Superseded: refine-project-management-tools.

Checks: openspec validate --strict passes for both changes, and oxfmt --check and docs:check pass.

Implementation issues are filed and linked from each task and from the #1357 tracker (#1875–#1899, #1907–#1911). The release (#1877) follows the @sentry/api switch (#1876).

via @BYK.

--

View Session in Sentry

@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sentry-local Error Error Nov 4, 2026 9:57am UTC

Request Review

@github-actions github-actions Bot added the risk: low PR risk score: low label Oct 26, 2026
BYK added a commit that referenced this pull request Oct 26, 2026
…ry (#1671)

`search_issues` was sending AND/OR to the issues endpoint, which rejects
them with `Boolean statements containing "OR" or "AND" are not supported
in this search`. That's the top `search_issues` failure in
`sentry/mcp-server`: about 93K a week across many users, mostly `is:X OR
is:Y`.

Two things in MCP let them through:

- The repair prompt (`search-issues/config.ts`) listed `AND, OR, NOT` as
valid syntax and told the agent to keep explicit tokens.
- With no agent provider configured, queries went to the API unchecked.

**What changes**

- **The CLI sanitizer moves to `@sentry/toolkit-core/search-query`.**
`rewriteSearchQuery` returns `{ query, notes }` and throws
`SearchQueryError`. Each product decides how to report: the CLI wrapper
(`cli/src/lib/search-query.ts`) keeps its stderr warning and
`ValidationError`, and MCP throws `UserInputError`. CLI behavior is
unchanged except one error line, which now reads `key:[val1,val2]`
instead of `--query "key:[val1,val2]"`.
- **`search_issues` runs it after the optional agent repair**, right
before `listIssues`, so it covers the no-provider path too. When it
rewrites a query, the result says so and shows the query it ran. Queries
it can't rewrite fail with a `UserInputError` that lists alternatives,
before any API call.
- **New `issueSearch` option** (MCP only for now):
- `is:A OR is:B` becomes `status:[A,B]` or `substatus:[A,B]`. `is:`
rejects in-lists, but the status and substatus filters it maps to accept
them.
- Substatus lists must share one parent status, because the endpoint
adds a status filter for the first substatus only
(`expand_substatus_query_values`). So `is:ongoing OR
is:archived_forever` is still rejected.
- Parentheses that reduce to plain terms are unwrapped: `is:unresolved
(is:regressed OR is:escalating)` becomes `is:unresolved
substatus:[regressed,escalating]`.
- **The agent prompt and tool description now say** there's no
AND/OR/NOT or parentheses, and give the in-list, `status:`, and
`substatus:` forms instead.

**Checked against the live API** (sentry org, `junior` project, 14d,
`X-Hits`):

- `status:[unresolved,resolved]` returned 165, which is 159
(`is:unresolved`) + 6 (`is:resolved`).
- `substatus:[new,ongoing]` returned 159, which is 53 (`is:new`) + 106
(`is:ongoing`).
- `substatus:[ongoing,archived_forever]` returned only unresolved
issues, and `[archived_forever,ongoing]` returned none. That's the
first-substatus behavior the parent-status check guards against.
- `(is:unresolved)` returned `[]` and `NOT is:resolved` returned `[]`
(NOT is searched as free text), both with a 200. That's why the prompt
now says `!` instead of NOT, and why parentheses get unwrapped.
- `is:[unresolved,resolved]` returned a 400: `"in" syntax invalid for
"is" search`.

**Reviewing the move**

Git shows the core file as new, because the CLI path still holds the
wrapper. To see only what changed in the moved code:

```
git diff main:packages/cli/src/lib/search-query.ts HEAD:packages/toolkit-core/src/search-query.ts
```

**Not in this PR**

- The CLI's `issue list` could opt into `issueSearch`. I left it off to
keep CLI behavior unchanged.
- MCP's other query tools don't use the shared sanitizer yet.
- The 429 retry in `get_sentry_resource` is task 4.1 of #1669.

**Checks**

- `pnpm run tsc` and `pnpm run lint` pass.
- Tests pass for `toolkit-core` (356), `mcp-core` (1833), and the CLI
search-query, issue, log, span, and replay tests (532).
- The full CLI `test/lib` + `test/commands` run has 6 failures in
filesystem-permission tests (completions, upgrade lock, code scanner,
file-changes). The completions and upgrade-lock failures also happen on
`main` in this sandbox, so they look environment-specific.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC08J1NSPU6S%3A1792772794.175999 -->

--

[View Junior
Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
Co-authored-by: Andrew Liu <andrew.liu@sentry.io>
BYK added a commit that referenced this pull request Oct 26, 2026
…1672)

The CLI and MCP core pinned different `@sentry/api` versions, so the two
products could disagree on generated types and request helpers. This
adds `@sentry/api: 0.400.0` to the pnpm catalog and points
`packages/cli` and `packages/mcp-core` at `catalog:`. The lockfile now
resolves a single `@sentry/api` version.

MCP core needed no code changes for the bump.

This is task 1 of the `unify-sentry-api-layer` OpenSpec change in #1669.

Checks run locally:

- `pnpm install --frozen-lockfile`
- `pnpm --filter @sentry/mcp-core run tsc`
- `pnpm --filter @sentry/mcp-core run test` (131 files, 1,831 passed, 6
skipped)
- `pnpm --filter @sentry/mcp-core run build`
- `tsc` for `@sentry/mcp-server` and `@sentry/mcp-cloudflare`
- Formatter check on changed files

The full root quality gate and the CLI suite were not run locally; CI
covers them.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf -->

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
@sentry-junior

sentry-junior Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Plan update (a81439e): retries and 429s.

  • New task 5.0: a pure, Workers-safe toolkit-core/src/api-retry.ts. It retries only GET/HEAD/OPTIONS unless a request opts in with idempotent. It waits for Retry-After (seconds or HTTP-date) or, on a 429, X-Sentry-Rate-Limit-Reset, measured against the response Date when the local clock is off. Each product sets per-wait and total-wait caps. Concurrency 429s and responses without headers fall back to jittered backoff, and sleeps take an abort signal. The MCP adopts it first. The CLI's interim fix moves onto it in a follow-up.
  • 5.1: the transport now does idempotent-only retry through api-retry, and 401 refresh stays allowed for any method. The CLI's old all-methods retry is called out as a bug, fixed separately.
  • 6.1: SentryApiService drops its own retry when it moves onto the transport, so retries don't stack.
  • New spec delta specs/sentry-api-transport/spec.md. Its scenarios: a 429 with a reset header waits within the cap and retries; a POST without the opt-in is never retried; a 401 refresh replays any method; a wait over the cap returns a rate-limit error that says when to retry; and retries happen in one layer only.
  • design.md gains a short "Retries are for idempotent requests and honor rate-limit headers" decision, and proposal.md lists the new capability.

openspec validate unify-sentry-api-layer --strict passes.

@sentry-junior

sentry-junior Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my earlier plan comment (b5523f5), following review on #1673:

  • 429s are retried for any method. Sentry's RatelimitMiddleware.process_request rejects the request before the view runs, so a replay can't repeat a write.
  • 5xx responses, timeouts and network errors are retried only for GET/HEAD/OPTIONS or with the idempotent opt-in.
  • Retried statuses: 429, 500, 502, 503 and 504. Other 5xx codes such as 501 and 505 are deterministic, so they aren't retried.
  • Clock skew: the wording now matches fix(mcp): Retry rate-limited GETs using Sentry rate-limit reset headers #1673. The delay is kept within the range the response Date allows.

The sentry-api-transport requirements are split up so that openspec validate --strict passes.

BYK added a commit that referenced this pull request Oct 27, 2026
…tions on missing org (#1678)

## Problem
Telemetry from `sentry/mcp-server` (spans over 7 days, extrapolated from
0.3 sampling, so user counts are lower bounds):
- **Missing `organizationSlug`:** about 23.8K failures from 861+ users.
One user sent ~17.7K of them, all `search_issues` with `period:"1h"`.
Without them it's ~6.1K failures across ~860 users. All of these came in
on the unscoped `/mcp` URL. They fail in the MCP SDK's input validation
before any handler runs, so the zod error message is the only place to
add guidance without changing the schema.
- **`period: "1h"` on `search_issues`:** about 25.6K failures from ~50
users, mostly that same user.
- **Minute periods:** values like `30m`, `15m` and `10m` on
`search_errors`, `search_logs` and `search_traces` were never accepted.
That's about 1.5K failures from ~300 users. Sentry reads `m` as minutes
(read-only GET check: `30m` returned 28 errors, against 1,269 for
`24h`), and the issues endpoint accepts `statsPeriod=1h`.

## Changes
- **`SEARCH_ISSUES_PERIOD_VALUES`** now includes `1h`.
- **`ParamPeriod`** now accepts minutes (`/^\d+[mhdw]$/`), and the
rejection message says "(minutes)".
- **A missing or null `organizationSlug`** now returns: `Required. Call
find_organizations() to list your organizations, then retry with
organizationSlug set to one of their slugs.` This works on SDK v1 and v2
and doesn't change the JSON schema.
- **Tool definitions** are regenerated. Token overhead goes up by 13
(10,066 → 10,079).

## Held back
- **Defaulting to the user's only org.** This would mean making
`organizationSlug` optional on ~50 tools, and fetching the org list plus
a region lookup on every call that has no org. That's the org endpoint,
cache and region-routing code that #1669 reworks, so it's left for that
work.
- **Listing the user's orgs in the error, with a ready-to-copy retry.**
Same blocker: the SDK rejects the call before our handler sees it.

## Checks
- `pnpm run tsc` and `pnpm run lint` pass.
- mcp-core: 1847 passed, 6 skipped.

## Risk
A model that means months by `3m` would get 3 minutes of data. Telemetry
shows callers mean minutes; there are only a handful of `6m`/`12m`. None
of this fixes the stuck script.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC08J1NSPU6S%3A1792772794.175999 -->

--

[View Junior
Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Oct 27, 2026
…#1679)

## Problem
These are the most common field and sort errors in `sentry/mcp-server`
telemetry (7 days, extrapolated from 0.3 sampling, so user counts are
lower bounds). Each fix was checked against the live API.
- **`search_logs` with `min/max(timestamp)`:** ~2.6K failures from ~470
users. Log timestamps are strings, so the fix is
`min/max(timestamp_precise)` (nanoseconds). Metrics behaves the same way
(43 failures).
- **`search_errors` with `first_seen()`:** ~2.3K failures from ~590
users. That function doesn't exist; use `min(timestamp)`, plus
`max(timestamp)` or `last_seen()`. About 700 of these came from fields
the embedded agent picked.
- **`search_errors` sorted by `issue`:** ~1K failures from ~270 users.
Sentry rejects this sort even when `issue` is selected; use `-count()`
or `issue.id`.

## Changes
- **New `tools/support/search-events/field-hints.ts`.** It adds a hint
only when the request actually sent the rejected expression and Sentry's
error message matches it.
- Hints are fixed text and never include caller input, and Sentry's own
message is kept.
- Anything that doesn't match is re-thrown unchanged as
`ApiValidationError`.
- **Hints are added in two places:** the validation failure, and the
final Events API 400. The second is needed because the validate endpoint
accepts the `first_seen()` and `issue`-sort cases.
- **`min(timestamp)`** is added to the embedded agent's errors field
list.

## Not covered
- `timestamp.to_day` / `to_hour` on spans and logs (~600 failures):
there's no clear one-line replacement.
- Metrics `count()` arity (~350), `p10`/`p25` (~150), and `count(span)`
(0).

## Checks
- tsc, lint (oxfmt) and all mcp-core tests pass (1837 passed, 6
skipped).
- Tool definitions are unchanged (10,066 tokens).

## Relation to #1669
Task 7.3 moves the explore, traces and logs endpoints onto
`@sentry/api`. These hints only depend on the error being an
`ApiValidationError` and on its message, so they should carry over as
long as 400s still map to that class.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC08J1NSPU6S%3A1792772794.175999 -->

--

[View Junior
Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Oct 27, 2026
…rs (#1673)

The hosted MCP gets rate limited by the Sentry API a lot. In the last 7
days `sentry/mcp-server` logged about 32.6K `http.client` 429s, nearly
all on GETs, usually "Limit is 40 requests in 1 seconds", plus about 72
concurrency-limit hits. `SentryApiService.request()` only retried
502/503/504, so every 429 failed the tool call.
`ApiRateLimitError.retryAfter` was read from body fields Sentry never
sends, so nothing could say when to try again.

Sentry's 429 body is just `{"detail": ...}`. The timing comes in headers
set by `src/sentry/middleware/ratelimit.py`: `X-Sentry-Rate-Limit-Reset`
is the Unix second when the next window starts, and `Remaining` is `0`
only when the fixed window is used up. Concurrency 429s carry a reset
that doesn't apply to them.

This adds `@sentry/toolkit-core/api-retry`, a pure retry policy with no
Node APIs, so it is safe on Workers. It is task 5.0 in #1669. The MCP
adopts it here:

- **What gets retried:** GET and HEAD requests that get a 429 or a
502/503/504. Writes are never retried. The policy's `idempotent` opt-in
exists for the CLI's content-addressed uploads, but the MCP doesn't use
it.
- **How long a 429 waits:** until `Retry-After` (seconds or HTTP-date)
if a proxy sent one, otherwise until `X-Sentry-Rate-Limit-Reset`. The
reset is measured against the response `Date` when the local clock is
off by more than 2s, plus a small jitter. Concurrency 429s and gateway
errors use equal-jitter exponential backoff starting at 250ms.
- **Caps:** at most 2 retries, 2s per wait and 3s in total. Sentry's
windows are mostly 1s, so the reset is usually under a second away and
both retries fit in about 2s. A reset further out than 2s means a longer
window. Then the call fails at once instead of holding the Worker
request open. The error is an `ApiRateLimitError` with a real
`retryAfterMs`, and its message ends with "Retry in N seconds." When
there is no header, it says to wait a few seconds and make fewer
parallel requests.
- **Abort:** aborting the request's `signal` ends the wait immediately.

`ApiRateLimitError.retryAfter` (seconds, from the body) is replaced by
`retryAfterMs`, taken from the headers. Nothing read the old field.

The CLI currently has its own interim retry fix in a separate CLI-only
PR. It moves onto this shared module in a follow-up after that PR lands.
Once the MCP is on the shared transport (#1669 task 6.1),
`SentryApiService` drops this loop so retries don't stack.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A0bac2c6dfda59b43b43e21a1118f2684 -->

--

[View Junior
Session](https://junior-prod.sentry.dev/conversations/agent%3A0bac2c6dfda59b43b43e21a1118f2684)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A0bac2c6dfda59b43b43e21a1118f2684/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Oct 27, 2026
…reset on 429 (#1677)

## Problem
`packages/cli/src/lib/sentry-client.ts` retried **every** HTTP method,
including POST/PUT/DELETE, on 408/429/500/502/503/504 and on network
errors and timeouts. That can replay writes. A 429 also only got the
generic 1s/2s backoff: the client never read Sentry's
`X-Sentry-Rate-Limit-Reset` header, and Sentry doesn't send
`Retry-After` on API rate limits.

## Behavior changes
- **5xx, timeouts and network errors** now retry only for GET, HEAD and
OPTIONS, or when a request sets the new `idempotent: true` opt-in on
`getSdkConfig` / `ApiRequestOptions`.
- **429 retries for any method.** Sentry's rate-limit middleware rejects
the request in `process_view`, before the endpoint runs, so nothing was
applied.
- **Retried statuses stay 408/429/500/502/503/504.** Deterministic 5xx
like 501 and 505 are not retried.
- **PUT and DELETE are deliberately not retried.** Sentry PUTs have side
effects (merge, activity), and a replayed DELETE returns 404.
- **The 401-refresh retry still covers every method**, and no longer
waits 1s first.
- **A timed-out write** now gets a "may still have been applied" hint.
- **The 429 wait** is the longer of `X-Sentry-Rate-Limit-Reset` (read
only when Remaining is 0) and `Retry-After` (seconds or HTTP date), both
measured against the response `Date` to guard against clock skew.
Concurrency-limit 429s, or 429s with no hint, use jittered backoff: the
upper half of min(1s·2^n, 10s).
- **Caps are unchanged:** `MAX_RETRIES` is 2 and each wait is at most
10s.
- **New `RateLimitError`** (extends `ApiError`, status 429). It's thrown
when the wait would exceed the cap or retries run out. The message is
agent-friendly: server detail, then "Retry in N seconds; do not repeat
the request before then. Make fewer requests in parallel."
- **The policy is pure**, with no CLI imports, in
`packages/cli/src/lib/retry-policy.ts`. That way the CLI can switch to
the shared `@sentry/toolkit-core/api-retry` from #1673 without a
rewrite.

## POST callers given the `idempotent` opt-in
Each was checked against its getsentry/sentry handler:
- **Chunk uploads** (`chunk-upload.ts`): blobs are stored by SHA-1
(`FileBlob.from_files`).
- **Artifact-bundle assemble and `pollAssembly`** (`sourcemaps.ts`).
- **DIF assemble** in `debug-files.ts`, `proguard.ts` and
`dart-symbols.ts`: the server checks the cached assemble status by
checksum.

**Not opted in:** preprod assemble (each call creates a new
`PreprodArtifact`), Seer `/autofix/`, snapshot create and archive
trigger, and SDK and `sentry api` writes.

## Tests
- The POST-retry body-cloning tests now go through the 429 and opt-in
paths.
- New cases:
  - a rate-limited POST is resent with the same body
  - a 501 is not retried
  - a 503 on a non-idempotent method is not retried
- the reset-header and `Retry-After` wait, clock skew, and the cap
leading to `RateLimitError`
  - the exact rate-limit message text
- `api-client.coverage` used 429 as a sample error and now uses 400.

## Checks
- `tsc --noEmit` and `pnpm run lint` pass.
- The sentry-client, retry-policy, api-client and api-client.coverage
tests pass: 204/204.
- Full CLI suite on `bf3b710`: 11,482 passed and 6 failed. The same 6
fail on `main` (`37c7193`) in this sandbox. They're chmod-based
permission tests (completions ×2, code scanner, init file-changes,
telemetry readonly, upgrade lock), and the sandbox runs with full
capabilities, so they can't fail the way they expect.
- After the 429-any-method change (`7099e2b`), all other CLI test files
that mention 429 pass (1,195), apart from the 2 known sandbox permission
failures.

## Relation to #1669
This matches the retry policy in #1669 (task 5.0 and the
`sentry-api-transport` spec). Task 5.1 will move this behavior into
toolkit-core's shared transport unchanged.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC08J1NSPU6S%3A1792772794.175999 -->

--

[View Junior
Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Oct 27, 2026
The CLI's fallback "immutable" cache tier kept any `/events/<id>/`,
`/trace/<id>/`, or `/trace-items/<id>/` response for 24 hours. It was
also checked before the `/issues/` volatile pattern, so `getLatestEvent`
(`/issues/<id>/events/latest/`) was cached for a day: re-running `sentry
issue view` an hour later could show an outdated "latest" event.

The getsentry/sentry#128818 spike
([findings](getsentry/sentry#128818 (comment)))
found that none of these responses are actually immutable. Event
payloads embed `nextEventID`/`previousEventID`, user reports, and
release data, reprocessing and merges can change them for the same ID,
`latest`/`oldest`/`recommended` resolve to different events over time,
and full traces gain spans as they arrive.

This removes the `immutable` tier:

| Tier | TTL | Matches |
| --- | --- | --- |
| no-cache | 0 | autofix and root-cause polling (unchanged) |
| volatile | 60 s | `/issues/` (now including issue events), project
event details, full traces, `dataset=logs\|spans`, trace logs |
| stable | 5 m | everything else, now including organization event
details and trace-item details |

Server `Cache-Control` headers still take precedence, so when
getsentry/sentry#128828 ships its `private, max-age=300` headers the CLI
follows them without a release. A TODO points at
getsentry/sentry-api-schema#81 and #102 for classifying by generated
`@sentry/api` operation metadata instead of URL regexes.

Notes for review:

- **No migration.** Freshness is recomputed from `createdAt` and the
current tier on read, so existing 24-hour entries on disk are treated by
the new rules immediately.
- **`immutableMinTimeToLive` removed.** It pointed at the deleted tier
constant. The `http-cache-semantics` default is the same 24 hours, and
it only applies to responses where the server sends `immutable`.
- **Cost:** repeated reads of project/issue events and full traces will
hit the API after 60 seconds instead of 24 hours; organization event and
trace-item details after 5 minutes.

Task group 2 of the `unify-sentry-api-layer` OpenSpec change in #1669.

**Checks run locally:**

- New `classifyUrl` cases plus fake-clock expiry tests for org event
details (hit at 4 min, miss at 6 min), issue latest event, project event
details, full traces, and server `max-age` overriding the fallback. 8 of
them fail against the old classifier.
- `pnpm exec vitest run test/lib/response-cache*.test.ts`: 60 passed
- CLI `typecheck`, `lint`, and `oxlint`/`oxfmt` on the changed files:
clean
- `pnpm run test:unit` (first revision): 11,583 passed, 6 failed. The
same 6 fail on unmodified `main` in my sandbox: filesystem-permission
tests (`completions`, `code-scanner`, `file-changes`, `telemetry`
readonly DB, `upgrade` lock) that depend on the environment, not this
change.

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf -->

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
BYK added a commit that referenced this pull request Oct 27, 2026
Moves the CLI's pagination core into `@sentry/toolkit-core/pagination`
so the MCP can reuse it. This is task group 3 of the
unify-sentry-api-layer proposal (#1669).

What moved: page sizing (an even split over the fewest requests, at most
100 rows per page), resume-safe cursors (the skip count is written into
`is_prev` so the cursor stays a valid Sentry cursor, page-number cursors
keep their page size, and `cli1~` cursors are still read), `paginate`,
and `fetchAllPages`.

The shared module does no I/O and doesn't depend on the CLI logger,
`ApiError`, or Valibot:

- `paginate` takes `maxPages` and an optional `warn` sink, and returns
`stoppedAtPageCap`. The spec's "Bounded page count" requirement asks for
a structured page-cap signal, so the MCP can report it instead of
logging.
- `fetchAllPages` takes a row parser and returns `{ ok: false, reason }`
(`invalid-page`, `repeated-cursor`, or `page-cap`) instead of throwing.

The CLI keeps thin wrappers in
`packages/cli/src/lib/api/infrastructure.ts` with the same signatures,
default limit, `SENTRY_MAX_PAGINATION_PAGES` cap, page-cap warning, and
`ApiError` messages. Existing imports and behavior don't change, and the
CLI pagination tests pass without edits. `autoPaginate` and the
`sentry-cli/no-manual-pagination` lint rule stay CLI-only. The lint rule
only scans CLI source, so the loop in toolkit-core isn't covered by it.

Tests: `packages/toolkit-core/src/pagination.test.ts` has a case for
each `sentry-api-pagination` scenario: a limit above one page and a
limit within one page, stopping inside a page, cursors that the API and
older clients accept, page-number cursor page size, the page cap, and a
repeated cursor. It also covers invalid pages, the `fetchAllPages` page
cap, and unreadable `cli1~` cursors. The README now documents the
module.

Checks: `pnpm run tsc`, `pnpm run lint`, `pnpm run docs:check`,
toolkit-core tests (401), and the CLI pagination, replay, and alert
suites (319) pass locally.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf -->

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
sentry-junior Bot added a commit that referenced this pull request Oct 27, 2026
Moves the CLI's cache policy into `@sentry/toolkit-core/response-cache`
so the MCP can use the same rules (task 4.1 of the unify-sentry-api-layer
proposal, #1669):

- `classifyUrl` and `FALLBACK_TTL_MS`: the volatile/stable/no-cache tiers
  for responses without server cache headers.
- `normalizeUrl` and `buildCacheKey`: identity-scoped keys over the method
  and full URL. Key derivation now uses WebCrypto HMAC-SHA256 instead of
  `node:crypto`, so it runs in Workers and browsers. The output is
  byte-identical, so existing CLI cache files still hit, and an empty
  identity is rejected instead of producing an unscoped key.
- `computeInvalidationPrefixes`: moved unchanged from
  `cli/src/lib/cache-keys.ts`, with its tests.

The module has no storage or I/O. The CLI's disk cache imports it, and its
`buildCacheKey` wrapper (which defaults to the active identity) is now
async, as are the tests that call it.

WebCrypto HMAC runs on the libuv threadpool, so parallel requests can
finish their cache lookups in either order. The `getLogs` batching test
no longer assumes which batch reaches fetch first.

Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK added a commit that referenced this pull request Nov 3, 2026
…1836)

When a tool call leaves out `organizationSlug` and the login can reach
exactly one organization, the call now uses that organization. When the
login can reach several, a tool that requires the slug fails with a
retry the model can run as is, plus the other organizations. This is the
MCP version of the CLI recovery in #1634, and it finishes the part #1678
held back. The server never chooses between organizations.

## Why now
Over the last 7 days (`sentry/mcp-server` spans, extrapolated counts):
- `organizationSlug: Required` failures: about 4.3K, from search_issues,
find_projects, search_errors, search_logs, and calls through
execute_sentry_tool. `get_sentry_resource` adds about 1.9K more with
"`organizationSlug` is required when not using a URL".
- The script that sent about 89% of the earlier ~31K/week has stopped.
The rest is spread out: no user accounts for more than 2 of the 40
sampled failures.

## How it works
- **No schema or handler edits.** `getFilteredInputSchema` makes a
required `organizationSlug` optional at registration time, so the MCP
SDK no longer rejects the call before our code runs. Then
`prepareToolCallParams` runs `applyDefaultOrganization`. Both top-level
calls and `execute_sentry_tool` go through it, before
`withRoutedCredential`, so stdio account routing (#1802) gets the
defaulted slug.
- It runs only when the call has no slug after constraint injection and
no Sentry URL parameter (it reuses `getTargetOrganizationSlug`).
Org-scoped sessions never reach it.
- **One organization:** the call uses its slug. If the tool takes
`regionUrl` and the caller didn't set one, the call also gets the
organization's `regionUrl`. On self-hosted that value is empty, so
`regionUrl` stays unset.
- **Several organizations:** for a required slug, the error looks like
``Retry example: `search_issues(organizationSlug='org-a', period='1h')`
`` and lists the other organizations. Catalog tools get the
`execute_sentry_tool(name=…, arguments=…)` form, and values equal to a
schema default are left out. For an optional slug (URL-or-ID tools such
as `get_sentry_resource`), the call runs as before.
- **Listing fails** (for example, a token without `org:read`): the error
falls back to the find_organizations hint from #1678.
- **Multiple logins:** organizations from every account are merged.

## Fit with #1669
- The organization list comes from `SentryApiService.listOrganizations`,
which already uses the shared `endpoints/organizations` wrapper and
transport (#1722, #1737). No new endpoint code.
- The resolve step lives in mcp-core's constraint and param-prep layer.
The design keeps that layer in mcp-core ("an MCP tool fills them from
tool parameters and session constraints"), so 11.3 can keep it as is.
- The MCP hosts don't wire a `CacheStore` yet (7.1 is still open).
Instead, the list is memoized per context and credential for 5 minutes,
which matches the `stable` tier. When 7.1 lands, the `/organizations/`
request gets cached per identity on its own, and this memo can be
dropped. It doesn't conflict with 8.4–8.7 or 11.x.
- This doesn't complete a task in the plan. The closest is 11.3's
resolve step.

## Token cost
`measure-tokens` is unchanged at 9,014. That script reads the raw tool
schemas, and those didn't change. In the registered schemas,
`organizationSlug` is no longer in the `required` list on 8 direct
tools, which comes to 5,014 → 4,990 input-schema tokens.

## Open questions
- A `null` slug still gets the find_organizations hint. Accepting `null`
would add `anyOf` to every schema.
- When an optional-slug tool like `get_sentry_resource` is called by a
user with several organizations, it keeps its own error. It doesn't list
the organizations yet.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A0222050e9c46ef2289687231ff960e16 -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3A0222050e9c46ef2289687231ff960e16)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A0222050e9c46ef2289687231ff960e16/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 3, 2026
#1828)

Task 8.4 of the shared API client plan
([#1669](#1669)): the next
domain after organizations (#1741).

This moves the team and project endpoints from `SentryApiService` into
`toolkit-core`'s `api-client/endpoints/`. `SentryApiService` now calls
them with its Zod schemas. The code moves as is.

**Changes**
- `endpoints/teams` adds `listTeams` (on `@sentry/api`'s
`listOrganizationTeams`) and `createTeam`.
- `endpoints/projects` adds `listProjects` (on
`listOrganizationProjects`), `getProject`, `createProject`,
`updateProject`, and the project's teams: `listProjectTeams`,
`addProjectTeam`, and `removeProjectTeam`.
- Writes go through the request core, since the core's `sdkClient` only
sends GETs. `getProject` and `listProjectTeams` keep their hand-built
requests. `@sentry/api` has operations for both (`getProject`,
`listProjectTeams`), but moving them onto those operations is a separate
change.
- The core gains `nextPageCursor(response)`. `listPage` and
`listProjectTeams` use it, and the next domains will too.
- The endpoint tests share a scripted-transport helper in
`toolkit-core/src/test-utils/`. That folder is left out of the browser
typecheck, and `organizations.test.ts` uses the helper now as well.

**One change in behaviour:** as with #1741, a response that fails its
schema in these calls now throws the shared `ApiResponseValidationError`
instead of a `ZodError`. Both still reach the tool error handler as
unexpected errors.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
BYK pushed a commit that referenced this pull request Nov 3, 2026
#1834)

Task 8.4 of the shared API client plan
([#1669](#1669)). This is
stacked on #1828.

This moves the project setup endpoints from `SentryApiService` into
`toolkit-core`'s `api-client/endpoints/`. `SentryApiService` calls them
with its Zod schemas. The code moves as is.

- `endpoints/client-keys`: `listClientKeys`, `createClientKey`, and
`updateClientKey`. `updateClientKey` still sends only the fields that
are set, and keeps an explicit `rateLimit: null`.
`SentryApiService.updateClientKey` now takes the shared
`UpdateClientKeyParams` type, and its local request types are deleted.
- `endpoints/repositories`: `listRepositories`, which walks every page,
and `linkProjectRepository`, which sends the bulk code-mapping POST with
one root mapping.
- `endpoints/organizations` gains `updateAgenticOnboardingStatus`.

`@sentry/api` has no operation for `POST
/organizations/{org}/onboarding/agent/status/`, so that wrapper will
stay hand-written.

**One change in behaviour:** as with #1741, a response that fails its
schema now throws the shared `ApiResponseValidationError` instead of a
`ZodError`.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
@BYK BYK self-assigned this Nov 3, 2026
BYK pushed a commit that referenced this pull request Nov 3, 2026
…#1832)

Task 6.3 of #1669, on top of the transport's `fresh()` callback from
#1720.

**What changes**
- `ParamFresh` in `mcp-core/src/schema.ts`: `fresh?: boolean`, described
as "Bypass cached reads." The description is kept short because every
read tool's schema carries it.
- `internal/fresh-reads.ts`: `withFreshReadInput(tool)` adds the input
and wraps the handler. With `fresh: true`, the handler runs inside an
`AsyncLocalStorage` scope, and it never sees `fresh` in its params.
`isFreshRead()` reads that scope.
- `tools/catalog/index.ts` applies the wrapper to every catalog tool
that has `readOnlyHint: true` and `openWorldHint: true`, except
`search_docs` and `get_doc`, which read docs.sentry.io rather than the
Sentry API. The wrapper sits on the tool itself, so direct calls and
`execute_sentry_tool` both get it. No tool file or `execute_sentry_tool`
changes.
- `SentryApiService`'s transport passes `fresh: isFreshRead`, so the
shared client sees the scope only through the callback. Shared code
still has no `AsyncLocalStorage`.
- `docs/contributing/adding-tools.md` explains the rule. Tool
definitions are regenerated.

**Behavior today:** none visible. The MCP transport has no cache store
yet (that's 7.x), so `fresh` has nothing to bypass until one is wired
in. This PR makes the input and the scope ready for that.

**Token cost** (`pnpm run measure-tokens`): 9,014 → 9,144 (+130, +1.4%).
Of the 14 direct tools, 10 gain the input at 13 tokens each:
`find_organizations`, `find_projects`, `search_issues`,
`get_sentry_resource`, and the six `search_*` tools.

**Checks** (all run with `SENTRY_AUTH_TOKEN` unset)
- `pnpm run tsc`, `pnpm run lint` and `docs:check` pass.
- mcp-core: 1930 passed, 6 skipped. The new `fresh-reads.test.ts` covers
the scope being set only when `fresh: true`, the scope surviving an
`await`, `fresh` being stripped from handler params, a non-boolean being
rejected, and which catalog tools get the input. `server.test.ts` adds
`fresh` to the `get_replay_details` schema keys it expects.
- mcp-cloudflare: 447 passed. mcp-server bundle tests: 15 passed.

Other in-flight work on `execute_sentry_tool`, `get_sentry_resource`,
`find_dropped_events` and `get_alert_rule` doesn't touch these files.
Expect only a `toolDefinitions.json` regeneration on rebase.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ac6985c0a2dff5c8ae42a2a02dff4a60b -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ac6985c0a2dff5c8ae42a2a02dff4a60b)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ac6985c0a2dff5c8ae42a2a02dff4a60b/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
BYK pushed a commit that referenced this pull request Nov 3, 2026
Task 8.4 of the shared API client plan
([#1669](#1669)). This is
stacked on #1834.

This moves the dashboard and release endpoints from `SentryApiService`
into `toolkit-core`'s `api-client/endpoints/`. `SentryApiService` calls
them with its Zod schemas. The code moves as is.

- `endpoints/dashboards`: `listDashboards` (one page, with the next
cursor) and `getDashboard`.
- `endpoints/releases`: `listReleases`, `getRelease`,
`listReleaseDeploys`, `listReleaseCommits`, and `getEventCommitters`.
With a project, release reads still go to the project's endpoints,
otherwise to the organization's.
- `api-path` gains `withQuery`, for the path-plus-optional-query pattern
the wrappers repeat. `getOrganization` and `getProject` use it too; the
URLs they send don't change.

`@sentry/api` has no operation for the project-scoped release details
route or for event committers, so those wrappers stay hand-written.

**One change in behaviour:** as with #1741, a response that fails its
schema now throws the shared `ApiResponseValidationError` instead of a
`ZodError`.

Checked with `pnpm run tsc`, `pnpm run lint`, `pnpm run docs:check`, and
the toolkit-core, mcp-core, mcp-server bundle, and mcp-cloudflare tests.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 3, 2026
…1839)

Task 8.4 of the shared API client plan
([#1669](#1669)). This is
stacked on #1838.

This moves the cron monitor endpoints and the time range helpers from
`SentryApiService` into `toolkit-core`'s shared API client.
`SentryApiService` calls them with its Zod schemas. The code moves as
is.

- `api-client/time-params`: `normalizeStatsPeriod`, `parseStatsPeriod`,
`applyTimeParams` (`statsPeriod` or a `start`/`end` pair), and
`applyStatsMixinTimeParams` (the `since`/`until` seconds `StatsMixin`
endpoints read). It rejects a range that sets both a period and dates,
or only one end. `defaultStatsPeriod` holds the check-in list's "24
hours unless a range is given" default, which the uptime-check list
reuses in the next PR. Most other `SentryApiService` methods still call
these helpers; they now import them from `toolkit-core`.
- `endpoints/monitors`: `listMonitors`, `getMonitor` (organization or
project route), `deleteMonitorEnvironment` (a 404 still counts as
already deleted), `listMonitorCheckIns`, and `getMonitorStats`.

`@sentry/api` has no operation for monitor stats or for deleting a
monitor environment, so those wrappers stay hand-written.

**One change in behaviour:** as with #1741, a response that fails its
schema now throws the shared `ApiResponseValidationError` instead of a
`ZodError`.

Checked with `pnpm run tsc`, `pnpm run lint`, `pnpm run docs:check`, and
the toolkit-core, mcp-core, mcp-server bundle, and mcp-cloudflare tests.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 3, 2026
Task 8.4 of the shared API client plan
([#1669](#1669)). This is
stacked on #1839.

This moves the uptime monitor endpoints from `SentryApiService` into
`toolkit-core`'s `api-client/endpoints/uptime-monitors`.
`SentryApiService` calls them with its Zod schemas. The code moves as
is.

- `listUptimeMonitors` takes a numeric `projectId`, since the endpoint
filters by ID. `SentryApiService` still looks the project slug up first.
- `getUptimeMonitor` and `listUptimeMonitorChecks`. The checks list
still defaults to 24 hours unless a range is given, through
`defaultStatsPeriod` from #1839.
- `createUptimeMonitor` and `updateUptimeMonitor` share one field list,
so the body still holds every field that is set (an explicit `null`
included), in the same order as before.
- `deleteUptimeMonitor` still counts a 404 as already deleted.

`@sentry/api` has no uptime operations at all, so these wrappers stay
hand-written.

**One change in behaviour:** as with #1741, a response that fails its
schema now throws the shared `ApiResponseValidationError` instead of a
`ZodError`.

Checked with `pnpm run tsc`, `pnpm run lint`, `pnpm run docs:check`, and
the toolkit-core, mcp-core, mcp-server bundle, and mcp-cloudflare tests
(run on the top of the stack, the replay branch at `73bfc0b3`, before
the later rebases), plus the uptime tool tests on this commit.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 3, 2026
Task 8.4 of the shared API client plan
([#1669](#1669)). This is
stacked on #1840.

This moves the trace, profiling, and preprod snapshot endpoints from
`SentryApiService` into `toolkit-core`'s `api-client/endpoints/`.
`SentryApiService` calls them with its Zod schemas. The code moves as
is.

- `endpoints/traces`: `getTraceMeta` and `getTrace`, with the same
defaults (`14d`, `limit=1000`, `project=-1`).
- `endpoints/profiling`: `getFlamegraph` (still escapes the transaction
name), `getTransactionProfile`, and `getProfileChunks`.
`SentryApiService.getProfileChunk` keeps the MCP-only shaping: it throws
`ApiNotFoundError` when there are no chunks and backfills `profiler_id`.
- `endpoints/snapshots`: `getSnapshot`, `getSnapshotImage`, and
`getLatestBaseSnapshot`. They return the body unvalidated, as before.
`fetchImageByUrl` stays in `SentryApiService`, since it isn't an
endpoint.

`@sentry/api` has operations for all of these routes. The wrappers still
build their requests by hand so the change stays a move. Switching them
to the operations can follow.

**Intentional changes:**

- As with #1741, a response that fails its schema now throws the shared
`ApiResponseValidationError` instead of a `ZodError`.
- `getSnapshot` with `compactMetadata: false` no longer sends a trailing
`?` with an empty query.
- The `ApiNotFoundError` for an empty chunk list carries the validated
body as `responseBody`, not the raw one.

Checked with `pnpm run tsc`, `pnpm run lint`, `pnpm run docs:check`, and
the toolkit-core, mcp-core, mcp-server bundle, and mcp-cloudflare tests
on the top of the stack, plus the trace, profile, and snapshot tool
tests on this commit.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 3, 2026
Task 8.4 of the shared API client plan
([#1669](#1669)). This is
stacked on #1841 and is the last PR in the stack.

This moves the replay endpoints from `SentryApiService` into
`toolkit-core`'s `api-client/endpoints/replays`. `SentryApiService`
calls them with its Zod schemas. The code moves as is.

- `searchReplays` keeps the same query: `query`, `per_page`, `project`,
`sort`, repeated `environment` and `field`, and the time range.
- `getReplay` and `listIssueReplayIds`. `SentryApiService` still unwraps
the `{ data }` envelope and picks the issue's entry from the keyed
replay-count result.
- `visitReplayRecordingSegments` still pages 100 segments at a time,
follows the `Link` cursor, and stops at `maxSegments`, with `truncated`
set the same way as before. It reads the cursor with the shared
`nextPageCursor`, which parses the header the same way the old
`getNextCursor` did.
- `docs/contributing/api-patterns.md` gets a short "Where Endpoint Code
Lives" section that describes the pattern this stack follows.

**One change in behaviour:** as with #1741, a response that fails its
schema now throws the shared `ApiResponseValidationError` instead of a
`ZodError`.

Checked with `pnpm run tsc`, `pnpm run lint`, `pnpm run docs:check`, and
the toolkit-core, mcp-core, mcp-server bundle, and mcp-cloudflare tests
on this branch before the last rebase. `pnpm run tsc` passes again after
rebasing onto the current main.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ab061171f2ec2e6cf6f09a6828cadc9fa/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK added a commit that referenced this pull request Nov 3, 2026
…#1829)

Two `get_sentry_resource` input failures from the last 7 days in
`sentry/mcp-server`:

- **"Either `url` or `resourceType` must be provided": ~4.6K/week.** The
calls carry the right value under the wrong key. The most common keys
are `resourceId` without a type (~830, all issue short IDs), `issueId` /
`issue_id` (~900), `resource` + `id` (~630 / ~360, mostly `resource:
"issue"`), `resourceUrl` (~530) and `issueUrl` (~210).
- **"Could not determine resource type from URL": ~2.3K/week.** The
largest groups are project event URLs
`/projects/{project}/events/{eventId}/` (~500), issue list URLs
`/issues/?query=…` (~460), alert rule URLs (~380), Explore logs/traces
search URLs (~250), and `/api/0/…` URLs (~180).

What changed:
- New undocumented schema aliases: `issueUrl` and `resourceUrl` map to
`url`, and `issueId` and `id` map to `resourceId`. `issueId` also
implies `resourceType: issue`. The schema grows from 683 to 710 tokens.
- A `resourceId` with no `resourceType` resolves as an issue only when
it looks like an issue ID: an uppercase short ID (`PROJECT-123`) or a
numeric ID. Hex IDs, slugs and titles still need an explicit type.
- The URL parser handles
`/[organizations/{org}/]projects/{project}/events/{eventId}/`. These
resolve as an event by event ID, which `get_issue_details` already
supports.
- List and search URLs get a specific error that names the right tool:
`search_issues`, `find_alert_rules` / `get_alert_rule`, `search_logs`,
`search_traces`, or the web URL for API links. Other unknown URLs keep
the old error.

Open question: multi-credential routing (`credential-routing.ts`) reads
the org only from `url`, so a call with only an `issueUrl` /
`resourceUrl` alias falls back to the default credential. I left it
alone because routing belongs to the #1669 work.

<!-- junior-request-attribution:start -->
via @BYK.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Ae64545cd46d346c18e8b9993b3fbb43e -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Ae64545cd46d346c18e8b9993b3fbb43e)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ae64545cd46d346c18e8b9993b3fbb43e/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
sentry-junior Bot and others added 2 commits November 3, 2026 16:46
…-layer-proposal

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
…PI client into @sentry/api

Add cli-first-code-mode-mcp: CLI parity with the MCP catalog, a two-tool
Code Mode surface (search_sentry, execute_sentry_code) over the CLI's
commands, discovery from CLI introspection, every published API route,
completion for agents, and an eval-gated catalog removal.

Re-scope unify-sentry-api-layer: stop staging in toolkit-core, move the
client into @sentry/api next, then the CLI's wrappers, then the endpoints
missing from the published schema. Drop the MCP domain wrappers, the
compression dictionary, and per-tool porting; park the R2 cache tier.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@sentry-junior sentry-junior Bot changed the title docs(openspec): Propose a shared Sentry API layer for the CLI and MCP docs(openspec): Plan the MCP as Code Mode over the CLI, on @sentry/api Nov 3, 2026
sentry-junior Bot and others added 2 commits November 3, 2026 18:25
…un the SDK on Workers

Rename cli-first-code-mode-mcp to cli-first-mcp. The MCP lists two tools,
search_sentry and execute_sentry_command, which dispatches one exposed
command with validated JSON parameters through createSentrySDK(), so no
code sandbox is needed. An all-tools mode lists each command as a tool for
harnesses that search tools themselves.

The hosted MCP runs the CLI SDK with storage split by purpose behind
Workers backends and per-request state, starting with a workerd build
spike. A Durable Object SQLite adapter is the fallback for storage, and
the per-domain portable-operation split is the fallback for domains
Workers cannot run. Code Mode becomes an optional later tool.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
Hosted creates one SDK instance per request with the OAuth grant's token
and turns persistence off instead of adding Workers storage backends.
Pagination becomes stateless through nextCursor. The remaining cross-user
risk is module-level state, which gets an audit and the concurrent
two-identity test. Keep @sentry/mcp-server.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
sentry-junior Bot and others added 4 commits November 3, 2026 20:57
…entation issues

MCP hosts allowlist and approve by tool name (#1254), so execute becomes
execute_sentry_read and execute_sentry_write. The release now follows the
@sentry/api switch (8.10). Each task links its tracking issue.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
…-layer-proposal

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
The SDK runs on workerd once five small load-time issues are fixed and the
hosted worker moves to a 2026 compatibility date. The bundle fits the
64 MiB limit, but it is the whole CLI because the route loaders import
every command. The spike also found that the SDK inherits the worker's
process.env, so the worker's own SENTRY_DSN redirected an untargeted
command. Records the stdio-only additions and ranks what is left.

Refs #1882

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
Harnesses search large tool lists themselves, so the plan drops the
search_sentry and execute_sentry_read/write meta-tools: every exposed
command is its own MCP tool, with hints from its permission class.
Completion reaches agents through schema examples and validation errors,
with no completion tool. #1891's all-tools mode is folded into #1890.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>

This branch had an error being deployed

1 failed deployment
Preview — 6cf1dc31 Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: low PR risk score: low

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants