From 880d25230f04994741b421306ae4bd9575251be4 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:16:57 +0000 Subject: [PATCH 01/28] docs(openspec): propose a shared Sentry API layer for the CLI and MCP 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 --- .../unify-sentry-api-layer/.openspec.yaml | 2 + .../changes/unify-sentry-api-layer/design.md | 278 ++++++++++++++++++ .../unify-sentry-api-layer/proposal.md | 89 ++++++ .../specs/mcp-host-transport/spec.md | 64 ++++ .../specs/sentry-api-pagination/spec.md | 72 +++++ .../specs/sentry-api-response-cache/spec.md | 129 ++++++++ .../changes/unify-sentry-api-layer/tasks.md | 63 ++++ 7 files changed, 697 insertions(+) create mode 100644 openspec/changes/unify-sentry-api-layer/.openspec.yaml create mode 100644 openspec/changes/unify-sentry-api-layer/design.md create mode 100644 openspec/changes/unify-sentry-api-layer/proposal.md create mode 100644 openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md create mode 100644 openspec/changes/unify-sentry-api-layer/specs/sentry-api-pagination/spec.md create mode 100644 openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md create mode 100644 openspec/changes/unify-sentry-api-layer/tasks.md diff --git a/openspec/changes/unify-sentry-api-layer/.openspec.yaml b/openspec/changes/unify-sentry-api-layer/.openspec.yaml new file mode 100644 index 000000000..d58ba0232 --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-26 diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md new file mode 100644 index 000000000..b9d188260 --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -0,0 +1,278 @@ +# Design + +## Context + +See proposal.md for motivation. Current state that shapes the approach: + +- **Dependency direction.** `packages/cli` depends on `@sentry/mcp-server` + (`workspace:*`), and its build compiles `mcp-core` and `mcp-server` first. + `cli/src/lib/mcp.ts` already passes credentials into `runMcpServer()` + through `resolveAccessToken` and `onUpstreamUnauthorized`. Shared code + must therefore live below both products, in `toolkit-core`. +- **CLI request stack.** + - `sentry-client.ts` handles auth, refresh on 401, timeouts, GET retries, + redirect validation, and custom headers. + - `response-cache.ts` caches responses as JSON files under + `~/.sentry/cache/responses/`, using `http-cache-semantics`, an HMAC + identity key, TTL tiers, and a 500-entry cap. + - `api/infrastructure.ts` has `paginate()` and `fetchAllPages()`, with + mid-page skip counts encoded in the cursor's `is_prev` field. +- **MCP request stack.** `mcp-core/src/api-client/client.ts` has about 105 + methods and hits 91 distinct endpoint paths. It owns its own + `request`/`requestOnce`, does GET-only gateway retries, validates with Zod, + and has no response cache. Against `@sentry/api@0.404.0`, about 67 of + those paths have generated operations; the rest are missing, mainly + legacy alert rules, uptime, monitor stats, issue activities/notes/user + reports, the search agent, and event committers. +- **Workers runtime.** + - 128 MB memory per isolate, and one isolate serves many users at once. + - No persistent filesystem. + - `nodejs_compat` is enabled: `AsyncLocalStorage`, `node:crypto`, and + `node:zlib` are available. + - `node:zlib` zstd works under the current compatibility date + (`2025-03-21`) in local workerd, verified with `wrangler dev` 4.151.3. + Cloudflare's docs list only gzip, deflate, and brotli, so this must be + confirmed on a deployed preview. + - `CompressionStream` has no zstd. +- **`@sentry/api` already has** `fetchPage_*`, `paginateAll_*`, and + `paginateUpTo_*` helpers, plus `./zod`, `./valibot`, and `./browser` + entry points. Its `paginateUpTo` drops `nextCursor` when trimming a page + would make continuing unsafe. The CLI's skip-count cursor closes exactly + that gap. + +## Goals / Non-Goals + +**Goals:** + +- One implementation of pagination, cache policy, and transport, used by + the CLI, `sentry mcp`, and the hosted MCP. +- Shared modules run unchanged in Node and Workers, and avoid + Node-only APIs where a web standard exists, so they can later move into + `@sentry/api` and run in the browser. +- No CLI behavior change while its internals move. + +**Non-Goals:** + +- Client-side caching in the Sentry frontend. Browser HTTP caching driven + by server headers covers that (getsentry/sentry#128818). +- Using `createSentrySDK()` from the MCP. +- Moving code into `@sentry/api` in this change. +- Replacing MCP Zod response validation with generated schemas everywhere. + That happens per domain where `@sentry/api/zod` covers the endpoint. + +## Decisions + +### Shared code lives in `toolkit-core`, behind injected dependencies + +New `toolkit-core` modules: + +- `pagination`: `paginate`, `fetchAllPages`, cursor encode/decode, and page + sizing. Pure logic. Errors come back as typed results, and the host maps + them to its own error classes. +- `response-cache`: the cache policy. Covers tier classification, key + derivation, invalidation prefixes (from `cli/src/lib/cache-keys.ts`), and + freshness through `http-cache-semantics`. Storage sits behind a + `CacheStore` interface. +- `api-transport`: a fetch wrapper that takes a `CredentialProvider` (get the + token, refresh on 401), an optional `CacheStore`, a logger, a span hook, + and retry/timeout options. Redirect validation and region routing move + with it. + +Product code keeps credential storage, error classes, user-facing +messages, and telemetry setup. + +_Alternative: a new package._ Rejected for now. `toolkit-core` is already +the bundled-private shared package, and a split can come later when moving +code into `@sentry/api`. + +### `CacheStore` is three byte-based operations + +```ts +interface CacheStore { + get(key: string): Promise; + set(key: string, entry: CacheEntry, ttlMs: number): Promise; + deleteMatching(identity: string, urlPrefix: string): Promise; +} +``` + +`CacheEntry` stores the compressed body bytes plus metadata: URL, identity, +tier, encoding, serialized cache policy, and size. Every backend enforces a +byte budget and a per-entry cap, not an entry count. + +Backends: + +- **Filesystem (CLI).** Keeps today's on-disk layout and adds compression. + It moves from a 500-entry cap to a byte budget, with the default set so + current users see a similar footprint. +- **Memory (hosted MCP, tests).** + - LRU limited by bytes, counted over compressed bytes. + - Default budget 24 MB per isolate and a 1 MB per-entry cap, configurable + by env. + - Module-scoped, so it lives as long as the isolate. `deleteMatching` + scans the in-memory index. +- **R2 (hosted MCP, immutable tier only).** + - Object key is `/`, with the expiry in + custom metadata and checked on read. + - A bucket lifecycle rule deletes objects after 1 day. + - Writes happen in `ctx.waitUntil()`, so they never delay a response. + - The immutable tier is never invalidated, so R2 never needs + `deleteMatching`. + +_Alternatives:_ + +- **Workers KV.** Rejected because it is eventually consistent, which + breaks invalidation after mutations. +- **D1.** A good fit for prefix deletes, but it has a single primary region, + is single-threaded, and limits rows to 2 MB. Deferred until hit-rate data + says stable-tier sharing across isolates is worth it. +- **Durable Objects per user.** Deferred for the same reason. + +### Composite store for hosted + +`TieredStore(memory, r2)`: + +- `get` checks memory first, then R2 for immutable keys, and copies R2 hits + into memory. +- `set` writes to memory, and also to R2 when the tier is immutable. +- `deleteMatching` only touches memory, since only volatile and stable + entries are ever invalidated. + +### Identity + +The identity string is an HMAC input. It is never a raw token, and it stays +stable across token refresh. + +- **CLI:** keeps `getIdentityFingerprint()`, which hashes the refresh token + for OAuth logins. +- **Hosted:** hashes the user ID and grant ID taken from the MCP wrapper + bearer token (`userId:grantId:secret`, parsed in + `mcp-cloudflare/src/server/lib/mcp-handler.ts`). +- **Standalone MCP with a static token:** hashes the token. + +Key derivation uses WebCrypto (`crypto.subtle`) HMAC-SHA256 instead of +`node:crypto`, so the module also runs in browsers. This makes key +derivation async. The cache API is already async, so no call sites change +shape. + +### Compression: zstd level 3, with fallbacks + +Measured on Node 22 against MCP mock fixtures: + +- **5.7 MB trace payload.** zstd level 3 compresses about 52x in about 5 ms + and decompresses in about 4 ms. Level 9 reaches 72x but takes about + 24 ms to compress. +- **Small payloads (median 1.2 KB).** zstd, brotli at quality 4, and gzip + all land between 2.9x and 3.2x. + +Decisions: + +- **Memory tier: zstd level 3.** It runs on the response path and is + charged as Worker CPU time. +- **R2 tier: zstd level 9.** Compression runs in `waitUntil()` and the entry + is read many times. +- **Small bodies: stored uncompressed** below a 1 KB threshold, where the + savings do not pay for the CPU. +- **Fallback: brotli (quality 4) via `node:zlib`, then uncompressed.** The + CLI supports Node.js 20+, which lacks zstd, so the codec is detected at + runtime the way `api/infrastructure.ts` already detects + `zstdCompress`. +- The `encoding` is recorded per entry, so mixed entries stay readable + after upgrades. + +Compression saves memory and storage. It does not save CPU: a cache hit +costs decompression instead of JSON from the network, and on Workers the +network wait is not billed CPU. Its job is to fit more entries into the +budget. + +_Future:_ a zstd dictionary trained on Sentry API responses would mostly +help small payloads. Deferred until there is real hit data. + +### `runMcpServer()` takes a transport + +`McpServerOptions` gains `fetch?: typeof fetch`. + +- `sentry mcp` passes the CLI's authenticated fetch, so local sessions get + CLI auth, refresh, retries, and the on-disk cache. +- `mcp-cloudflare` builds an `api-transport` around the OAuth token and the + tiered store. + +Inside `mcp-core`, `SentryApiService` sends all requests through the +injected fetch, and `@sentry/api` operations receive the same fetch through +their client options. + +### `fresh` is a shared read-tool input + +Read-only tools gain an optional `fresh: boolean`, defined once in the +shared tool schema helpers. The tool handler sets a request-scoped bypass +(`AsyncLocalStorage`), and the transport honors it. The tool count does not +change. Token cost is checked with `pnpm run measure-tokens`, and the +description stays short. + +### Move MCP methods onto `@sentry/api` by domain + +The order is: + +1. organizations, projects, teams (already partial) +2. issues and events +3. explore, traces, logs +4. releases and replays +5. monitors, alerts, and uptime + +Endpoints that `@sentry/api` lacks call the shared transport directly and +keep their Zod schemas. Each domain lands as its own PR. Endpoint behavior +is checked against `~/src/sentry`, as `AGENTS.md` requires. + +### Version alignment first + +Add `@sentry/api` to the pnpm catalog at `0.400.0`, which is already in +`minimumReleaseAgeExclude`, and point both packages at `catalog:`. Later +bumps move both products together. + +## Risks / Trade-offs + +- **Cross-user leakage on the shared Worker.** Every key includes an + identity HMAC. There is a test that two identities never share entries + for the same URL, and R2 keys are prefixed by identity. +- **Stale data in agent answers.** Volatile entries last 60 s. There is + invalidation after mutations, and `fresh` for explicit bypass. The tool + description tells agents when to use it. +- **Worker OOM.** The memory store has a byte budget and a per-entry cap. + Compressed size is what counts. A load test fills the store with many + identities and checks its peak memory. +- **zstd unavailable in production Workers.** Confirm on a preview deploy + before relying on it. The brotli fallback keeps caching working either + way. +- **Transport move regresses the CLI.** The CLI's existing tests run against + the shared modules unchanged. The move happens one module at a time, and + each step has no behavior change. +- **Generated `@sentry/api` types disagree with responses.** Keep Zod + validation at the MCP boundary. Pin and bump `@sentry/api` through the + catalog. +- **Snapshot and eval churn.** Each domain migration lands with its own + snapshot updates and an eval run. +- **CLI-issued tokens lack MCP scopes.** The MCP and CLI use different OAuth + client IDs and scope lists today. Compare them before deprecating the + standalone login. If they differ, widen the CLI's requested scopes. + +## Migration Plan + +1. Land the catalog alignment and the pure `toolkit-core` modules (pagination, + cache policy, codecs), with the CLI switched over and no behavior change. +2. Land `api-transport` and the CLI switch-over. +3. Land the MCP transport injection and `sentry mcp` passing the CLI fetch, + then the hosted memory store behind a flag (`MCP_RESPONSE_CACHE=memory`). +4. Enable the hosted memory cache. Add the R2 binding and the immutable tier + behind `MCP_RESPONSE_CACHE=tiered`. +5. Run the domain-by-domain `@sentry/api` migration PRs. +6. Deprecate the standalone MCP login. + +**Rollback:** the hosted cache is gated by an env flag, and setting it to +`off` restores today's uncached behavior. Each CLI step is a refactor that +can be reverted on its own. + +## Open Questions + +- The exact byte budgets (CLI disk, Worker memory) and the per-entry cap. + To be tuned with hit-rate and memory metrics after launch. +- Whether R2 reads beat API latency for immutable lookups in practice. + Metrics will decide whether the R2 tier stays. diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md new file mode 100644 index 000000000..0420a101b --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -0,0 +1,89 @@ +# 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. +- 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. +- Move the CLI's request transport into `@sentry/toolkit-core`, with + credentials, storage, and telemetry injected by the host. It covers retries, + 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 immutable responses (events, traces) + - zstd 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` and `ETag` for immutable API responses + (getsentry/sentry#128818). +- 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. +- `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`, 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. + 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 + immutable 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`. diff --git a/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md b/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md new file mode 100644 index 000000000..c59fca7d6 --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md @@ -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 immutable resource +- **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` diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-pagination/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-pagination/spec.md new file mode 100644 index 000000000..cbcb7e147 --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-pagination/spec.md @@ -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 diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md new file mode 100644 index 000000000..ff8b5495e --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md @@ -0,0 +1,129 @@ +# Spec Delta + +## Purpose + +Lets the CLI and MCP reuse Sentry API GET responses without one user or +server ever seeing another's data, and without holding more memory or +storage than the host allows. + +## ADDED Requirements + +### Requirement: Identity-scoped entries + +Every cache entry SHALL be keyed by the requesting identity, the request +method, and the full request URL including host and normalized query. The +identity SHALL stay stable across access-token rotation of the same login. + +#### Scenario: Different users, same URL + +- **WHEN** two identities request the same URL +- **THEN** neither receives the other's cached response + +#### Scenario: Different servers, same path + +- **WHEN** one identity requests the same path on two Sentry hosts +- **THEN** the responses are cached separately + +#### Scenario: Token refresh + +- **WHEN** an OAuth access token is refreshed for the same login +- **THEN** entries cached under the previous access token remain usable + +### Requirement: Server cache headers take precedence + +When a response carries HTTP caching headers, the cache SHALL follow them +(RFC 9111). Only responses without such headers SHALL use the TTL tiers. + +#### Scenario: Server sends no-store + +- **WHEN** a response carries `Cache-Control: no-store` +- **THEN** the response is not cached + +#### Scenario: Server sends a validator + +- **WHEN** a stale entry has an `ETag` and the server answers the revalidation with 304 +- **THEN** the cached body is served and its freshness is renewed + +### Requirement: Fallback TTL tiers + +Responses without caching headers SHALL use a TTL by URL class: + +- immutable: 24 hours (event, trace, and trace-item lookups by ID) +- volatile: 60 seconds (issue endpoints, log and span queries) +- no-cache: never cached (autofix and root-cause polling) +- stable: 5 minutes (everything else) + +#### Scenario: Event lookup + +- **WHEN** an event is fetched by ID without caching headers +- **THEN** the response is reused for up to 24 hours + +#### Scenario: Polling endpoint + +- **WHEN** autofix state is fetched +- **THEN** the response is never served from cache + +### Requirement: Mutations invalidate related entries + +A successful non-GET request SHALL invalidate the requesting identity's +cached entries whose URLs fall under the mutated resource's prefixes. Other +identities' entries SHALL be unaffected. + +#### Scenario: Resolve then list + +- **WHEN** an identity updates an issue and then lists issues within the volatile TTL +- **THEN** the list is fetched from the API, not from cache + +### Requirement: Fresh reads bypass the cache + +A caller SHALL be able to request a fresh read. A fresh read SHALL fetch +from the API and replace the cached entry. The CLI exposes this as `--fresh` +/ `-f`, and MCP read tools as a `fresh` input. + +#### Scenario: MCP fresh read + +- **WHEN** an MCP read tool is called with `fresh: true` +- **THEN** every API request made by that call bypasses cached entries + +### Requirement: Bounded storage + +Each storage backend SHALL enforce a byte budget and a per-entry size cap. +Entries over the cap SHALL not be stored. When the budget is exceeded, the +least recently used entries SHALL be evicted first. + +#### Scenario: Oversized response + +- **WHEN** a response is larger than the per-entry cap +- **THEN** it is returned to the caller and not stored + +#### Scenario: Shared Worker isolate + +- **WHEN** many identities fill the in-memory cache of one Cloudflare Worker isolate +- **THEN** total cache memory stays within the configured budget and the isolate does not run out of memory + +### Requirement: Compressed entries + +Stored entries SHALL be compressed with zstd when the runtime supports it, +and otherwise stored with a supported fallback. Reading an entry SHALL +return a body identical to the original response body. + +#### Scenario: Runtime without zstd + +- **WHEN** the CLI runs on a Node.js version without zstd support +- **THEN** caching still works using the fallback encoding + +### Requirement: Hosted tiers + +The hosted MCP SHALL cache every tier in memory, and SHALL also persist +immutable-tier entries to durable object storage shared across isolates. +Volatile and stable entries SHALL never be persisted outside the isolate. + +#### Scenario: Immutable hit in another isolate + +- **WHEN** an identity fetches an event in one isolate and later fetches it in another +- **THEN** the second fetch is served from durable storage without calling the API + +#### Scenario: Volatile entry + +- **WHEN** an issue list is cached on the hosted MCP +- **THEN** it exists only in that isolate's memory diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md new file mode 100644 index 000000000..9621dd9fb --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -0,0 +1,63 @@ +# Tasks + +## 1. Version alignment + +- [ ] 1.1 Add `@sentry/api: 0.400.0` to the pnpm catalog and point `packages/cli` and `packages/mcp-core` at `catalog:`. Verify `pnpm install --frozen-lockfile` succeeds and the lockfile has one `@sentry/api` version. +- [ ] 1.2 Fix any `mcp-core` breakage from the `0.337.0` → `0.400.0` bump. Verify `pnpm --filter @sentry/mcp-core run tsc` and `pnpm --filter @sentry/mcp-core run test` pass. + +## 2. Shared pagination + +- [ ] 2.1 Move `paginate`, `fetchAllPages`, page sizing, and cursor encode/decode from `cli/src/lib/api/infrastructure.ts` into `toolkit-core/src/pagination.ts`. Failures are returned as typed results, and the host supplies the logger. Verify with toolkit-core unit tests that cover each `sentry-api-pagination` scenario. +- [ ] 2.2 Switch the CLI to the shared module, mapping errors to `ApiError` and keeping the `sentry-cli/no-manual-pagination` lint rule. Verify the CLI unit tests pass with no snapshot changes. +- [ ] 2.3 Document the module in `packages/toolkit-core/README.md`, and verify `pnpm run docs:check` passes. + +## 3. Shared cache policy and codecs + +- [ ] 3.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. +- [ ] 3.2 Add a codec module (zstd level N, brotli fallback, raw below 1 KB) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. +- [ ] 3.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. +- [ ] 3.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. +- [ ] 3.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. + +## 4. Shared transport + +- [ ] 4.1 Move auth headers, 401 refresh, timeouts, GET retry/backoff, redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. Verify with transport unit tests. +- [ ] 4.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. +- [ ] 4.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. + +## 5. MCP transport injection + +- [ ] 5.1 Add `fetch` to `McpServerOptions`, and route all `SentryApiService` requests and `@sentry/api` calls through it. Verify an `mcp-server` test where a supplied fetch receives every tool request. +- [ ] 5.2 Pass the CLI's authenticated fetch from `cli/src/lib/mcp.ts`. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, and `/mcp-qa` stdio QA passes. +- [ ] 5.3 Add the `fresh` input to read tools through the shared schema helpers. Regenerate definitions, then verify `pnpm run measure-tokens` and the tool tests pass. + +## 6. Hosted cache + +- [ ] 6.1 Wire the memory store into `mcp-cloudflare` behind `MCP_RESPONSE_CACHE` (`off`/`memory`/`tiered`), with identity taken from the wrapper token's user and grant IDs. Verify the Workers tests pass, and a two-identity isolation test. +- [ ] 6.2 Confirm `node:zlib` zstd on a deployed preview Worker. If it is unavailable, keep brotli as the hosted codec and record that in `docs/cloudflare/architecture.md`. +- [ ] 6.3 Add the R2 binding, the immutable-tier store, and `TieredStore`, with writes in `waitUntil` and a 1-day lifecycle rule. Verify tests for cross-isolate immutable hits and memory-only volatile entries. +- [ ] 6.4 Add cache hit, miss, and eviction metrics and memory-budget logging. Document them in `docs/operations/monitoring.md`, and verify they appear in a preview deploy. + +## 7. Move MCP endpoints onto `@sentry/api` + +- [ ] 7.1 Organizations, projects, and teams. Verify the MCP tests and snapshots for the affected tools. +- [ ] 7.2 Issues and events. Verify the MCP tests and snapshots for the affected tools. +- [ ] 7.3 Explore, traces, and logs. Verify the MCP tests, snapshots, and a search-events eval run. +- [ ] 7.4 Releases and replays. Verify the MCP tests and snapshots for the affected tools. +- [ ] 7.5 Monitors, alerts, and uptime, keeping the shared-transport fallback for the endpoints `@sentry/api` lacks. Verify the MCP tests and snapshots. +- [ ] 7.6 Update `docs/contributing/api-patterns.md` for the new client layering, and verify `pnpm run docs:check` passes. + +## 8. Standalone login deprecation + +- [ ] 8.1 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed. Verify a test that the CLI's scope list covers every MCP tool's required scopes. +- [ ] 8.2 Add a deprecation notice to the standalone `@sentry/mcp-server` auth flow and docs pointing to `sentry mcp`. Verify with an output test and an update to `docs/operations/stdio-auth.md`. + +## 9. Integration + +- [ ] 9.1 Run `pnpm run tsc && pnpm run lint && pnpm run test` and the MCP eval suite on the final state, and record the results in the PR. + +## Workflow follow-up + +- Archive the change after review once all tasks are complete. +- Follow up on getsentry/sentry#128818 and revisit the TTL tiers when server cache headers ship. +- Propose moving pagination, then the transport, into `@sentry/api`. From 9ba30f434db6cb7bbb9b959d62489aeaae8d8b81 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:27:46 +0000 Subject: [PATCH 02/28] docs(openspec): pick codecs from real-response benchmarks 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 --- .../changes/unify-sentry-api-layer/design.md | 110 +++++++++++++----- .../specs/sentry-api-response-cache/spec.md | 19 ++- .../changes/unify-sentry-api-layer/tasks.md | 12 +- 3 files changed, 102 insertions(+), 39 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index b9d188260..babbccf2f 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -106,8 +106,8 @@ Backends: current users see a similar footprint. - **Memory (hosted MCP, tests).** - LRU limited by bytes, counted over compressed bytes. - - Default budget 24 MB per isolate and a 1 MB per-entry cap, configurable - by env. + - Default budget 24 MB per isolate and a 1 MB per-entry cap on compressed + size, configurable by env. - Module-scoped, so it lives as long as the isolate. `deleteMatching` scans the in-memory index. - **R2 (hosted MCP, immutable tier only).** @@ -154,38 +154,88 @@ Key derivation uses WebCrypto (`crypto.subtle`) HMAC-SHA256 instead of derivation async. The cache API is already async, so no call sites change shape. -### Compression: zstd level 3, with fallbacks - -Measured on Node 22 against MCP mock fixtures: - -- **5.7 MB trace payload.** zstd level 3 compresses about 52x in about 5 ms - and decompresses in about 4 ms. Level 9 reaches 72x but takes about - 24 ms to compress. -- **Small payloads (median 1.2 KB).** zstd, brotli at quality 4, and gzip - all land between 2.9x and 3.2x. +### Compression: plain zstd now, trained dictionary as a follow-up + +Benchmarked on Node 22 with real API responses from Sentry's own +organization. The samples were kept in the sandbox and not committed: + +- 255 responses from one project (events, issue details, issue lists, + traces, logs, spans, projects, teams), split 70/30 into training and test + sets. +- 90 responses from five other projects (Android, Node, Next.js, Rust, + Python) as a cross-project check. + +Ratios are total raw bytes divided by total compressed bytes, with each +response compressed on its own, the way the cache stores entries: + +| Codec | Same project, ≤1 MB | Other projects | >256 KB (traces) | Compress, ms/MB | Decompress, ms/MB | +| ---------------------------------- | ------------------- | -------------- | ---------------- | --------------- | ----------------- | +| zstd-3 | 8.7x | 6.3x | 37x | 1.6 | 1.2 | +| zstd-9 | — | — | 45x | 5.6 | 0.9 | +| zstd-3 + trained 110 KB dictionary | 17.0x | 7.6x | 37x | 2.0 | 0.8 | +| brotli-4 | 9.3x | 6.6x | 42x | 3.0 | 0.6 | +| brotli-6 | — | — | 45x | 5.7 | 0.8 | +| brotli-11 | — | — | 49x | 775 | 2.3 | +| gzip-6 | — | — | 30x | 3.9 | 0.8 | + +Findings: + +- **Brotli is not the better target.** + - At the same CPU, zstd-9 and brotli-6 tie. + - brotli-4 beats zstd-3 by 5–12% but costs about 2x the compression CPU. + - Brotli's best ratio, quality 11, costs about 500x the CPU of zstd-3. + - `node:zlib` brotli has no custom dictionary support. +- **A trained dictionary is the big lever for small and medium responses, + which are most entries by count.** On the same project it doubled the + overall ratio (8.7x → 17.0x), and below 16 KB it went from 3.4x to 9.8x. + On other projects the gain shrank to about 20% (6.3x → 7.6x). The + dictionary had learned project-specific values (paths, function names, + slugs), not just JSON keys. It did nothing for large traces. +- **Dictionaries built from public data did not help.** One trained on the + repo's mock fixtures, and one trained on response skeletons with all values + blanked, stayed at about 3.4–3.8x for small responses. The gain comes from + real values. +- **Workers ignores `dictionary`.** Local workerd (`wrangler dev` 4.151.3) + accepts the `dictionary` option on `zstdCompressSync` but silently ignores + it: output is byte-for-byte the same size as without a dictionary. Node 22 + honors it (5.3 KB issue: 1,819 → 632 bytes). +- **The per-entry cap must apply to compressed size.** Traces in this + sample had a median of 1.5 MB raw (max 14 MB), but compress 37–45x. A cap on + raw size would make most traces impossible to cache. Decisions: -- **Memory tier: zstd level 3.** It runs on the response path and is - charged as Worker CPU time. -- **R2 tier: zstd level 9.** Compression runs in `waitUntil()` and the entry +- **Memory tier: zstd level 3, no dictionary.** It runs on the response path + and is charged as Worker CPU time. +- **R2 tier: zstd level 9.** Compression runs in `waitUntil()`, and the entry is read many times. -- **Small bodies: stored uncompressed** below a 1 KB threshold, where the - savings do not pay for the CPU. -- **Fallback: brotli (quality 4) via `node:zlib`, then uncompressed.** The - CLI supports Node.js 20+, which lacks zstd, so the codec is detected at - runtime the way `api/infrastructure.ts` already detects - `zstdCompress`. -- The `encoding` is recorded per entry, so mixed entries stay readable - after upgrades. - -Compression saves memory and storage. It does not save CPU: a cache hit -costs decompression instead of JSON from the network, and on Workers the -network wait is not billed CPU. Its job is to fit more entries into the -budget. - -_Future:_ a zstd dictionary trained on Sentry API responses would mostly -help small payloads. Deferred until there is real hit data. +- **Fallback: brotli quality 4 via `node:zlib`, then uncompressed.** The CLI + supports Node.js 20+, which lacks zstd, so the codec is detected at + runtime, the way `api/infrastructure.ts` already detects `zstdCompress`. +- **Small bodies:** stored uncompressed below 256 bytes. +- **Per-entry cap on compressed size**, plus a raw-size ceiling (default + 32 MB) that bounds memory spikes during decompression. +- **Encoding recorded per entry.** `encoding` and `dictId` are recorded per + entry, so mixed entries stay readable after upgrades and dictionary + rotations. +- **Dictionary support is a follow-up (task group 9), gated on three + checks:** + 1. **Training data safety.** A dictionary embeds verbatim strings from its + training data, so one trained on customer or internal data must never + ship in a public package (`sentry`, `@sentry/mcp-server`). Options: + - train on a public demo organization + - train per deployment, for example the hosted MCP from its own cache + contents within an organization + 2. **Workers support.** Use a WASM zstd build for dictionary compression, + or wait for workerd to honor the option. A startup self-test compresses + a known sample and checks that the dictionary took effect, so a silently + ignored option is caught. + 3. **A broader benchmark** across many organizations and projects before + committing to dictionary size and rotation. + +Compression saves memory and storage, not CPU. A cache hit costs +decompression instead of JSON from the network, and on Workers the network +wait is not billed CPU. Its job is to fit more entries into the budget. ### `runMcpServer()` takes a transport diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md index ff8b5495e..f67f9f381 100644 --- a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md @@ -87,15 +87,21 @@ from the API and replace the cached entry. The CLI exposes this as `--fresh` ### Requirement: Bounded storage -Each storage backend SHALL enforce a byte budget and a per-entry size cap. -Entries over the cap SHALL not be stored. When the budget is exceeded, the -least recently used entries SHALL be evicted first. +Each storage backend SHALL enforce a byte budget over stored bytes. It SHALL +enforce a per-entry cap on stored (compressed) size and a ceiling on raw +size. Entries over either limit SHALL not be stored. When the budget is +exceeded, the least recently used entries SHALL be evicted first. #### Scenario: Oversized response -- **WHEN** a response is larger than the per-entry cap +- **WHEN** a response's compressed size is larger than the per-entry cap - **THEN** it is returned to the caller and not stored +#### Scenario: Large but compressible response + +- **WHEN** a 5 MB trace compresses below the per-entry cap and its raw size is under the ceiling +- **THEN** it is stored + #### Scenario: Shared Worker isolate - **WHEN** many identities fill the in-memory cache of one Cloudflare Worker isolate @@ -104,8 +110,9 @@ least recently used entries SHALL be evicted first. ### Requirement: Compressed entries Stored entries SHALL be compressed with zstd when the runtime supports it, -and otherwise stored with a supported fallback. Reading an entry SHALL -return a body identical to the original response body. +and otherwise stored with a supported fallback. Each entry SHALL record its +encoding. Reading an entry SHALL return a body identical to the original +response body. #### Scenario: Runtime without zstd diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 9621dd9fb..2007562b4 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -14,7 +14,7 @@ ## 3. Shared cache policy and codecs - [ ] 3.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. -- [ ] 3.2 Add a codec module (zstd level N, brotli fallback, raw below 1 KB) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. +- [ ] 3.2 Add a codec module (zstd level 3 or 9 by tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. - [ ] 3.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. - [ ] 3.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. - [ ] 3.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. @@ -52,9 +52,15 @@ - [ ] 8.1 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed. Verify a test that the CLI's scope list covers every MCP tool's required scopes. - [ ] 8.2 Add a deprecation notice to the standalone `@sentry/mcp-server` auth flow and docs pointing to `sentry mcp`. Verify with an output test and an update to `docs/operations/stdio-auth.md`. -## 9. Integration +## 9. Trained compression dictionary (follow-up) -- [ ] 9.1 Run `pnpm run tsc && pnpm run lint && pnpm run test` and the MCP eval suite on the final state, and record the results in the PR. +- [ ] 9.1 Build a training corpus that is safe to ship: a public demo organization, or a per-deployment corpus that never leaves the hosted MCP. Verify that no customer or internal strings appear in a shipped dictionary (`strings` audit in CI). +- [ ] 9.2 Add dictionary compression with a versioned `dictId` and a startup self-test that detects a silently ignored `dictionary` option (as in workerd today). Verify a test where an ignored dictionary falls back to plain zstd. +- [ ] 9.3 On Workers, evaluate WASM zstd for dictionary compression: bundle size, CPU in ms/MB, and ratio against native zstd-3. Record the results in design.md and adopt it only if the memory-tier gain outweighs the CPU cost. + +## 10. Integration + +- [ ] 10.1 Run `pnpm run tsc && pnpm run lint && pnpm run test` and the MCP eval suite on the final state, and record the results in the PR. ## Workflow follow-up From c3ccf46b59e05d4429eee782f5178a76130bf94e Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:13:41 +0000 Subject: [PATCH 03/28] docs(openspec): narrow cache tiers to what the server spike allows 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//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 --- .../changes/unify-sentry-api-layer/design.md | 139 ++++++++++++++---- .../unify-sentry-api-layer/proposal.md | 19 ++- .../specs/mcp-host-transport/spec.md | 2 +- .../specs/sentry-api-response-cache/spec.md | 64 ++++++-- .../changes/unify-sentry-api-layer/tasks.md | 86 ++++++----- 5 files changed, 224 insertions(+), 86 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index babbccf2f..be9e4b78a 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -14,7 +14,10 @@ See proposal.md for motivation. Current state that shapes the approach: redirect validation, and custom headers. - `response-cache.ts` caches responses as JSON files under `~/.sentry/cache/responses/`, using `http-cache-semantics`, an HMAC - identity key, TTL tiers, and a 500-entry cap. + identity key, TTL tiers, and a 500-entry cap. Its 24-hour "immutable" + tier matches any `/events//`, `/trace/<32 hex>/`, or + `/trace-items//` URL, and is checked before the `/issues/` volatile + pattern, so `/issues//events/latest/` is cached for 24 hours too. - `api/infrastructure.ts` has `paginate()` and `fetchAllPages()`, with mid-page skip counts encoded in the cursor's `is_prev` field. - **MCP request stack.** `mcp-core/src/api-client/client.ts` has about 105 @@ -49,7 +52,8 @@ See proposal.md for motivation. Current state that shapes the approach: - Shared modules run unchanged in Node and Workers, and avoid Node-only APIs where a web standard exists, so they can later move into `@sentry/api` and run in the browser. -- No CLI behavior change while its internals move. +- No CLI behavior change while its internals move, apart from the cache + tier correction, which lands first as its own PR. **Non-Goals:** @@ -62,6 +66,61 @@ See proposal.md for motivation. Current state that shapes the approach: ## Decisions +### Narrow the CLI's cache tiers before moving them + +The getsentry/sentry#128818 spike (findings in +[this comment](https://github.com/getsentry/sentry/issues/128818#issuecomment-6426889162), +checked against sentry `06a3bc41dda`) found that none of the "immutable" +endpoints are safe to mark `immutable`: + +- **Project and issue event details** embed `nextEventID`, + `previousEventID`, user reports, and release data, all of which change + later. Reprocessing and issue merges can change the response for the same + event ID. `latest`, `oldest`, and `recommended` resolve to different events + over time. +- **Full traces** gain spans as they arrive. +- **Organization event details** (excluding `?averageColumn`, which reads + live aggregates) and **project trace-item details** (excluding + superuser/staff requests and `?debug`) are stable enough for + `Cache-Control: private, max-age=300`. The prototype in + getsentry/sentry#128828 sends exactly that, off by default, with + `Vary: Origin, Cookie, Authorization`. +- Every endpoint can still change visibility: deletion and access + revocation are not reflected until the entry expires. That bounds the TTL, + not just the tier. + +So the shared policy replaces `immutable` with `detail`: + +| Tier | TTL | Matches | +| -------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| no-cache | 0 | autofix and root-cause polling | +| detail | 5 m | `/organizations/{org}/events/{project}:{event_id}/` without `averageColumn`; `/projects/{org}/{project}/trace-items/{id}/` without `debug` | +| volatile | 60 s | `/issues/` (including issue events), `/projects/{org}/{project}/events/{id}/`, `/organizations/{org}/trace/{id}/`, log and span queries | +| stable | 5 m | everything else | + +- Patterns are exact path shapes, anchored on the API prefix, not + substring matches on `/events/`. Anything that does not match an + allowlisted shape falls through to volatile or stable. +- `detail` has the same TTL as `stable` in the CLI. It exists as its own + tier because it is the only tier eligible for R2 on the hosted MCP. +- Today the CLI reads events through project and issue endpoints + (`getEvent`, `getLatestEvent` in `cli/src/lib/api/events.ts`), and the MCP + through `/organizations/{org}/issues/{id}/events/{event_id}/`. So in + practice the detail tier covers trace items until a caller moves to the + organization event endpoint. That is worth doing per call site only where + the response has every field the caller uses. +- When server headers are present they still win. The CLI keeps honoring + `max-age`, so if the server later extends it, the CLI follows without a + release. +- This lands as its own CLI PR before the move into `toolkit-core`, so the + move itself stays a no-behavior-change refactor. The existing + `response-cache.property.test.ts` cases that assert 24-hour event and + trace caching are replaced with scenarios from the spec delta. + +The cost is more API calls for repeated event and trace reads. A 24-hour +window was a correctness bug, not a tunable: an agent re-reading an issue's +latest event an hour later got the wrong event. + ### Shared code lives in `toolkit-core`, behind injected dependencies New `toolkit-core` modules: @@ -110,13 +169,20 @@ Backends: size, configurable by env. - Module-scoped, so it lives as long as the isolate. `deleteMatching` scans the in-memory index. -- **R2 (hosted MCP, immutable tier only).** - - Object key is `/`, with the expiry in - custom metadata and checked on read. - - A bucket lifecycle rule deletes objects after 1 day. +- **R2 (hosted MCP, detail tier only).** + - Persists an entry only when it is in the detail tier _and_ the response + carries `Cache-Control: private` with a positive `max-age`. The server + header is the opt-in: until getsentry/sentry#128828 ships and is + enabled, R2 stores nothing. + - Object key is `/`, with the expiry, + the lesser of the server `max-age` and 5 minutes, in custom metadata and + checked on read. + - A bucket lifecycle rule deletes objects after 1 day, the shortest rule + R2 supports. It is garbage collection only; reads enforce the expiry. - Writes happen in `ctx.waitUntil()`, so they never delay a response. - - The immutable tier is never invalidated, so R2 never needs - `deleteMatching`. + - Detail entries are never targeted by mutation invalidation, so R2 never + needs `deleteMatching`. The short expiry bounds how long a revoked or + deleted item can still be served. _Alternatives:_ @@ -131,9 +197,10 @@ _Alternatives:_ `TieredStore(memory, r2)`: -- `get` checks memory first, then R2 for immutable keys, and copies R2 hits - into memory. -- `set` writes to memory, and also to R2 when the tier is immutable. +- `get` checks memory first, then R2 for detail keys, and copies R2 hits + into memory with the remaining expiry. +- `set` writes to memory, and also to R2 when the entry is R2-eligible (see + above). - `deleteMatching` only touches memory, since only volatile and stable entries are ever invalidated. @@ -205,10 +272,14 @@ Findings: Decisions: -- **Memory tier: zstd level 3, no dictionary.** It runs on the response path - and is charged as Worker CPU time. -- **R2 tier: zstd level 9.** Compression runs in `waitUntil()`, and the entry - is read many times. +- **zstd level 3 everywhere, no dictionary.** One codec setting for the + CLI disk, the hosted memory tier, and R2. + - It runs on the response path and is charged as Worker CPU time. + - zstd-9 only helps large traces (37x → 45x) at 3.5x the compression + CPU. With R2 entries now living at most 5 minutes, an entry is read + only a few times, so the extra CPU does not pay back. + - A single level keeps entries portable between tiers: an R2 hit is + copied into memory without recompressing. - **Fallback: brotli quality 4 via `node:zlib`, then uncompressed.** The CLI supports Node.js 20+, which lacks zstd, so the codec is detected at runtime, the way `api/infrastructure.ts` already detects `zstdCompress`. @@ -218,7 +289,7 @@ Decisions: - **Encoding recorded per entry.** `encoding` and `dictId` are recorded per entry, so mixed entries stay readable after upgrades and dictionary rotations. -- **Dictionary support is a follow-up (task group 9), gated on three +- **Dictionary support is a follow-up (task group 10), gated on three checks:** 1. **Training data safety.** A dictionary embeds verbatim strings from its training data, so one trained on customer or internal data must never @@ -283,9 +354,16 @@ bumps move both products together. - **Cross-user leakage on the shared Worker.** Every key includes an identity HMAC. There is a test that two identities never share entries for the same URL, and R2 keys are prefixed by identity. -- **Stale data in agent answers.** Volatile entries last 60 s. There is +- **Stale data in agent answers.** Volatile entries last 60 s, and no entry + lasts longer than 5 minutes without a server header. There is invalidation after mutations, and `fresh` for explicit bypass. The tool description tells agents when to use it. +- **Revoked access or deleted data served from cache.** A user who loses + access to a project, or whose event is deleted, can still get a cached + copy until it expires: up to 5 minutes for detail entries, including + from R2 on the hosted MCP. Enabling R2 in production needs a privacy and + security sign-off on that window, alongside the server-side headers in + getsentry/sentry#128828. - **Worker OOM.** The memory store has a byte budget and a per-entry cap. Compressed size is what counts. A load test fills the store with many identities and checks its peak memory. @@ -306,15 +384,18 @@ bumps move both products together. ## Migration Plan -1. Land the catalog alignment and the pure `toolkit-core` modules (pagination, - cache policy, codecs), with the CLI switched over and no behavior change. -2. Land `api-transport` and the CLI switch-over. -3. Land the MCP transport injection and `sentry mcp` passing the CLI fetch, +1. Land the catalog alignment (#1672) and the CLI cache tier correction, + each as its own PR. +2. Land the pure `toolkit-core` modules (pagination, cache policy, codecs), + with the CLI switched over and no behavior change. +3. Land `api-transport` and the CLI switch-over. +4. Land the MCP transport injection and `sentry mcp` passing the CLI fetch, then the hosted memory store behind a flag (`MCP_RESPONSE_CACHE=memory`). -4. Enable the hosted memory cache. Add the R2 binding and the immutable tier - behind `MCP_RESPONSE_CACHE=tiered`. -5. Run the domain-by-domain `@sentry/api` migration PRs. -6. Deprecate the standalone MCP login. +5. Enable the hosted memory cache. Add the R2 binding and the detail tier + behind `MCP_RESPONSE_CACHE=tiered`, and enable it only after the server + headers ship and the security sign-off. +6. Run the domain-by-domain `@sentry/api` migration PRs. +7. Deprecate the standalone MCP login. **Rollback:** the hosted cache is gated by an env flag, and setting it to `off` restores today's uncached behavior. Each CLI step is a refactor that @@ -324,5 +405,9 @@ can be reverted on its own. - The exact byte budgets (CLI disk, Worker memory) and the per-entry cap. To be tuned with hit-rate and memory metrics after launch. -- Whether R2 reads beat API latency for immutable lookups in practice. - Metrics will decide whether the R2 tier stays. +- Whether R2 pays off with a 5-minute window. Cross-isolate hits only help + when the same identity re-reads the same event or span within minutes, + which agents often do, but metrics will decide whether the R2 tier stays. +- Whether the server should send `private, max-age` on more endpoints + later, such as stable organization and project metadata. The CLI follows + server headers automatically; R2 eligibility stays an explicit allowlist. diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index 0420a101b..c56632dcb 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -23,6 +23,12 @@ The shared layer must therefore sit below both products, on `@sentry/api`. - 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//` and `/trace//` 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 @@ -35,8 +41,9 @@ The shared layer must therefore sit below both products, on `@sentry/api`. 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 immutable responses (events, traces) - - zstd compression on both + - 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 @@ -47,8 +54,8 @@ The shared layer must therefore sit below both products, on `@sentry/api`. Out of scope, tracked separately: -- Server-side `Cache-Control` and `ETag` for immutable API responses - (getsentry/sentry#128818). +- 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`. @@ -75,7 +82,7 @@ None. - `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. - CLI behavior does not change. + 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 @@ -84,6 +91,6 @@ None. 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 - immutable responses, and a wrangler config change. + 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`. diff --git a/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md b/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md index c59fca7d6..16ad37513 100644 --- a/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md +++ b/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md @@ -35,7 +35,7 @@ refresh, and response cache. It SHALL not start a separate login flow. #### Scenario: Cache shared with CLI commands -- **WHEN** a CLI command and an MCP tool, for the same identity, request the same immutable resource +- **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 diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md index f67f9f381..a2331f7c4 100644 --- a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md @@ -48,21 +48,53 @@ When a response carries HTTP caching headers, the cache SHALL follow them Responses without caching headers SHALL use a TTL by URL class: -- immutable: 24 hours (event, trace, and trace-item lookups by ID) -- volatile: 60 seconds (issue endpoints, log and span queries) +- detail: 5 minutes (organization event details and project trace-item + details, by exact path) +- volatile: 60 seconds (issue endpoints, project event details, full + traces, log and span queries) - no-cache: never cached (autofix and root-cause polling) - stable: 5 minutes (everything else) -#### Scenario: Event lookup +#### Scenario: Organization event lookup -- **WHEN** an event is fetched by ID without caching headers -- **THEN** the response is reused for up to 24 hours +- **WHEN** `/organizations/{org}/events/{project}:{event_id}/` is fetched without caching headers +- **THEN** the response is reused for up to 5 minutes + +#### Scenario: Live aggregate on an event + +- **WHEN** an organization event detail is fetched with an `averageColumn` query +- **THEN** it is classified as stable, not detail + +#### Scenario: Trace-item debug lookup + +- **WHEN** a project trace-item detail is fetched with a `debug` query +- **THEN** it is classified as stable, not detail + +#### Scenario: Issue event lookup + +- **WHEN** an issue's latest, oldest, recommended, or specific event is fetched without caching headers +- **THEN** the response is reused for at most 60 seconds + +#### Scenario: Full trace lookup + +- **WHEN** a full trace is fetched by trace ID without caching headers +- **THEN** the response is reused for at most 60 seconds, since spans can still arrive #### Scenario: Polling endpoint - **WHEN** autofix state is fetched - **THEN** the response is never served from cache +### Requirement: No immutable fallback + +No URL class SHALL treat a response as immutable. Without server caching +headers, no response SHALL be reused for longer than 5 minutes. + +#### Scenario: Event re-read after an hour + +- **WHEN** any event or trace URL was cached without server headers more than 5 minutes ago +- **THEN** the next read fetches from the API + ### Requirement: Mutations invalidate related entries A successful non-GET request SHALL invalidate the requesting identity's @@ -109,8 +141,8 @@ exceeded, the least recently used entries SHALL be evicted first. ### Requirement: Compressed entries -Stored entries SHALL be compressed with zstd when the runtime supports it, -and otherwise stored with a supported fallback. Each entry SHALL record its +Stored entries SHALL be compressed with zstd at level 3 when the runtime +supports it, and otherwise stored with a supported fallback. Each entry SHALL record its encoding. Reading an entry SHALL return a body identical to the original response body. @@ -121,15 +153,23 @@ response body. ### Requirement: Hosted tiers -The hosted MCP SHALL cache every tier in memory, and SHALL also persist -immutable-tier entries to durable object storage shared across isolates. -Volatile and stable entries SHALL never be persisted outside the isolate. +The hosted MCP SHALL cache every tier in memory. It SHALL persist an entry +to durable object storage shared across isolates only when the entry is in +the detail tier and the response carries a server `Cache-Control` with +`private` and a positive `max-age`. A persisted entry SHALL expire at the +lesser of the server `max-age` and 5 minutes. Volatile and stable entries +SHALL never be persisted outside the isolate. -#### Scenario: Immutable hit in another isolate +#### Scenario: Detail hit in another isolate -- **WHEN** an identity fetches an event in one isolate and later fetches it in another +- **WHEN** an identity fetches an organization event detail that carries `Cache-Control: private, max-age=300` in one isolate, and fetches it again in another isolate within 5 minutes - **THEN** the second fetch is served from durable storage without calling the API +#### Scenario: Detail response without server headers + +- **WHEN** a detail-tier response carries no server caching headers +- **THEN** it is cached in that isolate's memory only + #### Scenario: Volatile entry - **WHEN** an issue list is cached on the hosted MCP diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 2007562b4..9be576118 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -5,65 +5,71 @@ - [ ] 1.1 Add `@sentry/api: 0.400.0` to the pnpm catalog and point `packages/cli` and `packages/mcp-core` at `catalog:`. Verify `pnpm install --frozen-lockfile` succeeds and the lockfile has one `@sentry/api` version. - [ ] 1.2 Fix any `mcp-core` breakage from the `0.337.0` → `0.400.0` bump. Verify `pnpm --filter @sentry/mcp-core run tsc` and `pnpm --filter @sentry/mcp-core run test` pass. -## 2. Shared pagination +## 2. CLI cache tier correction -- [ ] 2.1 Move `paginate`, `fetchAllPages`, page sizing, and cursor encode/decode from `cli/src/lib/api/infrastructure.ts` into `toolkit-core/src/pagination.ts`. Failures are returned as typed results, and the host supplies the logger. Verify with toolkit-core unit tests that cover each `sentry-api-pagination` scenario. -- [ ] 2.2 Switch the CLI to the shared module, mapping errors to `ApiError` and keeping the `sentry-cli/no-manual-pagination` lint rule. Verify the CLI unit tests pass with no snapshot changes. -- [ ] 2.3 Document the module in `packages/toolkit-core/README.md`, and verify `pnpm run docs:check` passes. +- [ ] 2.1 Replace the CLI's `immutable` tier in `packages/cli/src/lib/response-cache.ts` with `detail` (5 minutes): exact path shapes for organization event details without `averageColumn` and project trace-item details without `debug`. Move issue events (including `latest`, `oldest`, `recommended`), project event details, and full traces to `volatile`, and check `/issues/` before any event pattern. Verify with `response-cache.property.test.ts` cases for each `Fallback TTL tiers` scenario, replacing the 24-hour assertions. +- [ ] 2.2 Ship it as its own PR with a changelog note, since repeated event and trace reads will now call the API more often. Verify the CLI unit tests pass. -## 3. Shared cache policy and codecs +## 3. Shared pagination -- [ ] 3.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. -- [ ] 3.2 Add a codec module (zstd level 3 or 9 by tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. -- [ ] 3.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. -- [ ] 3.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. -- [ ] 3.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. +- [ ] 3.1 Move `paginate`, `fetchAllPages`, page sizing, and cursor encode/decode from `cli/src/lib/api/infrastructure.ts` into `toolkit-core/src/pagination.ts`. Failures are returned as typed results, and the host supplies the logger. Verify with toolkit-core unit tests that cover each `sentry-api-pagination` scenario. +- [ ] 3.2 Switch the CLI to the shared module, mapping errors to `ApiError` and keeping the `sentry-cli/no-manual-pagination` lint rule. Verify the CLI unit tests pass with no snapshot changes. +- [ ] 3.3 Document the module in `packages/toolkit-core/README.md`, and verify `pnpm run docs:check` passes. -## 4. Shared transport +## 4. Shared cache policy and codecs -- [ ] 4.1 Move auth headers, 401 refresh, timeouts, GET retry/backoff, redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. Verify with transport unit tests. -- [ ] 4.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. -- [ ] 4.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. +- [ ] 4.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. +- [ ] 4.2 Add a codec module (zstd level 3 for every tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. +- [ ] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. +- [ ] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. +- [ ] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. -## 5. MCP transport injection +## 5. Shared transport -- [ ] 5.1 Add `fetch` to `McpServerOptions`, and route all `SentryApiService` requests and `@sentry/api` calls through it. Verify an `mcp-server` test where a supplied fetch receives every tool request. -- [ ] 5.2 Pass the CLI's authenticated fetch from `cli/src/lib/mcp.ts`. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, and `/mcp-qa` stdio QA passes. -- [ ] 5.3 Add the `fresh` input to read tools through the shared schema helpers. Regenerate definitions, then verify `pnpm run measure-tokens` and the tool tests pass. +- [ ] 5.1 Move auth headers, 401 refresh, timeouts, GET retry/backoff, redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. Verify with transport unit tests. +- [ ] 5.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. +- [ ] 5.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. -## 6. Hosted cache +## 6. MCP transport injection -- [ ] 6.1 Wire the memory store into `mcp-cloudflare` behind `MCP_RESPONSE_CACHE` (`off`/`memory`/`tiered`), with identity taken from the wrapper token's user and grant IDs. Verify the Workers tests pass, and a two-identity isolation test. -- [ ] 6.2 Confirm `node:zlib` zstd on a deployed preview Worker. If it is unavailable, keep brotli as the hosted codec and record that in `docs/cloudflare/architecture.md`. -- [ ] 6.3 Add the R2 binding, the immutable-tier store, and `TieredStore`, with writes in `waitUntil` and a 1-day lifecycle rule. Verify tests for cross-isolate immutable hits and memory-only volatile entries. -- [ ] 6.4 Add cache hit, miss, and eviction metrics and memory-budget logging. Document them in `docs/operations/monitoring.md`, and verify they appear in a preview deploy. +- [ ] 6.1 Add `fetch` to `McpServerOptions`, and route all `SentryApiService` requests and `@sentry/api` calls through it. Verify an `mcp-server` test where a supplied fetch receives every tool request. +- [ ] 6.2 Pass the CLI's authenticated fetch from `cli/src/lib/mcp.ts`. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, and `/mcp-qa` stdio QA passes. +- [ ] 6.3 Add the `fresh` input to read tools through the shared schema helpers. Regenerate definitions, then verify `pnpm run measure-tokens` and the tool tests pass. -## 7. Move MCP endpoints onto `@sentry/api` +## 7. Hosted cache -- [ ] 7.1 Organizations, projects, and teams. Verify the MCP tests and snapshots for the affected tools. -- [ ] 7.2 Issues and events. Verify the MCP tests and snapshots for the affected tools. -- [ ] 7.3 Explore, traces, and logs. Verify the MCP tests, snapshots, and a search-events eval run. -- [ ] 7.4 Releases and replays. Verify the MCP tests and snapshots for the affected tools. -- [ ] 7.5 Monitors, alerts, and uptime, keeping the shared-transport fallback for the endpoints `@sentry/api` lacks. Verify the MCP tests and snapshots. -- [ ] 7.6 Update `docs/contributing/api-patterns.md` for the new client layering, and verify `pnpm run docs:check` passes. +- [ ] 7.1 Wire the memory store into `mcp-cloudflare` behind `MCP_RESPONSE_CACHE` (`off`/`memory`/`tiered`), with identity taken from the wrapper token's user and grant IDs. Verify the Workers tests pass, and a two-identity isolation test. +- [ ] 7.2 Confirm `node:zlib` zstd on a deployed preview Worker. If it is unavailable, keep brotli as the hosted codec and record that in `docs/cloudflare/architecture.md`. +- [ ] 7.3 Add the R2 binding, the detail-tier store, and `TieredStore`. Persist only detail-tier entries whose response carries `Cache-Control: private` with a positive `max-age`, expiring at the lesser of that `max-age` and 5 minutes. Write in `waitUntil` and add a 1-day lifecycle rule for garbage collection. Verify tests for a cross-isolate detail hit, no R2 write without server headers, expiry enforced on read, and memory-only volatile entries. +- [ ] 7.4 Get privacy and security sign-off on the 5-minute stale window after access revocation or deletion before setting `MCP_RESPONSE_CACHE=tiered` in production. Record the decision in `docs/cloudflare/architecture.md`. +- [ ] 7.5 Add cache hit, miss, and eviction metrics and memory-budget logging. Document them in `docs/operations/monitoring.md`, and verify they appear in a preview deploy. -## 8. Standalone login deprecation +## 8. Move MCP endpoints onto `@sentry/api` -- [ ] 8.1 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed. Verify a test that the CLI's scope list covers every MCP tool's required scopes. -- [ ] 8.2 Add a deprecation notice to the standalone `@sentry/mcp-server` auth flow and docs pointing to `sentry mcp`. Verify with an output test and an update to `docs/operations/stdio-auth.md`. +- [ ] 8.1 Organizations, projects, and teams. Verify the MCP tests and snapshots for the affected tools. +- [ ] 8.2 Issues and events. Verify the MCP tests and snapshots for the affected tools. +- [ ] 8.3 Explore, traces, and logs. Verify the MCP tests, snapshots, and a search-events eval run. +- [ ] 8.4 Releases and replays. Verify the MCP tests and snapshots for the affected tools. +- [ ] 8.5 Monitors, alerts, and uptime, keeping the shared-transport fallback for the endpoints `@sentry/api` lacks. Verify the MCP tests and snapshots. +- [ ] 8.6 Update `docs/contributing/api-patterns.md` for the new client layering, and verify `pnpm run docs:check` passes. -## 9. Trained compression dictionary (follow-up) +## 9. Standalone login deprecation -- [ ] 9.1 Build a training corpus that is safe to ship: a public demo organization, or a per-deployment corpus that never leaves the hosted MCP. Verify that no customer or internal strings appear in a shipped dictionary (`strings` audit in CI). -- [ ] 9.2 Add dictionary compression with a versioned `dictId` and a startup self-test that detects a silently ignored `dictionary` option (as in workerd today). Verify a test where an ignored dictionary falls back to plain zstd. -- [ ] 9.3 On Workers, evaluate WASM zstd for dictionary compression: bundle size, CPU in ms/MB, and ratio against native zstd-3. Record the results in design.md and adopt it only if the memory-tier gain outweighs the CPU cost. +- [ ] 9.1 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed. Verify a test that the CLI's scope list covers every MCP tool's required scopes. +- [ ] 9.2 Add a deprecation notice to the standalone `@sentry/mcp-server` auth flow and docs pointing to `sentry mcp`. Verify with an output test and an update to `docs/operations/stdio-auth.md`. -## 10. Integration +## 10. Trained compression dictionary (follow-up) -- [ ] 10.1 Run `pnpm run tsc && pnpm run lint && pnpm run test` and the MCP eval suite on the final state, and record the results in the PR. +- [ ] 10.1 Build a training corpus that is safe to ship: a public demo organization, or a per-deployment corpus that never leaves the hosted MCP. Verify that no customer or internal strings appear in a shipped dictionary (`strings` audit in CI). +- [ ] 10.2 Add dictionary compression with a versioned `dictId` and a startup self-test that detects a silently ignored `dictionary` option (as in workerd today). Verify a test where an ignored dictionary falls back to plain zstd. +- [ ] 10.3 On Workers, evaluate WASM zstd for dictionary compression: bundle size, CPU in ms/MB, and ratio against native zstd-3. Record the results in design.md and adopt it only if the memory-tier gain outweighs the CPU cost. + +## 11. Integration + +- [ ] 11.1 Run `pnpm run tsc && pnpm run lint && pnpm run test` and the MCP eval suite on the final state, and record the results in the PR. ## Workflow follow-up - Archive the change after review once all tasks are complete. -- Follow up on getsentry/sentry#128818 and revisit the TTL tiers when server cache headers ship. +- Follow up on getsentry/sentry#128818 and getsentry/sentry#128828. When the server headers ship, confirm the CLI honors them and enable the R2 tier. - Propose moving pagination, then the transport, into `@sentry/api`. From a81439e7224be8834c7917786807bebfc43c9d61 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:27:43 +0000 Subject: [PATCH 04/28] docs(openspec): plan a shared retry policy that honors Sentry 429 headers 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> --- .../changes/unify-sentry-api-layer/design.md | 42 ++++++++- .../unify-sentry-api-layer/proposal.md | 15 +++- .../specs/sentry-api-transport/spec.md | 88 +++++++++++++++++++ .../changes/unify-sentry-api-layer/tasks.md | 5 +- 4 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index be9e4b78a..0796ce72c 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -132,10 +132,13 @@ New `toolkit-core` modules: derivation, invalidation prefixes (from `cli/src/lib/cache-keys.ts`), and freshness through `http-cache-semantics`. Storage sits behind a `CacheStore` interface. +- `api-retry`: the retry policy. Pure logic with no Node APIs, so Workers + can use it. It lands first, ahead of the transport, so the MCP can fix + its 429 handling now. - `api-transport`: a fetch wrapper that takes a `CredentialProvider` (get the token, refresh on 401), an optional `CacheStore`, a logger, a span hook, - and retry/timeout options. Redirect validation and region routing move - with it. + and retry/timeout options. Retries go through `api-retry`. Redirect + validation and region routing move with it. Product code keeps credential storage, error classes, user-facing messages, and telemetry setup. @@ -144,6 +147,37 @@ _Alternative: a new package._ Rejected for now. `toolkit-core` is already the bundled-private shared package, and a split can come later when moving code into `@sentry/api`. +### Retries are for idempotent requests and honor rate-limit headers + +Neither product handles 429s well today. The CLI retries every method on +408, 429, and 5xx, POSTs included, and ignores rate-limit headers. The MCP +retries only GETs on 502, 503, and 504, so a 429 fails at once. In 7 days +the hosted MCP saw about 32,600 429s from the Sentry API, nearly all on GETs +("Limit is 40 requests in 1 seconds"). + +Sentry's 429 body is only `{"detail": ...}`. The timing is in headers: +`X-Sentry-Rate-Limit-Reset` is the Unix second the next window starts, with +`-Remaining`, `-Limit`, `-ConcurrentRemaining`, and `-ConcurrentLimit` +alongside (`src/sentry/middleware/ratelimit.py`). Sentry sends no +`Retry-After`, but a proxy in front of it can. + +`api-retry` decides whether and how long to wait: + +- Only GET, HEAD, and OPTIONS are retried, unless a request opts in with + `idempotent: true`. A replayed write can duplicate a comment or an alert. +- `Retry-After` wins when present. Otherwise a 429 waits until + `X-Sentry-Rate-Limit-Reset`, measured against the response `Date` when the + local clock disagrees with it by more than a couple of seconds. +- A concurrency-limit 429 (`ConcurrentRemaining: 0`) and a response with no + usable header fall back to jittered exponential backoff. +- Each product sets a per-wait and a total-wait cap. When the header asks for + longer, the request fails at once with a rate-limit error that says when + to retry, rather than holding a Worker request open. +- Sleeps take an `AbortSignal`. + +The 401 refresh is not a retry in this sense. It replays any method once, +because the request never reached the endpoint. + ### `CacheStore` is three byte-based operations ```ts @@ -384,8 +418,8 @@ bumps move both products together. ## Migration Plan -1. Land the catalog alignment (#1672) and the CLI cache tier correction, - each as its own PR. +1. Land the catalog alignment (#1672), the CLI cache tier correction, and + `api-retry` with the MCP 429 fix, each as its own PR. 2. Land the pure `toolkit-core` modules (pagination, cache policy, codecs), with the CLI switched over and no behavior change. 3. Land `api-transport` and the CLI switch-over. diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index c56632dcb..0ace61610 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -33,9 +33,14 @@ The shared layer must therefore sit below both products, on `@sentry/api`. 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`: only idempotent + requests are retried, and 429s wait for Sentry's rate-limit reset header + within a cap. 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, - timeouts, redirect validation, and region routing. + 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. @@ -68,6 +73,8 @@ Out of scope, tracked separately: - `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. @@ -77,8 +84,8 @@ None. ## Impact -- `packages/toolkit-core`: new `pagination`, `response-cache`, and transport - modules, with tests. +- `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. diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md new file mode 100644 index 000000000..7c6ffff62 --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md @@ -0,0 +1,88 @@ +# Spec Delta + +## Purpose + +Defines when a Sentry API request from the CLI or MCP is retried, how long +it waits first, and what the caller sees when it gives up. + +## ADDED Requirements + +### Requirement: Only idempotent requests are retried + +The transport SHALL automatically retry a failed request only when its +method is GET, HEAD, or OPTIONS, or when the caller marks that request +`idempotent`. A 401 that leads to a successful credential refresh SHALL be +replayed once for any method. + +#### Scenario: POST without the opt-in + +- **WHEN** a POST without the `idempotent` opt-in gets a 429, a 5xx, or a timeout +- **THEN** the transport returns that failure without sending the request again + +#### Scenario: POST with the opt-in + +- **WHEN** a POST marked `idempotent`, such as a content-addressed chunk upload, gets a 503 +- **THEN** the transport retries it like a GET + +#### Scenario: Token refresh + +- **WHEN** any request, of any method, gets a 401 and the credential provider refreshes the token +- **THEN** the transport sends the request once more with the new token + +### Requirement: Rate-limited requests wait for the reset + +On a retryable 429, the transport SHALL wait for the delay the response +gives before retrying: `Retry-After` (seconds or an HTTP-date) when +present, otherwise `X-Sentry-Rate-Limit-Reset`. It SHALL measure the reset +against the response `Date` when the local clock disagrees with it by more +than a few seconds. When the response gives no usable delay, or the 429 is +for concurrency (`X-Sentry-Rate-Limit-ConcurrentRemaining: 0`), it SHALL use +jittered exponential backoff. + +#### Scenario: Reset header within the cap + +- **WHEN** a GET gets a 429 whose `X-Sentry-Rate-Limit-Reset` is 1 second away and the per-wait cap is 2 seconds +- **THEN** the transport waits about 1 second and retries the GET + +#### Scenario: Skewed local clock + +- **WHEN** the local clock is 10 minutes behind the response `Date` and the reset is 1 second after that `Date` +- **THEN** the transport waits about 1 second, not 10 minutes + +#### Scenario: Concurrency limit + +- **WHEN** a GET gets a 429 with `X-Sentry-Rate-Limit-ConcurrentRemaining: 0` +- **THEN** the transport retries after a jittered backoff delay, not at the window reset + +### Requirement: Retry waits are capped + +Each product SHALL set a maximum number of retries, a per-wait cap, and a +total-wait cap. When the next wait would exceed either cap, the transport +SHALL stop retrying and return a rate-limit error that says how many seconds +to wait before trying again. An aborted request SHALL stop waiting at once. + +#### Scenario: Reset beyond the cap + +- **WHEN** a GET gets a 429 whose reset is 30 seconds away and the per-wait cap is 2 seconds +- **THEN** the transport does not wait and returns a rate-limit error saying to retry in 30 seconds + +#### Scenario: Retries exhausted + +- **WHEN** a GET is still rate limited after the last allowed retry +- **THEN** the caller gets a rate-limit error with the delay from the last response + +#### Scenario: Aborted during a wait + +- **WHEN** the caller aborts a request while it waits to retry +- **THEN** the wait ends and the request fails with the abort reason + +### Requirement: Retries do not stack + +A request SHALL be retried by one layer only. When the MCP sends its +requests through a host-supplied or shared transport, `SentryApiService` +SHALL not retry them again. + +#### Scenario: MCP on the shared transport + +- **WHEN** an MCP tool's GET is rate limited on every attempt through the shared transport +- **THEN** the Sentry API receives at most `maxRetries + 1` requests for that call diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 9be576118..bc28196b9 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -26,13 +26,14 @@ ## 5. Shared transport -- [ ] 5.1 Move auth headers, 401 refresh, timeouts, GET retry/backoff, redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. Verify with transport unit tests. +- [ ] 5.0 Add a pure, Workers-safe retry policy in `toolkit-core/src/api-retry.ts`: retryable statuses; retryable methods (GET, HEAD, and OPTIONS by default, plus an explicit per-request `idempotent` opt-in, such as content-addressed chunk uploads); the delay from `Retry-After` (seconds or HTTP-date) or, on a 429, `X-Sentry-Rate-Limit-Reset`, with a clock-skew guard against the response `Date` and a per-wait and total-wait cap; a jittered exponential fallback, used for concurrency-limit 429s too; and an abortable sleep. The 401 refresh is not part of this policy and stays allowed for any method. MCP `request()` adopts it in the same PR, retrying rate-limited GETs and filling `ApiRateLimitError.retryAfter` from the headers. Verify with toolkit-core unit tests for each `sentry-api-transport` retry scenario and MCP client tests. The CLI's interim local fix switches to this module in a follow-up PR. +- [ ] 5.1 Move auth headers, 401 refresh (any method), timeouts, idempotent-method retry and backoff through `api-retry`, redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. The CLI used to retry every method, POSTs included; that was a bug, fixed separately before this move. Verify with transport unit tests. - [ ] 5.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. - [ ] 5.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. ## 6. MCP transport injection -- [ ] 6.1 Add `fetch` to `McpServerOptions`, and route all `SentryApiService` requests and `@sentry/api` calls through it. Verify an `mcp-server` test where a supplied fetch receives every tool request. +- [ ] 6.1 Add `fetch` to `McpServerOptions`, and route all `SentryApiService` requests and `@sentry/api` calls through it. `SentryApiService` drops its own retry when it moves onto the transport, so retries do not stack. Verify an `mcp-server` test where a supplied fetch receives every tool request, and a test where a rate-limited GET reaches the API at most `maxRetries + 1` times. - [ ] 6.2 Pass the CLI's authenticated fetch from `cli/src/lib/mcp.ts`. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, and `/mcp-qa` stdio QA passes. - [ ] 6.3 Add the `fresh` input to read tools through the shared schema helpers. Regenerate definitions, then verify `pnpm run measure-tokens` and the tool tests pass. From b5523f5e76f39ed55ce600725056d90997feaed7 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:30:07 +0000 Subject: [PATCH 05/28] docs(openspec): Retry 429s for any method and add 500 to retried statuses 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. --- .../changes/unify-sentry-api-layer/design.md | 13 +++-- .../unify-sentry-api-layer/proposal.md | 6 +-- .../specs/sentry-api-transport/spec.md | 52 ++++++++++++++----- .../changes/unify-sentry-api-layer/tasks.md | 4 +- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index 0796ce72c..b9845750f 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -147,7 +147,7 @@ _Alternative: a new package._ Rejected for now. `toolkit-core` is already the bundled-private shared package, and a split can come later when moving code into `@sentry/api`. -### Retries are for idempotent requests and honor rate-limit headers +### Retries replay only requests that cannot repeat a write, and honor rate-limit headers Neither product handles 429s well today. The CLI retries every method on 408, 429, and 5xx, POSTs included, and ignores rate-limit headers. The MCP @@ -163,11 +163,16 @@ alongside (`src/sentry/middleware/ratelimit.py`). Sentry sends no `api-retry` decides whether and how long to wait: -- Only GET, HEAD, and OPTIONS are retried, unless a request opts in with +- The retried statuses are 429, 500, 502, 503, and 504. The other 5xx codes, + such as 501 and 505, are deterministic, so they are not retried. +- A 429 is retried for any method. `RatelimitMiddleware.process_request` + rejects it before the view runs, so replaying it cannot repeat a write. +- A 5xx, a timeout, or a network error may come after the endpoint ran. Those + are retried only for GET, HEAD, and OPTIONS, unless a request opts in with `idempotent: true`. A replayed write can duplicate a comment or an alert. - `Retry-After` wins when present. Otherwise a 429 waits until - `X-Sentry-Rate-Limit-Reset`, measured against the response `Date` when the - local clock disagrees with it by more than a couple of seconds. + `X-Sentry-Rate-Limit-Reset`. The delay is kept within the range the response + `Date` allows, so a skewed local clock moves it by at most a second. - A concurrency-limit 429 (`ConcurrentRemaining: 0`) and a response with no usable header fall back to jittered exponential backoff. - Each product sets a per-wait and a total-wait cap. When the header asks for diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index 0ace61610..826ac06a1 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -33,9 +33,9 @@ The shared layer must therefore sit below both products, on `@sentry/api`. 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`: only idempotent - requests are retried, and 429s wait for Sentry's rate-limit reset header - within a cap. The MCP adopts it first, so rate-limited GETs retry instead of +- 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 diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md index 7c6ffff62..b39a57559 100644 --- a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md @@ -7,16 +7,39 @@ it waits first, and what the caller sees when it gives up. ## ADDED Requirements -### Requirement: Only idempotent requests are retried +### Requirement: Retried statuses -The transport SHALL automatically retry a failed request only when its -method is GET, HEAD, or OPTIONS, or when the caller marks that request -`idempotent`. A 401 that leads to a successful credential refresh SHALL be -replayed once for any method. +The transport SHALL retry only 429, 500, 502, 503, and 504 responses, plus +timeouts and network errors where the product enables them. Other 5xx codes, +such as 501 and 505, are deterministic, so retrying them only adds latency. + +#### Scenario: Deterministic 5xx + +- **WHEN** a GET gets a 501 +- **THEN** the transport returns the failure without retrying + +### Requirement: Rate-limited requests are retried for any method + +The transport SHALL retry a 429 for any method. Sentry's +`RatelimitMiddleware` rejects the request in `process_request`, before the +view runs (`src/sentry/middleware/ratelimit.py`), so replaying it cannot +repeat a write. + +#### Scenario: Rate-limited POST + +- **WHEN** a POST without the `idempotent` opt-in gets a 429 whose reset is within the cap +- **THEN** the transport waits for the reset and sends the POST again + +### Requirement: Other failures are retried only for idempotent requests + +A 5xx, a timeout, or a network error may come after the endpoint ran. The +transport SHALL NOT retry one unless the method is GET, HEAD, or OPTIONS, or +the caller marks the request `idempotent`. A 401 that leads to a successful +credential refresh SHALL be replayed once for any method. #### Scenario: POST without the opt-in -- **WHEN** a POST without the `idempotent` opt-in gets a 429, a 5xx, or a timeout +- **WHEN** a POST without the `idempotent` opt-in gets a 5xx, a timeout, or a network error - **THEN** the transport returns that failure without sending the request again #### Scenario: POST with the opt-in @@ -31,13 +54,10 @@ replayed once for any method. ### Requirement: Rate-limited requests wait for the reset -On a retryable 429, the transport SHALL wait for the delay the response -gives before retrying: `Retry-After` (seconds or an HTTP-date) when -present, otherwise `X-Sentry-Rate-Limit-Reset`. It SHALL measure the reset -against the response `Date` when the local clock disagrees with it by more -than a few seconds. When the response gives no usable delay, or the 429 is -for concurrency (`X-Sentry-Rate-Limit-ConcurrentRemaining: 0`), it SHALL use -jittered exponential backoff. +On a 429, the transport SHALL wait for `Retry-After` (seconds or an +HTTP-date) when present, otherwise for `X-Sentry-Rate-Limit-Reset`. It SHALL +keep that delay within the range the response `Date` allows, so a skewed +local clock moves it by at most one second. #### Scenario: Reset header within the cap @@ -49,6 +69,12 @@ jittered exponential backoff. - **WHEN** the local clock is 10 minutes behind the response `Date` and the reset is 1 second after that `Date` - **THEN** the transport waits about 1 second, not 10 minutes +### Requirement: Backoff without a usable delay + +When a retryable response gives no usable delay, or a 429 is for +concurrency (`X-Sentry-Rate-Limit-ConcurrentRemaining: 0`), the transport +SHALL wait a jittered exponential backoff delay. + #### Scenario: Concurrency limit - **WHEN** a GET gets a 429 with `X-Sentry-Rate-Limit-ConcurrentRemaining: 0` diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index bc28196b9..6fb96f250 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -26,8 +26,8 @@ ## 5. Shared transport -- [ ] 5.0 Add a pure, Workers-safe retry policy in `toolkit-core/src/api-retry.ts`: retryable statuses; retryable methods (GET, HEAD, and OPTIONS by default, plus an explicit per-request `idempotent` opt-in, such as content-addressed chunk uploads); the delay from `Retry-After` (seconds or HTTP-date) or, on a 429, `X-Sentry-Rate-Limit-Reset`, with a clock-skew guard against the response `Date` and a per-wait and total-wait cap; a jittered exponential fallback, used for concurrency-limit 429s too; and an abortable sleep. The 401 refresh is not part of this policy and stays allowed for any method. MCP `request()` adopts it in the same PR, retrying rate-limited GETs and filling `ApiRateLimitError.retryAfter` from the headers. Verify with toolkit-core unit tests for each `sentry-api-transport` retry scenario and MCP client tests. The CLI's interim local fix switches to this module in a follow-up PR. -- [ ] 5.1 Move auth headers, 401 refresh (any method), timeouts, idempotent-method retry and backoff through `api-retry`, redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. The CLI used to retry every method, POSTs included; that was a bug, fixed separately before this move. Verify with transport unit tests. +- [ ] 5.0 Add a pure, Workers-safe retry policy in `toolkit-core/src/api-retry.ts`: retryable statuses (429, 500, 502, 503, and 504, not every 5xx, since 501, 505, and the rest are deterministic); a 429 retried for any method, because Sentry's `RatelimitMiddleware` rejects it before the view runs; 5xx, timeouts, and network errors retried only for GET, HEAD, and OPTIONS, or for an explicit per-request `idempotent` opt-in, such as content-addressed chunk uploads; the delay from `Retry-After` (seconds or HTTP-date) or, on a 429, `X-Sentry-Rate-Limit-Reset`, kept within the range the response `Date` allows, and a per-wait and total-wait cap; a jittered exponential fallback, used for concurrency-limit 429s too; and an abortable sleep. The 401 refresh is not part of this policy and stays allowed for any method. MCP `request()` adopts it in the same PR, retrying rate-limited requests and filling `ApiRateLimitError.retryAfter` from the headers. Verify with toolkit-core unit tests for each `sentry-api-transport` retry scenario and MCP client tests. The CLI's interim local fix switches to this module in a follow-up PR. +- [ ] 5.1 Move auth headers, 401 refresh (any method), timeouts, retry and backoff through `api-retry` (429 for any method; 5xx, timeouts, and network errors for idempotent methods only), redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. The CLI used to retry every method on 5xx and timeouts, POSTs included; that was a bug, fixed separately before this move. Verify with transport unit tests. - [ ] 5.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. - [ ] 5.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. From a2a032a8c673e8efcefb54a12c6123a5a8100c11 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:41:15 +0000 Subject: [PATCH 06/28] docs(openspec): mark version alignment done (#1672) Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 6fb96f250..32d73b664 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -2,8 +2,8 @@ ## 1. Version alignment -- [ ] 1.1 Add `@sentry/api: 0.400.0` to the pnpm catalog and point `packages/cli` and `packages/mcp-core` at `catalog:`. Verify `pnpm install --frozen-lockfile` succeeds and the lockfile has one `@sentry/api` version. -- [ ] 1.2 Fix any `mcp-core` breakage from the `0.337.0` → `0.400.0` bump. Verify `pnpm --filter @sentry/mcp-core run tsc` and `pnpm --filter @sentry/mcp-core run test` pass. +- [x] 1.1 Add `@sentry/api: 0.400.0` to the pnpm catalog and point `packages/cli` and `packages/mcp-core` at `catalog:`. Verify `pnpm install --frozen-lockfile` succeeds and the lockfile has one `@sentry/api` version. +- [x] 1.2 Fix any `mcp-core` breakage from the `0.337.0` → `0.400.0` bump. Verify `pnpm --filter @sentry/mcp-core run tsc` and `pnpm --filter @sentry/mcp-core run test` pass. ## 2. CLI cache tier correction From 317a80dd87edc1cab08792269ba04d3c248860ea Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:12:03 +0000 Subject: [PATCH 07/28] docs(openspec): Fold the detail tier into stable and allowlist R2 instead Matches #1681 as merged: the CLI has no separate detail tier, since it would share the stable TTL. R2 eligibility on the hosted MCP becomes an explicit URL allowlist owned by that store. Mark task group 2 done. Co-Authored-By: BYK --- .../changes/unify-sentry-api-layer/design.md | 71 ++++++++++--------- .../unify-sentry-api-layer/proposal.md | 19 ++--- .../specs/sentry-api-response-cache/spec.md | 31 +++----- .../changes/unify-sentry-api-layer/tasks.md | 6 +- 4 files changed, 59 insertions(+), 68 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index b9845750f..3810f2f95 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -89,33 +89,26 @@ endpoints are safe to mark `immutable`: revocation are not reflected until the entry expires. That bounds the TTL, not just the tier. -So the shared policy replaces `immutable` with `detail`: - -| Tier | TTL | Matches | -| -------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| no-cache | 0 | autofix and root-cause polling | -| detail | 5 m | `/organizations/{org}/events/{project}:{event_id}/` without `averageColumn`; `/projects/{org}/{project}/trace-items/{id}/` without `debug` | -| volatile | 60 s | `/issues/` (including issue events), `/projects/{org}/{project}/events/{id}/`, `/organizations/{org}/trace/{id}/`, log and span queries | -| stable | 5 m | everything else | - -- Patterns are exact path shapes, anchored on the API prefix, not - substring matches on `/events/`. Anything that does not match an - allowlisted shape falls through to volatile or stable. -- `detail` has the same TTL as `stable` in the CLI. It exists as its own - tier because it is the only tier eligible for R2 on the hosted MCP. -- Today the CLI reads events through project and issue endpoints - (`getEvent`, `getLatestEvent` in `cli/src/lib/api/events.ts`), and the MCP - through `/organizations/{org}/issues/{id}/events/{event_id}/`. So in - practice the detail tier covers trace items until a caller moves to the - organization event endpoint. That is worth doing per call site only where - the response has every field the caller uses. +So the CLI drops `immutable` (getsentry/toolkit#1681): + +| Tier | TTL | Matches | +| -------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------- | +| no-cache | 0 | autofix and root-cause polling | +| volatile | 60 s | `/issues/` (including issue events), `/projects/{org}/{project}/events/{id}/`, `/organizations/{org}/trace/{id}/`, log and span queries | +| stable | 5 m | everything else, including organization event details and trace-item details | + +- No separate tier for the safe detail endpoints. In the CLI it would have + the same TTL as `stable`, so it was folded in during review. Which + responses the hosted MCP may persist to R2 is a decision for that store, + made with its own allowlist (see the R2 backend below), not a TTL tier. - When server headers are present they still win. The CLI keeps honoring `max-age`, so if the server later extends it, the CLI follows without a release. -- This lands as its own CLI PR before the move into `toolkit-core`, so the - move itself stays a no-behavior-change refactor. The existing - `response-cache.property.test.ts` cases that assert 24-hour event and - trace caching are replaced with scenarios from the spec delta. +- Classification stays URL-regex based for now. A TODO in + `response-cache.ts` tracks moving it to generated `@sentry/api` operation + metadata (getsentry/sentry-api-schema#81, #102). +- This landed as its own CLI PR before the move into `toolkit-core`, so the + move itself stays a no-behavior-change refactor. The cost is more API calls for repeated event and trace reads. A 24-hour window was a correctness bug, not a tunable: an agent re-reading an issue's @@ -208,19 +201,27 @@ Backends: size, configurable by env. - Module-scoped, so it lives as long as the isolate. `deleteMatching` scans the in-memory index. -- **R2 (hosted MCP, detail tier only).** - - Persists an entry only when it is in the detail tier _and_ the response - carries `Cache-Control: private` with a positive `max-age`. The server - header is the opt-in: until getsentry/sentry#128828 ships and is - enabled, R2 stores nothing. +- **R2 (hosted MCP, allowlisted detail endpoints only).** + - Persists an entry only when its URL matches the store's explicit + allowlist _and_ the response carries `Cache-Control: private` with a + positive `max-age`. The allowlist holds exact path shapes: + `/organizations/{org}/events/{project}:{event_id}/` without + `averageColumn`, and `/projects/{org}/{project}/trace-items/{id}/` + without `debug`. The server header is the opt-in: until + getsentry/sentry#128828 ships and is enabled, R2 stores nothing. + - Today the CLI and MCP read events through project and issue endpoints, + which are not on the allowlist. So in practice R2 covers trace items + until a caller moves to the organization event endpoint, which is worth + doing per call site only where the response has every field the caller + uses. - Object key is `/`, with the expiry, the lesser of the server `max-age` and 5 minutes, in custom metadata and checked on read. - A bucket lifecycle rule deletes objects after 1 day, the shortest rule R2 supports. It is garbage collection only; reads enforce the expiry. - Writes happen in `ctx.waitUntil()`, so they never delay a response. - - Detail entries are never targeted by mutation invalidation, so R2 never - needs `deleteMatching`. The short expiry bounds how long a revoked or + - Allowlisted entries are never targeted by mutation invalidation, so R2 + never needs `deleteMatching`. The short expiry bounds how long a revoked or deleted item can still be served. _Alternatives:_ @@ -236,8 +237,8 @@ _Alternatives:_ `TieredStore(memory, r2)`: -- `get` checks memory first, then R2 for detail keys, and copies R2 hits - into memory with the remaining expiry. +- `get` checks memory first, then R2 for allowlisted URLs, and copies R2 + hits into memory with the remaining expiry. - `set` writes to memory, and also to R2 when the entry is R2-eligible (see above). - `deleteMatching` only touches memory, since only volatile and stable @@ -399,7 +400,7 @@ bumps move both products together. description tells agents when to use it. - **Revoked access or deleted data served from cache.** A user who loses access to a project, or whose event is deleted, can still get a cached - copy until it expires: up to 5 minutes for detail entries, including + copy until it expires: up to 5 minutes for stable entries, including from R2 on the hosted MCP. Enabling R2 in production needs a privacy and security sign-off on that window, alongside the server-side headers in getsentry/sentry#128828. @@ -430,7 +431,7 @@ bumps move both products together. 3. Land `api-transport` and the CLI switch-over. 4. Land the MCP transport injection and `sentry mcp` passing the CLI fetch, then the hosted memory store behind a flag (`MCP_RESPONSE_CACHE=memory`). -5. Enable the hosted memory cache. Add the R2 binding and the detail tier +5. Enable the hosted memory cache. Add the R2 binding and its allowlist behind `MCP_RESPONSE_CACHE=tiered`, and enable it only after the server headers ship and the security sign-off. 6. Run the domain-by-domain `@sentry/api` migration PRs. diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index 826ac06a1..6aa3c0af5 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -23,12 +23,12 @@ The shared layer must therefore sit below both products, on `@sentry/api`. - 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//` and `/trace//` 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. +- **Behavior change (landed in #1681):** drop the CLI's 24-hour + "immutable" cache tier. It matched every `/events//` and + `/trace//` 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 @@ -46,8 +46,9 @@ The shared layer must therefore sit below both products, on `@sentry/api`. 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 + - 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. @@ -98,6 +99,6 @@ None. 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. + allowlisted 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`. diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md index a2331f7c4..bac7fea02 100644 --- a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md @@ -48,28 +48,17 @@ When a response carries HTTP caching headers, the cache SHALL follow them Responses without caching headers SHALL use a TTL by URL class: -- detail: 5 minutes (organization event details and project trace-item - details, by exact path) - volatile: 60 seconds (issue endpoints, project event details, full traces, log and span queries) - no-cache: never cached (autofix and root-cause polling) -- stable: 5 minutes (everything else) +- stable: 5 minutes (everything else, including organization event details + and trace-item details) #### Scenario: Organization event lookup - **WHEN** `/organizations/{org}/events/{project}:{event_id}/` is fetched without caching headers - **THEN** the response is reused for up to 5 minutes -#### Scenario: Live aggregate on an event - -- **WHEN** an organization event detail is fetched with an `averageColumn` query -- **THEN** it is classified as stable, not detail - -#### Scenario: Trace-item debug lookup - -- **WHEN** a project trace-item detail is fetched with a `debug` query -- **THEN** it is classified as stable, not detail - #### Scenario: Issue event lookup - **WHEN** an issue's latest, oldest, recommended, or specific event is fetched without caching headers @@ -154,20 +143,20 @@ response body. ### Requirement: Hosted tiers The hosted MCP SHALL cache every tier in memory. It SHALL persist an entry -to durable object storage shared across isolates only when the entry is in -the detail tier and the response carries a server `Cache-Control` with -`private` and a positive `max-age`. A persisted entry SHALL expire at the -lesser of the server `max-age` and 5 minutes. Volatile and stable entries -SHALL never be persisted outside the isolate. +to durable object storage shared across isolates only when its URL matches +an explicit allowlist of detail endpoints and the response carries a server +`Cache-Control` with `private` and a positive `max-age`. A persisted entry SHALL expire at the +lesser of the server `max-age` and 5 minutes. Entries outside the +allowlist SHALL never be persisted outside the isolate. -#### Scenario: Detail hit in another isolate +#### Scenario: Allowlisted hit in another isolate - **WHEN** an identity fetches an organization event detail that carries `Cache-Control: private, max-age=300` in one isolate, and fetches it again in another isolate within 5 minutes - **THEN** the second fetch is served from durable storage without calling the API -#### Scenario: Detail response without server headers +#### Scenario: Allowlisted response without server headers -- **WHEN** a detail-tier response carries no server caching headers +- **WHEN** an allowlisted response carries no server caching headers - **THEN** it is cached in that isolate's memory only #### Scenario: Volatile entry diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 32d73b664..6f9a822ce 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -7,8 +7,8 @@ ## 2. CLI cache tier correction -- [ ] 2.1 Replace the CLI's `immutable` tier in `packages/cli/src/lib/response-cache.ts` with `detail` (5 minutes): exact path shapes for organization event details without `averageColumn` and project trace-item details without `debug`. Move issue events (including `latest`, `oldest`, `recommended`), project event details, and full traces to `volatile`, and check `/issues/` before any event pattern. Verify with `response-cache.property.test.ts` cases for each `Fallback TTL tiers` scenario, replacing the 24-hour assertions. -- [ ] 2.2 Ship it as its own PR with a changelog note, since repeated event and trace reads will now call the API more often. Verify the CLI unit tests pass. +- [x] 2.1 Remove the CLI's `immutable` tier in `packages/cli/src/lib/response-cache.ts`. Move issue events (including `latest`, `oldest`, `recommended`), project event details, and full traces to `volatile`; organization event and trace-item details fall to `stable`. Verify with classifier and fake-clock expiry tests for each `Fallback TTL tiers` scenario. +- [x] 2.2 Ship it as its own PR (#1681). Verify the CLI unit tests pass. ## 3. Shared pagination @@ -41,7 +41,7 @@ - [ ] 7.1 Wire the memory store into `mcp-cloudflare` behind `MCP_RESPONSE_CACHE` (`off`/`memory`/`tiered`), with identity taken from the wrapper token's user and grant IDs. Verify the Workers tests pass, and a two-identity isolation test. - [ ] 7.2 Confirm `node:zlib` zstd on a deployed preview Worker. If it is unavailable, keep brotli as the hosted codec and record that in `docs/cloudflare/architecture.md`. -- [ ] 7.3 Add the R2 binding, the detail-tier store, and `TieredStore`. Persist only detail-tier entries whose response carries `Cache-Control: private` with a positive `max-age`, expiring at the lesser of that `max-age` and 5 minutes. Write in `waitUntil` and add a 1-day lifecycle rule for garbage collection. Verify tests for a cross-isolate detail hit, no R2 write without server headers, expiry enforced on read, and memory-only volatile entries. +- [ ] 7.3 Add the R2 binding, its URL allowlist (organization event details without `averageColumn`, project trace-item details without `debug`), and `TieredStore`. Persist only allowlisted entries whose response carries `Cache-Control: private` with a positive `max-age`, expiring at the lesser of that `max-age` and 5 minutes. Write in `waitUntil` and add a 1-day lifecycle rule for garbage collection. Verify tests for a cross-isolate allowlisted hit, no R2 write without server headers, expiry enforced on read, and memory-only volatile entries. - [ ] 7.4 Get privacy and security sign-off on the 5-minute stale window after access revocation or deletion before setting `MCP_RESPONSE_CACHE=tiered` in production. Record the decision in `docs/cloudflare/architecture.md`. - [ ] 7.5 Add cache hit, miss, and eviction metrics and memory-budget logging. Document them in `docs/operations/monitoring.md`, and verify they appear in a preview deploy. From 5f877f28ecd098ba25c4780ac7f65fdf8d790f9e Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:35:56 +0000 Subject: [PATCH 08/28] docs(openspec): Mark shared pagination done (#1683) Task group 3 moved paginate, fetchAllPages, page sizing, and cursor encoding into @sentry/toolkit-core/pagination behind CLI wrappers. Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 6f9a822ce..fa750bf82 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -12,9 +12,9 @@ ## 3. Shared pagination -- [ ] 3.1 Move `paginate`, `fetchAllPages`, page sizing, and cursor encode/decode from `cli/src/lib/api/infrastructure.ts` into `toolkit-core/src/pagination.ts`. Failures are returned as typed results, and the host supplies the logger. Verify with toolkit-core unit tests that cover each `sentry-api-pagination` scenario. -- [ ] 3.2 Switch the CLI to the shared module, mapping errors to `ApiError` and keeping the `sentry-cli/no-manual-pagination` lint rule. Verify the CLI unit tests pass with no snapshot changes. -- [ ] 3.3 Document the module in `packages/toolkit-core/README.md`, and verify `pnpm run docs:check` passes. +- [x] 3.1 Move `paginate`, `fetchAllPages`, page sizing, and cursor encode/decode from `cli/src/lib/api/infrastructure.ts` into `toolkit-core/src/pagination.ts`. Failures are returned as typed results, and the host supplies the logger. Verify with toolkit-core unit tests that cover each `sentry-api-pagination` scenario. +- [x] 3.2 Switch the CLI to the shared module, mapping errors to `ApiError` and keeping the `sentry-cli/no-manual-pagination` lint rule. Verify the CLI unit tests pass with no snapshot changes. +- [x] 3.3 Document the module in `packages/toolkit-core/README.md`, and verify `pnpm run docs:check` passes. Shipped in #1683. ## 4. Shared cache policy and codecs From 00e1737311b486258a25879437123f279eaa2150 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:11:40 +0000 Subject: [PATCH 09/28] docs(openspec): Mark the shared cache policy done (#1688) Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index fa750bf82..051df2459 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -18,7 +18,7 @@ ## 4. Shared cache policy and codecs -- [ ] 4.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. +- [x] 4.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. Shipped in #1688. - [ ] 4.2 Add a codec module (zstd level 3 for every tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. - [ ] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. - [ ] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. From 66efad2d489900970e65d491556536ffa93554c7 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:10:23 +0000 Subject: [PATCH 10/28] docs(openspec): Mark the cache body codec done (#1695) Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 051df2459..abf6749a8 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -19,7 +19,7 @@ ## 4. Shared cache policy and codecs - [x] 4.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. Shipped in #1688. -- [ ] 4.2 Add a codec module (zstd level 3 for every tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. +- [x] 4.2 Add a codec module (zstd level 3 for every tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. Shipped in #1695. - [ ] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. - [ ] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. - [ ] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. From b710f968ffdb4895ecda1ba87560599943b1da6b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:48:23 +0000 Subject: [PATCH 11/28] docs(openspec): Mark the CacheStore task in review (#1696) --- openspec/changes/unify-sentry-api-layer/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index abf6749a8..06bc8bccf 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -20,7 +20,7 @@ - [x] 4.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. Shipped in #1688. - [x] 4.2 Add a codec module (zstd level 3 for every tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. Shipped in #1695. -- [ ] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. +- [ ] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. In review in #1696. - [ ] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. - [ ] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. From 60c283382f8b1f147eed5a65acb77ad81e9e427b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:30:10 +0000 Subject: [PATCH 12/28] docs(openspec): Mark the CacheStore done (#1696) and the CLI store in review (#1697) Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 06bc8bccf..0f5e38acd 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -20,8 +20,8 @@ - [x] 4.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. Shipped in #1688. - [x] 4.2 Add a codec module (zstd level 3 for every tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. Shipped in #1695. -- [ ] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. In review in #1696. -- [ ] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. +- [x] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. Shipped in #1696. +- [ ] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. In review in #1697. - [ ] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. ## 5. Shared transport From 5856128d85bc236e3df415f24f8d530474904910 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:44:52 +0000 Subject: [PATCH 13/28] docs(openspec): Mark the CLI store done (#1697) and revalidation in review (#1698) Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 0f5e38acd..66ddb091b 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -21,8 +21,8 @@ - [x] 4.1 Move tier classification, key derivation (moved to WebCrypto HMAC), URL normalization, and invalidation prefixes into `toolkit-core/src/response-cache/`. Verify with unit tests for the tier, identity-scoping, and invalidation scenarios. Shipped in #1688. - [x] 4.2 Add a codec module (zstd level 3 for every tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. Shipped in #1695. - [x] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. Shipped in #1696. -- [ ] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. In review in #1697. -- [ ] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. +- [x] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. Shipped in #1697. +- [ ] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. In review in #1698. ## 5. Shared transport From b686aa11fed41703d4b83a81a36d3889fb61e039 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:42:36 +0000 Subject: [PATCH 14/28] docs(openspec): Mark revalidation done (#1698) and add the shared cache policy task Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 66ddb091b..6a78eb8ea 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -22,7 +22,8 @@ - [x] 4.2 Add a codec module (zstd level 3 for every tier, brotli-4 fallback, raw below 256 bytes, `encoding` and `dictId` recorded per entry) with runtime detection. Verify round-trip tests in Node 22, and a test with zstd stubbed out. Shipped in #1695. - [x] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. Shipped in #1696. - [x] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. Shipped in #1697. -- [ ] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. In review in #1698. +- [x] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. Shipped in #1698. +- [ ] 4.6 Move the cache policy from `packages/cli/src/lib/response-cache.ts` into `toolkit-core/src/response-cache/`: freshness checks (server cache directives win, the URL tier is the fallback), stale lookup and 304 revalidation, retention (the freshness lifetime plus a 24-hour revalidation window for entries with an `ETag`), response header rebuild, and cache-entry construction. `http-cache-semantics` becomes a `toolkit-core` dependency. The CLI keeps the filesystem store, cache directory, identity fingerprint, env kill switches, telemetry, and the cache-age hint. Verify with toolkit-core unit tests for the moved logic, and the CLI cache tests passing unchanged. ## 5. Shared transport From 0abd1e3ffc86f0f60272648cd869db5adcf005bf Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:25:48 +0000 Subject: [PATCH 15/28] docs(openspec): Mark the shared cache policy done (#1704) --- openspec/changes/unify-sentry-api-layer/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 6a78eb8ea..71f2e9b37 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -23,7 +23,7 @@ - [x] 4.3 Define the `CacheStore` interface and add the byte-budgeted memory store. Verify tests for LRU eviction, the per-entry cap, and the budget staying within its bound under many identities. Shipped in #1696. - [x] 4.4 Re-implement the CLI filesystem store on `CacheStore`, with compressed entries and a byte budget, while reading existing uncompressed entries. Verify the CLI cache tests pass and an old-format entry is still served. Shipped in #1697. - [x] 4.5 Add conditional revalidation: send `If-None-Match` for stale entries that have an `ETag`, and treat a 304 as a hit. Verify with a test against a mocked `ETag` endpoint. Shipped in #1698. -- [ ] 4.6 Move the cache policy from `packages/cli/src/lib/response-cache.ts` into `toolkit-core/src/response-cache/`: freshness checks (server cache directives win, the URL tier is the fallback), stale lookup and 304 revalidation, retention (the freshness lifetime plus a 24-hour revalidation window for entries with an `ETag`), response header rebuild, and cache-entry construction. `http-cache-semantics` becomes a `toolkit-core` dependency. The CLI keeps the filesystem store, cache directory, identity fingerprint, env kill switches, telemetry, and the cache-age hint. Verify with toolkit-core unit tests for the moved logic, and the CLI cache tests passing unchanged. +- [x] 4.6 Move the cache policy from `packages/cli/src/lib/response-cache.ts` into `toolkit-core/src/response-cache/`: freshness checks (server cache directives win, the URL tier is the fallback), stale lookup and 304 revalidation, retention (the freshness lifetime plus a 24-hour revalidation window for entries with an `ETag`), response header rebuild, and cache-entry construction. `http-cache-semantics` becomes a `toolkit-core` dependency. The CLI keeps the filesystem store, cache directory, identity fingerprint, env kill switches, telemetry, and the cache-age hint. Verify with toolkit-core unit tests for the moved logic, and the CLI cache tests passing unchanged. Shipped in #1704. ## 5. Shared transport From 157b4b56ce7ff0668c04a773091e09cfe86c62ac Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:14:40 +0000 Subject: [PATCH 16/28] docs(openspec): Mark task 5.0 done Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 71f2e9b37..338e1780a 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -27,7 +27,7 @@ ## 5. Shared transport -- [ ] 5.0 Add a pure, Workers-safe retry policy in `toolkit-core/src/api-retry.ts`: retryable statuses (429, 500, 502, 503, and 504, not every 5xx, since 501, 505, and the rest are deterministic); a 429 retried for any method, because Sentry's `RatelimitMiddleware` rejects it before the view runs; 5xx, timeouts, and network errors retried only for GET, HEAD, and OPTIONS, or for an explicit per-request `idempotent` opt-in, such as content-addressed chunk uploads; the delay from `Retry-After` (seconds or HTTP-date) or, on a 429, `X-Sentry-Rate-Limit-Reset`, kept within the range the response `Date` allows, and a per-wait and total-wait cap; a jittered exponential fallback, used for concurrency-limit 429s too; and an abortable sleep. The 401 refresh is not part of this policy and stays allowed for any method. MCP `request()` adopts it in the same PR, retrying rate-limited requests and filling `ApiRateLimitError.retryAfter` from the headers. Verify with toolkit-core unit tests for each `sentry-api-transport` retry scenario and MCP client tests. The CLI's interim local fix switches to this module in a follow-up PR. +- [x] 5.0 Add a pure, Workers-safe retry policy in `toolkit-core/src/api-retry.ts`: retryable statuses (429, 500, 502, 503, and 504, not every 5xx, since 501, 505, and the rest are deterministic); a 429 retried for any method, because Sentry's `RatelimitMiddleware` rejects it before the view runs; 5xx, timeouts, and network errors retried only for GET, HEAD, and OPTIONS, or for an explicit per-request `idempotent` opt-in, such as content-addressed chunk uploads; the delay from `Retry-After` (seconds or HTTP-date) or, on a 429, `X-Sentry-Rate-Limit-Reset`, kept within the range the response `Date` allows, and a per-wait and total-wait cap; a jittered exponential fallback, used for concurrency-limit 429s too; and an abortable sleep. The 401 refresh is not part of this policy and stays allowed for any method. MCP `request()` adopts it in the same PR, retrying rate-limited requests and filling `ApiRateLimitError.retryAfter` from the headers. Verify with toolkit-core unit tests for each `sentry-api-transport` retry scenario and MCP client tests. Shipped in #1673; the CLI's interim local fix (#1677) switched to this module in #1710. - [ ] 5.1 Move auth headers, 401 refresh (any method), timeouts, retry and backoff through `api-retry` (429 for any method; 5xx, timeouts, and network errors for idempotent methods only), redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. The CLI used to retry every method on 5xx and timeouts, POSTs included; that was a bug, fixed separately before this move. Verify with transport unit tests. - [ ] 5.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. - [ ] 5.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. From bf76b296dba2827495cbaa4370c0dc153202cf74 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:53:17 +0000 Subject: [PATCH 17/28] docs(openspec): Mark issues and events on @sentry/api done (#1713) Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 338e1780a..070b43c22 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -49,7 +49,7 @@ ## 8. Move MCP endpoints onto `@sentry/api` - [ ] 8.1 Organizations, projects, and teams. Verify the MCP tests and snapshots for the affected tools. -- [ ] 8.2 Issues and events. Verify the MCP tests and snapshots for the affected tools. +- [x] 8.2 Issues and events. Verify the MCP tests and snapshots for the affected tools. Shipped in #1713; issue notes, activity, user reports, event committers, writes, and the attachment download stay on the shared transport until 6.1 or until `@sentry/api` gains operations. - [ ] 8.3 Explore, traces, and logs. Verify the MCP tests, snapshots, and a search-events eval run. - [ ] 8.4 Releases and replays. Verify the MCP tests and snapshots for the affected tools. - [ ] 8.5 Monitors, alerts, and uptime, keeping the shared-transport fallback for the endpoints `@sentry/api` lacks. Verify the MCP tests and snapshots. From 0e2a1293a403ad98f5a8216f34bf13cb03766363 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:17:45 +0000 Subject: [PATCH 18/28] docs(openspec): Mark the shared transport done (#1719) 5.3 is in review in #1720, and 6.1 is stacked on it. Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 070b43c22..c2621371b 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -28,9 +28,9 @@ ## 5. Shared transport - [x] 5.0 Add a pure, Workers-safe retry policy in `toolkit-core/src/api-retry.ts`: retryable statuses (429, 500, 502, 503, and 504, not every 5xx, since 501, 505, and the rest are deterministic); a 429 retried for any method, because Sentry's `RatelimitMiddleware` rejects it before the view runs; 5xx, timeouts, and network errors retried only for GET, HEAD, and OPTIONS, or for an explicit per-request `idempotent` opt-in, such as content-addressed chunk uploads; the delay from `Retry-After` (seconds or HTTP-date) or, on a 429, `X-Sentry-Rate-Limit-Reset`, kept within the range the response `Date` allows, and a per-wait and total-wait cap; a jittered exponential fallback, used for concurrency-limit 429s too; and an abortable sleep. The 401 refresh is not part of this policy and stays allowed for any method. MCP `request()` adopts it in the same PR, retrying rate-limited requests and filling `ApiRateLimitError.retryAfter` from the headers. Verify with toolkit-core unit tests for each `sentry-api-transport` retry scenario and MCP client tests. Shipped in #1673; the CLI's interim local fix (#1677) switched to this module in #1710. -- [ ] 5.1 Move auth headers, 401 refresh (any method), timeouts, retry and backoff through `api-retry` (429 for any method; 5xx, timeouts, and network errors for idempotent methods only), redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. The CLI used to retry every method on 5xx and timeouts, POSTs included; that was a bug, fixed separately before this move. Verify with transport unit tests. -- [ ] 5.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. -- [ ] 5.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. +- [x] 5.1 Move auth headers, 401 refresh (any method), timeouts, retry and backoff through `api-retry` (429 for any method; 5xx, timeouts, and network errors for idempotent methods only), redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. The CLI used to retry every method on 5xx and timeouts, POSTs included; that was a bug, fixed separately before this move. Verify with transport unit tests. Shipped in #1719. +- [x] 5.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. Shipped in #1719. +- [ ] 5.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. Next: in review in #1720, with 6.1 stacked on it. ## 6. MCP transport injection From d4bb22a5e1a464730b7353266f105d15c44cc935 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:41:11 +0000 Subject: [PATCH 19/28] docs(openspec): Plan the shared API client in two steps Step 1 consolidates the MCP's SentryApiService and the CLI's API modules into one browser-safe client in toolkit-core, in one PR. Step 2 moves it into @sentry/api. The host-supplied MCP fetch and sentry mcp on the CLI session move to a deferred phase. Co-Authored-By: BYK --- .../changes/unify-sentry-api-layer/design.md | 156 +++++++++++++----- .../unify-sentry-api-layer/proposal.md | 68 ++++++-- .../specs/mcp-host-transport/spec.md | 64 ------- .../specs/sentry-api-client/spec.md | 94 +++++++++++ .../specs/sentry-api-response-cache/spec.md | 6 + .../specs/sentry-api-transport/spec.md | 4 +- .../changes/unify-sentry-api-layer/tasks.md | 44 +++-- 7 files changed, 293 insertions(+), 143 deletions(-) delete mode 100644 openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md create mode 100644 openspec/changes/unify-sentry-api-layer/specs/sentry-api-client/spec.md diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index 3810f2f95..3ff3d8c75 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -49,9 +49,11 @@ See proposal.md for motivation. Current state that shapes the approach: - One implementation of pagination, cache policy, and transport, used by the CLI, `sentry mcp`, and the hosted MCP. -- Shared modules run unchanged in Node and Workers, and avoid - Node-only APIs where a web standard exists, so they can later move into - `@sentry/api` and run in the browser. +- One Sentry API client, with endpoint wrappers, errors, and validation, in + place of the MCP's `SentryApiService` and the CLI's API modules. It lives + in `toolkit-core` first and in `@sentry/api` after step 2. +- Shared modules run unchanged in Node, Workers, and browsers, using only + web-standard APIs, so the Sentry frontend can use the published client. - No CLI behavior change while its internals move, apart from the cache tier correction, which lands first as its own PR. @@ -59,10 +61,12 @@ See proposal.md for motivation. Current state that shapes the approach: - Client-side caching in the Sentry frontend. Browser HTTP caching driven by server headers covers that (getsentry/sentry#128818). -- Using `createSentrySDK()` from the MCP. -- Moving code into `@sentry/api` in this change. -- Replacing MCP Zod response validation with generated schemas everywhere. - That happens per domain where `@sentry/api/zod` covers the endpoint. +- Using `createSentrySDK()`, or the CLI as an SDK in general, from the MCP, + and `sentry mcp` passing the CLI's fetch. Both are deferred to a later + phase after step 2 (task group 11). +- Replacing every hand-written response schema with a generated one. The + shared client uses `@sentry/api/zod` where it covers the endpoint and + keeps hand-written Zod schemas elsewhere. ## Decisions @@ -348,40 +352,99 @@ Compression saves memory and storage, not CPU. A cache hit costs decompression instead of JSON from the network, and on Workers the network wait is not billed CPU. Its job is to fit more entries into the budget. -### `runMcpServer()` takes a transport - -`McpServerOptions` gains `fetch?: typeof fetch`. - -- `sentry mcp` passes the CLI's authenticated fetch, so local sessions get - CLI auth, refresh, retries, and the on-disk cache. -- `mcp-cloudflare` builds an `api-transport` around the OAuth token and the - tiered store. - -Inside `mcp-core`, `SentryApiService` sends all requests through the -injected fetch, and `@sentry/api` operations receive the same fetch through -their client options. +### Two steps: consolidate in `toolkit-core`, then move into `@sentry/api` + +Everything `SentryApiService`-like ends up in `@sentry/api`, so the CLI, +both MCP servers, and the Sentry frontend share one client. Doing that +directly would mean designing the shared API inside sentry-api-schema while +the code that defines it still lives in toolkit, with a publish round trip +for every fix. So: + +1. **Step 1, in `toolkit-core` (task group 8).** Build the shared client in + `toolkit-core/src/api-client/`, with the file layout it will have inside + `@sentry/api`. Switch the MCP and the CLI onto it and delete the code it + replaces, in one PR rather than one per domain: two half-migrated layers + would cost more than one large review. +2. **Step 2, into `@sentry/api` (task group 9).** Copy the modules into + getsentry/sentry-api-schema unchanged, publish a new `@sentry/api`, + switch toolkit's imports, and delete the toolkit-core copies. + +Step 1 ships nothing a user can see, apart from changes called out in its +PR. It makes step 2 a copy, not a redesign. + +### The shared client + +`toolkit-core/src/api-client/` holds: + +- **Core:** building URLs for a region, setting headers, reading responses, + and mapping error responses to errors. Requests go through the shared + transport, so there is one retry layer. +- **Errors:** `ApiError` with the status and the server's detail, and the + rate-limit error with `retryAfterMs` from `Retry-After` or + `X-Sentry-Rate-Limit-Reset`. Hosts map them to their own presentation: + CLI exit codes and messages, MCP tool results. +- **Validation:** Zod. `@sentry/api/zod` where it covers the endpoint, + hand-written schemas otherwise. The MCP already validates with Zod and + needs Zod for tool schemas. The CLI's valibot schemas for endpoints that + move are converted. +- **Pagination, cache policy, retry policy, and transport:** the existing + `pagination`, `response-cache` policy, `api-retry`, and `api-transport` + modules, which move with it in step 2. +- **Endpoints by domain:** organizations, projects, teams, issues, events, + explore, traces, logs, releases, replays, monitors, alerts, and so on, on + top of `@sentry/api` operations where they exist and the shared transport + otherwise. Where both products call the same endpoint, they call the same + wrapper. + +Hosts keep: + +- **CLI:** credential storage and refresh, the SQLite and filesystem cache + store, region lookup storage, telemetry spans, and mapping errors to exit + codes. +- **MCP:** the session's access token, its Zod-to-tool output formatting, and + the request-scoped `fresh` flag. +- **Browser:** session credentials, and no cache store, since browser HTTP + caching driven by server headers covers it. + +### Portable shared code, enforced + +The shared client imports only `@sentry/api` and the toolkit-core modules +that move with it. It uses only web-standard APIs (`fetch`, `Request`, +`Response`, `Headers`, `URL`, `AbortSignal`, `crypto.subtle`, +`TextEncoder`), so it runs in Node, Workers, and browsers. It never imports +`node:*` or uses `Buffer`, `fs`, `process`, or `AsyncLocalStorage`. + +Credentials, cache storage, the fresh-read scope, logging, and telemetry are +interfaces. Each host passes its own implementation. + +Two checks keep it that way: + +- A lint rule on the shared modules bans `node:*` imports and Node globals. +- A typecheck of the shared modules against the DOM library, with no Node + types, catches the rest, such as `NodeJS.Timeout` types. + +The cache codec uses `node:zlib`, which Node and Workers have and browsers do +not. It is storage, so it stays a host-side module outside the guarded set +and does not move in step 2. ### `fresh` is a shared read-tool input Read-only tools gain an optional `fresh: boolean`, defined once in the -shared tool schema helpers. The tool handler sets a request-scoped bypass -(`AsyncLocalStorage`), and the transport honors it. The tool count does not +shared tool schema helpers. The MCP tool handler sets a request-scoped +bypass with `AsyncLocalStorage`, and the shared transport reads it only +through its `fresh()` callback. The flag is host state: shared code never +touches `AsyncLocalStorage`, which browsers lack. The tool count does not change. Token cost is checked with `pnpm run measure-tokens`, and the description stays short. -### Move MCP methods onto `@sentry/api` by domain - -The order is: - -1. organizations, projects, teams (already partial) -2. issues and events -3. explore, traces, logs -4. releases and replays -5. monitors, alerts, and uptime +### Endpoints move in one PR, not by domain -Endpoints that `@sentry/api` lacks call the shared transport directly and -keep their Zod schemas. Each domain lands as its own PR. Endpoint behavior -is checked against `~/src/sentry`, as `AGENTS.md` requires. +The earlier plan moved MCP methods onto `@sentry/api` one domain per PR. +Issues and events landed that way (#1713). The rest moves in the step 1 +consolidation PR instead, together with the CLI's modules, so neither +product is left half on the old layer. Endpoints that `@sentry/api` lacks +call the shared transport directly and keep their Zod schemas. Endpoint +behavior is checked against `~/src/sentry`, as `AGENTS.md` requires. ### Version alignment first @@ -416,11 +479,18 @@ bumps move both products together. - **Generated `@sentry/api` types disagree with responses.** Keep Zod validation at the MCP boundary. Pin and bump `@sentry/api` through the catalog. -- **Snapshot and eval churn.** Each domain migration lands with its own - snapshot updates and an eval run. +- **One large consolidation PR.** It is harder to review than per-domain + PRs. Both products' existing test suites run against the shared client, + and the PR body lists every intentional behavior change. +- **Zod on the CLI's request path.** The CLI validates with valibot today and + loads Zod only for `sentry mcp`. Shared schemas bring Zod into every API + call. Check CLI startup time and bundle size in the consolidation PR. +- **Node-only APIs creep into shared code.** The lint rule and the + browser-target typecheck fail CI. - **CLI-issued tokens lack MCP scopes.** The MCP and CLI use different OAuth - client IDs and scope lists today. Compare them before deprecating the - standalone login. If they differ, widen the CLI's requested scopes. + client IDs and scope lists today. When the deferred `sentry mcp` phase is + picked up, compare them before deprecating the standalone login. If they + differ, widen the CLI's requested scopes. ## Migration Plan @@ -429,13 +499,17 @@ bumps move both products together. 2. Land the pure `toolkit-core` modules (pagination, cache policy, codecs), with the CLI switched over and no behavior change. 3. Land `api-transport` and the CLI switch-over. -4. Land the MCP transport injection and `sentry mcp` passing the CLI fetch, - then the hosted memory store behind a flag (`MCP_RESPONSE_CACHE=memory`). +4. Land the MCP on the shared transport (#1722), then the hosted memory + store behind a flag (`MCP_RESPONSE_CACHE=memory`). 5. Enable the hosted memory cache. Add the R2 binding and its allowlist behind `MCP_RESPONSE_CACHE=tiered`, and enable it only after the server headers ship and the security sign-off. -6. Run the domain-by-domain `@sentry/api` migration PRs. -7. Deprecate the standalone MCP login. +6. Step 1: land the consolidation PR, with the portability guard, the shared + client, and both products switched over. +7. Step 2: copy the client into getsentry/sentry-api-schema, publish + `@sentry/api`, switch toolkit to it, and delete the toolkit-core copies. +8. Later phase: `sentry mcp` on the CLI session, and deprecating the + standalone MCP login. **Rollback:** the hosted cache is gated by an env flag, and setting it to `off` restores today's uncached behavior. Each CLI step is a refactor that diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index 6aa3c0af5..a2bb59bc6 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -17,6 +17,18 @@ features land in one product and not the other, and the two pin different - 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 @@ -41,9 +53,22 @@ The shared layer must therefore sit below both products, on `@sentry/api`. 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 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 one PR + that also switches both products and deletes the code it replaces. It + covers endpoint wrappers by domain on top of `@sentry/api` operations, + the error types (`ApiError`, rate limits, `retryAfterMs`), response + validation (`@sentry/api/zod` where it covers the endpoint), 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 @@ -51,19 +76,21 @@ The shared layer must therefore sit below both products, on `@sentry/api`. 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 + 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. -- **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`. +- A later phase, after step 2: `sentry mcp` passes the CLI's authenticated + fetch so local MCP sessions reuse the CLI login and on-disk cache, the + CLI's OAuth scopes are widened to cover MCP tools, and the standalone + `@sentry/mcp-server` login is deprecated. Using the CLI as an SDK from + the MCP belongs to that phase too. ## Capabilities @@ -76,8 +103,9 @@ Out of scope, tracked separately: 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. +- `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 @@ -86,19 +114,23 @@ None. ## Impact - `packages/toolkit-core`: new `pagination`, `response-cache`, retry, and - transport modules, with tests. + 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/client.ts`: the transport is replaced and - methods move to `@sentry/api`. +- `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-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 allowlisted detail responses, and a wrangler config change. -- `pnpm-workspace.yaml`: a catalog entry for `@sentry/api`. +- `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`. diff --git a/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md b/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md deleted file mode 100644 index 16ad37513..000000000 --- a/openspec/changes/unify-sentry-api-layer/specs/mcp-host-transport/spec.md +++ /dev/null @@ -1,64 +0,0 @@ -# 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` diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-client/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-client/spec.md new file mode 100644 index 000000000..958dc0357 --- /dev/null +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-client/spec.md @@ -0,0 +1,94 @@ +# 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 validated result type and the same error classes + +#### 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 its own 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 diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md index bac7fea02..b515ce78f 100644 --- a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-response-cache/spec.md @@ -106,6 +106,12 @@ from the API and replace the cached entry. The CLI exposes this as `--fresh` - **WHEN** an MCP read tool is called with `fresh: true` - **THEN** every API request made by that call bypasses cached entries +#### Scenario: Fresh scope is host state + +- **WHEN** a host marks a scope fresh, such as one MCP tool call +- **THEN** the host tracks the scope with its own request-scoped state, such as `AsyncLocalStorage` +- **THEN** the shared client reads it only through an injected `fresh()` callback + ### Requirement: Bounded storage Each storage backend SHALL enforce a byte budget over stored bytes. It SHALL diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md index b39a57559..e2c0529ef 100644 --- a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-transport/spec.md @@ -105,8 +105,8 @@ to wait before trying again. An aborted request SHALL stop waiting at once. ### Requirement: Retries do not stack A request SHALL be retried by one layer only. When the MCP sends its -requests through a host-supplied or shared transport, `SentryApiService` -SHALL not retry them again. +requests through the shared transport, `SentryApiService` SHALL not retry +them again. #### Scenario: MCP on the shared transport diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index c2621371b..5250d1361 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -30,13 +30,13 @@ - [x] 5.0 Add a pure, Workers-safe retry policy in `toolkit-core/src/api-retry.ts`: retryable statuses (429, 500, 502, 503, and 504, not every 5xx, since 501, 505, and the rest are deterministic); a 429 retried for any method, because Sentry's `RatelimitMiddleware` rejects it before the view runs; 5xx, timeouts, and network errors retried only for GET, HEAD, and OPTIONS, or for an explicit per-request `idempotent` opt-in, such as content-addressed chunk uploads; the delay from `Retry-After` (seconds or HTTP-date) or, on a 429, `X-Sentry-Rate-Limit-Reset`, kept within the range the response `Date` allows, and a per-wait and total-wait cap; a jittered exponential fallback, used for concurrency-limit 429s too; and an abortable sleep. The 401 refresh is not part of this policy and stays allowed for any method. MCP `request()` adopts it in the same PR, retrying rate-limited requests and filling `ApiRateLimitError.retryAfter` from the headers. Verify with toolkit-core unit tests for each `sentry-api-transport` retry scenario and MCP client tests. Shipped in #1673; the CLI's interim local fix (#1677) switched to this module in #1710. - [x] 5.1 Move auth headers, 401 refresh (any method), timeouts, retry and backoff through `api-retry` (429 for any method; 5xx, timeouts, and network errors for idempotent methods only), redirect validation, and region routing from `cli/src/lib/sentry-client.ts` into `toolkit-core/src/api-transport.ts`, with an injected `CredentialProvider`, `CacheStore`, logger, and span hook. The CLI used to retry every method on 5xx and timeouts, POSTs included; that was a bug, fixed separately before this move. Verify with transport unit tests. Shipped in #1719. - [x] 5.2 Switch the CLI to the shared transport, wiring in the database credentials, the filesystem store, and telemetry spans. Verify the CLI unit and e2e tests pass. Shipped in #1719. -- [ ] 5.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. Next: in review in #1720, with 6.1 stacked on it. +- [x] 5.3 Add a request-scoped fresh-read bypass to the transport, and verify a test that a bypassed request skips cache reads and replaces the entry. Shipped in #1720. -## 6. MCP transport injection +## 6. MCP on the shared transport -- [ ] 6.1 Add `fetch` to `McpServerOptions`, and route all `SentryApiService` requests and `@sentry/api` calls through it. `SentryApiService` drops its own retry when it moves onto the transport, so retries do not stack. Verify an `mcp-server` test where a supplied fetch receives every tool request, and a test where a rate-limited GET reaches the API at most `maxRetries + 1` times. -- [ ] 6.2 Pass the CLI's authenticated fetch from `cli/src/lib/mcp.ts`. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, and `/mcp-qa` stdio QA passes. -- [ ] 6.3 Add the `fresh` input to read tools through the shared schema helpers. Regenerate definitions, then verify `pnpm run measure-tokens` and the tool tests pass. +- [ ] 6.1 Route every `SentryApiService` request, including `@sentry/api` operations, through `createApiTransport`, and drop `SentryApiService`'s own retry loop so retries do not stack. No host-supplied fetch: hosts share the client from group 8 instead. Verify a test where a rate-limited GET reaches the API at most `maxRetries + 1` times, and one where an in-flight abort keeps the caller's reason. In review in #1722. +- 6.2 Moved to group 11 (deferred): `sentry mcp` passing the CLI's fetch. +- [ ] 6.3 Add the `fresh` input to read tools through the shared schema helpers. The request-scoped flag lives in the MCP host (`AsyncLocalStorage` in `mcp-core`'s tool handler), and the shared client reads it only through the transport's `fresh()` callback. Regenerate definitions, then verify `pnpm run measure-tokens` and the tool tests pass. ## 7. Hosted cache @@ -46,19 +46,20 @@ - [ ] 7.4 Get privacy and security sign-off on the 5-minute stale window after access revocation or deletion before setting `MCP_RESPONSE_CACHE=tiered` in production. Record the decision in `docs/cloudflare/architecture.md`. - [ ] 7.5 Add cache hit, miss, and eviction metrics and memory-budget logging. Document them in `docs/operations/monitoring.md`, and verify they appear in a preview deploy. -## 8. Move MCP endpoints onto `@sentry/api` +## 8. Step 1: consolidate the API layer in `toolkit-core` -- [ ] 8.1 Organizations, projects, and teams. Verify the MCP tests and snapshots for the affected tools. -- [x] 8.2 Issues and events. Verify the MCP tests and snapshots for the affected tools. Shipped in #1713; issue notes, activity, user reports, event committers, writes, and the attachment download stay on the shared transport until 6.1 or until `@sentry/api` gains operations. -- [ ] 8.3 Explore, traces, and logs. Verify the MCP tests, snapshots, and a search-events eval run. -- [ ] 8.4 Releases and replays. Verify the MCP tests and snapshots for the affected tools. -- [ ] 8.5 Monitors, alerts, and uptime, keeping the shared-transport fallback for the endpoints `@sentry/api` lacks. Verify the MCP tests and snapshots. -- [ ] 8.6 Update `docs/contributing/api-patterns.md` for the new client layering, and verify `pnpm run docs:check` passes. +One PR, stacked on 6.1, not one per domain. Earlier, issues and events moved onto `@sentry/api` operations inside `SentryApiService` (#1713, formerly 8.2); the rest of the old per-domain plan folds into this group. -## 9. Standalone login deprecation +- [ ] 8.1 Audit the existing `toolkit-core` modules for anything that will not run in a browser, fix it, and add a guard: a lint rule that bans `node:*` imports and Node globals (`Buffer`, `process`, `AsyncLocalStorage`, and the like) in the shared modules, and a typecheck of them against the DOM library with no Node types. Modules that are host storage, such as the `node:zlib` cache codec, stay outside the guarded set and do not move in step 2. Verify the guard fails on a planted `node:` import. +- [ ] 8.2 Create `toolkit-core/src/api-client/` with the layout it will have inside `@sentry/api`: core request handling, errors (`ApiError`, rate limits with `retryAfterMs`), response validation (`@sentry/api/zod` where it covers the endpoint, hand-written Zod schemas otherwise), pagination, cache policy, retry policy, transport, and endpoint wrappers by domain on top of `@sentry/api` operations. Credentials, cache storage, the fresh-read scope, logging, and telemetry are injected interfaces. Verify toolkit-core unit tests for the core and each domain. +- [ ] 8.3 Switch the MCP: `SentryApiService`'s endpoints, schemas, and errors come from the shared client, and the replaced code is deleted from `mcp-core`. The MCP keeps its Zod-to-tool output formatting. Verify the `mcp-core`, `mcp-server`, and `mcp-cloudflare` suites and snapshots pass. +- [ ] 8.4 Switch the CLI: its `lib/api/` endpoint wrappers, API errors, and pagination come from the shared client, and the replaced code is deleted. The CLI keeps credential storage and refresh, the SQLite and filesystem cache store, region lookup storage, telemetry, and the mapping of API errors to exit codes. Verify the CLI unit tests pass. +- [ ] 8.5 Keep behavior the same, and call out every intentional change in the PR body. Update `docs/contributing/api-patterns.md` and `packages/toolkit-core/README.md` for the new layering, and verify `pnpm run docs:check` passes. -- [ ] 9.1 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed. Verify a test that the CLI's scope list covers every MCP tool's required scopes. -- [ ] 9.2 Add a deprecation notice to the standalone `@sentry/mcp-server` auth flow and docs pointing to `sentry mcp`. Verify with an output test and an update to `docs/operations/stdio-auth.md`. +## 9. Step 2: move the shared client into `@sentry/api` + +- [ ] 9.1 Copy `toolkit-core/src/api-client/` and the modules it depends on into getsentry/sentry-api-schema with the same layout, and publish a new `@sentry/api` version. Verify the package's own tests there, and that its browser entry builds and makes a request with a mocked `fetch`, so the Sentry frontend can use it with session credentials and no cache store. +- [ ] 9.2 Bump `@sentry/api` in the pnpm catalog, switch the CLI and MCP imports to `@sentry/api`, and delete the toolkit-core copies in the same PR. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. ## 10. Trained compression dictionary (follow-up) @@ -66,12 +67,19 @@ - [ ] 10.2 Add dictionary compression with a versioned `dictId` and a startup self-test that detects a silently ignored `dictionary` option (as in workerd today). Verify a test where an ignored dictionary falls back to plain zstd. - [ ] 10.3 On Workers, evaluate WASM zstd for dictionary compression: bundle size, CPU in ms/MB, and ratio against native zstd-3. Record the results in design.md and adopt it only if the memory-tier gain outweighs the CPU cost. -## 11. Integration +## 11. Deferred: MCP on the CLI session + +Picked up after step 2; it gets its own spec then. Using the CLI as an SDK from the MCP is part of this later phase. + +- [ ] 11.1 Pass the CLI's authenticated fetch from `cli/src/lib/mcp.ts` (formerly 6.2). Verify a CLI test where `sentry mcp` tool calls hit the shared cache, and `/mcp-qa` stdio QA passes. +- [ ] 11.2 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed (formerly 9.1). Verify a test that the CLI's scope list covers every MCP tool's required scopes. +- [ ] 11.3 Add a deprecation notice to the standalone `@sentry/mcp-server` auth flow and docs pointing to `sentry mcp` (formerly 9.2). Verify with an output test and an update to `docs/operations/stdio-auth.md`. + +## 12. Integration -- [ ] 11.1 Run `pnpm run tsc && pnpm run lint && pnpm run test` and the MCP eval suite on the final state, and record the results in the PR. +- [ ] 12.1 Run `pnpm run tsc && pnpm run lint && pnpm run test` and the MCP eval suite on the final state, and record the results in the PR. ## Workflow follow-up - Archive the change after review once all tasks are complete. - Follow up on getsentry/sentry#128818 and getsentry/sentry#128828. When the server headers ship, confirm the CLI honors them and enable the R2 tier. -- Propose moving pagination, then the transport, into `@sentry/api`. From 5213bd40802dca05ed72a030130bf283c0d178e1 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:19:31 +0000 Subject: [PATCH 20/28] docs(openspec): Validate shared API responses through Standard Schema The shared client takes host-supplied Standard Schema validators, so the MCP keeps Zod and the CLI keeps valibot, and shared code depends on neither at runtime. Co-Authored-By: BYK --- .../changes/unify-sentry-api-layer/design.md | 33 ++++++++++++------- .../unify-sentry-api-layer/proposal.md | 3 +- .../specs/sentry-api-client/spec.md | 25 ++++++++++++-- .../changes/unify-sentry-api-layer/tasks.md | 2 +- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index 3ff3d8c75..b1e44b09a 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -64,9 +64,8 @@ See proposal.md for motivation. Current state that shapes the approach: - Using `createSentrySDK()`, or the CLI as an SDK in general, from the MCP, and `sentry mcp` passing the CLI's fetch. Both are deferred to a later phase after step 2 (task group 11). -- Replacing every hand-written response schema with a generated one. The - shared client uses `@sentry/api/zod` where it covers the endpoint and - keeps hand-written Zod schemas elsewhere. +- Replacing every hand-written response schema with a generated one, or + moving either product to the other's validation library. ## Decisions @@ -383,10 +382,18 @@ PR. It makes step 2 a copy, not a redesign. rate-limit error with `retryAfterMs` from `Retry-After` or `X-Sentry-Rate-Limit-Reset`. Hosts map them to their own presentation: CLI exit codes and messages, MCP tool results. -- **Validation:** Zod. `@sentry/api/zod` where it covers the endpoint, - hand-written schemas otherwise. The MCP already validates with Zod and - needs Zod for tool schemas. The CLI's valibot schemas for endpoints that - move are converted. +- **Validation:** library-agnostic, through the + [Standard Schema](https://standardschema.dev) interface + (`@standard-schema/spec`), which Zod and valibot both implement. Shared + code depends only on that interface's types, vendored or type-only, and + never on Zod or valibot at runtime. Each wrapper validates with a schema + the host passes in, so the host's schema decides the result type: the + MCP keeps Zod (`@sentry/api/zod` where it covers the endpoint), which + suits long-running servers and is the more widespread library, and the + CLI keeps valibot (`@sentry/api/valibot`), which suits short-lived + processes where startup and bundle size matter. Validation failures + surface as one shared error type with the issues from the Standard + Schema result. - **Pagination, cache policy, retry policy, and transport:** the existing `pagination`, `response-cache` policy, `api-retry`, and `api-transport` modules, which move with it in step 2. @@ -443,7 +450,8 @@ The earlier plan moved MCP methods onto `@sentry/api` one domain per PR. Issues and events landed that way (#1713). The rest moves in the step 1 consolidation PR instead, together with the CLI's modules, so neither product is left half on the old layer. Endpoints that `@sentry/api` lacks -call the shared transport directly and keep their Zod schemas. Endpoint +call the shared transport directly, and hosts keep their own schemas for +them. Endpoint behavior is checked against `~/src/sentry`, as `AGENTS.md` requires. ### Version alignment first @@ -482,9 +490,12 @@ bumps move both products together. - **One large consolidation PR.** It is harder to review than per-domain PRs. Both products' existing test suites run against the shared client, and the PR body lists every intentional behavior change. -- **Zod on the CLI's request path.** The CLI validates with valibot today and - loads Zod only for `sentry mcp`. Shared schemas bring Zod into every API - call. Check CLI startup time and bundle size in the consolidation PR. +- **Two validation libraries.** The MCP's Zod schemas and the CLI's valibot + schemas for the same endpoint can drift apart. Standard Schema keeps the + shared client out of that choice, so neither product pays for the other's + library, and the CLI keeps valibot off Zod's startup cost. Where + `@sentry/api` generates both (`./zod` and `./valibot`), hosts use the + generated schemas, which come from one OpenAPI source. - **Node-only APIs creep into shared code.** The lint rule and the browser-target typecheck fail CI. - **CLI-issued tokens lack MCP scopes.** The MCP and CLI use different OAuth diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index a2bb59bc6..3cafb806d 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -59,7 +59,8 @@ The plan has two steps: that also switches both products and deletes the code it replaces. It covers endpoint wrappers by domain on top of `@sentry/api` operations, the error types (`ApiError`, rate limits, `retryAfterMs`), response - validation (`@sentry/api/zod` where it covers the endpoint), pagination, + 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, diff --git a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-client/spec.md b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-client/spec.md index 958dc0357..f6a4fa646 100644 --- a/openspec/changes/unify-sentry-api-layer/specs/sentry-api-client/spec.md +++ b/openspec/changes/unify-sentry-api-layer/specs/sentry-api-client/spec.md @@ -21,12 +21,33 @@ shared client covers. - **WHEN** the CLI and an MCP tool fetch the same resource - **THEN** both call the same shared endpoint wrapper -- **THEN** both get the same validated result type and the same error classes +- **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 its own schema +- **THEN** the shared wrapper calls it through the shared transport and validates the response with the host's schema ### Requirement: Portable shared code diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 5250d1361..72a0a87c3 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -51,7 +51,7 @@ One PR, stacked on 6.1, not one per domain. Earlier, issues and events moved onto `@sentry/api` operations inside `SentryApiService` (#1713, formerly 8.2); the rest of the old per-domain plan folds into this group. - [ ] 8.1 Audit the existing `toolkit-core` modules for anything that will not run in a browser, fix it, and add a guard: a lint rule that bans `node:*` imports and Node globals (`Buffer`, `process`, `AsyncLocalStorage`, and the like) in the shared modules, and a typecheck of them against the DOM library with no Node types. Modules that are host storage, such as the `node:zlib` cache codec, stay outside the guarded set and do not move in step 2. Verify the guard fails on a planted `node:` import. -- [ ] 8.2 Create `toolkit-core/src/api-client/` with the layout it will have inside `@sentry/api`: core request handling, errors (`ApiError`, rate limits with `retryAfterMs`), response validation (`@sentry/api/zod` where it covers the endpoint, hand-written Zod schemas otherwise), pagination, cache policy, retry policy, transport, and endpoint wrappers by domain on top of `@sentry/api` operations. Credentials, cache storage, the fresh-read scope, logging, and telemetry are injected interfaces. Verify toolkit-core unit tests for the core and each domain. +- [ ] 8.2 Create `toolkit-core/src/api-client/` with the layout it will have inside `@sentry/api`: core request handling, errors (`ApiError`, rate limits with `retryAfterMs`), response validation through the Standard Schema interface with host-supplied schemas (Zod in the MCP, valibot in the CLI; no Zod or valibot runtime dependency in shared code), pagination, cache policy, retry policy, transport, and endpoint wrappers by domain on top of `@sentry/api` operations. Credentials, cache storage, the fresh-read scope, logging, and telemetry are injected interfaces. Verify toolkit-core unit tests for the core and each domain. - [ ] 8.3 Switch the MCP: `SentryApiService`'s endpoints, schemas, and errors come from the shared client, and the replaced code is deleted from `mcp-core`. The MCP keeps its Zod-to-tool output formatting. Verify the `mcp-core`, `mcp-server`, and `mcp-cloudflare` suites and snapshots pass. - [ ] 8.4 Switch the CLI: its `lib/api/` endpoint wrappers, API errors, and pagination come from the shared client, and the replaced code is deleted. The CLI keeps credential storage and refresh, the SQLite and filesystem cache store, region lookup storage, telemetry, and the mapping of API errors to exit codes. Verify the CLI unit tests pass. - [ ] 8.5 Keep behavior the same, and call out every intentional change in the PR body. Update `docs/contributing/api-patterns.md` and `packages/toolkit-core/README.md` for the new layering, and verify `pnpm run docs:check` passes. From 420a5bc6914bc8427c74868d0410b9a4e70ff995 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:48:55 +0000 Subject: [PATCH 21/28] docs(openspec): Keep MCP dependencies pointing down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dependencies run cli → mcp-core → toolkit-core, and mcp-cloudflare → mcp-core; mcp-core never imports the CLI. The MCP shares the CLI SDK's operations by moving them into the shared client, and the CLI's host-only capabilities become optional interfaces that `sentry mcp` injects. Task group 11 picks this up, alongside the @sentry/mcp-server retirement and its lint and manifest cycle checks. Co-Authored-By: BYK --- .../changes/unify-sentry-api-layer/design.md | 58 +++++++++++++++++-- .../unify-sentry-api-layer/proposal.md | 7 ++- .../changes/unify-sentry-api-layer/tasks.md | 12 ++-- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index b1e44b09a..2b254b4cf 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -9,6 +9,8 @@ See proposal.md for motivation. Current state that shapes the approach: `cli/src/lib/mcp.ts` already passes credentials into `runMcpServer()` through `resolveAccessToken` and `onUpstreamUnauthorized`. Shared code must therefore live below both products, in `toolkit-core`. + `@sentry/mcp-server` is being retired in favor of `sentry mcp`, after + which the CLI depends on `mcp-core` directly. - **CLI request stack.** - `sentry-client.ts` handles auth, refresh on 401, timeouts, GET retries, redirect validation, and custom headers. @@ -61,9 +63,11 @@ See proposal.md for motivation. Current state that shapes the approach: - Client-side caching in the Sentry frontend. Browser HTTP caching driven by server headers covers that (getsentry/sentry#128818). -- Using `createSentrySDK()`, or the CLI as an SDK in general, from the MCP, - and `sentry mcp` passing the CLI's fetch. Both are deferred to a later - phase after step 2 (task group 11). +- Importing the CLI package from the MCP, including `createSentrySDK()`. + The MCP reaches the same operations through the shared client instead + (see "Dependencies point down"). Moving the CLI SDK's operations down and + injecting the CLI's host capabilities into `sentry mcp` are deferred to a + later phase after step 2 (task group 11). - Replacing every hand-written response schema with a generated one, or moving either product to the other's validation library. @@ -434,6 +438,45 @@ The cache codec uses `node:zlib`, which Node and Workers have and browsers do not. It is storage, so it stays a host-side module outside the guarded set and does not move in step 2. +### Dependencies point down; the MCP never imports the CLI + +Package dependencies only point down: + +``` +cli → mcp-core → toolkit-core (→ @sentry/api) +mcp-cloudflare → mcp-core +``` + +`mcp-core` never imports `sentry`, not even type-only. `sentry mcp` is the +composition root that builds an MCP server from `mcp-core` and the CLI's +host pieces. + +"The MCP uses the CLI SDK underneath" is done by moving the CLI SDK's +portable operations down into the shared client layer, not by importing +the CLI. `createSentrySDK()`, the CLI commands, and the MCP tools then +become thin faces over the same operations. This also keeps the hosted MCP +working on Workers, since the CLI SDK is Node-only today (SQLite, the +filesystem, and `process`). + +Capabilities only the CLI host has are optional interfaces declared in +`mcp-core` or `toolkit-core`: + +- the SQLite response cache store +- the region cache +- DSN detection from local files +- file uploads + +`sentry mcp` injects the CLI's implementations. `mcp-cloudflare` injects +its own, such as the memory cache store, or omits them, and tools that +need a missing capability say so or stay unavailable. + +Two checks enforce the direction, both added by the `@sentry/mcp-server` +retirement PR: a lint rule that bans importing `sentry` from `mcp-core` +and `mcp-cloudflare`, type-only imports included, and a check over the +workspace package manifests that fails on a dependency cycle. That PR also +moves the MCP registry entry (`server.json`) from `@sentry/mcp-server` to +`sentry mcp`. + ### `fresh` is a shared read-tool input Read-only tools gain an optional `fresh: boolean`, defined once in the @@ -500,7 +543,7 @@ bumps move both products together. browser-target typecheck fail CI. - **CLI-issued tokens lack MCP scopes.** The MCP and CLI use different OAuth client IDs and scope lists today. When the deferred `sentry mcp` phase is - picked up, compare them before deprecating the standalone login. If they + picked up, compare them before relying on the CLI session for MCP tools. If they differ, widen the CLI's requested scopes. ## Migration Plan @@ -519,8 +562,11 @@ bumps move both products together. client, and both products switched over. 7. Step 2: copy the client into getsentry/sentry-api-schema, publish `@sentry/api`, switch toolkit to it, and delete the toolkit-core copies. -8. Later phase: `sentry mcp` on the CLI session, and deprecating the - standalone MCP login. +8. Later phase: move the CLI SDK's portable operations into the shared + client, and have `sentry mcp` inject the CLI's host capabilities + (session, cache store, region cache, DSN detection, file uploads). + `@sentry/mcp-server` is retired separately, with the registry entry + moved to `sentry mcp`. **Rollback:** the hosted cache is gated by an env flag, and setting it to `off` restores today's uncached behavior. Each CLI step is a refactor that diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index 3cafb806d..841b1fc67 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -89,9 +89,10 @@ Out of scope, tracked separately: (getsentry/sentry#128818, prototype in getsentry/sentry#128828). - A later phase, after step 2: `sentry mcp` passes the CLI's authenticated fetch so local MCP sessions reuse the CLI login and on-disk cache, the - CLI's OAuth scopes are widened to cover MCP tools, and the standalone - `@sentry/mcp-server` login is deprecated. Using the CLI as an SDK from - the MCP belongs to that phase too. + CLI's OAuth scopes are widened to cover MCP tools, and the CLI SDK's + portable operations move into the shared client so the CLI and MCP share + them without the MCP importing the CLI. `@sentry/mcp-server` is retired + in favor of `sentry mcp` in its own PR. ## Capabilities diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 72a0a87c3..821ab8f9b 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -67,13 +67,15 @@ One PR, stacked on 6.1, not one per domain. Earlier, issues and events moved ont - [ ] 10.2 Add dictionary compression with a versioned `dictId` and a startup self-test that detects a silently ignored `dictionary` option (as in workerd today). Verify a test where an ignored dictionary falls back to plain zstd. - [ ] 10.3 On Workers, evaluate WASM zstd for dictionary compression: bundle size, CPU in ms/MB, and ratio against native zstd-3. Record the results in design.md and adopt it only if the memory-tier gain outweighs the CPU cost. -## 11. Deferred: MCP on the CLI session +## 11. Deferred: MCP on the CLI's operations and host capabilities -Picked up after step 2; it gets its own spec then. Using the CLI as an SDK from the MCP is part of this later phase. +Picked up after step 2; it gets its own spec then. Dependencies only point down (`cli → mcp-core → toolkit-core`, `mcp-cloudflare → mcp-core`), and `mcp-core` never imports `sentry`, not even type-only. See "Dependencies point down" in design.md. -- [ ] 11.1 Pass the CLI's authenticated fetch from `cli/src/lib/mcp.ts` (formerly 6.2). Verify a CLI test where `sentry mcp` tool calls hit the shared cache, and `/mcp-qa` stdio QA passes. -- [ ] 11.2 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed (formerly 9.1). Verify a test that the CLI's scope list covers every MCP tool's required scopes. -- [ ] 11.3 Add a deprecation notice to the standalone `@sentry/mcp-server` auth flow and docs pointing to `sentry mcp` (formerly 9.2). Verify with an output test and an update to `docs/operations/stdio-auth.md`. +- [ ] 11.1 Move the CLI SDK's portable operations into the shared client layer, so `createSentrySDK()`, the CLI commands, and the MCP tools are thin faces over the same operations. Verify the CLI SDK tests and the `mcp-core` and `mcp-cloudflare` suites pass, and that `mcp-core` has no import of `sentry`. +- [ ] 11.2 Declare the CLI-only host capabilities as optional interfaces in `mcp-core` or `toolkit-core`: the SQLite response cache store, the region cache, DSN detection from local files, and file uploads. `sentry mcp` injects the CLI's implementations and authenticated fetch from `cli/src/lib/mcp.ts` (formerly 6.2); `mcp-cloudflare` injects its own or omits them. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, a Workers test with the capabilities omitted, and `/mcp-qa` stdio QA. +- [ ] 11.3 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed (formerly 9.1). Verify a test that the CLI's scope list covers every MCP tool's required scopes. +- [ ] 11.4 Retire `@sentry/mcp-server` in favor of `sentry mcp`, and move the MCP registry entry (`server.json`) to `sentry mcp` (replaces the former deprecation notice, 9.2). Tracked by the `@sentry/mcp-server` retirement PR. Verify `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` point to `sentry mcp`. +- [ ] 11.5 Enforce the dependency direction: a lint rule that bans importing `sentry` from `mcp-core` and `mcp-cloudflare`, type-only imports included, and a manifest check that fails on a workspace dependency cycle. Both land in the retirement PR. Verify each fails on a planted import or dependency. ## 12. Integration From e51ad5290844121a81b1ff3e07999d389554273a Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:35:44 +0000 Subject: [PATCH 22/28] docs(openspec): Land step 1 as stacked PRs by layer Group 8 now ships as a stack split by layer: the foundation (#1732), the transport-layer move, endpoint wrappers with the MCP switch, the CLI host seam and switch, then merged shared wrappers. 6.1 is done in #1722. Co-Authored-By: BYK --- .../changes/unify-sentry-api-layer/design.md | 33 ++++++++++++------- .../unify-sentry-api-layer/proposal.md | 5 +-- .../changes/unify-sentry-api-layer/tasks.md | 16 +++++---- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index 2b254b4cf..3f893bb67 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -366,8 +366,7 @@ for every fix. So: 1. **Step 1, in `toolkit-core` (task group 8).** Build the shared client in `toolkit-core/src/api-client/`, with the file layout it will have inside `@sentry/api`. Switch the MCP and the CLI onto it and delete the code it - replaces, in one PR rather than one per domain: two half-migrated layers - would cost more than one large review. + replaces, in stacked PRs by layer rather than by domain (see below). 2. **Step 2, into `@sentry/api` (task group 9).** Copy the modules into getsentry/sentry-api-schema unchanged, publish a new `@sentry/api`, switch toolkit's imports, and delete the toolkit-core copies. @@ -487,12 +486,22 @@ touches `AsyncLocalStorage`, which browsers lack. The tool count does not change. Token cost is checked with `pnpm run measure-tokens`, and the description stays short. -### Endpoints move in one PR, not by domain +### Endpoints move in stacked PRs by layer, not by domain The earlier plan moved MCP methods onto `@sentry/api` one domain per PR. -Issues and events landed that way (#1713). The rest moves in the step 1 -consolidation PR instead, together with the CLI's modules, so neither -product is left half on the old layer. Endpoints that `@sentry/api` lacks +Issues and events landed that way (#1713). The rest moves in a stack of +step 1 PRs split by layer instead: + +1. The foundation: errors, `apiPath`, validation, and response reading + (#1732). +2. Pagination, cache policy, retry, and transport moved under + `api-client/`. +3. Endpoint wrappers by domain, with the MCP switched onto them. +4. The CLI host seam, with the CLI switched. +5. One wrapper for each endpoint both products call. + +Each PR switches every caller of the layer it moves, so neither product is +left half on the old layer for that layer. Endpoints that `@sentry/api` lacks call the shared transport directly, and hosts keep their own schemas for them. Endpoint behavior is checked against `~/src/sentry`, as `AGENTS.md` requires. @@ -530,9 +539,10 @@ bumps move both products together. - **Generated `@sentry/api` types disagree with responses.** Keep Zod validation at the MCP boundary. Pin and bump `@sentry/api` through the catalog. -- **One large consolidation PR.** It is harder to review than per-domain - PRs. Both products' existing test suites run against the shared client, - and the PR body lists every intentional behavior change. +- **A long stack of PRs.** Each PR waits on the one below it and needs a + rebase when that one merges. Splitting by layer keeps each PR coherent, + both products' test suites run against every PR, and each PR body lists + its intentional behavior changes. - **Two validation libraries.** The MCP's Zod schemas and the CLI's valibot schemas for the same endpoint can drift apart. Standard Schema keeps the shared client out of that choice, so neither product pays for the other's @@ -558,8 +568,9 @@ bumps move both products together. 5. Enable the hosted memory cache. Add the R2 binding and its allowlist behind `MCP_RESPONSE_CACHE=tiered`, and enable it only after the server headers ship and the security sign-off. -6. Step 1: land the consolidation PR, with the portability guard, the shared - client, and both products switched over. +6. Step 1: land the stacked PRs by layer: the foundation with the + portability guard, the transport-layer move, the endpoint wrappers with + the MCP switch, the CLI switch, and the merged shared wrappers. 7. Step 2: copy the client into getsentry/sentry-api-schema, publish `@sentry/api`, switch toolkit to it, and delete the toolkit-core copies. 8. Later phase: move the CLI SDK's portable operations into the shared diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index 841b1fc67..b329ef2a9 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -55,8 +55,9 @@ The plan has two steps: 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 one PR - that also switches both products and deletes the code it replaces. It +- **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 diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 821ab8f9b..56df796f3 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -34,7 +34,7 @@ ## 6. MCP on the shared transport -- [ ] 6.1 Route every `SentryApiService` request, including `@sentry/api` operations, through `createApiTransport`, and drop `SentryApiService`'s own retry loop so retries do not stack. No host-supplied fetch: hosts share the client from group 8 instead. Verify a test where a rate-limited GET reaches the API at most `maxRetries + 1` times, and one where an in-flight abort keeps the caller's reason. In review in #1722. +- [x] 6.1 Route every `SentryApiService` request, including `@sentry/api` operations, through `createApiTransport`, and drop `SentryApiService`'s own retry loop so retries do not stack. No host-supplied fetch: hosts share the client from group 8 instead. Verify a test where a rate-limited GET reaches the API at most `maxRetries + 1` times, and one where an in-flight abort keeps the caller's reason. Done in #1722. - 6.2 Moved to group 11 (deferred): `sentry mcp` passing the CLI's fetch. - [ ] 6.3 Add the `fresh` input to read tools through the shared schema helpers. The request-scoped flag lives in the MCP host (`AsyncLocalStorage` in `mcp-core`'s tool handler), and the shared client reads it only through the transport's `fresh()` callback. Regenerate definitions, then verify `pnpm run measure-tokens` and the tool tests pass. @@ -48,13 +48,15 @@ ## 8. Step 1: consolidate the API layer in `toolkit-core` -One PR, stacked on 6.1, not one per domain. Earlier, issues and events moved onto `@sentry/api` operations inside `SentryApiService` (#1713, formerly 8.2); the rest of the old per-domain plan folds into this group. +Stacked PRs by layer, not by domain. Each one is based on the PR below it and keeps both products green. Earlier, issues and events moved onto `@sentry/api` operations inside `SentryApiService` (#1713, formerly 8.2); the rest of the old per-domain plan folds into this group. Mechanical moves are codemods run from outside the tree. Each run is a generated commit whose message gives the command, and the scripts go in the PR description. -- [ ] 8.1 Audit the existing `toolkit-core` modules for anything that will not run in a browser, fix it, and add a guard: a lint rule that bans `node:*` imports and Node globals (`Buffer`, `process`, `AsyncLocalStorage`, and the like) in the shared modules, and a typecheck of them against the DOM library with no Node types. Modules that are host storage, such as the `node:zlib` cache codec, stay outside the guarded set and do not move in step 2. Verify the guard fails on a planted `node:` import. -- [ ] 8.2 Create `toolkit-core/src/api-client/` with the layout it will have inside `@sentry/api`: core request handling, errors (`ApiError`, rate limits with `retryAfterMs`), response validation through the Standard Schema interface with host-supplied schemas (Zod in the MCP, valibot in the CLI; no Zod or valibot runtime dependency in shared code), pagination, cache policy, retry policy, transport, and endpoint wrappers by domain on top of `@sentry/api` operations. Credentials, cache storage, the fresh-read scope, logging, and telemetry are injected interfaces. Verify toolkit-core unit tests for the core and each domain. -- [ ] 8.3 Switch the MCP: `SentryApiService`'s endpoints, schemas, and errors come from the shared client, and the replaced code is deleted from `mcp-core`. The MCP keeps its Zod-to-tool output formatting. Verify the `mcp-core`, `mcp-server`, and `mcp-cloudflare` suites and snapshots pass. -- [ ] 8.4 Switch the CLI: its `lib/api/` endpoint wrappers, API errors, and pagination come from the shared client, and the replaced code is deleted. The CLI keeps credential storage and refresh, the SQLite and filesystem cache store, region lookup storage, telemetry, and the mapping of API errors to exit codes. Verify the CLI unit tests pass. -- [ ] 8.5 Keep behavior the same, and call out every intentional change in the PR body. Update `docs/contributing/api-patterns.md` and `packages/toolkit-core/README.md` for the new layering, and verify `pnpm run docs:check` passes. +- [ ] 8.1 Audit the existing `toolkit-core` modules for anything that will not run in a browser, fix it, and add a guard: a lint rule that bans `node:*` imports and Node globals (`Buffer`, `process`, `AsyncLocalStorage`, and the like) in the shared modules, and a typecheck of them against the DOM library with no Node types. Modules that are host storage, such as the `node:zlib` cache codec, stay outside the guarded set and do not move in step 2. Verify the guard fails on a planted `node:` import. In review in #1732. +- [ ] 8.2 Foundation: create `toolkit-core/src/api-client/` with errors (`ApiError`, rate limits with `retryAfterMs`), `apiPath`, response validation through the Standard Schema interface with host-supplied schemas (Zod in the MCP, valibot in the CLI; no Zod or valibot runtime dependency in shared code), and response reading. The MCP and the CLI use them. Verify the toolkit-core unit tests and both products' suites pass. In review in #1732. +- [ ] 8.3 Move pagination, cache policy, retry policy, and transport under `api-client/`, with the layout they will have inside `@sentry/api`, and rewrite every importer. Verify tsc, lint, and every suite pass with no behavior change. +- [ ] 8.4 Add the core request handling and endpoint wrappers by domain on top of `@sentry/api` operations, and switch `SentryApiService` onto them: its endpoints, schemas, and errors come from the shared client, and the replaced code is deleted from `mcp-core`. The MCP keeps its Zod-to-tool output formatting. Credentials, cache storage, the fresh-read scope, logging, and telemetry are injected interfaces. Verify toolkit-core unit tests for the core and each domain, and the `mcp-core`, `mcp-server`, and `mcp-cloudflare` suites and snapshots. +- [ ] 8.5 Add the CLI host seam and switch the CLI: its `lib/api/` endpoint wrappers, API errors, and pagination come from the shared client, and the replaced code is deleted. The CLI keeps credential storage and refresh, the SQLite and filesystem cache store, region lookup storage, telemetry, and the mapping of API errors to exit codes. Verify the CLI unit tests pass. +- [ ] 8.6 Merge the wrappers both products call for the same endpoint into one, so the CLI and MCP share it. Verify both products' suites pass. +- [ ] 8.7 In every PR, keep behavior the same and call out each intentional change in the PR body. Update `docs/contributing/api-patterns.md` and `packages/toolkit-core/README.md` for the new layering, and verify `pnpm run docs:check` passes. ## 9. Step 2: move the shared client into `@sentry/api` From 5af8eb6399bb9dcfb33b7d37ba02d9b433468fc9 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:26:17 +0000 Subject: [PATCH 23/28] docs(openspec): Mark the api-client foundation done (#1732) and track 8.3 and 8.4 Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 56df796f3..acc8f338a 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -50,10 +50,10 @@ Stacked PRs by layer, not by domain. Each one is based on the PR below it and keeps both products green. Earlier, issues and events moved onto `@sentry/api` operations inside `SentryApiService` (#1713, formerly 8.2); the rest of the old per-domain plan folds into this group. Mechanical moves are codemods run from outside the tree. Each run is a generated commit whose message gives the command, and the scripts go in the PR description. -- [ ] 8.1 Audit the existing `toolkit-core` modules for anything that will not run in a browser, fix it, and add a guard: a lint rule that bans `node:*` imports and Node globals (`Buffer`, `process`, `AsyncLocalStorage`, and the like) in the shared modules, and a typecheck of them against the DOM library with no Node types. Modules that are host storage, such as the `node:zlib` cache codec, stay outside the guarded set and do not move in step 2. Verify the guard fails on a planted `node:` import. In review in #1732. -- [ ] 8.2 Foundation: create `toolkit-core/src/api-client/` with errors (`ApiError`, rate limits with `retryAfterMs`), `apiPath`, response validation through the Standard Schema interface with host-supplied schemas (Zod in the MCP, valibot in the CLI; no Zod or valibot runtime dependency in shared code), and response reading. The MCP and the CLI use them. Verify the toolkit-core unit tests and both products' suites pass. In review in #1732. -- [ ] 8.3 Move pagination, cache policy, retry policy, and transport under `api-client/`, with the layout they will have inside `@sentry/api`, and rewrite every importer. Verify tsc, lint, and every suite pass with no behavior change. -- [ ] 8.4 Add the core request handling and endpoint wrappers by domain on top of `@sentry/api` operations, and switch `SentryApiService` onto them: its endpoints, schemas, and errors come from the shared client, and the replaced code is deleted from `mcp-core`. The MCP keeps its Zod-to-tool output formatting. Credentials, cache storage, the fresh-read scope, logging, and telemetry are injected interfaces. Verify toolkit-core unit tests for the core and each domain, and the `mcp-core`, `mcp-server`, and `mcp-cloudflare` suites and snapshots. +- [x] 8.1 Audit the existing `toolkit-core` modules for anything that will not run in a browser, fix it, and add a guard: a lint rule that bans `node:*` imports and Node globals (`Buffer`, `process`, `AsyncLocalStorage`, and the like) in the shared modules, and a typecheck of them against the DOM library with no Node types. Modules that are host storage, such as the `node:zlib` cache codec, stay outside the guarded set and do not move in step 2. Verify the guard fails on a planted `node:` import. Done in #1732. +- [x] 8.2 Foundation: create `toolkit-core/src/api-client/` with errors (`ApiError`, rate limits with `retryAfterMs`), `apiPath`, response validation through the Standard Schema interface with host-supplied schemas (Zod in the MCP, valibot in the CLI; no Zod or valibot runtime dependency in shared code), and response reading. The MCP and the CLI use them. Verify the toolkit-core unit tests and both products' suites pass. Done in #1732. +- [ ] 8.3 Move pagination, cache policy, retry policy, and transport under `api-client/`, with the layout they will have inside `@sentry/api`, and rewrite every importer. Verify tsc, lint, and every suite pass with no behavior change. In review in #1736. +- [ ] 8.4 Add the core request handling and endpoint wrappers by domain on top of `@sentry/api` operations, and switch `SentryApiService` onto them: its endpoints, schemas, and errors come from the shared client, and the replaced code is deleted from `mcp-core`. The MCP keeps its Zod-to-tool output formatting. Credentials, cache storage, the fresh-read scope, logging, and telemetry are injected interfaces. Verify toolkit-core unit tests for the core and each domain, and the `mcp-core`, `mcp-server`, and `mcp-cloudflare` suites and snapshots. Lands as a core PR, then one PR per domain: the request core (`createApiClientCore`, with `SentryApiService` sending every request through it) is in review in #1737. - [ ] 8.5 Add the CLI host seam and switch the CLI: its `lib/api/` endpoint wrappers, API errors, and pagination come from the shared client, and the replaced code is deleted. The CLI keeps credential storage and refresh, the SQLite and filesystem cache store, region lookup storage, telemetry, and the mapping of API errors to exit codes. Verify the CLI unit tests pass. - [ ] 8.6 Merge the wrappers both products call for the same endpoint into one, so the CLI and MCP share it. Verify both products' suites pass. - [ ] 8.7 In every PR, keep behavior the same and call out each intentional change in the PR body. Update `docs/contributing/api-patterns.md` and `packages/toolkit-core/README.md` for the new layering, and verify `pnpm run docs:check` passes. From 124643033fccef52a3ff8727050eabe066a92ed8 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:46:14 +0000 Subject: [PATCH 24/28] docs(openspec): Make MCP tools a portable face of the CLI The hosted MCP stays on Workers and the CLI stays single-user, so the CLI owns the operations and the MCP tool definitions. Commands split into host-specific resolution, portable operations, and renderers; the MCP renderer is a third output mode next to human and JSON. Tools buffer multi-yield commands and skip unbounded streams. The portable part ships as a `sentry/mcp` entry that `sentry mcp` serves over stdio and mcp-cloudflare bundles. mcp-core keeps the MCP plumbing and still never imports `sentry`. What @sentry/mcp-server becomes is an open question. Co-Authored-By: BYK --- .../changes/unify-sentry-api-layer/design.md | 103 ++++++++++++------ .../unify-sentry-api-layer/proposal.md | 13 ++- .../changes/unify-sentry-api-layer/tasks.md | 16 +-- 3 files changed, 88 insertions(+), 44 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index 3f893bb67..6e808c060 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -63,11 +63,12 @@ See proposal.md for motivation. Current state that shapes the approach: - Client-side caching in the Sentry frontend. Browser HTTP caching driven by server headers covers that (getsentry/sentry#128818). -- Importing the CLI package from the MCP, including `createSentrySDK()`. - The MCP reaches the same operations through the shared client instead - (see "Dependencies point down"). Moving the CLI SDK's operations down and - injecting the CLI's host capabilities into `sentry mcp` are deferred to a - later phase after step 2 (task group 11). +- Calling `createSentrySDK()` from the hosted MCP. It is Node-only. The + hosted MCP reaches the CLI's operations through the portable `sentry/mcp` + entry instead (see "MCP tools are a portable face of the CLI's + commands"), in a later phase after step 2 (task group 11). +- Moving the hosted MCP off Cloudflare Workers, or making the CLI serve + more than one user. - Replacing every hand-written response schema with a generated one, or moving either product to the other's validation library. @@ -437,28 +438,63 @@ The cache codec uses `node:zlib`, which Node and Workers have and browsers do not. It is storage, so it stays a host-side module outside the guarded set and does not move in step 2. -### Dependencies point down; the MCP never imports the CLI +### MCP tools are a portable face of the CLI's commands -Package dependencies only point down: +The hosted MCP stays on Cloudflare Workers, and the CLI stays a local, +single-user tool. Neither changes. So the CLI owns the operations and the +MCP tool definitions, and the part the hosted MCP needs is portable: ``` -cli → mcp-core → toolkit-core (→ @sentry/api) -mcp-cloudflare → mcp-core +sentry (cli) + commands/ Node: resolve inputs from the cwd, config, and DSN; render for the terminal + operations/ portable: one function per command; calls the shared client, returns typed data + mcp/ portable: MCP tool definitions over operations, with the agent renderer +sentry mcp stdio composition root: mcp/ plus the CLI's host capabilities +mcp-cloudflare bundles sentry's portable `sentry/mcp` entry; supplies OAuth, KV, and its cache ``` -`mcp-core` never imports `sentry`, not even type-only. `sentry mcp` is the -composition root that builds an MCP server from `mcp-core` and the CLI's -host pieces. +Each command splits into three layers: + +- **Resolve** is host-specific. The CLI fills organization, project, and + defaults from the cwd, its config, DSN detection, and SQLite. An MCP tool + fills them from tool parameters and session constraints. +- **Operate** is portable. It takes resolved inputs and an `ApiTarget` from + the shared client, and returns the data the command yields today as + `CommandOutput`. +- **Render** has three modes: the command's `output.human` for the + terminal, JSON for `--json` and the SDK, and an agent renderer for MCP. + Commands already yield `CommandOutput` and declare `output: { human }` + (`cli/src/lib/command.ts`), so the MCP renderer is a third output mode, + not a rewrite. + +Commands are async generators, and some yield more than once. An MCP tool +buffers the yields and returns one result, as `createSentrySDK()` already +does when it collects multiple yields into an array +(`cli/src/lib/sdk-invoke.ts`). Commands with unbounded streams, the +follow and tail flags `sdk-invoke.ts` already detects, are not exposed as +MCP tools, or only in a bounded form. + +`operations/` and `mcp/` get the same portability guard as the shared +client: no `node:*` imports or Node globals, and a typecheck against the +DOM library. The CLI package exposes them as a separate entry point, +`sentry/mcp`, so `mcp-cloudflare` bundles them without command handlers, +SQLite, or Ink. `createSentrySDK()` stays Node-only, and the hosted MCP does +not call it. + +Package dependencies: -"The MCP uses the CLI SDK underneath" is done by moving the CLI SDK's -portable operations down into the shared client layer, not by importing -the CLI. `createSentrySDK()`, the CLI commands, and the MCP tools then -become thin faces over the same operations. This also keeps the hosted MCP -working on Workers, since the CLI SDK is Node-only today (SQLite, the -filesystem, and `process`). +``` +sentry → mcp-core → toolkit-core (→ @sentry/api) +mcp-cloudflare → sentry/mcp (portable entry only), mcp-core +``` + +`mcp-core` keeps the MCP plumbing: tool registration, the stdio transport, +scopes and constraints, and shared formatting helpers. It holds no Sentry +operations and never imports `sentry`. As tools move to `sentry/mcp`, their +`mcp-core` copies are deleted, and the MCP evals (`mcp-server-evals`) guard +the agent output. -Capabilities only the CLI host has are optional interfaces declared in -`mcp-core` or `toolkit-core`: +Capabilities only the CLI host has stay optional interfaces: - the SQLite response cache store - the region cache @@ -469,12 +505,11 @@ Capabilities only the CLI host has are optional interfaces declared in its own, such as the memory cache store, or omits them, and tools that need a missing capability say so or stay unavailable. -Two checks enforce the direction, both added by the `@sentry/mcp-server` -retirement PR: a lint rule that bans importing `sentry` from `mcp-core` -and `mcp-cloudflare`, type-only imports included, and a check over the -workspace package manifests that fails on a dependency cycle. That PR also -moves the MCP registry entry (`server.json`) from `@sentry/mcp-server` to -`sentry mcp`. +Two checks enforce the direction. The `@sentry/mcp-server` retirement PR +adds a lint rule that bans importing `sentry` from `mcp-core` and +`mcp-cloudflare`, type-only imports included, and a manifest check that +fails on a workspace dependency cycle. When tools move, the rule is relaxed +to let `mcp-cloudflare` import `sentry/mcp` and nothing else from `sentry`. ### `fresh` is a shared read-tool input @@ -573,11 +608,12 @@ bumps move both products together. the MCP switch, the CLI switch, and the merged shared wrappers. 7. Step 2: copy the client into getsentry/sentry-api-schema, publish `@sentry/api`, switch toolkit to it, and delete the toolkit-core copies. -8. Later phase: move the CLI SDK's portable operations into the shared - client, and have `sentry mcp` inject the CLI's host capabilities - (session, cache store, region cache, DSN detection, file uploads). - `@sentry/mcp-server` is retired separately, with the registry entry - moved to `sentry mcp`. +8. Later phase: split CLI commands into resolve, operate, and render; move + the MCP tools into the CLI's portable `sentry/mcp` entry; and have + `sentry mcp` inject the CLI's host capabilities (session, cache store, + region cache, DSN detection, file uploads) while `mcp-cloudflare` + bundles the same entry. The registry entry moves to `sentry mcp` in the + `@sentry/mcp-server` retirement PR. **Rollback:** the hosted cache is gated by an env flag, and setting it to `off` restores today's uncached behavior. Each CLI step is a refactor that @@ -593,3 +629,8 @@ can be reverted on its own. - Whether the server should send `private, max-age` on more endpoints later, such as stable organization and project metadata. The CLI follows server headers automatically; R2 eligibility stays an explicit allowlist. +- What `@sentry/mcp-server` becomes: retired in favor of `sentry mcp`, or a + zero-dependency stdio proxy to the hosted MCP. The hosted MCP accepts + `Authorization: Sentry-Bearer `, so the proxy needs no OAuth flow, + but it serves only one Sentry host, so self-hosted users would use + `sentry mcp`. diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index b329ef2a9..11d57c10a 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -88,12 +88,13 @@ 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: `sentry mcp` passes the CLI's authenticated - fetch so local MCP sessions reuse the CLI login and on-disk cache, the - CLI's OAuth scopes are widened to cover MCP tools, and the CLI SDK's - portable operations move into the shared client so the CLI and MCP share - them without the MCP importing the CLI. `@sentry/mcp-server` is retired - in favor of `sentry mcp` in its own PR. +- 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. + The future of `@sentry/mcp-server` is decided in its own PR. ## Capabilities diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index acc8f338a..5ba3f1cf2 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -69,15 +69,17 @@ Stacked PRs by layer, not by domain. Each one is based on the PR below it and ke - [ ] 10.2 Add dictionary compression with a versioned `dictId` and a startup self-test that detects a silently ignored `dictionary` option (as in workerd today). Verify a test where an ignored dictionary falls back to plain zstd. - [ ] 10.3 On Workers, evaluate WASM zstd for dictionary compression: bundle size, CPU in ms/MB, and ratio against native zstd-3. Record the results in design.md and adopt it only if the memory-tier gain outweighs the CPU cost. -## 11. Deferred: MCP on the CLI's operations and host capabilities +## 11. Later phase: MCP tools as a portable face of the CLI -Picked up after step 2; it gets its own spec then. Dependencies only point down (`cli → mcp-core → toolkit-core`, `mcp-cloudflare → mcp-core`), and `mcp-core` never imports `sentry`, not even type-only. See "Dependencies point down" in design.md. +Picked up after step 2; it gets its own spec then. The hosted MCP stays on Workers and the CLI stays single-user. See "MCP tools are a portable face of the CLI's commands" in design.md. -- [ ] 11.1 Move the CLI SDK's portable operations into the shared client layer, so `createSentrySDK()`, the CLI commands, and the MCP tools are thin faces over the same operations. Verify the CLI SDK tests and the `mcp-core` and `mcp-cloudflare` suites pass, and that `mcp-core` has no import of `sentry`. -- [ ] 11.2 Declare the CLI-only host capabilities as optional interfaces in `mcp-core` or `toolkit-core`: the SQLite response cache store, the region cache, DSN detection from local files, and file uploads. `sentry mcp` injects the CLI's implementations and authenticated fetch from `cli/src/lib/mcp.ts` (formerly 6.2); `mcp-cloudflare` injects its own or omits them. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, a Workers test with the capabilities omitted, and `/mcp-qa` stdio QA. -- [ ] 11.3 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed (formerly 9.1). Verify a test that the CLI's scope list covers every MCP tool's required scopes. -- [ ] 11.4 Retire `@sentry/mcp-server` in favor of `sentry mcp`, and move the MCP registry entry (`server.json`) to `sentry mcp` (replaces the former deprecation notice, 9.2). Tracked by the `@sentry/mcp-server` retirement PR. Verify `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` point to `sentry mcp`. -- [ ] 11.5 Enforce the dependency direction: a lint rule that bans importing `sentry` from `mcp-core` and `mcp-cloudflare`, type-only imports included, and a manifest check that fails on a workspace dependency cycle. Both land in the retirement PR. Verify each fails on a planted import or dependency. +- [ ] 11.1 Split CLI commands into resolve, operate, and render, domain by domain in stacked PRs: each command's API work moves into a portable function under `operations/` that returns the data the command yields as `CommandOutput`, and the command keeps input resolution and its human renderer. Verify the CLI unit and SDK tests pass with output unchanged. +- [ ] 11.2 Guard `operations/` and `mcp/` with the shared-client portability checks, and expose them as a separate `sentry/mcp` entry point. Verify the guard fails on a planted `node:` import, and that `mcp-cloudflare` bundles the entry for Workers without command handlers, SQLite, or Ink. +- [ ] 11.3 Move the MCP tools into `sentry/mcp`, domain by domain: each tool resolves inputs from its parameters and session constraints, calls the operation, and renders for agents. Tools buffer multi-yield commands into one result; commands with unbounded streams are not exposed, or only in a bounded form. Delete the `mcp-core` copies. Verify the `mcp-core` and `mcp-cloudflare` suites and snapshots, and that the MCP eval suite does not regress. +- [ ] 11.4 Declare the CLI-only host capabilities as optional interfaces: the SQLite response cache store, the region cache, DSN detection from local files, and file uploads. `sentry mcp` injects the CLI's implementations and authenticated fetch from `cli/src/lib/mcp.ts` (formerly 6.2); `mcp-cloudflare` injects its own or omits them. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, a Workers test with the capabilities omitted, and `/mcp-qa` stdio QA. +- [ ] 11.5 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed (formerly 9.1). Verify a test that the CLI's scope list covers every MCP tool's required scopes. +- [ ] 11.6 Move the MCP registry entry (`server.json`) to `sentry mcp`, and decide whether `@sentry/mcp-server` is retired or becomes a zero-dependency stdio proxy to the hosted MCP (see Open Questions in design.md). Tracked by the `@sentry/mcp-server` retirement PR. Verify `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` point to `sentry mcp`. +- [ ] 11.7 Enforce the dependency direction: a lint rule that bans importing `sentry` from `mcp-core`, and from `mcp-cloudflare` except the `sentry/mcp` entry, type-only imports included, and a manifest check that fails on a workspace dependency cycle. The retirement PR adds both with a full ban; 11.3 relaxes it for `sentry/mcp`. Verify each fails on a planted import or dependency. ## 12. Integration From a11def64b85b8e0a99be232f9642ecb3a7e1b403 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:46:14 +0000 Subject: [PATCH 25/28] docs(openspec): Mark 8.3 done (#1736) Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 5ba3f1cf2..ac0896e7e 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -52,7 +52,7 @@ Stacked PRs by layer, not by domain. Each one is based on the PR below it and ke - [x] 8.1 Audit the existing `toolkit-core` modules for anything that will not run in a browser, fix it, and add a guard: a lint rule that bans `node:*` imports and Node globals (`Buffer`, `process`, `AsyncLocalStorage`, and the like) in the shared modules, and a typecheck of them against the DOM library with no Node types. Modules that are host storage, such as the `node:zlib` cache codec, stay outside the guarded set and do not move in step 2. Verify the guard fails on a planted `node:` import. Done in #1732. - [x] 8.2 Foundation: create `toolkit-core/src/api-client/` with errors (`ApiError`, rate limits with `retryAfterMs`), `apiPath`, response validation through the Standard Schema interface with host-supplied schemas (Zod in the MCP, valibot in the CLI; no Zod or valibot runtime dependency in shared code), and response reading. The MCP and the CLI use them. Verify the toolkit-core unit tests and both products' suites pass. Done in #1732. -- [ ] 8.3 Move pagination, cache policy, retry policy, and transport under `api-client/`, with the layout they will have inside `@sentry/api`, and rewrite every importer. Verify tsc, lint, and every suite pass with no behavior change. In review in #1736. +- [x] 8.3 Move pagination, cache policy, retry policy, and transport under `api-client/`, with the layout they will have inside `@sentry/api`, and rewrite every importer. Verify tsc, lint, and every suite pass with no behavior change. Done in #1736. - [ ] 8.4 Add the core request handling and endpoint wrappers by domain on top of `@sentry/api` operations, and switch `SentryApiService` onto them: its endpoints, schemas, and errors come from the shared client, and the replaced code is deleted from `mcp-core`. The MCP keeps its Zod-to-tool output formatting. Credentials, cache storage, the fresh-read scope, logging, and telemetry are injected interfaces. Verify toolkit-core unit tests for the core and each domain, and the `mcp-core`, `mcp-server`, and `mcp-cloudflare` suites and snapshots. Lands as a core PR, then one PR per domain: the request core (`createApiClientCore`, with `SentryApiService` sending every request through it) is in review in #1737. - [ ] 8.5 Add the CLI host seam and switch the CLI: its `lib/api/` endpoint wrappers, API errors, and pagination come from the shared client, and the replaced code is deleted. The CLI keeps credential storage and refresh, the SQLite and filesystem cache store, region lookup storage, telemetry, and the mapping of API errors to exit codes. Verify the CLI unit tests pass. - [ ] 8.6 Merge the wrappers both products call for the same endpoint into one, so the CLI and MCP share it. Verify both products' suites pass. From b4e1b2812329d32b8e3c2e7a9c09de7ed17412de Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:56:33 +0000 Subject: [PATCH 26/28] docs(openspec): Keep @sentry/mcp-server as a stdio proxy @sentry/mcp-server is kept, not retired. It becomes a stdio proxy to the hosted MCP that depends only on the MCP SDK and sends the user's token as `Sentry-Bearer`. For any other Sentry host it points to `npx sentry mcp`, which stays the full local server and owns the registry entry. Task 11.6 and the open question are updated to match. Co-Authored-By: BYK --- .../changes/unify-sentry-api-layer/design.md | 34 +++++++++++++------ .../unify-sentry-api-layer/proposal.md | 3 +- .../changes/unify-sentry-api-layer/tasks.md | 4 +-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index 6e808c060..abdfffdba 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -9,8 +9,9 @@ See proposal.md for motivation. Current state that shapes the approach: `cli/src/lib/mcp.ts` already passes credentials into `runMcpServer()` through `resolveAccessToken` and `onUpstreamUnauthorized`. Shared code must therefore live below both products, in `toolkit-core`. - `@sentry/mcp-server` is being retired in favor of `sentry mcp`, after - which the CLI depends on `mcp-core` directly. + The stdio server is moving into the CLI as `sentry mcp`, after which the + CLI depends on `mcp-core` directly and `@sentry/mcp-server` becomes a + stdio proxy to the hosted MCP with no toolkit dependencies. - **CLI request stack.** - `sentry-client.ts` handles auth, refresh on 401, timeouts, GET retries, redirect validation, and custom headers. @@ -505,12 +506,29 @@ Capabilities only the CLI host has stay optional interfaces: its own, such as the memory cache store, or omits them, and tools that need a missing capability say so or stay unavailable. -Two checks enforce the direction. The `@sentry/mcp-server` retirement PR +Two checks enforce the direction. The PR that moves the stdio server into +`sentry mcp` (#1731) adds a lint rule that bans importing `sentry` from `mcp-core` and `mcp-cloudflare`, type-only imports included, and a manifest check that fails on a workspace dependency cycle. When tools move, the rule is relaxed to let `mcp-cloudflare` import `sentry/mcp` and nothing else from `sentry`. +### `@sentry/mcp-server` stays as a stdio proxy + +`@sentry/mcp-server` is kept, not retired. It becomes a small stdio proxy +to the hosted MCP with no toolkit dependencies, only the MCP SDK. The CLI +has grown large, and a client that only needs Sentry's tools over stdio +should not have to download it. + +- It forwards stdio to the hosted MCP's Streamable HTTP endpoint and sends + the user's token as `Authorization: Sentry-Bearer `, which the + hosted MCP already accepts, so it needs no OAuth flow of its own. +- The hosted MCP serves one Sentry host. For any other host, such as + self-hosted Sentry, the proxy exits with an error that points to + `npx sentry mcp`, which runs the tools locally against any host. +- `sentry mcp` stays the full local server and owns the MCP registry entry + (`server.json`). + ### `fresh` is a shared read-tool input Read-only tools gain an optional `fresh: boolean`, defined once in the @@ -612,8 +630,9 @@ bumps move both products together. the MCP tools into the CLI's portable `sentry/mcp` entry; and have `sentry mcp` inject the CLI's host capabilities (session, cache store, region cache, DSN detection, file uploads) while `mcp-cloudflare` - bundles the same entry. The registry entry moves to `sentry mcp` in the - `@sentry/mcp-server` retirement PR. + bundles the same entry. The registry entry moves to `sentry mcp` in + #1731, and `@sentry/mcp-server` becomes a stdio proxy to the hosted MCP + (see "`@sentry/mcp-server` stays as a stdio proxy"). **Rollback:** the hosted cache is gated by an env flag, and setting it to `off` restores today's uncached behavior. Each CLI step is a refactor that @@ -629,8 +648,3 @@ can be reverted on its own. - Whether the server should send `private, max-age` on more endpoints later, such as stable organization and project metadata. The CLI follows server headers automatically; R2 eligibility stays an explicit allowlist. -- What `@sentry/mcp-server` becomes: retired in favor of `sentry mcp`, or a - zero-dependency stdio proxy to the hosted MCP. The hosted MCP accepts - `Authorization: Sentry-Bearer `, so the proxy needs no OAuth flow, - but it serves only one Sentry host, so self-hosted users would use - `sentry mcp`. diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index 11d57c10a..fb6daf065 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -94,7 +94,8 @@ Out of scope, tracked separately: 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. - The future of `@sentry/mcp-server` is decided in its own PR. + `@sentry/mcp-server` becomes a stdio proxy to the hosted MCP with no + toolkit dependencies, in its own PR. ## Capabilities diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index ac0896e7e..a352a053f 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -78,8 +78,8 @@ Picked up after step 2; it gets its own spec then. The hosted MCP stays on Worke - [ ] 11.3 Move the MCP tools into `sentry/mcp`, domain by domain: each tool resolves inputs from its parameters and session constraints, calls the operation, and renders for agents. Tools buffer multi-yield commands into one result; commands with unbounded streams are not exposed, or only in a bounded form. Delete the `mcp-core` copies. Verify the `mcp-core` and `mcp-cloudflare` suites and snapshots, and that the MCP eval suite does not regress. - [ ] 11.4 Declare the CLI-only host capabilities as optional interfaces: the SQLite response cache store, the region cache, DSN detection from local files, and file uploads. `sentry mcp` injects the CLI's implementations and authenticated fetch from `cli/src/lib/mcp.ts` (formerly 6.2); `mcp-cloudflare` injects its own or omits them. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, a Workers test with the capabilities omitted, and `/mcp-qa` stdio QA. - [ ] 11.5 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed (formerly 9.1). Verify a test that the CLI's scope list covers every MCP tool's required scopes. -- [ ] 11.6 Move the MCP registry entry (`server.json`) to `sentry mcp`, and decide whether `@sentry/mcp-server` is retired or becomes a zero-dependency stdio proxy to the hosted MCP (see Open Questions in design.md). Tracked by the `@sentry/mcp-server` retirement PR. Verify `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` point to `sentry mcp`. -- [ ] 11.7 Enforce the dependency direction: a lint rule that bans importing `sentry` from `mcp-core`, and from `mcp-cloudflare` except the `sentry/mcp` entry, type-only imports included, and a manifest check that fails on a workspace dependency cycle. The retirement PR adds both with a full ban; 11.3 relaxes it for `sentry/mcp`. Verify each fails on a planted import or dependency. +- [ ] 11.6 Move the MCP registry entry (`server.json`) to `sentry mcp` (#1731), and turn `@sentry/mcp-server` into a stdio proxy to the hosted MCP that depends only on the MCP SDK: it forwards stdio to the Streamable HTTP endpoint with `Authorization: Sentry-Bearer `, and for any host other than the hosted MCP's it exits with an error pointing to `npx sentry mcp`. Verify a proxy test against a mocked hosted endpoint, the self-hosted error, and that the published package has no workspace dependencies. Verify `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` describe both `sentry mcp` and the proxy. +- [ ] 11.7 Enforce the dependency direction: a lint rule that bans importing `sentry` from `mcp-core`, and from `mcp-cloudflare` except the `sentry/mcp` entry, type-only imports included, and a manifest check that fails on a workspace dependency cycle. #1731 adds both with a full ban; 11.3 relaxes it for `sentry/mcp`. Verify each fails on a planted import or dependency. ## 12. Integration From c0be25fe31a4e73ad6c2b3d966ba1d6f43257e56 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:51:28 +0000 Subject: [PATCH 27/28] docs(openspec): Record @sentry/mcp-server as a slim bundle of sentry mcp The stdio proxy to the hosted MCP (#1743) is closed in favor of bundling the CLI's own server, which keeps one implementation and works with self-hosted Sentry. Update the design decision, rollout step, proposal, and task 11.6 to match #1731. Co-Authored-By: BYK --- .../changes/unify-sentry-api-layer/design.md | 45 ++++++++++++------- .../unify-sentry-api-layer/proposal.md | 4 +- .../changes/unify-sentry-api-layer/tasks.md | 2 +- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index abdfffdba..6044b4dd8 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -11,7 +11,7 @@ See proposal.md for motivation. Current state that shapes the approach: must therefore live below both products, in `toolkit-core`. The stdio server is moving into the CLI as `sentry mcp`, after which the CLI depends on `mcp-core` directly and `@sentry/mcp-server` becomes a - stdio proxy to the hosted MCP with no toolkit dependencies. + dependency-free bundle of that same server, built from CLI source. - **CLI request stack.** - `sentry-client.ts` handles auth, refresh on 401, timeouts, GET retries, redirect validation, and custom headers. @@ -513,22 +513,33 @@ adds a lint rule that bans importing `sentry` from `mcp-core` and fails on a workspace dependency cycle. When tools move, the rule is relaxed to let `mcp-cloudflare` import `sentry/mcp` and nothing else from `sentry`. -### `@sentry/mcp-server` stays as a stdio proxy - -`@sentry/mcp-server` is kept, not retired. It becomes a small stdio proxy -to the hosted MCP with no toolkit dependencies, only the MCP SDK. The CLI -has grown large, and a client that only needs Sentry's tools over stdio -should not have to download it. - -- It forwards stdio to the hosted MCP's Streamable HTTP endpoint and sends - the user's token as `Authorization: Sentry-Bearer `, which the - hosted MCP already accepts, so it needs no OAuth flow of its own. -- The hosted MCP serves one Sentry host. For any other host, such as - self-hosted Sentry, the proxy exits with an error that points to - `npx sentry mcp`, which runs the tools locally against any host. -- `sentry mcp` stays the full local server and owns the MCP registry entry +### `@sentry/mcp-server` is a slim bundle of `sentry mcp` + +`@sentry/mcp-server` is kept, not retired. The CLI has grown large, and a +client that only needs Sentry's tools over stdio should not have to +download it. The package ships the same server as `sentry mcp`, bundled +from CLI source into one file with no runtime dependencies (#1731): + +- One implementation serves both, so the package keeps the same flags, + tools, CLI session, and self-hosted support as `sentry mcp`. +- The bundle leaves out what does not serve the MCP server: CLI commands, + the markdown renderer, agent skills, completions, terminal images, the + Ink UI, debug-file processing, and the SQLite WASM fallback. Without the + fallback, the package requires Node.js 22.15 or later. The bundle script + fails if any of these reach the output or the file grows past its size + budget. +- `sentry-mcp auth login` runs the CLI's device flow with plain-text + output. Other account management stays in `sentry auth`, which reads the + same session. +- When the tools move into the CLI's `sentry/mcp` entry, the bundle picks + them up from the same source with no package change. +- `sentry mcp` stays the canonical server and owns the MCP registry entry (`server.json`). +A stdio proxy to the hosted MCP was considered and rejected (#1743): it +cannot reach self-hosted Sentry, and it would be a second implementation +to keep in step. + ### `fresh` is a shared read-tool input Read-only tools gain an optional `fresh: boolean`, defined once in the @@ -631,8 +642,8 @@ bumps move both products together. `sentry mcp` inject the CLI's host capabilities (session, cache store, region cache, DSN detection, file uploads) while `mcp-cloudflare` bundles the same entry. The registry entry moves to `sentry mcp` in - #1731, and `@sentry/mcp-server` becomes a stdio proxy to the hosted MCP - (see "`@sentry/mcp-server` stays as a stdio proxy"). + #1731, and `@sentry/mcp-server` becomes a slim bundle of `sentry mcp` + (see "`@sentry/mcp-server` is a slim bundle of `sentry mcp`"). **Rollback:** the hosted cache is gated by an env flag, and setting it to `off` restores today's uncached behavior. Each CLI step is a refactor that diff --git a/openspec/changes/unify-sentry-api-layer/proposal.md b/openspec/changes/unify-sentry-api-layer/proposal.md index fb6daf065..8e87bbcf9 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -94,8 +94,8 @@ Out of scope, tracked separately: 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 stdio proxy to the hosted MCP with no - toolkit dependencies, in its own PR. + `@sentry/mcp-server` becomes a dependency-free bundle of `sentry mcp` + built from CLI source (#1731). ## Capabilities diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index a352a053f..6b3394c24 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -78,7 +78,7 @@ Picked up after step 2; it gets its own spec then. The hosted MCP stays on Worke - [ ] 11.3 Move the MCP tools into `sentry/mcp`, domain by domain: each tool resolves inputs from its parameters and session constraints, calls the operation, and renders for agents. Tools buffer multi-yield commands into one result; commands with unbounded streams are not exposed, or only in a bounded form. Delete the `mcp-core` copies. Verify the `mcp-core` and `mcp-cloudflare` suites and snapshots, and that the MCP eval suite does not regress. - [ ] 11.4 Declare the CLI-only host capabilities as optional interfaces: the SQLite response cache store, the region cache, DSN detection from local files, and file uploads. `sentry mcp` injects the CLI's implementations and authenticated fetch from `cli/src/lib/mcp.ts` (formerly 6.2); `mcp-cloudflare` injects its own or omits them. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, a Workers test with the capabilities omitted, and `/mcp-qa` stdio QA. - [ ] 11.5 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed (formerly 9.1). Verify a test that the CLI's scope list covers every MCP tool's required scopes. -- [ ] 11.6 Move the MCP registry entry (`server.json`) to `sentry mcp` (#1731), and turn `@sentry/mcp-server` into a stdio proxy to the hosted MCP that depends only on the MCP SDK: it forwards stdio to the Streamable HTTP endpoint with `Authorization: Sentry-Bearer `, and for any host other than the hosted MCP's it exits with an error pointing to `npx sentry mcp`. Verify a proxy test against a mocked hosted endpoint, the self-hosted error, and that the published package has no workspace dependencies. Verify `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` describe both `sentry mcp` and the proxy. +- [ ] 11.6 Move the MCP registry entry (`server.json`) to `sentry mcp`, and ship `@sentry/mcp-server` as a dependency-free bundle of the same server built from CLI source: plain-text `sentry-mcp auth login`, Node.js 22.15+ with no SQLite WASM fallback, and a bundle guard that fails on CLI-only modules (commands, markdown renderer, agent skills, completions, terminal images, Ink UI, debug files) or a file over the size budget. Verify the bundle test (`initialize` and `tools/list` over stdio, no runtime dependencies, `--help`/`--version`, `auth`), a self-hosted `--host` smoke test, and that `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` describe both entry points. In review in #1731; the hosted-MCP proxy alternative (#1743) was closed. - [ ] 11.7 Enforce the dependency direction: a lint rule that bans importing `sentry` from `mcp-core`, and from `mcp-cloudflare` except the `sentry/mcp` entry, type-only imports included, and a manifest check that fails on a workspace dependency cycle. #1731 adds both with a full ban; 11.3 relaxes it for `sentry/mcp`. Verify each fails on a planted import or dependency. ## 12. Integration From ad7e34e3d9a9623d45a432633bb905f0b729c6a6 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:56:41 +0000 Subject: [PATCH 28/28] docs(openspec): Keep the MCP registry entry on @sentry/mcp-server #1731 now leaves server.json and the recommended stdio install on @sentry/mcp-server, with sentry mcp as the same server for CLI users. Co-Authored-By: BYK --- openspec/changes/unify-sentry-api-layer/design.md | 9 +++++---- openspec/changes/unify-sentry-api-layer/tasks.md | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index 6044b4dd8..c07c300b6 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -533,8 +533,9 @@ from CLI source into one file with no runtime dependencies (#1731): same session. - When the tools move into the CLI's `sentry/mcp` entry, the bundle picks them up from the same source with no package change. -- `sentry mcp` stays the canonical server and owns the MCP registry entry - (`server.json`). +- `@sentry/mcp-server` stays the recommended stdio install and keeps the + MCP registry entry (`server.json`). `sentry mcp` runs the same server for + people who already use the CLI. A stdio proxy to the hosted MCP was considered and rejected (#1743): it cannot reach self-hosted Sentry, and it would be a second implementation @@ -641,8 +642,8 @@ bumps move both products together. the MCP tools into the CLI's portable `sentry/mcp` entry; and have `sentry mcp` inject the CLI's host capabilities (session, cache store, region cache, DSN detection, file uploads) while `mcp-cloudflare` - bundles the same entry. The registry entry moves to `sentry mcp` in - #1731, and `@sentry/mcp-server` becomes a slim bundle of `sentry mcp` + bundles the same entry. The registry entry and recommended install stay + on `@sentry/mcp-server`, which #1731 turns into a slim bundle of `sentry mcp` (see "`@sentry/mcp-server` is a slim bundle of `sentry mcp`"). **Rollback:** the hosted cache is gated by an env flag, and setting it to diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 6b3394c24..7490dff3c 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -78,7 +78,7 @@ Picked up after step 2; it gets its own spec then. The hosted MCP stays on Worke - [ ] 11.3 Move the MCP tools into `sentry/mcp`, domain by domain: each tool resolves inputs from its parameters and session constraints, calls the operation, and renders for agents. Tools buffer multi-yield commands into one result; commands with unbounded streams are not exposed, or only in a bounded form. Delete the `mcp-core` copies. Verify the `mcp-core` and `mcp-cloudflare` suites and snapshots, and that the MCP eval suite does not regress. - [ ] 11.4 Declare the CLI-only host capabilities as optional interfaces: the SQLite response cache store, the region cache, DSN detection from local files, and file uploads. `sentry mcp` injects the CLI's implementations and authenticated fetch from `cli/src/lib/mcp.ts` (formerly 6.2); `mcp-cloudflare` injects its own or omits them. Verify a CLI test where `sentry mcp` tool calls hit the shared cache, a Workers test with the capabilities omitted, and `/mcp-qa` stdio QA. - [ ] 11.5 Compare the OAuth scopes the CLI requests with the MCP `SCOPES`, and widen the CLI's requested scopes if needed (formerly 9.1). Verify a test that the CLI's scope list covers every MCP tool's required scopes. -- [ ] 11.6 Move the MCP registry entry (`server.json`) to `sentry mcp`, and ship `@sentry/mcp-server` as a dependency-free bundle of the same server built from CLI source: plain-text `sentry-mcp auth login`, Node.js 22.15+ with no SQLite WASM fallback, and a bundle guard that fails on CLI-only modules (commands, markdown renderer, agent skills, completions, terminal images, Ink UI, debug files) or a file over the size budget. Verify the bundle test (`initialize` and `tools/list` over stdio, no runtime dependencies, `--help`/`--version`, `auth`), a self-hosted `--host` smoke test, and that `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` describe both entry points. In review in #1731; the hosted-MCP proxy alternative (#1743) was closed. +- [ ] 11.6 Keep the MCP registry entry (`server.json`) and the recommended install on `@sentry/mcp-server`, and ship it as a dependency-free bundle of the same server built from CLI source: plain-text `sentry-mcp auth login`, Node.js 22.15+ with no SQLite WASM fallback, and a bundle guard that fails on CLI-only modules (commands, markdown renderer, agent skills, completions, terminal images, Ink UI, debug files) or a file over the size budget. Verify the bundle test (`initialize` and `tools/list` over stdio, no runtime dependencies, `--help`/`--version`, `auth`), a self-hosted `--host` smoke test, and that `docs/operations/stdio-auth.md` and `docs/releases/stdio.md` describe both entry points. In review in #1731; the hosted-MCP proxy alternative (#1743) was closed. - [ ] 11.7 Enforce the dependency direction: a lint rule that bans importing `sentry` from `mcp-core`, and from `mcp-cloudflare` except the `sentry/mcp` entry, type-only imports included, and a manifest check that fails on a workspace dependency cycle. #1731 adds both with a full ban; 11.3 relaxes it for `sentry/mcp`. Verify each fails on a planted import or dependency. ## 12. Integration