Skip to content

docs(openspec): Propose a shared Sentry API layer for the CLI and MCP - #1669

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

sentry-junior[bot] wants to merge 5 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

Adds the OpenSpec change unify-sentry-api-layer. It covers the proposal, three capability specs, the design, and the tasks. It is docs only; implementation lands in follow-up PRs.

What it proposes

  • Narrow the CLI's 24-hour "immutable" cache tier, which today also covers issue events (including /issues/<id>/events/latest/), project events, and full traces. Those change after they are first read. A 5-minute detail tier keeps only organization event details and project trace-item details; the rest drop to 60 seconds. This lands as its own CLI PR (task group 2).
  • Move the CLI's pagination, response-cache policy, and request transport into toolkit-core, on top of @sentry/api. The CLI and MCP then share one implementation, with credentials, storage, and telemetry supplied by each host.
  • runMcpServer() accepts a host-supplied fetch. sentry mcp passes the CLI's authenticated transport, so local MCP sessions reuse the CLI login, refresh, and on-disk cache.
  • The hosted MCP gets a tiered cache:
    • an in-memory LRU, limited by bytes, for every tier
    • R2 for organization event details and project trace-item details, only when the server marks them private, max-age, for at most 5 minutes. Enabling it in production needs a security sign-off on the stale window after access revocation or deletion.
    • zstd level 3 compression on both
  • MCP read tools gain a fresh input, like the CLI's --fresh.
  • MCP endpoints move onto @sentry/api operations one domain at a time, and both products use one @sentry/api version from the catalog.

Why not createSentrySDK()

  • The CLI already depends on @sentry/mcp-server, so the MCP calling the SDK would create a dependency cycle.
  • The SDK needs SQLite and a filesystem, which Workers lack.
  • SDK errors flatten HTTP status into exit codes, but the MCP needs the status to decide what gets logged to Sentry.

Evidence behind the design

  • Endpoint coverage. About 67 of the 91 endpoint paths in mcp-core/src/api-client/client.ts have operations in @sentry/api@0.404.0. This was a pattern match, so treat it as approximate.
  • Codecs. Benchmarked on real API responses, with results in design.md.
    • Plain zstd-3 is used for every tier. Brotli gains 5–12% in ratio for about 2x the CPU, and zstd-9 only helps large traces, which no longer pays off for entries that live 5 minutes.
    • A trained zstd dictionary doubles the ratio within one project, but gains only about 20% across projects.
    • Dictionary support is deferred to task group 10: workerd silently ignores the dictionary option, and a dictionary trained on real data embeds verbatim strings, so it must not ship in a public package.
  • Per-entry cap. It applies to compressed size, because traces were 1.5 MB raw at the median in the sample but compress 37–45x.
  • zstd on Workers. node:zlib zstd works in local workerd under the current compatibility date. Task 7.2 confirms it on a deployed preview.
  • Server cache headers. The Spike: HTTP cache headers for immutable API responses sentry#128818 spike (findings) found no endpoint safe for immutable, and only organization event details and project trace-item details safe for private, max-age=300. Prototype: feat(api): Add opt-in max-age mode to ConditionalGetResponseMixin sentry#128828.
  • Version alignment. Task group 1 is in chore(deps): Share one @sentry/api version through the pnpm catalog #1672.

Checks: openspec validate unify-sentry-api-layer --strict, oxfmt --check, and pnpm run docs:check all pass.

via BYK.

--

View Junior Session in Sentry

Share pagination, response caching, and request transport between the CLI
and MCP through toolkit-core on top of @sentry/api, let sentry mcp supply the
CLI's authenticated transport, and add a tiered (memory + R2), zstd-compressed
cache for the hosted MCP.

Co-Authored-By: BYK <burak.kaya@sentry.io>
@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 Ready Ready Preview Oct 27, 2026 9:33am UTC

Request Review

@github-actions github-actions Bot added the risk: low PR risk score: low label Oct 26, 2026
Benchmark zstd, brotli, gzip and trained zstd dictionaries on real API
responses. Keep plain zstd for now, cap entries on compressed size, and
move dictionary support to a follow-up gated on safe training data and
Workers support (workerd ignores the zstd dictionary option).

Co-Authored-By: BYK <burak.kaya@sentry.io>
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>
The getsentry/sentry#128818 spike found that no event or trace endpoint is
safe to cache as immutable. Issue and project event details embed
next/previous IDs and user reports, `latest` resolves to different events
over time, and full traces gain spans. The CLI's 24-hour tier still matches
all of them, including `/issues/<id>/events/latest/`.

Replace the immutable tier with a 5-minute detail tier limited to
organization event details and project trace-item details, and move issue
events, project events, and full traces to 60 seconds. Add the CLI tier
correction as its own task group ahead of the toolkit-core move.

R2 now persists only detail-tier entries that the server marks
`private, max-age`, for at most 5 minutes, and needs a security sign-off
before production. Use zstd level 3 for every tier instead of level 9 for R2.

Co-Authored-By: BYK <burak.kaya@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>
…ders

Neither product handles 429s well. The CLI retries every method, POSTs
included, and ignores rate-limit headers. The MCP retries only GET 502/503/504,
so the ~32.6K rate-limited GETs it sees a week fail immediately.

Add task 5.0 for a pure `toolkit-core/src/api-retry.ts` policy: retry only
GET/HEAD/OPTIONS unless a request opts in as idempotent, wait for
Retry-After or X-Sentry-Rate-Limit-Reset with a clock-skew guard and caps,
fall back to jittered backoff, and sleep abortably. The MCP adopts it first.
Task 5.1 now moves idempotent-only retry through it and calls the CLI's
all-methods retry a bug fixed separately; 6.1 drops the MCP's own retry once
it is on the transport so retries don't stack. Add a sentry-api-transport
spec delta for these rules.

Co-Authored-By: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
@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.

…uses

Sentry's RatelimitMiddleware rejects a 429 in process_request, before the view
runs, so replaying one cannot repeat a write. The sentry-api-transport spec
now retries 429s for any method, and retries 5xx, timeouts, and network errors
only for GET/HEAD/OPTIONS or an explicit idempotent opt-in.

The retried statuses are 429, 500, 502, 503, and 504. Other 5xx codes are
deterministic. The clock-skew wording now matches the Date-bounded delay from
#1673. The long requirements are split so `openspec validate --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>

This branch was successfully deployed

1 active deployment
Preview — b5523f5e Deployed Oct 27, 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.

0 participants