Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
880d252
docs(openspec): propose a shared Sentry API layer for the CLI and MCP
sentry-junior[bot] Oct 26, 2026
9ba30f4
docs(openspec): pick codecs from real-response benchmarks
sentry-junior[bot] Oct 26, 2026
c3ccf46
docs(openspec): narrow cache tiers to what the server spike allows
sentry-junior[bot] Oct 26, 2026
a81439e
docs(openspec): plan a shared retry policy that honors Sentry 429 hea…
sentry-junior[bot] Oct 26, 2026
b5523f5
docs(openspec): Retry 429s for any method and add 500 to retried stat…
sentry-junior[bot] Oct 27, 2026
a2a032a
docs(openspec): mark version alignment done (#1672)
sentry-junior[bot] Oct 27, 2026
317a80d
docs(openspec): Fold the detail tier into stable and allowlist R2 ins…
sentry-junior[bot] Oct 27, 2026
5f877f2
docs(openspec): Mark shared pagination done (#1683)
sentry-junior[bot] Oct 27, 2026
00e1737
docs(openspec): Mark the shared cache policy done (#1688)
sentry-junior[bot] Oct 27, 2026
66efad2
docs(openspec): Mark the cache body codec done (#1695)
sentry-junior[bot] Oct 28, 2026
b710f96
docs(openspec): Mark the CacheStore task in review (#1696)
sentry-junior[bot] Oct 28, 2026
60c2833
docs(openspec): Mark the CacheStore done (#1696) and the CLI store in…
sentry-junior[bot] Oct 28, 2026
5856128
docs(openspec): Mark the CLI store done (#1697) and revalidation in r…
sentry-junior[bot] Oct 28, 2026
b686aa1
docs(openspec): Mark revalidation done (#1698) and add the shared cac…
sentry-junior[bot] Oct 28, 2026
0abd1e3
docs(openspec): Mark the shared cache policy done (#1704)
sentry-junior[bot] Oct 28, 2026
157b4b5
docs(openspec): Mark task 5.0 done
sentry-junior[bot] Oct 29, 2026
bf76b29
docs(openspec): Mark issues and events on @sentry/api done (#1713)
sentry-junior[bot] Oct 29, 2026
0e2a129
docs(openspec): Mark the shared transport done (#1719)
sentry-junior[bot] Oct 29, 2026
d4bb22a
docs(openspec): Plan the shared API client in two steps
sentry-junior[bot] Oct 29, 2026
5213bd4
docs(openspec): Validate shared API responses through Standard Schema
sentry-junior[bot] Oct 29, 2026
420a5bc
docs(openspec): Keep MCP dependencies pointing down
sentry-junior[bot] Oct 29, 2026
e51ad52
docs(openspec): Land step 1 as stacked PRs by layer
sentry-junior[bot] Oct 29, 2026
5af8eb6
docs(openspec): Mark the api-client foundation done (#1732) and track…
sentry-junior[bot] Oct 29, 2026
1246430
docs(openspec): Make MCP tools a portable face of the CLI
sentry-junior[bot] Oct 29, 2026
a11def6
docs(openspec): Mark 8.3 done (#1736)
sentry-junior[bot] Oct 29, 2026
b4e1b28
docs(openspec): Keep @sentry/mcp-server as a stdio proxy
sentry-junior[bot] Oct 29, 2026
c0be25f
docs(openspec): Record @sentry/mcp-server as a slim bundle of sentry mcp
sentry-junior[bot] Oct 29, 2026
ad7e34e
docs(openspec): Keep the MCP registry entry on @sentry/mcp-server
sentry-junior[bot] Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions openspec/changes/unify-sentry-api-layer/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-26
662 changes: 662 additions & 0 deletions openspec/changes/unify-sentry-api-layer/design.md

Large diffs are not rendered by default.

141 changes: 141 additions & 0 deletions openspec/changes/unify-sentry-api-layer/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Proposal

## Why

The CLI and the MCP server talk to the same Sentry API through two separate
request layers. The CLI has resume-safe pagination, an identity-scoped
response cache, 401 refresh, timeouts, and validated redirects. The MCP has
its own transport in `mcp-core/src/api-client/client.ts` (about 6,000 lines),
uses `@sentry/api` for only three operations, and caches nothing. Fixes and
features land in one product and not the other, and the two pin different
`@sentry/api` versions (`0.337.0` and `^0.400.0`).

`createSentrySDK()` cannot be the shared layer:

- The CLI depends on `@sentry/mcp-server` for `sentry mcp`.
- The SDK needs SQLite and a filesystem, which Cloudflare Workers lack.
- SDK errors flatten HTTP status into exit codes.

The shared layer must therefore sit below both products, on `@sentry/api`.
Its end state is inside `@sentry/api` itself, so every consumer, including
the Sentry frontend in the browser, gets one Sentry API client.

The plan has two steps:

1. Consolidate every `SentryApiService`-like layer, the MCP's
`SentryApiService` and the CLI's API modules, into one shared client in
`@sentry/toolkit-core`, structured the way it will live in
`@sentry/api`.
2. Copy that client into getsentry/sentry-api-schema, publish a new
`@sentry/api`, switch toolkit's imports to it, and delete the
toolkit-core copies.

## What Changes

- Move the CLI's pagination into `@sentry/toolkit-core` and use it from both
products: limit-bounded walks, even page sizing, and cursors that resume
mid-page.
- **Behavior change (landed in #1681):** drop the CLI's 24-hour
"immutable" cache tier. It matched every `/events/<id>/` and
`/trace/<id>/` URL, but issue events, project events, and full traces
change after they are first read (getsentry/sentry#128818). Issue events,
project events, and full traces drop to 60 seconds; organization event
details and trace-item details fall to the default 5 minutes.
- Move the CLI's response-cache policy into `@sentry/toolkit-core`. That
covers TTL tiers, identity-scoped keys, invalidation after mutations, and
bypass. Storage becomes a pluggable backend with a byte budget and
compression.
- Add a shared retry policy to `@sentry/toolkit-core`: 429s are retried for
any method and wait for Sentry's rate-limit reset header within a cap, while
5xx responses and timeouts are retried only for idempotent requests. The MCP adopts it first, so rate-limited GETs retry instead of
failing.
- Move the CLI's request transport into `@sentry/toolkit-core`, with
credentials, storage, and telemetry injected by the host. It covers retries
through the shared policy, timeouts, redirect validation, and region
routing.
- The MCP's `SentryApiService` sends every request through the shared
transport and drops its own retry loop.
- **Step 1:** add a shared API client to `@sentry/toolkit-core`, in stacked
PRs by layer that also switch both products and delete the code they
replace. It
covers endpoint wrappers by domain on top of `@sentry/api` operations,
the error types (`ApiError`, rate limits, `retryAfterMs`), response
validation through the Standard Schema interface (the MCP keeps Zod and
the CLI keeps valibot; shared code depends on neither), pagination,
the cache policy, the retry policy, and the transport.
- **Portability rule:** the shared client imports only `@sentry/api` and
toolkit-core modules that move with it, and uses only web-standard APIs,
so it runs in Node, Workers, and browsers. No `node:*` imports, `Buffer`,
`fs`, `process`, or `AsyncLocalStorage`. Credentials, storage, the
fresh-read scope, and telemetry are interfaces each host provides. A lint
rule and a browser-target typecheck enforce it.
- **Step 2:** move the shared client into `@sentry/api`, publish it, and
switch toolkit over.
- The hosted MCP on Cloudflare uses a tiered cache:
- an in-memory LRU, limited by bytes, for every tier
- R2 for an allowlist of organization event details and project
trace-item details, only when the server marks them `private, max-age`,
for at most 5 minutes
- zstd level 3 compression on both
- MCP read tools accept a `fresh` parameter that bypasses the cache, matching
the CLI's `--fresh` flag. The request-scoped flag lives in the MCP host,
not in shared code.
- Shared endpoint wrappers use `@sentry/api` operations where they exist. The
endpoints `@sentry/api` lacks go through the same shared transport.
- Both products use one `@sentry/api` version from the pnpm catalog.

Out of scope, tracked separately:

- Server-side `Cache-Control` for event and trace-item detail responses
(getsentry/sentry#128818, prototype in getsentry/sentry#128828).
- A later phase, after step 2: CLI commands split into host-specific input
resolution, portable operations, and renderers. The MCP tools move into
the CLI as a portable `sentry/mcp` entry that `sentry mcp` serves over
stdio and the hosted MCP bundles on Workers. `sentry mcp` passes the
CLI's authenticated fetch so local MCP sessions reuse the CLI login and
on-disk cache, and the CLI's OAuth scopes are widened to cover MCP tools.
`@sentry/mcp-server` becomes a dependency-free bundle of `sentry mcp`
built from CLI source (#1731).

## Capabilities

### New Capabilities

- `sentry-api-pagination`: Limit-bounded, resume-safe pagination over Sentry
list endpoints, shared by the CLI and MCP.
- `sentry-api-response-cache`: Identity-scoped caching of Sentry API GET
responses. Covers TTL tiers, bypass, invalidation after mutations, and
bounded, compressed storage.
- `sentry-api-transport`: When a Sentry API request is retried, how long it
waits, and what a caller sees when it gives up.
- `sentry-api-client`: One portable Sentry API client, shared by the CLI and
MCP from `toolkit-core` and later from `@sentry/api`, with host-injected
credentials, storage, and telemetry.

### Modified Capabilities

None.

## Impact

- `packages/toolkit-core`: new `pagination`, `response-cache`, retry, and
transport modules, then the `api-client` module, with tests, a lint guard
against Node-only APIs, and a browser-target typecheck.
- `packages/cli/src/lib/api/infrastructure.ts`,
`packages/cli/src/lib/response-cache.ts`,
`packages/cli/src/lib/sentry-client.ts`: switch to the shared modules.
Apart from the narrower cache tiers, CLI behavior does not change.
- `packages/mcp-core/src/api-client/`: the transport is replaced, then the
endpoints, schemas, and errors move to the shared client. The MCP keeps
its Zod-to-tool output formatting.
- `packages/cli/src/lib/api/`: endpoint wrappers, API errors, and
pagination move to the shared client. The CLI keeps credential storage,
the cache store, region storage, telemetry, and exit-code mapping.
- `packages/mcp-core` read tools gain a `fresh` input, and generated tool
definitions change.
- `packages/mcp-cloudflare`: in-memory cache, an R2 bucket binding for
allowlisted detail responses, and a wrangler config change.
- `pnpm-workspace.yaml`: a catalog entry for `@sentry/api`, bumped in step 2
to the release that contains the shared client.
- getsentry/sentry-api-schema: receives the shared client in step 2.
- Snapshot and eval churn in `mcp-core` and `mcp-server-evals`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Spec Delta

## Purpose

Defines the shared Sentry API client: one implementation of endpoint
wrappers, errors, response validation, pagination, cache policy, retries,
and transport, used by the CLI and MCP from `@sentry/toolkit-core` first and
from `@sentry/api` once it moves there. It runs unchanged in Node,
Cloudflare Workers, and browsers.

## ADDED Requirements

### Requirement: One API client for every host

The CLI, the stdio MCP server, and the hosted MCP SHALL call the Sentry API
through the shared client. Neither product SHALL keep its own endpoint
wrappers, API error classes, retry loop, or pagination for an endpoint the
shared client covers.

#### Scenario: Same endpoint from both products

- **WHEN** the CLI and an MCP tool fetch the same resource
- **THEN** both call the same shared endpoint wrapper
- **THEN** both get the same error classes, and each gets the result type of the schema it passed

### Requirement: Library-agnostic validation

The shared client SHALL validate responses through the Standard Schema
interface with a schema the host supplies. It SHALL NOT depend on Zod,
valibot, or another validation library at runtime.

#### Scenario: MCP with Zod

- **WHEN** the MCP calls a shared wrapper with a Zod schema
- **THEN** the response is validated with it and the result has the schema's output type

#### Scenario: CLI with valibot

- **WHEN** the CLI calls the same wrapper with a valibot schema
- **THEN** the response is validated with it and Zod is not loaded

#### Scenario: Invalid response

- **WHEN** a response fails the host's schema
- **THEN** the shared client throws its validation error with the schema's issues

#### Scenario: Endpoint without a generated operation

- **WHEN** `@sentry/api` has no generated operation for an endpoint
- **THEN** the shared wrapper calls it through the shared transport and validates the response with the host's schema

### Requirement: Portable shared code

The shared client SHALL import only `@sentry/api` and other shared modules
that move with it, and SHALL use only web-standard APIs. It SHALL NOT import
`node:*` modules or use `Buffer`, `fs`, `process`, or `AsyncLocalStorage`. A
check in CI SHALL fail when shared code breaks this rule.

#### Scenario: Browser typecheck

- **WHEN** the shared modules are typechecked against the DOM library with no Node types
- **THEN** the typecheck passes

#### Scenario: Node-only API added

- **WHEN** a change imports a `node:*` module or uses a Node global in a shared module
- **THEN** lint fails on that change

### Requirement: Hosts inject their environment

The shared client SHALL take credentials, cache storage, a fresh-read scope,
logging, and telemetry as injected interfaces. Each host SHALL provide its
own implementation.

#### Scenario: CLI host

- **WHEN** the CLI creates the client
- **THEN** it passes its stored credentials with refresh, its on-disk cache store, and its telemetry spans

#### Scenario: MCP host

- **WHEN** the MCP creates the client for a tool call
- **THEN** it passes the session's access token, and its request-scoped fresh flag through `fresh()`

#### Scenario: Browser host

- **WHEN** a browser app creates the client with session credentials and no cache store
- **THEN** requests go through `fetch` with the browser's own HTTP caching

### Requirement: Errors keep their API meaning

Shared API errors SHALL carry the HTTP status, the server's detail, and, for
a 429, `retryAfterMs` from `Retry-After` or `X-Sentry-Rate-Limit-Reset`.
Hosts SHALL map them to their own presentation, such as CLI exit codes or
MCP tool messages, without losing those fields.

#### Scenario: Rate limited

- **WHEN** a request is still rate limited after the transport stops retrying
- **THEN** the shared client throws a rate-limit error with `retryAfterMs`
- **THEN** the CLI and MCP each report when to retry

### Requirement: Moves into @sentry/api unchanged

The shared client's file layout SHALL match its planned layout inside
`@sentry/api`, so step 2 copies the modules into
getsentry/sentry-api-schema without restructuring them. After a published
`@sentry/api` contains them, toolkit SHALL import them from `@sentry/api`
and delete its copies.

#### Scenario: Switch to the published package

- **WHEN** a `@sentry/api` release contains the shared client
- **THEN** the CLI and MCP import it from `@sentry/api`
- **THEN** the toolkit-core copies are deleted in the same change
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Spec Delta

## Purpose

Lets the CLI and MCP fetch a bounded number of rows from any Sentry list
endpoint and resume exactly where the previous call stopped.

## ADDED Requirements

### Requirement: Limit-bounded walks

A list operation SHALL return at most the requested number of rows. It SHALL
use the fewest requests that can hold them, with page sizes spread as evenly
as possible and no page larger than the API maximum of 100.

#### Scenario: Limit above one page

- **WHEN** a caller requests 201 rows from an endpoint with more than 201 rows
- **THEN** the walk makes three requests of 67 rows each and returns 201 rows

#### Scenario: Limit within one page

- **WHEN** a caller requests 25 rows
- **THEN** the walk makes one request with a page size of 25

### Requirement: Resume-safe cursors

A walk that stops before the end of the results SHALL return a cursor that
resumes at the first row the caller has not received, including when it
stopped inside a page.

#### Scenario: Stop inside a page

- **WHEN** a walk returns 30 rows of a 67-row page and the caller passes the returned cursor back
- **THEN** the next walk starts at row 31 of that page without repeating or skipping rows

#### Scenario: Cursor accepted by the API and older clients

- **WHEN** a caller sends a returned cursor directly to the Sentry API, or to an older CLI
- **THEN** the request succeeds and returns rows at or before the resume point, never after it

### Requirement: Page-number cursors keep their page size

Resuming a page-number cursor SHALL reuse the page size encoded in that
cursor, whatever the new limit.

#### Scenario: Resume with a different limit

- **WHEN** a page-number cursor produced with a page size of 50 is resumed with a limit of 10
- **THEN** the walk requests pages of 50 and returns the first 10 unseen rows

### Requirement: Bounded page count

A walk SHALL stop after a fixed maximum number of pages. It SHALL return
the rows collected so far, a resume cursor, and an indication that the page
cap stopped it.

#### Scenario: Page cap reached

- **WHEN** an endpoint keeps returning next pages beyond the page cap
- **THEN** the walk stops at the cap and returns a cursor that resumes after the last returned row

### Requirement: Complete walks fail loudly

A walk that must see every row, such as one whose result a mutation depends
on, SHALL fail rather than return partial results. That applies when a page
is invalid, a cursor repeats, or the page cap is reached.

#### Scenario: Repeated cursor

- **WHEN** an endpoint returns a next cursor it already returned during the same walk
- **THEN** the walk fails with an error naming the operation
Loading
Loading