Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
452 changes: 452 additions & 0 deletions openspec/changes/unify-sentry-api-layer/design.md

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions openspec/changes/unify-sentry-api-layer/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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`.

## 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:** narrow the CLI's 24-hour "immutable" cache tier. It
matches every `/events/<id>/` and `/trace/<id>/` URL, but issue events,
project events, and full traces change after they are first read
(getsentry/sentry#128818). Only organization event details and project
trace-item details stay in a separate tier, at 5 minutes. Issue events,
project events, and full traces drop to 60 seconds.
- 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.
- `runMcpServer()` accepts a host-supplied transport. `sentry mcp` passes the
CLI's authenticated transport, so local MCP sessions reuse the CLI login,
refresh, and on-disk cache.
- The hosted MCP on Cloudflare uses 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
- zstd level 3 compression on both
- MCP read tools accept a `fresh` parameter that bypasses the cache, matching
the CLI's `--fresh` flag.
- MCP API methods move onto `@sentry/api` operations domain by domain. The
endpoints `@sentry/api` lacks go through the same shared transport.
- Both products use one `@sentry/api` version from the pnpm catalog.
- **Deprecation:** the standalone `@sentry/mcp-server` device-code login is
deprecated in favor of `sentry mcp` and `sentry auth login`.

Out of scope, tracked separately:

- Server-side `Cache-Control` for event and trace-item detail responses
(getsentry/sentry#128818, prototype in getsentry/sentry#128828).
- Moving the shared infrastructure into `@sentry/api` once it has proven
itself in `toolkit-core`.

## 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.
- `mcp-host-transport`: How an MCP host supplies credentials and the Sentry
API transport, including `sentry mcp` reusing the CLI's login and cache.

### Modified Capabilities

None.

## Impact

- `packages/toolkit-core`: new `pagination`, `response-cache`, retry, and
transport modules, with tests.
- `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/client.ts`: the transport is replaced and
methods move to `@sentry/api`.
- `packages/mcp-core` read tools gain a `fresh` input, and generated tool
definitions change.
- `packages/mcp-server/src/index.ts`: `McpServerOptions` gains a transport
option, and the standalone auth flow is deprecated.
- `packages/cli/src/lib/mcp.ts`: passes the CLI transport.
- `packages/mcp-cloudflare`: in-memory cache, an R2 bucket binding for
detail responses, and a wrangler config change.
- `pnpm-workspace.yaml`: a catalog entry for `@sentry/api`.
- Snapshot and eval churn in `mcp-core` and `mcp-server-evals`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Spec Delta

## Purpose

Defines how a host process gives the MCP server its Sentry credentials and
API transport, so one login and one request stack serve both the CLI and
the local MCP server.

## ADDED Requirements

### Requirement: Host-supplied transport

The MCP server SHALL accept a Sentry API transport from its host. When one
is supplied, every Sentry API request made by MCP tools SHALL go through it.

#### Scenario: Transport supplied

- **WHEN** a host starts the MCP server with a transport
- **THEN** tool calls send their Sentry API requests through that transport

#### Scenario: No transport supplied

- **WHEN** the MCP server starts without a host transport
- **THEN** it uses the shared default transport with the configured access token

### Requirement: sentry mcp reuses the CLI session

`sentry mcp` SHALL start the MCP server with the CLI's stored login, token
refresh, and response cache. It SHALL not start a separate login flow.

#### Scenario: Logged-in CLI

- **WHEN** a user who has run `sentry auth login` starts `sentry mcp`
- **THEN** MCP tools authenticate with that login without prompting again

#### Scenario: Cache shared with CLI commands

- **WHEN** a CLI command and an MCP tool, for the same identity, request the same cacheable resource within its TTL
- **THEN** the second request is served from the shared on-disk cache

#### Scenario: Token rejected mid-session

- **WHEN** the API rejects the token during a `sentry mcp` session
- **THEN** the CLI refreshes the credential and later tool calls succeed without restarting the server

### Requirement: CLI login covers MCP tool scopes

The OAuth scopes granted by `sentry auth login` SHALL include every scope
that MCP tools require.

#### Scenario: Write tool after CLI login

- **WHEN** a user who logged in with `sentry auth login` calls an MCP tool that needs write scopes
- **THEN** the call is not rejected for missing scopes

### Requirement: Standalone login is deprecated

The standalone `@sentry/mcp-server` device-code login SHALL keep working and
SHALL tell users that `sentry mcp` is the supported local entry point.

#### Scenario: Standalone auth

- **WHEN** a user runs the standalone `@sentry/mcp-server` auth command
- **THEN** login succeeds and the output points the user to `sentry mcp`
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