From 79ba47ba374e9dbf506dec86aedb931d8daa44aa 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/35] 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 7f3a0b97d115d904bfea9455e6d45e5b8decb52e 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/35] 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 e8d161e95b8b9834f517a2bdab49a48ea6362c68 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/35] 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 5530d2215eaba8c9f94613ac9d8b76c5e48ce7ba 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/35] 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 c7a8b18ec4c47d38cf448fdbe848d9867a270cf7 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/35] 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 01deb0a2cdbb1fab4681d2f1e1d7a67af3b43d45 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/35] 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 62dfdb5acb3a0375c7db72e1d679f06e5cf4e13a 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/35] 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 53358ba17667b46afb13d9e721c93e1326ad05a6 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/35] 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 67524c2966cefaedfaaaebc2623844a40e18f121 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/35] 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 9eed4c07c84a988edb30deecfd7e900bcc2bcc80 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/35] 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 db7010fa7aaefd9bc6fa1fc1bd529e4edc23d0d1 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/35] 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 2f78136553dd6fe129021d7742ad2eb0daa5d2da 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/35] 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 e05b6c41c5b3770ea0264476ed79c5c5f5f468a4 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/35] 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 852b91b2d546ea3e014654581fde4ab4b18ae8c2 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/35] 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 d430a6e75eb05fb09fc4db0d288b5e7ba41f13af 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/35] 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 2c263bb323322da43614eec149d25cda523e4000 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/35] 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 be2fd27850cf2a0f7bfeb205205c94b80323bc7b 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/35] 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 95c2a0ed7ea485d5c5cd7df991472ebb95e45237 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/35] 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 bd1fb8c11123d2b2a5b40dc8d2ae5c166a316c41 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/35] 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 0e44e7459600962322d6c25e17ef77e421f97026 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/35] 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 8a0b84eca0546c21435fa556fa0d263a6cdde5ff 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/35] 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 9139c1954246f9c2909ae6f717aad8e6e8668277 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/35] 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 b5a97ab5929299531b9bcc478f7d0317a903c6ed 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/35] 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 86ce9bafecc4429406dcf5b40db5258c0c744cf0 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/35] 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 0211f1d86932be7e58ef8aebc972e2e7e63f7f5d 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/35] 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 32fe3b5f64692fb676790a4e634c1eff00e8fe1a 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/35] 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 e68ce608ab4d6b7575585f8d8eb3e217c56b75d5 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/35] 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 de407d50424a6e87e48cf894e4965a0570cd1e4c 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/35] 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 From e43f6ee956383ad537d51b15871820d92c3cd21f Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:22:59 +0000 Subject: [PATCH 29/35] docs(openspec): Refresh the checklist after #1731, #1737, #1741 and #1820 Co-Authored-By: Claude --- openspec/changes/unify-sentry-api-layer/tasks.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index 7490dff3c..21b4f6a9c 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -36,7 +36,7 @@ - [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. +- [ ] 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. In review in #1832: catalog tools with `readOnlyHint` and `openWorldHint` (not the docs tools) get the input, costing 130 tokens across the 10 direct read tools. ## 7. Hosted cache @@ -53,10 +53,10 @@ 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. - [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.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. Done so far: the request core (`createApiClientCore`, with `SentryApiService` sending every request through it) in #1737; the organizations domain (`getAuthenticatedUser`, `listOrganizations`, `getOrganization`, plus the core's `sdkClient` and `listPage`) in #1741; and the idempotent-write list that MCP and the CLI share for retries in #1820. The other domains are still open. - [ ] 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. +- [ ] 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. Ongoing: #1737, #1741, and #1820 updated both docs for their layers. ## 9. Step 2: move the shared client into `@sentry/api` @@ -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 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. +- [x] 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. Done in #1731: `packages/mcp-server/script/bundle.ts` holds the module guard and the 3.75 MB budget, `test/bundle.test.mjs` covers the stdio, dependency, `--help`/`--version`, and `auth` checks, and the CLI's `mcp` start tests cover self-hosted hosts. 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. Verify each fails on a planted import or dependency. Partly done in #1731: an oxlint `no-restricted-imports` rule bans `sentry` (type-only imports included) in `mcp-core` and `toolkit-core`, and `scripts/check-workspace-deps.mjs` (`lint:deps`) fails on any workspace cycle and on `mcp-core` or `toolkit-core` reaching the CLI. `mcp-cloudflare` has no rule yet; it gets one that allows only `sentry/mcp` once 11.2 adds that entry. ## 12. Integration From 8b69f44411c9a97f1c568be111cf7aeadf2b2449 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:50:33 +0000 Subject: [PATCH 30/35] docs(openspec): Plan the MCP as Code Mode over the CLI and move the API client into @sentry/api Add cli-first-code-mode-mcp: CLI parity with the MCP catalog, a two-tool Code Mode surface (search_sentry, execute_sentry_code) over the CLI's commands, discovery from CLI introspection, every published API route, completion for agents, and an eval-gated catalog removal. Re-scope unify-sentry-api-layer: stop staging in toolkit-core, move the client into @sentry/api next, then the CLI's wrappers, then the endpoints missing from the published schema. Drop the MCP domain wrappers, the compression dictionary, and per-tool porting; park the R2 cache tier. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- .../cli-first-code-mode-mcp/.openspec.yaml | 2 + .../changes/cli-first-code-mode-mcp/design.md | 281 ++++++++++++++++++ .../cli-first-code-mode-mcp/proposal.md | 83 ++++++ .../specs/mcp-code-mode/spec.md | 88 ++++++ .../changes/cli-first-code-mode-mcp/tasks.md | 63 ++++ .../changes/unify-sentry-api-layer/design.md | 138 +++------ .../unify-sentry-api-layer/proposal.md | 48 +-- .../changes/unify-sentry-api-layer/tasks.md | 48 +-- 8 files changed, 606 insertions(+), 145 deletions(-) create mode 100644 openspec/changes/cli-first-code-mode-mcp/.openspec.yaml create mode 100644 openspec/changes/cli-first-code-mode-mcp/design.md create mode 100644 openspec/changes/cli-first-code-mode-mcp/proposal.md create mode 100644 openspec/changes/cli-first-code-mode-mcp/specs/mcp-code-mode/spec.md create mode 100644 openspec/changes/cli-first-code-mode-mcp/tasks.md diff --git a/openspec/changes/cli-first-code-mode-mcp/.openspec.yaml b/openspec/changes/cli-first-code-mode-mcp/.openspec.yaml new file mode 100644 index 000000000..ac359ee85 --- /dev/null +++ b/openspec/changes/cli-first-code-mode-mcp/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-11-03 diff --git a/openspec/changes/cli-first-code-mode-mcp/design.md b/openspec/changes/cli-first-code-mode-mcp/design.md new file mode 100644 index 000000000..51e034573 --- /dev/null +++ b/openspec/changes/cli-first-code-mode-mcp/design.md @@ -0,0 +1,281 @@ +# Design + +## Context + +- The CLI's command tree is introspectable at runtime (`lib/introspect.ts`, + `sentry help --json`) and at build time (`generate-skill.ts`, + `generate-command-api.ts`). `src/generated/command-api.json` records which + API operations each command calls, and commands declare `destructive` + (`lib/command.ts`). +- `createSentrySDK()` (`src/index.ts`, `lib/sdk-invoke.ts`) calls command + handlers directly with typed flags and returns their data, buffering + commands that yield more than once. It needs Node: SQLite, the + filesystem, `node:os`. +- `sentry schema` (`commands/schema.ts`, `lib/api-schema.ts`) searches the + published API index from `@sentry/api`, and `sentry api` sends any + request. +- Shell completion is declared for every positional and flag (#1751, #1759) + and completes dataset fields and aggregates (#1782). Completers read the + SQLite completion cache, warmed at login (#1788, #1789). +- HTTP Code Mode (#1670, evals in #1675) runs agent JavaScript in a + Cloudflare Dynamic Worker with no token and no network. A broker in the + parent Worker checks each request against the shipped operation list and + the connection's API Read or API Write grant, then sends it through + `SentryApiService`. +- The hosted MCP runs on Cloudflare Workers. CLI command handlers do not run + there. + +## Goals / Non-Goals + +Goals: + +- One product surface: CLI commands. The MCP adds no Sentry logic of its own. +- Agents can do anything a command can, plus any published API operation. +- The same discovery, completion, and permission rules on stdio and hosted. + +Non-goals: + +- Running the full CLI on Workers. Hosted runs portable operations only. +- Changing the hosted MCP's OAuth flow or moving it off Workers. +- Keeping the catalog's agent-tuned output formats. Code returns data, and + can ask for the CLI's markdown rendering where that helps. + +## Decisions + +### Commands are the only product surface + +A capability an agent needs is a CLI command, built on `@sentry/api`. The +MCP gets no new hand-written tools; pending tool PRs (for example #1846, +#1837, #1517) become commands. Fixes to existing catalog tools are fine until +the cutover, since they ship to users now. + +### CLI parity inventory + +From a listing of `mcp-core/src/tools/catalog/` against +`cli/src/commands/` (by name and brief, not by behavior; each row is checked +when its command lands): + +| MCP catalog tools | CLI today | Gap | +| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------- | +| `whoami`, `find_organizations`, `find_projects`, `find_teams`, `find_dsns` | `auth whoami`, `org list`, `project list`, `team list`, `dsn list` | none | +| `create_project` | `project create` | none | +| `update_project` | none | `project edit` | +| `create_team`, `add_team_to_project`, `remove_team_from_project` | none | `team create`, `project team add/remove` | +| `create_dsn`, `update_dsn` | none | `dsn create`, `dsn edit` | +| `search_issues`, `get_issue_details`, `search_issue_events`, `get_issue_tag_values` | `issue list/view/events/tags` | none | +| `update_issue` | `issue resolve/unresolve/archive` | assignment: `issue assign` | +| `add_issue_note`, `get_issue_activity` | none | `issue comment`, `issue activity` | +| `get_issue_breadcrumbs`, `get_issue_user_reports` | partly in `event view`, `feedback list` | check, then flags or commands | +| `link_issue`, `unlink_issue`, `analyze_issue_with_seer` | `issue link/unlink/explain/plan` | none | +| `get_event_stacktrace` | `event view` | none | +| `get_event_attachment` | none | `event attachment list/download` | +| `search_events`, `search_errors`, `search_logs`, `search_traces`, `search_metrics`, `search_replays` | `event list`, `explore`, `log list`, `trace list`, `replay list` | metrics dataset to check; no natural-language mode (see below) | +| `get_trace_details`, `get_span_details`, `get_replay_details` | `trace view`, `span view`, `replay view` | none | +| `search_profiles`, `get_profile`, `get_profile_details` | none | `profile list/view` | +| `search_attributes` | `attribute list` | none | +| `search_agent_conversations`, `get_agent_conversation_details` | `agent-conversation list/view` | none | +| `find_dashboards`, `get_dashboard_details` | `dashboard list/view` | none | +| `find_releases`, `get_release_details` | `release list/view` | none | +| alert rule and metric monitor CRUD, `get_alert_options` | `alert issues ...`, `alert metrics ...` | `get_alert_options` to check | +| `find_monitors`, `get_monitor_details`, `delete_monitor_environment` | `monitor list` | `monitor view`, `monitor env delete` | +| uptime monitor CRUD (5 tools) | none | `monitor uptime list/view/create/edit/delete` | +| `find_dropped_events` | none | `event dropped` or similar (#1812 open) | +| `get_snapshot`, `get_latest_base_snapshot`, `get_snapshot_image` | `snapshots download/diff` | read commands to check | +| `get_sentry_resource` | URL positionals on most commands | a generic `sentry open ` resolver | +| `search_docs`, `get_doc`, `submit_feedback_to_sentry_team` | `docs list/query`, `cli feedback` | none | +| `get_sentry_mcp_info`, `onboarding_status_update` | MCP-specific | keep as host functions or drop | + +About 20 tools have no command yet, and a few more are partial. Each gap +is a command PR, grouped by domain. + +### The MCP surface: `search_sentry` and `execute_sentry_code` + +Two tools, on stdio and hosted: + +- **`search_sentry`** takes a free-text query and returns compact entries: + commands (path, brief, positionals and flags with types and completion + kinds, examples, output fields, permission class) and API operations + (method, path, summary, parameters). An entry can be expanded to a + TypeScript signature for the matching `sentry.*` call. It also takes a + partial argv and returns completions (see below). +- **`execute_sentry_code`** runs an async JavaScript body with one global, + `sentry`: + - `sentry..(params)`: the `createSentrySDK()` method, + returning the command's data. `{ render: "markdown" }` returns the + command's plain markdown output instead, for commands whose terminal + rendering already summarizes well, such as `issue view` or `trace view`. + - `sentry.api.request({ method, path, query, body })`: a published API + operation, as `sentry api` sends it. + - `sentry.complete(argv)`: completion candidates. + +The tool descriptions stay small; agents learn the surface through +`search_sentry`, not from a full type declaration in the tool list. + +### The sandbox orchestrates; the host runs commands + +The agent's code never runs CLI code and never sees a credential. Each +`sentry.*` call is an RPC to the host: + +1. The host checks the call against the connection's permissions and + constraints. +2. It runs the command operation, or the API request, with its own + credentials. +3. It returns the data, size-capped, to the sandbox. + +This keeps one execution model with two hosts: + +- **stdio (`sentry mcp`):** the host is the CLI process. It runs commands + through `createSentrySDK()` in Node, so every command works there as soon + as it exists, with the CLI's login, cache, and region lookup. The sandbox + is an in-process WebAssembly JavaScript engine (QuickJS); `node:vm` is not + a security boundary. +- **hosted (`mcp-cloudflare`):** the host is the parent Worker and the + sandbox is the Dynamic Worker from #1670. The host runs portable + operations from the CLI's `sentry/mcp` entry (next decision). + +### Portable operations for the hosted host + +The hosted MCP can list every command but can only run commands whose API +work is portable. Commands split, domain by domain, into: + +- **resolve** (host-specific): CLI flags, cwd, config, DSN detection; or + Code Mode parameters and session constraints; +- **operate** (portable): resolved inputs and an `@sentry/api` client in, + the command's data out; +- **render**: the existing `output.human` and JSON modes. + +`operations/` gets the portability guard from `unify-sentry-api-layer` +(no `node:*` imports or Node globals, a DOM-only typecheck) and ships as +the `sentry/mcp` entry with the Code Mode host. Generated metadata marks +each command `portable`; the hosted `search_sentry` shows only portable +commands, so hosted coverage grows as domains move. Commands with unbounded +streams (`--follow`, tail) are not exposed. Commands that need local files +(uploads, `init`, `sourcemap inject`) stay CLI-only. + +This replaces group 11 of `unify-sentry-api-layer`, except that MCP tools no +longer move into the CLI one by one: the Code Mode host exposes operations +generically. + +### One introspection source + +`sentry mcp` reads the route tree at runtime with `lib/introspect.ts`. The +build generates the same metadata as JSON (`command-catalog.json`: commands, +parameters, completion kinds, output fields, the API operations from +`command-api.json`, the permission class, `portable`), and the hosted MCP +reads that from the `sentry/mcp` entry. A test fails when the two differ. + +### API routes: one index for `sentry schema` and Code Mode + +`sentry schema` and #1670's `search_sentry_api` index the same `@sentry/api` +release separately. Both move to `@sentry/api`'s operation catalog +(`@sentry/api/operations`), and Code Mode's path matching uses the shared +matcher from #1807. Only published operations are reachable; endpoints the +CLI or MCP call that are not in the published schema are the main gap, and +`unify-sentry-api-layer` handles them. + +### Completion for agents + +Completers in `lib/complete.ts` read the SQLite completion cache. They are +split into portable completers that take a data source: + +- **stdio:** the SQLite cache, as for the shell, with a live API fallback. +- **hosted:** the live API through the shared client, with the in-memory + response cache. + +`sentry.complete(argv)` and `search_sentry`'s completion mode return +candidates with descriptions for the argument under the cursor: slugs, +issue IDs, enum values, dataset fields and aggregates, and search query +keys and values. This is how agents get valid queries without the catalog's +embedded search agents. + +### Embedded natural-language search agents are dropped + +`search_events`, `search_issues`, and `search_issue_events` translate +natural language with a second model call. Under Code Mode the calling agent +writes the query itself, with `search_sentry` documentation of the query +syntax and `sentry.complete()` for keys and values. Dropping them removes +the MCP's OpenAI and Anthropic provider configuration (#1754 becomes moot). + +### Permissions are command metadata + +Each command gets a permission class from its API operations in +`command-api.json`: **read** if it calls only GET, HEAD, or OPTIONS; +otherwise **write**; and **destructive** if it declares `destructive`. A +lint check fails when the class is missing or disagrees with the operations. + +- Hosted consent keeps #1670's API Read and API Write grants. API Read + allows read commands; API Write allows write commands and raw writes. + Destructive commands need API Write. The existing skill checkboxes + (Triage Issues, Manage Projects & Teams, and so on) map to command groups + until consent is simplified. +- Organization and project constraints from the session (`sentry mcp + /[]`, hosted URL constraints) fill and pin command inputs in + the resolve step. +- The host still checks every outgoing API request against the operation + list and grant (#1670's `ApiAuthorization`), so a command cannot exceed + the connection's grant. +- stdio adds `--read-only`, matching hosted API Read. + +### `@sentry/mcp-server` carries operations, not commands + +The slim bundle (#1731) bans command handlers and has a 3.75 MB budget. +Under Code Mode it needs the operations and the Code Mode host. It bundles +the `sentry/mcp` entry plus the stdio host and QuickJS, and its guard keeps +banning command handlers, Ink, the markdown terminal renderer, and SQLite +WASM. The budget is re-measured when the QuickJS sandbox lands. Commands +that are not yet portable are unavailable in the slim bundle and available +in `sentry mcp`. + +### Eval-gated cutover + +Code Mode ships next to the catalog: + +1. Extend #1675's harness to run the full `mcp-server-evals` suite on three + surfaces: catalog, Code Mode, and both. +2. Make Code Mode the default when it matches or beats the catalog on task + success, and record token cost and latency next to it. Run more than one + client and model, not only Claude Opus. +3. Keep the catalog behind a flag (`--catalog`, a hosted skill) for one + release. +4. Delete the catalog tools, their `SentryApiService` endpoint wrappers, + the search agents, and the provider configuration from `mcp-core`. + +## Risks / Trade-offs + +- **Weaker clients.** Code Mode depends on the model writing correct + JavaScript. Small models and clients that cap tool calls may do worse than + with typed tools. The eval gate runs several models, and the flag keeps + the catalog available during the window. +- **Output quality.** Catalog tools format stack traces, traces, and issue + summaries for agents. `render: "markdown"` reuses the CLI's renderers for + those commands; the evals check the gap. +- **Hosted coverage lags stdio.** Commands appear on hosted only when their + domain is portable. The parity table and `portable` flags make that + visible. +- **Sandbox on stdio.** QuickJS adds bundle size and its own escape surface. + It runs no host code and holds no credentials; every capability is an + RPC the host checks. +- **Unpublished endpoints.** Commands and raw requests can only reach what + `@sentry/api` covers. See `unify-sentry-api-layer`. + +## Migration Plan + +1. Command metadata (permission class, `portable`, generated catalog) and the + parity commands, by domain. +2. Code Mode on stdio over `createSentrySDK()`, behind a flag, with the + QuickJS sandbox and completion. +3. `sentry schema` and Code Mode on one `@sentry/api` operation index. +4. Hosted Code Mode (#1670) on portable operations, domain by domain. +5. Eval gate, default flip, catalog deletion. + +**Rollback:** until the catalog is deleted, the flag switches back. After +that, a revert of the deletion PR restores it. + +## Open Questions + +- Whether `get_sentry_mcp_info` and `onboarding_status_update` survive as + host functions on `sentry` or are dropped. +- The QuickJS bundle cost, and whether Node's permission model is good + enough on supported Node versions to use a worker thread instead. +- When to simplify hosted consent from skill checkboxes to API Read, API + Write, and destructive. diff --git a/openspec/changes/cli-first-code-mode-mcp/proposal.md b/openspec/changes/cli-first-code-mode-mcp/proposal.md new file mode 100644 index 000000000..926060606 --- /dev/null +++ b/openspec/changes/cli-first-code-mode-mcp/proposal.md @@ -0,0 +1,83 @@ +# Proposal + +## Why + +Sentry's product logic for agents lives in two places. The CLI has about 40 +command groups built on `@sentry/api`. The MCP server has 75 hand-written +catalog tools in `mcp-core/src/tools/catalog/`, with their own endpoint +wrappers (`mcp-core/src/api-client/client.ts`, about 5,000 lines), their own +Zod output formatting, and embedded natural-language search agents that need +an OpenAI or Anthropic key. Every capability is built twice, or exists in +only one product. + +The earlier plan (`unify-sentry-api-layer`, group 11) kept both surfaces and +moved each MCP tool into the CLI as a portable `sentry/mcp` tool definition. +That still means one hand-written MCP tool per command. + +Code Mode removes the second surface. HTTP Code Mode (#1670) already shows +agents completing API tasks the catalog cannot do, such as copying a +dashboard (#1675 evals). If the CLI is the only product surface, the MCP can +be a thin Code Mode layer over it: agents discover commands through the +CLI's own introspection and call them through the CLI SDK. + +## What Changes + +1. **CLI parity.** Every MCP catalog tool gets a CLI command. New Sentry + capabilities are added as CLI commands only; no new hand-written MCP + tools. Commands call the API through `@sentry/api` + (`unify-sentry-api-layer`). +2. **The MCP is Code Mode over the CLI.** The MCP exposes two tools: + `search_sentry` to discover commands and API operations, and + `execute_sentry_code` to run JavaScript that calls them through a + `sentry` object shaped like `createSentrySDK()`. The agent's code runs in + a sandbox with no credentials or network; each `sentry.*` call goes to + the host, which checks permissions and runs the command. +3. **One introspection source.** `sentry mcp` builds the tool surface from + the CLI's route tree at runtime (`lib/introspect.ts`). The hosted MCP + lists the same commands from metadata generated from the CLI at build + time, and runs the portable ones. +4. **Every published API route.** `search_sentry` also searches the API + schema that backs `sentry schema`, and `execute_sentry_code` exposes + `sentry.api.request()`, the equivalent of `sentry api`, for operations + with no command. +5. **Completion for agents.** The shell completion system + (`lib/complete.ts`, `lib/completion-target.ts`) answers + `sentry.complete(argv)` inside Code Mode and completion queries in + `search_sentry`: organization and project slugs, issue IDs, dataset + fields and aggregates, and search query terms. +6. **The catalog goes away after an eval gate.** Code Mode ships next to the + catalog. When the MCP eval suite shows it matching the catalog, it + becomes the default, and the catalog tools, their endpoint wrappers, and + the embedded search agents are deleted from `mcp-core`. + +## Capabilities + +### New Capabilities + +- `mcp-code-mode`: The MCP's tool surface: discovery, execution, permissions, + and completion over the CLI's commands and the published API. + +### Modified Capabilities + +None. The project-management tool contract in +`refine-project-management-tools` is superseded: those operations become CLI +commands. + +## Impact + +- `packages/cli`: new commands for the parity gaps; agent metadata on + commands (permission class, portability); a portable `sentry/mcp` entry + with the Code Mode host and portable operations; portable completers. +- `packages/mcp-core`: keeps MCP plumbing (registration, transports, + constraints, Code Mode tool definitions). Loses the catalog tools, the + `SentryApiService` endpoint wrappers they use, the search agents, and the + agent provider configuration. +- `packages/mcp-cloudflare`: Code Mode from #1670 switches from raw HTTP to + the CLI's portable operations, with `sentry.api.request()` kept for raw + calls. OAuth consent maps to command permission classes. +- `packages/mcp-server`: the slim bundle carries portable operations instead + of catalog tools. Its size budget and module guard change. +- `packages/mcp-server-evals`: Code Mode and catalog runs of the full suite, + then Code Mode only. +- Docs: mcp.sentry.dev setup, `docs/` architecture pages, and the agent + skills (#1357 skills audit). diff --git a/openspec/changes/cli-first-code-mode-mcp/specs/mcp-code-mode/spec.md b/openspec/changes/cli-first-code-mode-mcp/specs/mcp-code-mode/spec.md new file mode 100644 index 000000000..f033ab36e --- /dev/null +++ b/openspec/changes/cli-first-code-mode-mcp/specs/mcp-code-mode/spec.md @@ -0,0 +1,88 @@ +# Spec Delta + +## Purpose + +Defines the MCP's tool surface as Code Mode over the CLI: how agents +discover commands and API operations, how their code calls them, what each +connection may do, and how completion helps them build valid calls. + +## ADDED Requirements + +### Requirement: Commands are the product surface + +Every Sentry capability the MCP offers SHALL be a CLI command or a published +API operation. The MCP SHALL NOT define Sentry operations of its own. + +#### Scenario: A new capability + +- **WHEN** a Sentry capability is added for agents +- **THEN** it is added as a CLI command +- **THEN** it appears in `search_sentry` with no MCP code change + +### Requirement: Discovery through CLI introspection + +`search_sentry` SHALL return commands from the CLI's introspection metadata +and operations from the `@sentry/api` operation catalog. The stdio server +SHALL read the route tree at runtime; the hosted server SHALL read the +generated catalog, and SHALL list only commands marked `portable`. + +#### Scenario: Search finds a command and an operation + +- **WHEN** an agent searches for "copy dashboard" +- **THEN** the result includes the dashboard commands and the dashboard API operations, each with its permission class + +#### Scenario: Hosted hides non-portable commands + +- **WHEN** a command's operation is not portable +- **THEN** hosted `search_sentry` does not list it +- **THEN** `sentry mcp` lists it + +### Requirement: Code runs without credentials + +`execute_sentry_code` SHALL run agent code in a sandbox with no network +access and no credentials. Each `sentry.*` call SHALL be executed by the +host, which SHALL check the call against the connection's permissions and +constraints before running it. + +#### Scenario: Sandbox network access + +- **WHEN** agent code calls `fetch` directly +- **THEN** the call fails + +#### Scenario: Constrained session + +- **WHEN** a session is constrained to an organization and code calls a command for another organization +- **THEN** the host refuses the call + +### Requirement: Permission classes + +Every command SHALL have a permission class (read, write, or destructive) +that agrees with the API operations it calls. A read-only connection SHALL +run only read commands and read API operations. Every outgoing API request +SHALL also be checked against the published operation list and the grant. + +#### Scenario: Read-only connection runs a write command + +- **WHEN** a read-only connection calls `sentry.issue.resolve()` +- **THEN** the host refuses the call without sending an API request + +### Requirement: Completion for agents + +`sentry.complete(argv)` SHALL return completion candidates for the argument +under the cursor, from the same completers as shell completion, with a data +source suited to the host. + +#### Scenario: Dataset field completion + +- **WHEN** code calls `sentry.complete(["explore", "--dataset", "spans", "--field", "span.d"])` +- **THEN** the result includes span attribute keys that start with `span.d` + +### Requirement: Eval-gated catalog removal + +The catalog tools SHALL NOT be removed until the MCP eval suite shows Code +Mode matching the catalog on task success. + +#### Scenario: Default flip + +- **WHEN** Code Mode matches the catalog on the eval suite +- **THEN** Code Mode becomes the default and the catalog stays behind a flag for one release diff --git a/openspec/changes/cli-first-code-mode-mcp/tasks.md b/openspec/changes/cli-first-code-mode-mcp/tasks.md new file mode 100644 index 000000000..56ca1b21b --- /dev/null +++ b/openspec/changes/cli-first-code-mode-mcp/tasks.md @@ -0,0 +1,63 @@ +# Tasks + +The API work these tasks build on is `unify-sentry-api-layer`. New commands +use `@sentry/api` operations and do not wait for the shared client to move +there. + +## 1. Command metadata for agents + +- [ ] 1.1 Add a permission class (read, write, destructive) to every command, derived from its operations in `command-api.json` and its `destructive` flag, with a lint check that fails when a class is missing or disagrees with the operations. Verify the check fails on a planted GET-only command marked write and a write command marked read. +- [ ] 1.2 Generate `command-catalog.json` from the route tree: path, brief, parameters with types and completion kinds, examples, output fields, API operations, permission class, and `portable`. Verify a test that it matches `lib/introspect.ts` output for the full tree. + +## 2. CLI parity with the MCP catalog + +One PR per domain. Each adds the commands from the parity table in design.md, on `@sentry/api` operations, with unit tests, and ticks its rows. Pending MCP tool PRs for the same capability are redirected here. + +- [ ] 2.1 Projects and teams: `project edit`, `team create`, `project team add/remove`. Supersedes `refine-project-management-tools`. +- [ ] 2.2 DSNs: `dsn create`, `dsn edit`. +- [ ] 2.3 Issues: `issue assign`, `issue comment`, `issue activity`; breadcrumbs and user reports where `event view` and `feedback list` fall short. +- [ ] 2.4 Events: `event attachment list/download`; dropped events (#1812). +- [ ] 2.5 Monitors: `monitor view`, `monitor env delete`, `monitor uptime list/view/create/edit/delete` (the detectors work in #1837 moves here). +- [ ] 2.6 Profiles: `profile list/view`. +- [ ] 2.7 Snapshots, alert options, and the metrics dataset in `explore`: check against the MCP tools and fill the gaps. +- [ ] 2.8 `sentry open `: resolve any Sentry URL to the matching resource and command. +- [ ] 2.9 Mark every row in the parity table done or dropped, with the reason. + +## 3. Code Mode on stdio + +- [ ] 3.1 Add the Code Mode tool definitions (`search_sentry`, `execute_sentry_code`) to `mcp-core`, reusing #1670's descriptions and result handling. Verify `pnpm run measure-tokens`. +- [ ] 3.2 Add the stdio host in the CLI: a QuickJS sandbox with a `sentry` global whose calls are RPCs to the host, which runs them through `createSentrySDK()` with the session's credentials, constraints, and permission class, and size-caps results. Multi-yield commands are buffered; streaming commands are not exposed. Verify tests for each permission class, constraint pinning, a call that exceeds the result cap, and that the sandbox has no network or host globals. +- [ ] 3.3 Serve it from `sentry mcp --code-mode`, next to the catalog. Add `--read-only`. Verify `/mcp-qa` over stdio. +- [ ] 3.4 Add `render: "markdown"` for commands with a plain markdown renderer. Verify a test for `issue view` and `trace view`. + +## 4. API routes in Code Mode + +- [ ] 4.1 Move `sentry schema`'s index and #1670's API search onto `@sentry/api`'s operation catalog, with #1807's shared path matcher. Verify `sentry schema` output and the Code Mode search tests are unchanged. +- [ ] 4.2 Add `sentry.api.request()` on stdio, with the same operation list and permission checks as hosted. Verify a raw read, a raw write denied under `--read-only`, and an unpublished path refused. + +## 5. Completion for agents + +- [ ] 5.1 Split the completers in `lib/complete.ts` into portable completers over a data source: the SQLite cache with a live API fallback on stdio, the live API with the memory response cache on hosted. Verify the shell completion tests pass unchanged. +- [ ] 5.2 Add `sentry.complete(argv)` and `search_sentry`'s completion mode, returning candidates with descriptions. Verify tests for slugs, issue IDs, dataset fields and aggregates, and query keys and values. +- [ ] 5.3 Document the query syntax in `search_sentry` results for the search commands, replacing the embedded search agents' prompts. + +## 6. Hosted Code Mode on the CLI + +- [ ] 6.1 Land #1670 (HTTP Code Mode), #1675 (evals), and #1807 (shared path matcher) as the hosted base. +- [ ] 6.2 Split commands into resolve, operate, and render, domain by domain, starting with the read domains the evals use most (issues, events, traces, organizations and projects). Operations move to `operations/` and return the command's data. Verify the CLI unit and SDK tests pass with output unchanged. (Formerly `unify-sentry-api-layer` 11.1.) +- [ ] 6.3 Guard `operations/` and the Code Mode host with the portability checks and expose them as the `sentry/mcp` entry. Verify the guard fails on a planted `node:` import, and that `mcp-cloudflare` bundles the entry without command handlers, SQLite, or Ink. (Formerly 11.2.) +- [ ] 6.4 Switch hosted Code Mode to the `sentry/mcp` host: `search_sentry` lists portable commands from `command-catalog.json`, `execute_sentry_code` runs their operations, and `sentry.api.request()` keeps #1670's broker. Verify the Workers tests and the #1675 evals. +- [ ] 6.5 Map hosted consent to permission classes: API Read, API Write, destructive, and the existing skills as command groups. Verify a test per grant. +- [ ] 6.6 Allow `mcp-cloudflare` to import only `sentry/mcp` from `sentry`. Verify the rule fails on a planted import. (Formerly 11.7.) +- [ ] 6.7 Compare the CLI's OAuth scopes with the MCP `SCOPES` and widen the CLI's if needed. Verify a test that the CLI's scopes cover every command's operations. (Formerly 11.5.) + +## 7. Slim stdio bundle + +- [ ] 7.1 Bundle the `sentry/mcp` entry, the stdio host, and QuickJS into `@sentry/mcp-server`, keeping the guard against command handlers, Ink, the terminal renderer, and SQLite WASM. Re-measure the size budget. Verify the bundle test (`initialize`, `tools/list`, an `execute_sentry_code` call against mocks). + +## 8. Eval gate and cutover + +- [ ] 8.1 Run the full `mcp-server-evals` suite on catalog, Code Mode, and both, with at least two clients or models. Record task success, tokens, and latency in `docs/testing/`. +- [ ] 8.2 Make Code Mode the default on stdio and hosted once it matches the catalog. Keep the catalog behind `--catalog` and a hosted skill for one release. +- [ ] 8.3 Delete the catalog tools, their `SentryApiService` endpoint wrappers, the embedded search agents, and the agent provider configuration from `mcp-core`. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. +- [ ] 8.4 Update mcp.sentry.dev setup, `docs/` architecture and operations pages, and the agent skills for the Code Mode surface. diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index c07c300b6..6bef6c0ba 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -66,8 +66,7 @@ See proposal.md for motivation. Current state that shapes the approach: by server headers covers that (getsentry/sentry#128818). - 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). + entry instead, under `cli-first-code-mode-mcp`. - 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 @@ -357,24 +356,35 @@ 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. -### Two steps: consolidate in `toolkit-core`, then move into `@sentry/api` +### Into `@sentry/api` directly, from here on 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 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. - -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. +both MCP servers, and the Sentry frontend share one client. The first part +was built in `toolkit-core/src/api-client/` with the layout it will have in +`@sentry/api` (task group 8: #1732, #1736, #1737, #1741, #1820), so moving +it is a copy plus reconciliation with the errors and pagination +sentry-api-schema already has (`lib/sentry-errors.ts`, +`lib/sentry-pagination.ts`, `lib/browser-client.ts`). + +The rest no longer stages in `toolkit-core`. Under +`cli-first-code-mode-mcp`, the MCP's catalog tools and their endpoint +wrappers are deleted at the Code Mode cutover, so the only long-term caller +of endpoint wrappers is the CLI, and the MCP reaches the API through CLI +operations. Building shared MCP wrappers first would be thrown away. The +order is: + +1. Move `toolkit-core/src/api-client/` into `@sentry/api`, publish, switch + toolkit, and delete the copies (8.5, 8.6). +2. Move the CLI's endpoint wrappers onto the `@sentry/api` client, domain + by domain (8.7). A wrapper that only reshapes one generated operation is + replaced by the operation. +3. Get every endpoint still called by raw path into the published schema + (8.8). `sentry api`, `sentry schema`, and Code Mode only reach published + operations, so this also widens what agents can call. + +The cost is a publish round trip for client fixes. Develop against a local +sentry-api-schema checkout through a pnpm override, and release +`@sentry/api` as often as toolkit needs it. ### The shared client @@ -439,79 +449,13 @@ 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. -### MCP tools are a portable face of the CLI's commands +### MCP tools: replaced by Code Mode over the CLI -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: - -``` -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 -``` - -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: - -``` -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 stay optional interfaces: - -- 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. 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`. +This section planned to move each MCP tool into the CLI as a portable +`sentry/mcp` tool definition. `cli-first-code-mode-mcp` replaces it: the +MCP becomes Code Mode over the CLI's commands, discovered through CLI +introspection. The portable resolve/operate/render split and the +`sentry/mcp` entry survive there as what the hosted Code Mode host runs. ### `@sentry/mcp-server` is a slim bundle of `sentry mcp` @@ -531,8 +475,8 @@ from CLI source into one file with no runtime dependencies (#1731): - `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. +- Under Code Mode the bundle carries the portable operations and the Code + Mode host instead of catalog tools (`cli-first-code-mode-mcp`). - `@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. @@ -638,13 +582,9 @@ 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: 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 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`"). +8. Move the shared client into `@sentry/api`, then the CLI's wrappers, then + the endpoints missing from the published schema. The MCP side continues + in `cli-first-code-mode-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 8e87bbcf9..ef03bdf0b 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -20,15 +20,23 @@ 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. +The plan had two steps: build the shared client in `@sentry/toolkit-core`, +structured the way it will live in `@sentry/api`, then copy it there. The +transport, retry, cache policy, pagination, request core, and organizations +domain landed in `toolkit-core` that way (#1719, #1722, #1732, #1736, #1737, +#1741, #1820). + +**Updated 2026-11-03.** The MCP is becoming Code Mode over the CLI's +commands (`cli-first-code-mode-mcp`), so its catalog tools and their +endpoint wrappers will be deleted rather than shared. The remaining plan +therefore goes straight to `@sentry/api`: + +1. Move what `toolkit-core/src/api-client/` holds into + getsentry/sentry-api-schema now, reconciled with the errors and + pagination that package already ships, and switch toolkit to it. +2. Move the CLI's endpoint wrappers onto that client. +3. Get the endpoints the CLI and MCP still call by raw path into the + published schema, so they generate as `@sentry/api` operations. ## What Changes @@ -55,9 +63,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 stacked - PRs by layer that also switch both products and delete the code they - replace. It +- **Step 1 (stopped early, see Why):** 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 @@ -69,8 +77,9 @@ The plan has two steps: `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. +- **Step 2, now next:** move the shared client into `@sentry/api`, publish + it, and switch toolkit over. The CLI's endpoint wrappers then move onto + it, and endpoints missing from the published schema are added there. - 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 @@ -88,14 +97,9 @@ Out of scope, tracked separately: - Server-side `Cache-Control` for event and trace-item detail responses (getsentry/sentry#128818, prototype in getsentry/sentry#128828). -- A later phase, after step 2: CLI commands split into host-specific input - resolution, portable operations, and renderers. The MCP tools move into - the CLI as a portable `sentry/mcp` entry that `sentry mcp` serves over - stdio and the hosted MCP bundles on Workers. `sentry mcp` passes the - CLI's authenticated fetch so local MCP sessions reuse the CLI login and - on-disk cache, and the CLI's OAuth scopes are widened to cover MCP tools. - `@sentry/mcp-server` becomes a dependency-free bundle of `sentry mcp` - built from CLI source (#1731). +- The MCP tool surface: replaced by `cli-first-code-mode-mcp`, which makes + the MCP Code Mode over the CLI's commands. `@sentry/mcp-server` stays a + dependency-free bundle 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 21b4f6a9c..9195dcdd0 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -35,51 +35,51 @@ ## 6. MCP on the shared transport - [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. In review in #1832: catalog tools with `readOnlyHint` and `openWorldHint` (not the docs tools) get the input, costing 130 tokens across the 10 direct read tools. +- 6.2 Moved to `cli-first-code-mode-mcp`: the stdio Code Mode host runs commands through the CLI, so it uses the CLI's fetch and cache. +- [x] 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. Shipped in #1832: catalog tools with `readOnlyHint` and `openWorldHint` (not the docs tools) get the input, costing 130 tokens across the 10 direct read tools. ## 7. Hosted cache +Off the critical path since the Code Mode plan (`cli-first-code-mode-mcp`). The memory store (7.1, #1833) can land on its own. The R2 tier (7.3, 7.4, #1842) is parked until Code Mode traffic shows whether cross-isolate reuse pays; the cache policy itself moves into `@sentry/api` with the rest of the client. + - [ ] 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, 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. -## 8. Step 1: consolidate the API layer in `toolkit-core` +## 8. The shared client in `@sentry/api` -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. +The plan used to finish the whole client in `toolkit-core` and copy it into `@sentry/api` at the end. It now moves what exists into `@sentry/api` first and builds the rest there, since the CLI is the only long-term caller of endpoint wrappers: the MCP's catalog wrappers are deleted when Code Mode replaces the catalog (`cli-first-code-mode-mcp`), so porting them is wasted work. - [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. - [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. Done so far: the request core (`createApiClientCore`, with `SentryApiService` sending every request through it) in #1737; the organizations domain (`getAuthenticatedUser`, `listOrganizations`, `getOrganization`, plus the core's `sdkClient` and `listPage`) in #1741; and the idempotent-write list that MCP and the CLI share for retries in #1820. The other domains are still open. -- [ ] 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. Ongoing: #1737, #1741, and #1820 updated both docs for their layers. +- [x] 8.4 Request core and the first shared domains: `createApiClientCore` with `SentryApiService` on it (#1737), the organizations domain (#1741), and the idempotent-write list both products use for retries (#1820). **Dropped:** moving the remaining MCP domains onto shared wrappers. +- [ ] 8.5 Move `toolkit-core/src/api-client/` into getsentry/sentry-api-schema's `lib/`, reconciling it with what is already there (`browser-client.ts`, `sentry-errors.ts`, `sentry-pagination.ts`) so the package has one error type, one pagination helper, and one transport. Keep the injected interfaces (credentials, `CacheStore`, fresh-read scope, logger, span hook) and the portability rules. Publish a new `@sentry/api`. Verify the package's tests, and that its browser entry builds and makes a request with a mocked `fetch`. (Formerly 9.1.) +- [ ] 8.6 Bump `@sentry/api` in the pnpm catalog, switch the CLI, `mcp-core`, and `mcp-cloudflare` to it, and delete the `toolkit-core` copies in the same PR. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. (Formerly 9.2.) +- [ ] 8.7 Move the CLI's endpoint wrappers (`cli/src/lib/api/`) onto the `@sentry/api` client, domain by domain, and delete the CLI's own API errors and pagination. 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. Wrappers that only reshape one generated operation are replaced by the operation. Verify the CLI unit tests pass. (Formerly 8.5.) +- [ ] 8.8 Inventory the endpoints the CLI calls by raw path rather than through a generated operation (about 85 call sites in `cli/src/lib/api/` and 55 in `mcp-core/src/api-client/` by a path grep, many duplicates), and get each one into `@sentry/api`: publish it in getsentry/sentry's OpenAPI schema so it generates, or record why it stays raw. Verify the inventory is checked in and each new operation is used. +- [ ] 8.9 In every PR, keep behavior the same and call out each intentional change in the PR body. Update `docs/contributing/api-patterns.md` and the `@sentry/api` README for the new layering, and verify `pnpm run docs:check` passes. Ongoing: #1737, #1741, and #1820 updated the toolkit docs for their layers. (Formerly 8.7.) +- Old 8.6 **dropped:** merging the wrappers both products call. After the catalog is deleted, only the CLI calls endpoint wrappers; the MCP calls CLI operations. -## 9. Step 2: move the shared client into `@sentry/api` +## 9. Merged into group 8 -- [ ] 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`. +Moving the client into `@sentry/api` (old 9.1, 9.2) is now 8.5 and 8.6. -## 10. Trained compression dictionary (follow-up) +## 10. Trained compression dictionary -- [ ] 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. +**Dropped.** The hosted cache is off the critical path, and a shippable training corpus is not worth building for it. -## 11. Later phase: MCP tools as a portable face of the CLI +## 11. MCP tools as a portable face of the CLI -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. +**Replaced by `cli-first-code-mode-mcp`.** The MCP becomes Code Mode over the CLI's commands instead of one ported tool per command. -- [ ] 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. -- [x] 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. Done in #1731: `packages/mcp-server/script/bundle.ts` holds the module guard and the 3.75 MB budget, `test/bundle.test.mjs` covers the stdio, dependency, `--help`/`--version`, and `auth` checks, and the CLI's `mcp` start tests cover self-hosted hosts. 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. Verify each fails on a planted import or dependency. Partly done in #1731: an oxlint `no-restricted-imports` rule bans `sentry` (type-only imports included) in `mcp-core` and `toolkit-core`, and `scripts/check-workspace-deps.mjs` (`lint:deps`) fails on any workspace cycle and on `mcp-core` or `toolkit-core` reaching the CLI. `mcp-cloudflare` has no rule yet; it gets one that allows only `sentry/mcp` once 11.2 adds that entry. +- [x] 11.6 `@sentry/mcp-server` as a dependency-free bundle of the server built from CLI source. Done in #1731. Under Code Mode it carries portable operations instead of catalog tools (`cli-first-code-mode-mcp` 7.1). +- 11.1, 11.2, 11.4: moved to `cli-first-code-mode-mcp` group 6 (portable operations and the `sentry/mcp` entry for the hosted Code Mode host). 11.4's optional host capabilities become what the stdio host has and the hosted host lacks. +- 11.3 **Dropped:** moving each MCP tool into `sentry/mcp`. Code Mode exposes commands generically. +- 11.5 moved to `cli-first-code-mode-mcp` 6.7 (OAuth scope comparison). +- 11.7 moved to `cli-first-code-mode-mcp` 6.6. Partly done in #1731: an oxlint rule bans `sentry` in `mcp-core` and `toolkit-core`, and `lint:deps` fails on workspace cycles. ## 12. Integration From 77b487c9a8694080bd458efe38c32724823e8f4a Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:25:55 +0000 Subject: [PATCH 31/35] docs(openspec): Expose CLI commands through search and execute, and run the SDK on Workers Rename cli-first-code-mode-mcp to cli-first-mcp. The MCP lists two tools, search_sentry and execute_sentry_command, which dispatches one exposed command with validated JSON parameters through createSentrySDK(), so no code sandbox is needed. An all-tools mode lists each command as a tool for harnesses that search tools themselves. The hosted MCP runs the CLI SDK with storage split by purpose behind Workers backends and per-request state, starting with a workerd build spike. A Durable Object SQLite adapter is the fallback for storage, and the per-domain portable-operation split is the fallback for domains Workers cannot run. Code Mode becomes an optional later tool. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- .../changes/cli-first-code-mode-mcp/design.md | 281 --------------- .../cli-first-code-mode-mcp/proposal.md | 83 ----- .../specs/mcp-code-mode/spec.md | 88 ----- .../changes/cli-first-code-mode-mcp/tasks.md | 63 ---- .../.openspec.yaml | 0 openspec/changes/cli-first-mcp/design.md | 337 ++++++++++++++++++ openspec/changes/cli-first-mcp/proposal.md | 86 +++++ .../specs/mcp-command-surface/spec.md | 98 +++++ openspec/changes/cli-first-mcp/tasks.md | 69 ++++ .../changes/unify-sentry-api-layer/design.md | 28 +- .../unify-sentry-api-layer/proposal.md | 15 +- .../changes/unify-sentry-api-layer/tasks.md | 18 +- 12 files changed, 622 insertions(+), 544 deletions(-) delete mode 100644 openspec/changes/cli-first-code-mode-mcp/design.md delete mode 100644 openspec/changes/cli-first-code-mode-mcp/proposal.md delete mode 100644 openspec/changes/cli-first-code-mode-mcp/specs/mcp-code-mode/spec.md delete mode 100644 openspec/changes/cli-first-code-mode-mcp/tasks.md rename openspec/changes/{cli-first-code-mode-mcp => cli-first-mcp}/.openspec.yaml (100%) create mode 100644 openspec/changes/cli-first-mcp/design.md create mode 100644 openspec/changes/cli-first-mcp/proposal.md create mode 100644 openspec/changes/cli-first-mcp/specs/mcp-command-surface/spec.md create mode 100644 openspec/changes/cli-first-mcp/tasks.md diff --git a/openspec/changes/cli-first-code-mode-mcp/design.md b/openspec/changes/cli-first-code-mode-mcp/design.md deleted file mode 100644 index 51e034573..000000000 --- a/openspec/changes/cli-first-code-mode-mcp/design.md +++ /dev/null @@ -1,281 +0,0 @@ -# Design - -## Context - -- The CLI's command tree is introspectable at runtime (`lib/introspect.ts`, - `sentry help --json`) and at build time (`generate-skill.ts`, - `generate-command-api.ts`). `src/generated/command-api.json` records which - API operations each command calls, and commands declare `destructive` - (`lib/command.ts`). -- `createSentrySDK()` (`src/index.ts`, `lib/sdk-invoke.ts`) calls command - handlers directly with typed flags and returns their data, buffering - commands that yield more than once. It needs Node: SQLite, the - filesystem, `node:os`. -- `sentry schema` (`commands/schema.ts`, `lib/api-schema.ts`) searches the - published API index from `@sentry/api`, and `sentry api` sends any - request. -- Shell completion is declared for every positional and flag (#1751, #1759) - and completes dataset fields and aggregates (#1782). Completers read the - SQLite completion cache, warmed at login (#1788, #1789). -- HTTP Code Mode (#1670, evals in #1675) runs agent JavaScript in a - Cloudflare Dynamic Worker with no token and no network. A broker in the - parent Worker checks each request against the shipped operation list and - the connection's API Read or API Write grant, then sends it through - `SentryApiService`. -- The hosted MCP runs on Cloudflare Workers. CLI command handlers do not run - there. - -## Goals / Non-Goals - -Goals: - -- One product surface: CLI commands. The MCP adds no Sentry logic of its own. -- Agents can do anything a command can, plus any published API operation. -- The same discovery, completion, and permission rules on stdio and hosted. - -Non-goals: - -- Running the full CLI on Workers. Hosted runs portable operations only. -- Changing the hosted MCP's OAuth flow or moving it off Workers. -- Keeping the catalog's agent-tuned output formats. Code returns data, and - can ask for the CLI's markdown rendering where that helps. - -## Decisions - -### Commands are the only product surface - -A capability an agent needs is a CLI command, built on `@sentry/api`. The -MCP gets no new hand-written tools; pending tool PRs (for example #1846, -#1837, #1517) become commands. Fixes to existing catalog tools are fine until -the cutover, since they ship to users now. - -### CLI parity inventory - -From a listing of `mcp-core/src/tools/catalog/` against -`cli/src/commands/` (by name and brief, not by behavior; each row is checked -when its command lands): - -| MCP catalog tools | CLI today | Gap | -| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------- | -| `whoami`, `find_organizations`, `find_projects`, `find_teams`, `find_dsns` | `auth whoami`, `org list`, `project list`, `team list`, `dsn list` | none | -| `create_project` | `project create` | none | -| `update_project` | none | `project edit` | -| `create_team`, `add_team_to_project`, `remove_team_from_project` | none | `team create`, `project team add/remove` | -| `create_dsn`, `update_dsn` | none | `dsn create`, `dsn edit` | -| `search_issues`, `get_issue_details`, `search_issue_events`, `get_issue_tag_values` | `issue list/view/events/tags` | none | -| `update_issue` | `issue resolve/unresolve/archive` | assignment: `issue assign` | -| `add_issue_note`, `get_issue_activity` | none | `issue comment`, `issue activity` | -| `get_issue_breadcrumbs`, `get_issue_user_reports` | partly in `event view`, `feedback list` | check, then flags or commands | -| `link_issue`, `unlink_issue`, `analyze_issue_with_seer` | `issue link/unlink/explain/plan` | none | -| `get_event_stacktrace` | `event view` | none | -| `get_event_attachment` | none | `event attachment list/download` | -| `search_events`, `search_errors`, `search_logs`, `search_traces`, `search_metrics`, `search_replays` | `event list`, `explore`, `log list`, `trace list`, `replay list` | metrics dataset to check; no natural-language mode (see below) | -| `get_trace_details`, `get_span_details`, `get_replay_details` | `trace view`, `span view`, `replay view` | none | -| `search_profiles`, `get_profile`, `get_profile_details` | none | `profile list/view` | -| `search_attributes` | `attribute list` | none | -| `search_agent_conversations`, `get_agent_conversation_details` | `agent-conversation list/view` | none | -| `find_dashboards`, `get_dashboard_details` | `dashboard list/view` | none | -| `find_releases`, `get_release_details` | `release list/view` | none | -| alert rule and metric monitor CRUD, `get_alert_options` | `alert issues ...`, `alert metrics ...` | `get_alert_options` to check | -| `find_monitors`, `get_monitor_details`, `delete_monitor_environment` | `monitor list` | `monitor view`, `monitor env delete` | -| uptime monitor CRUD (5 tools) | none | `monitor uptime list/view/create/edit/delete` | -| `find_dropped_events` | none | `event dropped` or similar (#1812 open) | -| `get_snapshot`, `get_latest_base_snapshot`, `get_snapshot_image` | `snapshots download/diff` | read commands to check | -| `get_sentry_resource` | URL positionals on most commands | a generic `sentry open ` resolver | -| `search_docs`, `get_doc`, `submit_feedback_to_sentry_team` | `docs list/query`, `cli feedback` | none | -| `get_sentry_mcp_info`, `onboarding_status_update` | MCP-specific | keep as host functions or drop | - -About 20 tools have no command yet, and a few more are partial. Each gap -is a command PR, grouped by domain. - -### The MCP surface: `search_sentry` and `execute_sentry_code` - -Two tools, on stdio and hosted: - -- **`search_sentry`** takes a free-text query and returns compact entries: - commands (path, brief, positionals and flags with types and completion - kinds, examples, output fields, permission class) and API operations - (method, path, summary, parameters). An entry can be expanded to a - TypeScript signature for the matching `sentry.*` call. It also takes a - partial argv and returns completions (see below). -- **`execute_sentry_code`** runs an async JavaScript body with one global, - `sentry`: - - `sentry..(params)`: the `createSentrySDK()` method, - returning the command's data. `{ render: "markdown" }` returns the - command's plain markdown output instead, for commands whose terminal - rendering already summarizes well, such as `issue view` or `trace view`. - - `sentry.api.request({ method, path, query, body })`: a published API - operation, as `sentry api` sends it. - - `sentry.complete(argv)`: completion candidates. - -The tool descriptions stay small; agents learn the surface through -`search_sentry`, not from a full type declaration in the tool list. - -### The sandbox orchestrates; the host runs commands - -The agent's code never runs CLI code and never sees a credential. Each -`sentry.*` call is an RPC to the host: - -1. The host checks the call against the connection's permissions and - constraints. -2. It runs the command operation, or the API request, with its own - credentials. -3. It returns the data, size-capped, to the sandbox. - -This keeps one execution model with two hosts: - -- **stdio (`sentry mcp`):** the host is the CLI process. It runs commands - through `createSentrySDK()` in Node, so every command works there as soon - as it exists, with the CLI's login, cache, and region lookup. The sandbox - is an in-process WebAssembly JavaScript engine (QuickJS); `node:vm` is not - a security boundary. -- **hosted (`mcp-cloudflare`):** the host is the parent Worker and the - sandbox is the Dynamic Worker from #1670. The host runs portable - operations from the CLI's `sentry/mcp` entry (next decision). - -### Portable operations for the hosted host - -The hosted MCP can list every command but can only run commands whose API -work is portable. Commands split, domain by domain, into: - -- **resolve** (host-specific): CLI flags, cwd, config, DSN detection; or - Code Mode parameters and session constraints; -- **operate** (portable): resolved inputs and an `@sentry/api` client in, - the command's data out; -- **render**: the existing `output.human` and JSON modes. - -`operations/` gets the portability guard from `unify-sentry-api-layer` -(no `node:*` imports or Node globals, a DOM-only typecheck) and ships as -the `sentry/mcp` entry with the Code Mode host. Generated metadata marks -each command `portable`; the hosted `search_sentry` shows only portable -commands, so hosted coverage grows as domains move. Commands with unbounded -streams (`--follow`, tail) are not exposed. Commands that need local files -(uploads, `init`, `sourcemap inject`) stay CLI-only. - -This replaces group 11 of `unify-sentry-api-layer`, except that MCP tools no -longer move into the CLI one by one: the Code Mode host exposes operations -generically. - -### One introspection source - -`sentry mcp` reads the route tree at runtime with `lib/introspect.ts`. The -build generates the same metadata as JSON (`command-catalog.json`: commands, -parameters, completion kinds, output fields, the API operations from -`command-api.json`, the permission class, `portable`), and the hosted MCP -reads that from the `sentry/mcp` entry. A test fails when the two differ. - -### API routes: one index for `sentry schema` and Code Mode - -`sentry schema` and #1670's `search_sentry_api` index the same `@sentry/api` -release separately. Both move to `@sentry/api`'s operation catalog -(`@sentry/api/operations`), and Code Mode's path matching uses the shared -matcher from #1807. Only published operations are reachable; endpoints the -CLI or MCP call that are not in the published schema are the main gap, and -`unify-sentry-api-layer` handles them. - -### Completion for agents - -Completers in `lib/complete.ts` read the SQLite completion cache. They are -split into portable completers that take a data source: - -- **stdio:** the SQLite cache, as for the shell, with a live API fallback. -- **hosted:** the live API through the shared client, with the in-memory - response cache. - -`sentry.complete(argv)` and `search_sentry`'s completion mode return -candidates with descriptions for the argument under the cursor: slugs, -issue IDs, enum values, dataset fields and aggregates, and search query -keys and values. This is how agents get valid queries without the catalog's -embedded search agents. - -### Embedded natural-language search agents are dropped - -`search_events`, `search_issues`, and `search_issue_events` translate -natural language with a second model call. Under Code Mode the calling agent -writes the query itself, with `search_sentry` documentation of the query -syntax and `sentry.complete()` for keys and values. Dropping them removes -the MCP's OpenAI and Anthropic provider configuration (#1754 becomes moot). - -### Permissions are command metadata - -Each command gets a permission class from its API operations in -`command-api.json`: **read** if it calls only GET, HEAD, or OPTIONS; -otherwise **write**; and **destructive** if it declares `destructive`. A -lint check fails when the class is missing or disagrees with the operations. - -- Hosted consent keeps #1670's API Read and API Write grants. API Read - allows read commands; API Write allows write commands and raw writes. - Destructive commands need API Write. The existing skill checkboxes - (Triage Issues, Manage Projects & Teams, and so on) map to command groups - until consent is simplified. -- Organization and project constraints from the session (`sentry mcp - /[]`, hosted URL constraints) fill and pin command inputs in - the resolve step. -- The host still checks every outgoing API request against the operation - list and grant (#1670's `ApiAuthorization`), so a command cannot exceed - the connection's grant. -- stdio adds `--read-only`, matching hosted API Read. - -### `@sentry/mcp-server` carries operations, not commands - -The slim bundle (#1731) bans command handlers and has a 3.75 MB budget. -Under Code Mode it needs the operations and the Code Mode host. It bundles -the `sentry/mcp` entry plus the stdio host and QuickJS, and its guard keeps -banning command handlers, Ink, the markdown terminal renderer, and SQLite -WASM. The budget is re-measured when the QuickJS sandbox lands. Commands -that are not yet portable are unavailable in the slim bundle and available -in `sentry mcp`. - -### Eval-gated cutover - -Code Mode ships next to the catalog: - -1. Extend #1675's harness to run the full `mcp-server-evals` suite on three - surfaces: catalog, Code Mode, and both. -2. Make Code Mode the default when it matches or beats the catalog on task - success, and record token cost and latency next to it. Run more than one - client and model, not only Claude Opus. -3. Keep the catalog behind a flag (`--catalog`, a hosted skill) for one - release. -4. Delete the catalog tools, their `SentryApiService` endpoint wrappers, - the search agents, and the provider configuration from `mcp-core`. - -## Risks / Trade-offs - -- **Weaker clients.** Code Mode depends on the model writing correct - JavaScript. Small models and clients that cap tool calls may do worse than - with typed tools. The eval gate runs several models, and the flag keeps - the catalog available during the window. -- **Output quality.** Catalog tools format stack traces, traces, and issue - summaries for agents. `render: "markdown"` reuses the CLI's renderers for - those commands; the evals check the gap. -- **Hosted coverage lags stdio.** Commands appear on hosted only when their - domain is portable. The parity table and `portable` flags make that - visible. -- **Sandbox on stdio.** QuickJS adds bundle size and its own escape surface. - It runs no host code and holds no credentials; every capability is an - RPC the host checks. -- **Unpublished endpoints.** Commands and raw requests can only reach what - `@sentry/api` covers. See `unify-sentry-api-layer`. - -## Migration Plan - -1. Command metadata (permission class, `portable`, generated catalog) and the - parity commands, by domain. -2. Code Mode on stdio over `createSentrySDK()`, behind a flag, with the - QuickJS sandbox and completion. -3. `sentry schema` and Code Mode on one `@sentry/api` operation index. -4. Hosted Code Mode (#1670) on portable operations, domain by domain. -5. Eval gate, default flip, catalog deletion. - -**Rollback:** until the catalog is deleted, the flag switches back. After -that, a revert of the deletion PR restores it. - -## Open Questions - -- Whether `get_sentry_mcp_info` and `onboarding_status_update` survive as - host functions on `sentry` or are dropped. -- The QuickJS bundle cost, and whether Node's permission model is good - enough on supported Node versions to use a worker thread instead. -- When to simplify hosted consent from skill checkboxes to API Read, API - Write, and destructive. diff --git a/openspec/changes/cli-first-code-mode-mcp/proposal.md b/openspec/changes/cli-first-code-mode-mcp/proposal.md deleted file mode 100644 index 926060606..000000000 --- a/openspec/changes/cli-first-code-mode-mcp/proposal.md +++ /dev/null @@ -1,83 +0,0 @@ -# Proposal - -## Why - -Sentry's product logic for agents lives in two places. The CLI has about 40 -command groups built on `@sentry/api`. The MCP server has 75 hand-written -catalog tools in `mcp-core/src/tools/catalog/`, with their own endpoint -wrappers (`mcp-core/src/api-client/client.ts`, about 5,000 lines), their own -Zod output formatting, and embedded natural-language search agents that need -an OpenAI or Anthropic key. Every capability is built twice, or exists in -only one product. - -The earlier plan (`unify-sentry-api-layer`, group 11) kept both surfaces and -moved each MCP tool into the CLI as a portable `sentry/mcp` tool definition. -That still means one hand-written MCP tool per command. - -Code Mode removes the second surface. HTTP Code Mode (#1670) already shows -agents completing API tasks the catalog cannot do, such as copying a -dashboard (#1675 evals). If the CLI is the only product surface, the MCP can -be a thin Code Mode layer over it: agents discover commands through the -CLI's own introspection and call them through the CLI SDK. - -## What Changes - -1. **CLI parity.** Every MCP catalog tool gets a CLI command. New Sentry - capabilities are added as CLI commands only; no new hand-written MCP - tools. Commands call the API through `@sentry/api` - (`unify-sentry-api-layer`). -2. **The MCP is Code Mode over the CLI.** The MCP exposes two tools: - `search_sentry` to discover commands and API operations, and - `execute_sentry_code` to run JavaScript that calls them through a - `sentry` object shaped like `createSentrySDK()`. The agent's code runs in - a sandbox with no credentials or network; each `sentry.*` call goes to - the host, which checks permissions and runs the command. -3. **One introspection source.** `sentry mcp` builds the tool surface from - the CLI's route tree at runtime (`lib/introspect.ts`). The hosted MCP - lists the same commands from metadata generated from the CLI at build - time, and runs the portable ones. -4. **Every published API route.** `search_sentry` also searches the API - schema that backs `sentry schema`, and `execute_sentry_code` exposes - `sentry.api.request()`, the equivalent of `sentry api`, for operations - with no command. -5. **Completion for agents.** The shell completion system - (`lib/complete.ts`, `lib/completion-target.ts`) answers - `sentry.complete(argv)` inside Code Mode and completion queries in - `search_sentry`: organization and project slugs, issue IDs, dataset - fields and aggregates, and search query terms. -6. **The catalog goes away after an eval gate.** Code Mode ships next to the - catalog. When the MCP eval suite shows it matching the catalog, it - becomes the default, and the catalog tools, their endpoint wrappers, and - the embedded search agents are deleted from `mcp-core`. - -## Capabilities - -### New Capabilities - -- `mcp-code-mode`: The MCP's tool surface: discovery, execution, permissions, - and completion over the CLI's commands and the published API. - -### Modified Capabilities - -None. The project-management tool contract in -`refine-project-management-tools` is superseded: those operations become CLI -commands. - -## Impact - -- `packages/cli`: new commands for the parity gaps; agent metadata on - commands (permission class, portability); a portable `sentry/mcp` entry - with the Code Mode host and portable operations; portable completers. -- `packages/mcp-core`: keeps MCP plumbing (registration, transports, - constraints, Code Mode tool definitions). Loses the catalog tools, the - `SentryApiService` endpoint wrappers they use, the search agents, and the - agent provider configuration. -- `packages/mcp-cloudflare`: Code Mode from #1670 switches from raw HTTP to - the CLI's portable operations, with `sentry.api.request()` kept for raw - calls. OAuth consent maps to command permission classes. -- `packages/mcp-server`: the slim bundle carries portable operations instead - of catalog tools. Its size budget and module guard change. -- `packages/mcp-server-evals`: Code Mode and catalog runs of the full suite, - then Code Mode only. -- Docs: mcp.sentry.dev setup, `docs/` architecture pages, and the agent - skills (#1357 skills audit). diff --git a/openspec/changes/cli-first-code-mode-mcp/specs/mcp-code-mode/spec.md b/openspec/changes/cli-first-code-mode-mcp/specs/mcp-code-mode/spec.md deleted file mode 100644 index f033ab36e..000000000 --- a/openspec/changes/cli-first-code-mode-mcp/specs/mcp-code-mode/spec.md +++ /dev/null @@ -1,88 +0,0 @@ -# Spec Delta - -## Purpose - -Defines the MCP's tool surface as Code Mode over the CLI: how agents -discover commands and API operations, how their code calls them, what each -connection may do, and how completion helps them build valid calls. - -## ADDED Requirements - -### Requirement: Commands are the product surface - -Every Sentry capability the MCP offers SHALL be a CLI command or a published -API operation. The MCP SHALL NOT define Sentry operations of its own. - -#### Scenario: A new capability - -- **WHEN** a Sentry capability is added for agents -- **THEN** it is added as a CLI command -- **THEN** it appears in `search_sentry` with no MCP code change - -### Requirement: Discovery through CLI introspection - -`search_sentry` SHALL return commands from the CLI's introspection metadata -and operations from the `@sentry/api` operation catalog. The stdio server -SHALL read the route tree at runtime; the hosted server SHALL read the -generated catalog, and SHALL list only commands marked `portable`. - -#### Scenario: Search finds a command and an operation - -- **WHEN** an agent searches for "copy dashboard" -- **THEN** the result includes the dashboard commands and the dashboard API operations, each with its permission class - -#### Scenario: Hosted hides non-portable commands - -- **WHEN** a command's operation is not portable -- **THEN** hosted `search_sentry` does not list it -- **THEN** `sentry mcp` lists it - -### Requirement: Code runs without credentials - -`execute_sentry_code` SHALL run agent code in a sandbox with no network -access and no credentials. Each `sentry.*` call SHALL be executed by the -host, which SHALL check the call against the connection's permissions and -constraints before running it. - -#### Scenario: Sandbox network access - -- **WHEN** agent code calls `fetch` directly -- **THEN** the call fails - -#### Scenario: Constrained session - -- **WHEN** a session is constrained to an organization and code calls a command for another organization -- **THEN** the host refuses the call - -### Requirement: Permission classes - -Every command SHALL have a permission class (read, write, or destructive) -that agrees with the API operations it calls. A read-only connection SHALL -run only read commands and read API operations. Every outgoing API request -SHALL also be checked against the published operation list and the grant. - -#### Scenario: Read-only connection runs a write command - -- **WHEN** a read-only connection calls `sentry.issue.resolve()` -- **THEN** the host refuses the call without sending an API request - -### Requirement: Completion for agents - -`sentry.complete(argv)` SHALL return completion candidates for the argument -under the cursor, from the same completers as shell completion, with a data -source suited to the host. - -#### Scenario: Dataset field completion - -- **WHEN** code calls `sentry.complete(["explore", "--dataset", "spans", "--field", "span.d"])` -- **THEN** the result includes span attribute keys that start with `span.d` - -### Requirement: Eval-gated catalog removal - -The catalog tools SHALL NOT be removed until the MCP eval suite shows Code -Mode matching the catalog on task success. - -#### Scenario: Default flip - -- **WHEN** Code Mode matches the catalog on the eval suite -- **THEN** Code Mode becomes the default and the catalog stays behind a flag for one release diff --git a/openspec/changes/cli-first-code-mode-mcp/tasks.md b/openspec/changes/cli-first-code-mode-mcp/tasks.md deleted file mode 100644 index 56ca1b21b..000000000 --- a/openspec/changes/cli-first-code-mode-mcp/tasks.md +++ /dev/null @@ -1,63 +0,0 @@ -# Tasks - -The API work these tasks build on is `unify-sentry-api-layer`. New commands -use `@sentry/api` operations and do not wait for the shared client to move -there. - -## 1. Command metadata for agents - -- [ ] 1.1 Add a permission class (read, write, destructive) to every command, derived from its operations in `command-api.json` and its `destructive` flag, with a lint check that fails when a class is missing or disagrees with the operations. Verify the check fails on a planted GET-only command marked write and a write command marked read. -- [ ] 1.2 Generate `command-catalog.json` from the route tree: path, brief, parameters with types and completion kinds, examples, output fields, API operations, permission class, and `portable`. Verify a test that it matches `lib/introspect.ts` output for the full tree. - -## 2. CLI parity with the MCP catalog - -One PR per domain. Each adds the commands from the parity table in design.md, on `@sentry/api` operations, with unit tests, and ticks its rows. Pending MCP tool PRs for the same capability are redirected here. - -- [ ] 2.1 Projects and teams: `project edit`, `team create`, `project team add/remove`. Supersedes `refine-project-management-tools`. -- [ ] 2.2 DSNs: `dsn create`, `dsn edit`. -- [ ] 2.3 Issues: `issue assign`, `issue comment`, `issue activity`; breadcrumbs and user reports where `event view` and `feedback list` fall short. -- [ ] 2.4 Events: `event attachment list/download`; dropped events (#1812). -- [ ] 2.5 Monitors: `monitor view`, `monitor env delete`, `monitor uptime list/view/create/edit/delete` (the detectors work in #1837 moves here). -- [ ] 2.6 Profiles: `profile list/view`. -- [ ] 2.7 Snapshots, alert options, and the metrics dataset in `explore`: check against the MCP tools and fill the gaps. -- [ ] 2.8 `sentry open `: resolve any Sentry URL to the matching resource and command. -- [ ] 2.9 Mark every row in the parity table done or dropped, with the reason. - -## 3. Code Mode on stdio - -- [ ] 3.1 Add the Code Mode tool definitions (`search_sentry`, `execute_sentry_code`) to `mcp-core`, reusing #1670's descriptions and result handling. Verify `pnpm run measure-tokens`. -- [ ] 3.2 Add the stdio host in the CLI: a QuickJS sandbox with a `sentry` global whose calls are RPCs to the host, which runs them through `createSentrySDK()` with the session's credentials, constraints, and permission class, and size-caps results. Multi-yield commands are buffered; streaming commands are not exposed. Verify tests for each permission class, constraint pinning, a call that exceeds the result cap, and that the sandbox has no network or host globals. -- [ ] 3.3 Serve it from `sentry mcp --code-mode`, next to the catalog. Add `--read-only`. Verify `/mcp-qa` over stdio. -- [ ] 3.4 Add `render: "markdown"` for commands with a plain markdown renderer. Verify a test for `issue view` and `trace view`. - -## 4. API routes in Code Mode - -- [ ] 4.1 Move `sentry schema`'s index and #1670's API search onto `@sentry/api`'s operation catalog, with #1807's shared path matcher. Verify `sentry schema` output and the Code Mode search tests are unchanged. -- [ ] 4.2 Add `sentry.api.request()` on stdio, with the same operation list and permission checks as hosted. Verify a raw read, a raw write denied under `--read-only`, and an unpublished path refused. - -## 5. Completion for agents - -- [ ] 5.1 Split the completers in `lib/complete.ts` into portable completers over a data source: the SQLite cache with a live API fallback on stdio, the live API with the memory response cache on hosted. Verify the shell completion tests pass unchanged. -- [ ] 5.2 Add `sentry.complete(argv)` and `search_sentry`'s completion mode, returning candidates with descriptions. Verify tests for slugs, issue IDs, dataset fields and aggregates, and query keys and values. -- [ ] 5.3 Document the query syntax in `search_sentry` results for the search commands, replacing the embedded search agents' prompts. - -## 6. Hosted Code Mode on the CLI - -- [ ] 6.1 Land #1670 (HTTP Code Mode), #1675 (evals), and #1807 (shared path matcher) as the hosted base. -- [ ] 6.2 Split commands into resolve, operate, and render, domain by domain, starting with the read domains the evals use most (issues, events, traces, organizations and projects). Operations move to `operations/` and return the command's data. Verify the CLI unit and SDK tests pass with output unchanged. (Formerly `unify-sentry-api-layer` 11.1.) -- [ ] 6.3 Guard `operations/` and the Code Mode host with the portability checks and expose them as the `sentry/mcp` entry. Verify the guard fails on a planted `node:` import, and that `mcp-cloudflare` bundles the entry without command handlers, SQLite, or Ink. (Formerly 11.2.) -- [ ] 6.4 Switch hosted Code Mode to the `sentry/mcp` host: `search_sentry` lists portable commands from `command-catalog.json`, `execute_sentry_code` runs their operations, and `sentry.api.request()` keeps #1670's broker. Verify the Workers tests and the #1675 evals. -- [ ] 6.5 Map hosted consent to permission classes: API Read, API Write, destructive, and the existing skills as command groups. Verify a test per grant. -- [ ] 6.6 Allow `mcp-cloudflare` to import only `sentry/mcp` from `sentry`. Verify the rule fails on a planted import. (Formerly 11.7.) -- [ ] 6.7 Compare the CLI's OAuth scopes with the MCP `SCOPES` and widen the CLI's if needed. Verify a test that the CLI's scopes cover every command's operations. (Formerly 11.5.) - -## 7. Slim stdio bundle - -- [ ] 7.1 Bundle the `sentry/mcp` entry, the stdio host, and QuickJS into `@sentry/mcp-server`, keeping the guard against command handlers, Ink, the terminal renderer, and SQLite WASM. Re-measure the size budget. Verify the bundle test (`initialize`, `tools/list`, an `execute_sentry_code` call against mocks). - -## 8. Eval gate and cutover - -- [ ] 8.1 Run the full `mcp-server-evals` suite on catalog, Code Mode, and both, with at least two clients or models. Record task success, tokens, and latency in `docs/testing/`. -- [ ] 8.2 Make Code Mode the default on stdio and hosted once it matches the catalog. Keep the catalog behind `--catalog` and a hosted skill for one release. -- [ ] 8.3 Delete the catalog tools, their `SentryApiService` endpoint wrappers, the embedded search agents, and the agent provider configuration from `mcp-core`. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. -- [ ] 8.4 Update mcp.sentry.dev setup, `docs/` architecture and operations pages, and the agent skills for the Code Mode surface. diff --git a/openspec/changes/cli-first-code-mode-mcp/.openspec.yaml b/openspec/changes/cli-first-mcp/.openspec.yaml similarity index 100% rename from openspec/changes/cli-first-code-mode-mcp/.openspec.yaml rename to openspec/changes/cli-first-mcp/.openspec.yaml diff --git a/openspec/changes/cli-first-mcp/design.md b/openspec/changes/cli-first-mcp/design.md new file mode 100644 index 000000000..24eb02661 --- /dev/null +++ b/openspec/changes/cli-first-mcp/design.md @@ -0,0 +1,337 @@ +# Design + +## Context + +- The CLI's command tree is introspectable at runtime (`lib/introspect.ts`, + `sentry help --json`) and at build time (`generate-skill.ts`, + `generate-command-api.ts`). `src/generated/command-api.json` records which + API operations each command calls, and commands declare `destructive` + (`lib/command.ts`). +- `createSentrySDK()` (`src/index.ts`, `lib/sdk-invoke.ts`) calls command + handlers directly with typed flags and returns their data, buffering + commands that yield more than once. It takes a token, URL, org, project, + and `cwd` per instance, and scopes the environment per call with + `AsyncLocalStorage` (`lib/env.ts`). +- The CLI's local state goes through one synchronous SQLite adapter + (`lib/db/sqlite.ts`: `node:sqlite` `DatabaseSync`, or a WASM fallback), + opened as a module-level singleton by `getDatabase()` in 23 files. A + `detached` mode already uses an in-memory database. +- In `commands/` and `lib/`, 156 files import `node:` modules. The most + common are `path`, `fs`, `fs/promises`, `os`, `child_process` (23 files), + `tty` (14), and `crypto`. +- `sentry schema` (`commands/schema.ts`, `lib/api-schema.ts`) searches the + published API index from `@sentry/api`, and `sentry api` sends any + request. +- Shell completion is declared for every positional and flag (#1751, #1759) + and completes dataset fields and aggregates (#1782). Completers read the + SQLite completion cache, warmed at login (#1788, #1789). +- HTTP Code Mode (#1670, evals in #1675) runs agent JavaScript in a + Cloudflare Dynamic Worker with no token and no network. A broker in the + parent Worker checks each request against the shipped operation list and + the connection's API Read or API Write grant. +- The hosted MCP runs on Cloudflare Workers (`compatibility_date` + 2025-03-21, with KV and no Durable Objects or D1 today). CLI command + handlers do not run there yet. + +## Goals / Non-Goals + +Goals: + +- One product surface: CLI commands. The MCP adds no Sentry logic of its own. +- Agents can do anything a command can, plus any published API operation. +- The same discovery, completion, and permission rules on stdio and hosted. + +Non-goals: + +- Running commands that need the user's machine on Workers: local files, + child processes, the terminal. +- Changing the hosted MCP's OAuth flow or moving it off Workers. +- A code-execution sandbox. Code Mode (#1670) can come later as an extra + tool over the same SDK. +- Keeping the catalog's agent-tuned output formats. Commands return data, + and can return their own markdown rendering where that helps. + +## Decisions + +### Commands are the only product surface + +A capability an agent needs is a CLI command, built on `@sentry/api`. The +MCP gets no new hand-written tools; pending tool PRs (for example #1846, +#1837, #1517) become commands. Fixes to existing catalog tools are fine until +the cutover, since they ship to users now. + +### CLI parity inventory + +From a listing of `mcp-core/src/tools/catalog/` against +`cli/src/commands/` (by name and brief, not by behavior; each row is checked +when its command lands): + +| MCP catalog tools | CLI today | Gap | +| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------- | +| `whoami`, `find_organizations`, `find_projects`, `find_teams`, `find_dsns` | `auth whoami`, `org list`, `project list`, `team list`, `dsn list` | none | +| `create_project` | `project create` | none | +| `update_project` | none | `project edit` | +| `create_team`, `add_team_to_project`, `remove_team_from_project` | none | `team create`, `project team add/remove` | +| `create_dsn`, `update_dsn` | none | `dsn create`, `dsn edit` | +| `search_issues`, `get_issue_details`, `search_issue_events`, `get_issue_tag_values` | `issue list/view/events/tags` | none | +| `update_issue` | `issue resolve/unresolve/archive` | assignment: `issue assign` | +| `add_issue_note`, `get_issue_activity` | none | `issue comment`, `issue activity` | +| `get_issue_breadcrumbs`, `get_issue_user_reports` | partly in `event view`, `feedback list` | check, then flags or commands | +| `link_issue`, `unlink_issue`, `analyze_issue_with_seer` | `issue link/unlink/explain/plan` | none | +| `get_event_stacktrace` | `event view` | none | +| `get_event_attachment` | none | `event attachment list/download` | +| `search_events`, `search_errors`, `search_logs`, `search_traces`, `search_metrics`, `search_replays` | `event list`, `explore`, `log list`, `trace list`, `replay list` | metrics dataset to check; no natural-language mode (see below) | +| `get_trace_details`, `get_span_details`, `get_replay_details` | `trace view`, `span view`, `replay view` | none | +| `search_profiles`, `get_profile`, `get_profile_details` | none | `profile list/view` | +| `search_attributes` | `attribute list` | none | +| `search_agent_conversations`, `get_agent_conversation_details` | `agent-conversation list/view` | none | +| `find_dashboards`, `get_dashboard_details` | `dashboard list/view` | none | +| `find_releases`, `get_release_details` | `release list/view` | none | +| alert rule and metric monitor CRUD, `get_alert_options` | `alert issues ...`, `alert metrics ...` | `get_alert_options` to check | +| `find_monitors`, `get_monitor_details`, `delete_monitor_environment` | `monitor list` | `monitor view`, `monitor env delete` | +| uptime monitor CRUD (5 tools) | none | `monitor uptime list/view/create/edit/delete` | +| `find_dropped_events` | none | `event dropped` or similar (#1812 open) | +| `get_snapshot`, `get_latest_base_snapshot`, `get_snapshot_image` | `snapshots download/diff` | read commands to check | +| `get_sentry_resource` | URL positionals on most commands | a generic `sentry open ` resolver | +| `search_docs`, `get_doc`, `submit_feedback_to_sentry_team` | `docs list/query`, `cli feedback` | none | +| `get_sentry_mcp_info`, `onboarding_status_update` | MCP-specific | keep as host functions or drop | + +About 20 tools have no command yet, and a few more are partial. Each gap +is a command PR, grouped by domain. + +### Two tools: `search_sentry` and `execute_sentry_command` + +The MCP hides the command tree behind two tools, so the tool list stays +small however many commands the CLI has (well over 100 leaf commands): + +- **`search_sentry`** + - `query`: free text. Returns ranked commands with path, brief, + permission class, and whether the host can run it. + - `command`: a command path such as `issue list`. Returns its input schema + (positionals and flags as JSON Schema, from introspection), examples, + and output fields. + - `complete`: a command, a parameter, the value typed so far, and the + other parameters. Returns completion candidates (see below). +- **`execute_sentry_command`** takes `{ command, params }`: + 1. validates `params` against the command's schema; an invalid slug, + enum value, or query key fails with completion candidates in the + error; + 2. checks the command's permission class and the session's constraints; + 3. calls the matching `createSentrySDK()` method and returns its data, + size-capped, with the next cursor for paginated commands. With + `format: "markdown"` it returns the command's plain markdown output + instead, for commands whose terminal rendering already summarizes + well, such as `issue view` or `trace view`. + + Commands that yield more than once are buffered into one result, as the + SDK does today. Streaming flags (`--follow`, tail) are rejected. + +A second mode lists every exposed command as its own tool +(`sentry_issue_list`, with the same schema and description). It is for +harnesses that search large tool lists themselves. It comes from the same +metadata, so both modes always agree. The two-tool mode is the default. + +#### How this differs from Code Mode + +Code Mode (Cloudflare's term, and #1670) also hides tools behind search and +execute, but its execute tool takes a program: the model writes JavaScript +that calls several operations, loops, and filters, and only the program's +result goes back into context. That code is arbitrary, so it needs a sandbox. + +`execute_sentry_command` runs one known command per call. The agent cannot +run anything that is not a command, and the parameters are validated data, +so there is nothing to sandbox. The cost is that multi-step work takes one +model turn per call, and intermediate results pass through context. Command +flags (`--limit`, `--fields`, queries) and `format` keep results small. +If evals show multi-step tasks need it, Code Mode can be added later as a +third tool whose `sentry` object calls the same `execute_sentry_command` +path, permission checks included. + +### What is exposed + +Every command declares how agents may use it, and a lint check fails when a +command has no declaration: + +- **exposed**: available on stdio and hosted, once the hosted SDK build can + run it. +- **stdio-only**: needs the user's machine: local files (uploads, + `sourcemap inject`, `debug-files`, `init`), DSN detection from the `cwd`, + or the terminal. +- **hidden**: never exposed: session management (`auth login`, + `auth logout`), CLI maintenance (`cli upgrade`, `cli setup`, + `cli uninstall`), `local`, and `monitor run`, which runs an arbitrary + child process. + +### The CLI SDK on Workers, with pluggable backends + +The hosted MCP runs commands through `createSentrySDK()` too, rather than +through a second, portable copy of each command. Three things stand in the +way, and none is the API code itself: + +**Storage.** The CLI's state is all in one synchronous SQLite database. +D1 would be a poor fit: it is asynchronous, so every `getDatabase()` caller +would have to become async, and it is a shared remote database where the +CLI expects a private one. What the database holds is better split by +purpose behind small interfaces, each with a Workers backend: + +| State | stdio | hosted | +| ---------------------------- | ----------------------- | ---------------------------------------------------------------------------- | +| credentials, accounts | SQLite | the OAuth grant's token, per request | +| defaults (org, project, URL) | SQLite, config | session constraints, per request | +| response cache | filesystem `CacheStore` | memory `CacheStore`, later the R2 tier from `unify-sentry-api-layer` group 7 | +| region and completion caches | SQLite | memory per isolate; KV if hit rates warrant | +| DSN cache, telemetry state | SQLite | not used | + +If splitting the database by purpose turns out larger than expected, a +shorter path is a third adapter behind `lib/db/sqlite.ts` over Durable +Object SQLite storage, which is synchronous and private to the object. One +object per OAuth grant keeps the CLI's single-user model. Neither D1 nor +Durable Objects are used by the hosted MCP today. + +The filesystem needs no Workers backend. Commands that read or write local +files are stdio-only, and the cache directory is replaced by `CacheStore`. +R2 backs the response cache, not a filesystem. + +**Per-request state.** The CLI assumes one user per process: the database +handle, the env-token snapshots, and the caches are module-level. A Workers +isolate serves many users at once, so per-user state moves into the +invocation context, using `AsyncLocalStorage` as `withEnv()` already does. +A test runs two identities concurrently in one isolate and checks that +neither sees the other's credentials, constraints, or cache entries. + +**Node built-ins.** `nodejs_compat` provides `path`, `crypto`, `util`, +`zlib`, `async_hooks`, and timers. It does not provide `child_process`, +`tty`, or an HTTP server, and its `fs` is an in-memory file system that +likely needs a newer compatibility date than the hosted MCP uses (to confirm in the spike). What matters is +what the exposed commands import transitively, which only a build answers. +So the first task is a spike: bundle the SDK for workerd with the exposed +commands, and list what fails to bundle or run, and the bundle size against +the Workers script limit. + +Commands whose imports cannot be made Workers-safe are marked stdio-only +until fixed. If a whole domain is blocked, that domain's commands split into +input resolution and a portable operation (the old +`unify-sentry-api-layer` 11.1), for that domain only. + +### One introspection source + +`sentry mcp` reads the route tree at runtime with `lib/introspect.ts`. The +build generates the same metadata as JSON (`command-catalog.json`: commands, +JSON Schema for parameters, completion kinds, examples, output fields, the +API operations from `command-api.json`, the permission class, and the +exposure), and the hosted MCP reads that. A test fails when the two differ. + +### API routes through `sentry api` and `sentry schema` + +`sentry schema` and `sentry api` are exposed commands, so agents search the +published API with `execute_sentry_command({ command: "schema", ... })` and +call any published operation with `sentry api`. The permission class of a +`sentry api` call comes from its method, and hosted checks the path against +the published operation list with #1807's shared matcher, as #1670's broker +does. `sentry schema`'s index moves onto `@sentry/api`'s operation catalog +(`@sentry/api/operations`). Endpoints missing from the published schema +are the main gap, and `unify-sentry-api-layer` 8.8 handles them. + +### Completion for agents + +Completers in `lib/complete.ts` read the SQLite completion cache. They are +split into completers over a data source: + +- **stdio:** the SQLite cache, as for the shell, with a live API fallback. +- **hosted:** the live API, with the memory cache. + +`search_sentry`'s `complete` mode and `execute_sentry_command`'s validation +errors return candidates with descriptions for the parameter in question: +slugs, issue IDs, enum values, dataset fields and aggregates, and search +query keys and values. This is how agents build valid queries without the +catalog's embedded search agents. + +### Embedded natural-language search agents are dropped + +`search_events`, `search_issues`, and `search_issue_events` translate +natural language with a second model call. Under this plan the calling agent +writes the query itself, using the query syntax in the command's +`search_sentry` entry and completion for keys and values. Dropping them +removes the MCP's OpenAI and Anthropic provider configuration (#1754 becomes +moot). + +### Permissions are command metadata + +Each command gets a permission class from its API operations in +`command-api.json`: **read** if it calls only GET, HEAD, or OPTIONS; +otherwise **write**; and **destructive** if it declares `destructive`. A +lint check fails when the class is missing or disagrees with the operations. + +- Hosted consent keeps #1670's API Read and API Write grants. API Read + allows read commands; API Write allows write and destructive commands. + The existing skill checkboxes (Triage Issues, Manage Projects & Teams, and + so on) map to command groups until consent is simplified. +- Organization and project constraints from the session (`sentry mcp + /[]`, hosted URL constraints) fill and pin command + parameters; a call for another organization is refused. +- stdio adds `--read-only`, matching hosted API Read. + +### `@sentry/mcp-server` needs the exposed commands + +The slim bundle (#1731) bans command handlers and has a 3.75 MB budget. +Under this plan it must carry the exposed command handlers, since that is +what its tools run. It bundles them with the SDK and keeps banning the +stdio-only and hidden commands, Ink, the terminal markdown renderer, and +SQLite WASM. The budget is re-measured. Whether the package is still worth +keeping, rather than pointing installs at `npx sentry mcp`, is an open +question. + +### Eval-gated cutover + +The command surface ships next to the catalog: + +1. Extend #1675's harness to run the full `mcp-server-evals` suite on the + catalog, the two-tool mode, and the all-tools mode. +2. Make the command surface the default when it matches or beats the + catalog on task success, and record token cost and turn count next to + it. Run more than one client and model. +3. Keep the catalog behind a flag (`--catalog`, a hosted skill) for one + release. +4. Delete the catalog tools, their `SentryApiService` endpoint wrappers, + the search agents, and the provider configuration from `mcp-core`. + +## Risks / Trade-offs + +- **More turns than Code Mode** for multi-step tasks. The evals record turn + count; Code Mode stays available as a later addition. +- **Output quality.** Catalog tools format stack traces, traces, and issue + summaries for agents. `format: "markdown"` reuses the CLI's renderers; the + evals check the gap. +- **Cross-user leaks on hosted.** Per-request state is the riskiest part of + running the SDK on Workers. The concurrent two-identity test is required + before hosted serves any command. +- **Workers compatibility.** Unknown until the spike. Commands that fail + stay stdio-only, so hosted coverage can lag stdio. +- **Tool descriptions come from CLI help text.** Weak help text makes weak + tools. Improving it improves both the CLI and the MCP. +- **Unpublished endpoints.** Commands and `sentry api` can only reach what + `@sentry/api` covers. See `unify-sentry-api-layer`. + +## Migration Plan + +1. Command metadata (permission class, exposure, generated catalog) and the + parity commands, by domain. +2. The two tools on stdio over `createSentrySDK()`, behind a flag, with + completion. +3. The Workers spike, then pluggable backends and per-request state, then + hosted on the SDK. +4. Eval gate, default flip, catalog deletion. + +**Rollback:** until the catalog is deleted, the flag switches back. After +that, a revert of the deletion PR restores it. + +## Open Questions + +- Which mode each client should default to: two tools, or every command as + a tool for harnesses that search tools themselves. +- Whether to keep `@sentry/mcp-server` or point installs at `sentry mcp`. +- Memory per isolate versus Durable Object SQLite for hosted caches, decided + by hit rates after launch. +- Whether `get_sentry_mcp_info` and `onboarding_status_update` survive. +- When to simplify hosted consent from skill checkboxes to API Read, API + Write, and destructive. diff --git a/openspec/changes/cli-first-mcp/proposal.md b/openspec/changes/cli-first-mcp/proposal.md new file mode 100644 index 000000000..72c04e2c0 --- /dev/null +++ b/openspec/changes/cli-first-mcp/proposal.md @@ -0,0 +1,86 @@ +# Proposal + +## Why + +Sentry's product logic for agents lives in two places. The CLI has about 40 +command groups built on `@sentry/api`. The MCP server has 75 hand-written +catalog tools in `mcp-core/src/tools/catalog/`, with their own endpoint +wrappers (`mcp-core/src/api-client/client.ts`, about 5,000 lines), their own +Zod output formatting, and embedded natural-language search agents that need +an OpenAI or Anthropic key. Every capability is built twice, or exists in +only one product. + +The earlier plan (`unify-sentry-api-layer`, group 11) kept both surfaces and +moved each MCP tool into the CLI as a portable tool definition. That still +means one hand-written MCP tool per command. + +If the CLI is the only product surface, the MCP can be a thin layer that +exposes the CLI's commands as tools, found through the CLI's own +introspection and run through the CLI SDK. + +## What Changes + +1. **CLI parity.** Every MCP catalog tool gets a CLI command. New Sentry + capabilities are added as CLI commands only; no new hand-written MCP + tools. Commands call the API through `@sentry/api` + (`unify-sentry-api-layer`). +2. **The MCP exposes the CLI's commands.** By default it lists two tools: + `search_sentry` finds commands, and `execute_sentry_command` runs one + command by name with JSON parameters through `createSentrySDK()`. No + sandbox is involved: the agent can only run commands that exist, with + parameters their schema accepts. Harnesses that search large tool lists + themselves can instead list every command as its own tool. +3. **One introspection source.** Tool names, descriptions, and input schemas + come from the CLI's route tree (`lib/introspect.ts`) on stdio and from + metadata generated from it at build time on hosted. +4. **Every published API route.** `sentry api` and `sentry schema` are + commands like any other, so `search_sentry` and `execute_sentry_command` + reach every published `@sentry/api` operation with no extra tool. +5. **Completion for agents.** The shell completion system + (`lib/complete.ts`, `lib/completion-target.ts`) answers completion + queries in `search_sentry` and suggests values when a parameter fails + validation: organization and project slugs, issue IDs, dataset fields + and aggregates, and search query terms. +6. **The CLI SDK runs on the hosted MCP.** The SDK's storage (SQLite, the + filesystem, the config directory) becomes pluggable, with Workers + backends, and its per-process state becomes per-request. The hosted MCP + then runs commands through the same SDK as `sentry mcp`. +7. **The catalog goes away after an eval gate.** The command surface ships + next to the catalog. When the MCP eval suite shows it matching the + catalog, it becomes the default, and the catalog tools, their endpoint + wrappers, and the embedded search agents are deleted from `mcp-core`. + +HTTP Code Mode (#1670), where the agent writes JavaScript that a sandbox +runs, is not part of this plan's critical path. It can be added later as a +third tool over the same SDK, for multi-step work that benefits from code. + +## Capabilities + +### New Capabilities + +- `mcp-command-surface`: The MCP's tool surface: discovery, execution, + permissions, and completion over the CLI's commands. + +### Modified Capabilities + +None. The project-management tool contract in +`refine-project-management-tools` is superseded: those operations become CLI +commands. + +## Impact + +- `packages/cli`: new commands for the parity gaps; agent metadata on + commands (permission class, hosted availability); pluggable storage + backends; per-request SDK state; a Workers build of the SDK. +- `packages/mcp-core`: keeps MCP plumbing (registration, transports, + constraints, the two tool definitions). Loses the catalog tools, the + `SentryApiService` endpoint wrappers they use, the search agents, and the + agent provider configuration. +- `packages/mcp-cloudflare`: runs commands through the CLI SDK with Workers + storage backends. OAuth consent maps to command permission classes. +- `packages/mcp-server`: the slim bundle needs the command handlers it + exposes; its module guard and size budget change. +- `packages/mcp-server-evals`: command-surface and catalog runs of the full + suite, then the command surface only. +- Docs: mcp.sentry.dev setup, `docs/` architecture pages, and the agent + skills (#1357 skills audit). diff --git a/openspec/changes/cli-first-mcp/specs/mcp-command-surface/spec.md b/openspec/changes/cli-first-mcp/specs/mcp-command-surface/spec.md new file mode 100644 index 000000000..ece815008 --- /dev/null +++ b/openspec/changes/cli-first-mcp/specs/mcp-command-surface/spec.md @@ -0,0 +1,98 @@ +# Spec Delta + +## Purpose + +Defines the MCP's tool surface as the CLI's commands: how agents discover +them, how one is run, what each connection may do, and how completion helps +agents build valid calls. + +## ADDED Requirements + +### Requirement: Commands are the product surface + +Every Sentry capability the MCP offers SHALL be a CLI command. The MCP SHALL +NOT define Sentry operations of its own. + +#### Scenario: A new capability + +- **WHEN** a Sentry capability is added for agents +- **THEN** it is added as a CLI command +- **THEN** it appears in `search_sentry` with no MCP code change + +### Requirement: Discovery through CLI introspection + +`search_sentry` SHALL return commands, their input schemas, and their +permission classes from the CLI's introspection metadata. The stdio server +SHALL read the route tree at runtime; the hosted server SHALL read the +generated catalog, and SHALL list only commands its build can run. + +#### Scenario: Search finds a command + +- **WHEN** an agent searches for "resolve an issue" +- **THEN** the result includes `issue resolve` with its permission class +- **WHEN** the agent asks for the schema of `issue resolve` +- **THEN** the result is the command's parameters as JSON Schema + +### Requirement: Execution runs only exposed commands + +`execute_sentry_command` SHALL run only commands marked exposed, with +parameters that validate against the command's schema, through the CLI SDK. +It SHALL NOT run arbitrary code. + +#### Scenario: Hidden command + +- **WHEN** an agent calls `execute_sentry_command` with `monitor run` +- **THEN** the call fails without running anything + +#### Scenario: Invalid parameter value + +- **WHEN** an agent passes an organization slug that does not exist +- **THEN** the call fails with completion candidates for that parameter + +### Requirement: Permission classes + +Every command SHALL have a permission class (read, write, or destructive) +that agrees with the API operations it calls. A read-only connection SHALL +run only read commands, and `sentry api` calls only with read methods. +Session constraints SHALL pin organization and project parameters. + +#### Scenario: Read-only connection runs a write command + +- **WHEN** a read-only connection calls `issue resolve` +- **THEN** the call is refused without sending an API request + +#### Scenario: Constrained session + +- **WHEN** a session is constrained to an organization and a call names another organization +- **THEN** the call is refused + +### Requirement: Isolation on the hosted server + +When the hosted server runs the CLI SDK, no user's credentials, +constraints, or cached data SHALL be visible to another user's request. + +#### Scenario: Concurrent identities + +- **WHEN** two identities run commands concurrently in one isolate +- **THEN** each call uses only its own credentials, constraints, and cache entries + +### Requirement: Completion for agents + +`search_sentry`'s `complete` mode SHALL return completion candidates for a +parameter, from the same completers as shell completion, with a data source +suited to the host. + +#### Scenario: Dataset field completion + +- **WHEN** an agent asks to complete `--field` of `explore` with `--dataset spans` and the prefix `span.d` +- **THEN** the result includes span attribute keys that start with `span.d` + +### Requirement: Eval-gated catalog removal + +The catalog tools SHALL NOT be removed until the MCP eval suite shows the +command surface matching the catalog on task success. + +#### Scenario: Default flip + +- **WHEN** the command surface matches the catalog on the eval suite +- **THEN** it becomes the default and the catalog stays behind a flag for one release diff --git a/openspec/changes/cli-first-mcp/tasks.md b/openspec/changes/cli-first-mcp/tasks.md new file mode 100644 index 000000000..1878c65a3 --- /dev/null +++ b/openspec/changes/cli-first-mcp/tasks.md @@ -0,0 +1,69 @@ +# Tasks + +The API work these tasks build on is `unify-sentry-api-layer`. New commands +use `@sentry/api` operations and do not wait for the shared client to move +there. + +## 1. Command metadata for agents + +- [ ] 1.1 Add a permission class (read, write, destructive) to every command, derived from its operations in `command-api.json` and its `destructive` flag, with a lint check that fails when a class is missing or disagrees with the operations. Verify the check fails on a planted GET-only command marked write and a write command marked read. +- [ ] 1.2 Add an exposure (exposed, stdio-only, hidden) to every command, with a lint check that fails when one is missing. Mark session management, CLI maintenance, `local`, and `monitor run` hidden, and local-file commands stdio-only. +- [ ] 1.3 Generate `command-catalog.json` from the route tree: path, brief, JSON Schema for parameters, completion kinds, examples, output fields, API operations, permission class, and exposure. Verify a test that it matches `lib/introspect.ts` output for the full tree. + +## 2. CLI parity with the MCP catalog + +One PR per domain. Each adds the commands from the parity table in design.md, on `@sentry/api` operations, with unit tests, and ticks its rows. Pending MCP tool PRs for the same capability are redirected here. + +- [ ] 2.1 Projects and teams: `project edit`, `team create`, `project team add/remove`. Supersedes `refine-project-management-tools`. +- [ ] 2.2 DSNs: `dsn create`, `dsn edit`. +- [ ] 2.3 Issues: `issue assign`, `issue comment`, `issue activity`; breadcrumbs and user reports where `event view` and `feedback list` fall short. +- [ ] 2.4 Events: `event attachment list/download`; dropped events (#1812). +- [ ] 2.5 Monitors: `monitor view`, `monitor env delete`, `monitor uptime list/view/create/edit/delete` (the detectors work in #1837 moves here). +- [ ] 2.6 Profiles: `profile list/view`. +- [ ] 2.7 Snapshots, alert options, and the metrics dataset in `explore`: check against the MCP tools and fill the gaps. +- [ ] 2.8 `sentry open `: resolve any Sentry URL to the matching resource and command. +- [ ] 2.9 Mark every row in the parity table done or dropped, with the reason. + +## 3. The command surface on stdio + +- [ ] 3.1 Add `search_sentry` and `execute_sentry_command` to `mcp-core`: search by query, schema by command path, and the `complete` mode; execution with schema validation, the permission class, session constraints, a result size cap, cursors, and `format: "markdown"`. Verify `pnpm run measure-tokens`. +- [ ] 3.2 Serve them from `sentry mcp --commands`, next to the catalog, running commands through `createSentrySDK()` with the session's credentials. Reject streaming flags; buffer multi-yield commands. Add `--read-only`. Verify tests for each permission class, constraint pinning, an invalid parameter returning candidates, a hidden command refused, and `/mcp-qa` over stdio. +- [ ] 3.3 Add the all-tools mode, listing each exposed command as its own tool from the same metadata. Verify both modes list the same commands with the same schemas. +- [ ] 3.4 Add `format: "markdown"` for commands with a plain markdown renderer. Verify a test for `issue view` and `trace view`. + +## 4. API routes + +- [ ] 4.1 Expose `sentry api` and `sentry schema`. A `sentry api` call's permission class comes from its method, and hosted checks the path against the published operation list with #1807's shared matcher. Verify a raw read, a raw write denied under `--read-only`, and an unpublished path refused on hosted. +- [ ] 4.2 Move `sentry schema`'s index onto `@sentry/api`'s operation catalog. Verify `sentry schema` output is unchanged. + +## 5. Completion for agents + +- [ ] 5.1 Split the completers in `lib/complete.ts` into completers over a data source: the SQLite cache with a live API fallback on stdio, the live API with the memory cache on hosted. Verify the shell completion tests pass unchanged. +- [ ] 5.2 Wire them into `search_sentry`'s `complete` mode and `execute_sentry_command`'s validation errors, returning candidates with descriptions. Verify tests for slugs, issue IDs, dataset fields and aggregates, and query keys and values. +- [ ] 5.3 Document the query syntax in the `search_sentry` entries for the search commands, replacing the embedded search agents' prompts. + +## 6. The CLI SDK on the hosted MCP + +- [ ] 6.1 Spike: bundle `createSentrySDK()` with the exposed commands for workerd under `nodejs_compat` and a current compatibility date. Record what fails to bundle or run, by module and command, and the bundle size against the Workers script limit, in design.md. Mark blocked commands stdio-only. +- [ ] 6.2 Split the CLI's SQLite state by purpose behind interfaces (credentials, defaults, region cache, completion cache, DSN cache, telemetry state), with the SQLite implementations unchanged. Verify the CLI unit tests pass. If the spike shows this is too large, add a Durable Object SQLite adapter behind `lib/db/sqlite.ts` instead, with one object per OAuth grant. +- [ ] 6.3 Move per-user module state (the database handle, env-token snapshots, caches) into the invocation context with `AsyncLocalStorage`. Verify a test that runs two identities concurrently in one isolate and checks that neither sees the other's credentials, constraints, or cache entries. +- [ ] 6.4 Add the Workers backends: credentials and defaults from the OAuth grant and session constraints, `CacheStore` in memory, region and completion caches in memory. Verify Workers tests for an exposed read command, a write command, and constraint pinning. +- [ ] 6.5 Serve `search_sentry` and `execute_sentry_command` from `mcp-cloudflare` on the SDK, next to the catalog, listing only commands the hosted build can run. Verify the Workers tests and the #1675 harness against the command surface. +- [ ] 6.6 Map hosted consent to permission classes: API Read, API Write, and the existing skills as command groups. Verify a test per grant. +- [ ] 6.7 For any domain the spike blocks entirely, split its commands into input resolution and a portable operation (formerly `unify-sentry-api-layer` 11.1), for that domain only. +- [ ] 6.8 Compare the CLI's OAuth scopes with the MCP `SCOPES` and widen the CLI's if needed. Verify a test that the CLI's scopes cover every exposed command's operations. (Formerly 11.5.) + +## 7. Slim stdio bundle + +- [ ] 7.1 Bundle the exposed command handlers and the SDK into `@sentry/mcp-server`, keeping the guard against stdio-only and hidden commands, Ink, the terminal renderer, and SQLite WASM. Re-measure the size budget, and decide whether the package stays or installs point at `sentry mcp`. Verify the bundle test (`initialize`, `tools/list`, an `execute_sentry_command` call against mocks). + +## 8. Eval gate and cutover + +- [ ] 8.1 Run the full `mcp-server-evals` suite on the catalog, the two-tool mode, and the all-tools mode, with at least two clients or models. Record task success, tokens, and turns in `docs/testing/`. +- [ ] 8.2 Make the command surface the default on stdio and hosted once it matches the catalog. Keep the catalog behind `--catalog` and a hosted skill for one release. +- [ ] 8.3 Delete the catalog tools, their `SentryApiService` endpoint wrappers, the embedded search agents, and the agent provider configuration from `mcp-core`. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. +- [ ] 8.4 Update mcp.sentry.dev setup, `docs/` architecture and operations pages, and the agent skills for the command surface. + +## 9. Later: Code Mode over the SDK (optional) + +- [ ] 9.1 If 8.1 shows multi-step tasks losing to Code Mode (#1670), add `execute_sentry_code` whose `sentry` object calls the `execute_sentry_command` path, permission checks included, in #1670's sandbox on hosted. Decide the stdio sandbox then. diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index 6bef6c0ba..beb9d77ae 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: - Client-side caching in the Sentry frontend. Browser HTTP caching driven by server headers covers that (getsentry/sentry#128818). -- 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, under `cli-first-code-mode-mcp`. +- Calling `createSentrySDK()` from the hosted MCP. `cli-first-mcp` takes + that on, with Workers storage backends for the SDK. - 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 @@ -367,8 +366,8 @@ sentry-api-schema already has (`lib/sentry-errors.ts`, `lib/sentry-pagination.ts`, `lib/browser-client.ts`). The rest no longer stages in `toolkit-core`. Under -`cli-first-code-mode-mcp`, the MCP's catalog tools and their endpoint -wrappers are deleted at the Code Mode cutover, so the only long-term caller +`cli-first-mcp`, the MCP's catalog tools and their endpoint +wrappers are deleted at the cutover to the command surface, so the only long-term caller of endpoint wrappers is the CLI, and the MCP reaches the API through CLI operations. Building shared MCP wrappers first would be thrown away. The order is: @@ -379,7 +378,7 @@ order is: by domain (8.7). A wrapper that only reshapes one generated operation is replaced by the operation. 3. Get every endpoint still called by raw path into the published schema - (8.8). `sentry api`, `sentry schema`, and Code Mode only reach published + (8.8). `sentry api`, `sentry schema`, and so the MCP only reach published operations, so this also widens what agents can call. The cost is a publish round trip for client fixes. Develop against a local @@ -449,13 +448,14 @@ 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. -### MCP tools: replaced by Code Mode over the CLI +### MCP tools: replaced by the CLI's commands This section planned to move each MCP tool into the CLI as a portable -`sentry/mcp` tool definition. `cli-first-code-mode-mcp` replaces it: the -MCP becomes Code Mode over the CLI's commands, discovered through CLI -introspection. The portable resolve/operate/render split and the -`sentry/mcp` entry survive there as what the hosted Code Mode host runs. +`sentry/mcp` tool definition. `cli-first-mcp` replaces it: the +MCP exposes the CLI's commands through search and execute tools, +discovered through CLI introspection, and the hosted MCP runs the CLI SDK +with Workers storage backends. The resolve/operate/render split survives +there only as a fallback for domains Workers cannot run. ### `@sentry/mcp-server` is a slim bundle of `sentry mcp` @@ -475,8 +475,8 @@ from CLI source into one file with no runtime dependencies (#1731): - `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. -- Under Code Mode the bundle carries the portable operations and the Code - Mode host instead of catalog tools (`cli-first-code-mode-mcp`). +- Under `cli-first-mcp` the bundle carries the exposed command handlers + instead of catalog tools (`cli-first-mcp`). - `@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. @@ -584,7 +584,7 @@ bumps move both products together. `@sentry/api`, switch toolkit to it, and delete the toolkit-core copies. 8. Move the shared client into `@sentry/api`, then the CLI's wrappers, then the endpoints missing from the published schema. The MCP side continues - in `cli-first-code-mode-mcp`. + in `cli-first-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 ef03bdf0b..b38074306 100644 --- a/openspec/changes/unify-sentry-api-layer/proposal.md +++ b/openspec/changes/unify-sentry-api-layer/proposal.md @@ -10,13 +10,16 @@ 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: +`createSentrySDK()` cannot be the shared API 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`. +The shared API layer must therefore sit below both products, on +`@sentry/api`. (`cli-first-mcp` separately makes the SDK's storage +pluggable so the hosted MCP can run commands; the SDK still calls the API +through this client.) Its end state is inside `@sentry/api` itself, so every consumer, including the Sentry frontend in the browser, gets one Sentry API client. @@ -26,8 +29,8 @@ transport, retry, cache policy, pagination, request core, and organizations domain landed in `toolkit-core` that way (#1719, #1722, #1732, #1736, #1737, #1741, #1820). -**Updated 2026-11-03.** The MCP is becoming Code Mode over the CLI's -commands (`cli-first-code-mode-mcp`), so its catalog tools and their +**Updated 2026-11-03.** The MCP is becoming a search-and-execute surface +over the CLI's commands (`cli-first-mcp`), so its catalog tools and their endpoint wrappers will be deleted rather than shared. The remaining plan therefore goes straight to `@sentry/api`: @@ -97,8 +100,8 @@ Out of scope, tracked separately: - Server-side `Cache-Control` for event and trace-item detail responses (getsentry/sentry#128818, prototype in getsentry/sentry#128828). -- The MCP tool surface: replaced by `cli-first-code-mode-mcp`, which makes - the MCP Code Mode over the CLI's commands. `@sentry/mcp-server` stays a +- The MCP tool surface: replaced by `cli-first-mcp`, which exposes the + CLI's commands through search and execute tools. `@sentry/mcp-server` stays a dependency-free bundle 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 9195dcdd0..df670a84b 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -35,12 +35,12 @@ ## 6. MCP on the shared transport - [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 `cli-first-code-mode-mcp`: the stdio Code Mode host runs commands through the CLI, so it uses the CLI's fetch and cache. +- 6.2 Moved to `cli-first-mcp`: the MCP runs commands through the CLI SDK, so it uses the CLI's fetch and cache. - [x] 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. Shipped in #1832: catalog tools with `readOnlyHint` and `openWorldHint` (not the docs tools) get the input, costing 130 tokens across the 10 direct read tools. ## 7. Hosted cache -Off the critical path since the Code Mode plan (`cli-first-code-mode-mcp`). The memory store (7.1, #1833) can land on its own. The R2 tier (7.3, 7.4, #1842) is parked until Code Mode traffic shows whether cross-isolate reuse pays; the cache policy itself moves into `@sentry/api` with the rest of the client. +Off the critical path since `cli-first-mcp`. The memory store (7.1, #1833) can land on its own. The R2 tier (7.3, 7.4, #1842) is parked until traffic on the command surface shows whether cross-isolate reuse pays; the cache policy itself moves into `@sentry/api` with the rest of the client. - [ ] 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`. @@ -50,7 +50,7 @@ Off the critical path since the Code Mode plan (`cli-first-code-mode-mcp`). The ## 8. The shared client in `@sentry/api` -The plan used to finish the whole client in `toolkit-core` and copy it into `@sentry/api` at the end. It now moves what exists into `@sentry/api` first and builds the rest there, since the CLI is the only long-term caller of endpoint wrappers: the MCP's catalog wrappers are deleted when Code Mode replaces the catalog (`cli-first-code-mode-mcp`), so porting them is wasted work. +The plan used to finish the whole client in `toolkit-core` and copy it into `@sentry/api` at the end. It now moves what exists into `@sentry/api` first and builds the rest there, since the CLI is the only long-term caller of endpoint wrappers: the MCP's catalog wrappers are deleted when the CLI's commands replace the catalog (`cli-first-mcp`), so porting them is wasted work. - [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. @@ -73,13 +73,13 @@ Moving the client into `@sentry/api` (old 9.1, 9.2) is now 8.5 and 8.6. ## 11. MCP tools as a portable face of the CLI -**Replaced by `cli-first-code-mode-mcp`.** The MCP becomes Code Mode over the CLI's commands instead of one ported tool per command. +**Replaced by `cli-first-mcp`.** The MCP exposes the CLI's commands through search and execute tools instead of one ported tool per command. -- [x] 11.6 `@sentry/mcp-server` as a dependency-free bundle of the server built from CLI source. Done in #1731. Under Code Mode it carries portable operations instead of catalog tools (`cli-first-code-mode-mcp` 7.1). -- 11.1, 11.2, 11.4: moved to `cli-first-code-mode-mcp` group 6 (portable operations and the `sentry/mcp` entry for the hosted Code Mode host). 11.4's optional host capabilities become what the stdio host has and the hosted host lacks. -- 11.3 **Dropped:** moving each MCP tool into `sentry/mcp`. Code Mode exposes commands generically. -- 11.5 moved to `cli-first-code-mode-mcp` 6.7 (OAuth scope comparison). -- 11.7 moved to `cli-first-code-mode-mcp` 6.6. Partly done in #1731: an oxlint rule bans `sentry` in `mcp-core` and `toolkit-core`, and `lint:deps` fails on workspace cycles. +- [x] 11.6 `@sentry/mcp-server` as a dependency-free bundle of the server built from CLI source. Done in #1731. Under `cli-first-mcp` it carries the exposed command handlers instead of catalog tools (7.1 there). +- 11.1, 11.2, 11.4: replaced by `cli-first-mcp` group 6, which runs the CLI SDK itself on Workers with pluggable storage backends. The per-domain split into portable operations (11.1) survives only as a fallback for domains Workers cannot run (6.7 there). +- 11.3 **Dropped:** moving each MCP tool into `sentry/mcp`. The command surface exposes commands generically. +- 11.5 moved to `cli-first-mcp` 6.8 (OAuth scope comparison). +- 11.7 Partly done in #1731: an oxlint rule bans `sentry` in `mcp-core` and `toolkit-core`, and `lint:deps` fails on workspace cycles. `mcp-cloudflare` now imports the CLI SDK, so the remaining rule is set when `cli-first-mcp` 6.5 lands. ## 12. Integration From 311b1fe809c7511b5e315a767962de9ac3dfd478 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:37:00 +0000 Subject: [PATCH 32/35] docs(openspec): Run the hosted SDK per request with persistence off Hosted creates one SDK instance per request with the OAuth grant's token and turns persistence off instead of adding Workers storage backends. Pagination becomes stateless through nextCursor. The remaining cross-user risk is module-level state, which gets an audit and the concurrent two-identity test. Keep @sentry/mcp-server. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- openspec/changes/cli-first-mcp/design.md | 100 ++++++++++-------- openspec/changes/cli-first-mcp/proposal.md | 15 +-- openspec/changes/cli-first-mcp/tasks.md | 10 +- .../changes/unify-sentry-api-layer/design.md | 4 +- .../changes/unify-sentry-api-layer/tasks.md | 2 +- 5 files changed, 70 insertions(+), 61 deletions(-) diff --git a/openspec/changes/cli-first-mcp/design.md b/openspec/changes/cli-first-mcp/design.md index 24eb02661..7f0f88a8f 100644 --- a/openspec/changes/cli-first-mcp/design.md +++ b/openspec/changes/cli-first-mcp/design.md @@ -162,42 +162,51 @@ command has no declaration: `cli uninstall`), `local`, and `monitor run`, which runs an arbitrary child process. -### The CLI SDK on Workers, with pluggable backends +### The CLI SDK on Workers, without persistence The hosted MCP runs commands through `createSentrySDK()` too, rather than -through a second, portable copy of each command. Three things stand in the -way, and none is the API code itself: - -**Storage.** The CLI's state is all in one synchronous SQLite database. -D1 would be a poor fit: it is asynchronous, so every `getDatabase()` caller -would have to become async, and it is a shared remote database where the -CLI expects a private one. What the database holds is better split by -purpose behind small interfaces, each with a Workers backend: - -| State | stdio | hosted | -| ---------------------------- | ----------------------- | ---------------------------------------------------------------------------- | -| credentials, accounts | SQLite | the OAuth grant's token, per request | -| defaults (org, project, URL) | SQLite, config | session constraints, per request | -| response cache | filesystem `CacheStore` | memory `CacheStore`, later the R2 tier from `unify-sentry-api-layer` group 7 | -| region and completion caches | SQLite | memory per isolate; KV if hit rates warrant | -| DSN cache, telemetry state | SQLite | not used | - -If splitting the database by purpose turns out larger than expected, a -shorter path is a third adapter behind `lib/db/sqlite.ts` over Durable -Object SQLite storage, which is synchronous and private to the object. One -object per OAuth grant keeps the CLI's single-user model. Neither D1 nor -Durable Objects are used by the hosted MCP today. - -The filesystem needs no Workers backend. Commands that read or write local -files are stdio-only, and the cache directory is replaced by `CacheStore`. -R2 backs the response cache, not a filesystem. - -**Per-request state.** The CLI assumes one user per process: the database -handle, the env-token snapshots, and the caches are module-level. A Workers -isolate serves many users at once, so per-user state moves into the -invocation context, using `AsyncLocalStorage` as `withEnv()` already does. -A test runs two identities concurrently in one isolate and checks that -neither sees the other's credentials, constraints, or cache entries. +through a second, portable copy of each command. The hosted MCP's Hono app +creates one SDK instance per request, with the token from the OAuth grant +and the org and project from the session constraints. + +**No persistence.** Everything the CLI persists is for a long-lived, +single-user install: stored accounts and tokens, defaults, release channel +and version checks, pagination cursor stacks, and caches (projects, DSNs, +aliases, regions, repos, issue orgs, completion values, project roots, the +response cache). A server needs none of it. The SDK already takes a token +(`createSentrySDK({ token })` sets `SENTRY_AUTH_TOKEN` and +`SENTRY_FORCE_ENV_TOKEN` for that invocation), and with an env token the +auth code never reads the `accounts` table. So hosted runs the SDK with +persistence off: + +- `getDatabase()` callers (23 files, 20 of them in `lib/db/`) get a null + store: reads miss and writes do nothing. An in-memory SQLite database is + not an option, since workerd has no SQLite driver for the CLI's adapter. +- The response cache, `.sentryclirc` and config-directory reads, custom CA + and header files, version checks, and telemetry state are off. +- Pagination is stateless. List commands already return `nextCursor` in + their JSON envelope and accept a raw cursor, so hosted rejects the + `next`/`prev` keywords, which need the stored cursor stack, and the + agent passes the cursor it got back. +- Org-to-region lookups happen on every call that needs them. If that + latency matters, an in-memory cache keyed by token fingerprint comes + later, measured first. + +D1, R2, and Durable Object storage are not needed. Commands that read or +write local files are stdio-only. + +**Per-request state.** One SDK instance per request isolates what lives in +the instance and in `withEnv()`'s `AsyncLocalStorage`, but not module +scope: a Workers isolate reuses its modules across concurrent requests. The +auth path is already per invocation (token memos are keyed by the +invocation's env, and env tokens skip stored accounts). Some module-level +state is not, for example `regionCache` in `lib/region.ts` (keyed by org +slug only), `loginTrustAnchor` in `lib/token-host.ts`, and the cached +headers in `lib/custom-headers.ts`. With persistence off, the work is to +audit module-level state on the exposed commands' import graph and either +scope each item to the invocation or turn it off on hosted. A test runs +two identities concurrently in one isolate and checks that neither sees +the other's credentials, constraints, or results. **Node built-ins.** `nodejs_compat` provides `path`, `crypto`, `util`, `zlib`, `async_hooks`, and timers. It does not provide `child_process`, @@ -238,7 +247,7 @@ Completers in `lib/complete.ts` read the SQLite completion cache. They are split into completers over a data source: - **stdio:** the SQLite cache, as for the shell, with a live API fallback. -- **hosted:** the live API, with the memory cache. +- **hosted:** the live API, uncached. `search_sentry`'s `complete` mode and `execute_sentry_command`'s validation errors return candidates with descriptions for the parameter in question: @@ -277,9 +286,8 @@ The slim bundle (#1731) bans command handlers and has a 3.75 MB budget. Under this plan it must carry the exposed command handlers, since that is what its tools run. It bundles them with the SDK and keeps banning the stdio-only and hidden commands, Ink, the terminal markdown renderer, and -SQLite WASM. The budget is re-measured. Whether the package is still worth -keeping, rather than pointing installs at `npx sentry mcp`, is an open -question. +SQLite WASM. The budget is re-measured. The package stays; whether installs +move to `npx sentry mcp` later is out of scope here. ### Eval-gated cutover @@ -302,9 +310,12 @@ The command surface ships next to the catalog: - **Output quality.** Catalog tools format stack traces, traces, and issue summaries for agents. `format: "markdown"` reuses the CLI's renderers; the evals check the gap. -- **Cross-user leaks on hosted.** Per-request state is the riskiest part of - running the SDK on Workers. The concurrent two-identity test is required - before hosted serves any command. +- **Cross-user leaks on hosted.** With persistence off, the remaining risk + is module-level state shared by concurrent requests in one isolate. The + concurrent two-identity test is required before hosted serves any + command. +- **Extra round trips on hosted.** Without caches, region and slug + resolution hit the API on every call. Measure before adding a cache. - **Workers compatibility.** Unknown until the spike. Commands that fail stay stdio-only, so hosted coverage can lag stdio. - **Tool descriptions come from CLI help text.** Weak help text makes weak @@ -318,8 +329,8 @@ The command surface ships next to the catalog: parity commands, by domain. 2. The two tools on stdio over `createSentrySDK()`, behind a flag, with completion. -3. The Workers spike, then pluggable backends and per-request state, then - hosted on the SDK. +3. The Workers spike, then a no-persistence mode and the module-state + audit, then hosted on the SDK. 4. Eval gate, default flip, catalog deletion. **Rollback:** until the catalog is deleted, the flag switches back. After @@ -329,9 +340,6 @@ that, a revert of the deletion PR restores it. - Which mode each client should default to: two tools, or every command as a tool for harnesses that search tools themselves. -- Whether to keep `@sentry/mcp-server` or point installs at `sentry mcp`. -- Memory per isolate versus Durable Object SQLite for hosted caches, decided - by hit rates after launch. - Whether `get_sentry_mcp_info` and `onboarding_status_update` survive. - When to simplify hosted consent from skill checkboxes to API Read, API Write, and destructive. diff --git a/openspec/changes/cli-first-mcp/proposal.md b/openspec/changes/cli-first-mcp/proposal.md index 72c04e2c0..511fade80 100644 --- a/openspec/changes/cli-first-mcp/proposal.md +++ b/openspec/changes/cli-first-mcp/proposal.md @@ -41,9 +41,10 @@ introspection and run through the CLI SDK. queries in `search_sentry` and suggests values when a parameter fails validation: organization and project slugs, issue IDs, dataset fields and aggregates, and search query terms. -6. **The CLI SDK runs on the hosted MCP.** The SDK's storage (SQLite, the - filesystem, the config directory) becomes pluggable, with Workers - backends, and its per-process state becomes per-request. The hosted MCP +6. **The CLI SDK runs on the hosted MCP.** Hosted creates one SDK instance + per request with the OAuth grant's token and runs it with persistence + off: no SQLite, no config directory, no caches. Module-level state on + the exposed commands' import graph becomes per-request. The hosted MCP then runs commands through the same SDK as `sentry mcp`. 7. **The catalog goes away after an eval gate.** The command surface ships next to the catalog. When the MCP eval suite shows it matching the @@ -70,14 +71,14 @@ commands. ## Impact - `packages/cli`: new commands for the parity gaps; agent metadata on - commands (permission class, hosted availability); pluggable storage - backends; per-request SDK state; a Workers build of the SDK. + commands (permission class, hosted availability); a no-persistence + mode; per-request SDK state; a Workers build of the SDK. - `packages/mcp-core`: keeps MCP plumbing (registration, transports, constraints, the two tool definitions). Loses the catalog tools, the `SentryApiService` endpoint wrappers they use, the search agents, and the agent provider configuration. -- `packages/mcp-cloudflare`: runs commands through the CLI SDK with Workers - storage backends. OAuth consent maps to command permission classes. +- `packages/mcp-cloudflare`: runs commands through the CLI SDK, one + instance per request, with persistence off. OAuth consent maps to command permission classes. - `packages/mcp-server`: the slim bundle needs the command handlers it exposes; its module guard and size budget change. - `packages/mcp-server-evals`: command-surface and catalog runs of the full diff --git a/openspec/changes/cli-first-mcp/tasks.md b/openspec/changes/cli-first-mcp/tasks.md index 1878c65a3..df0a4318f 100644 --- a/openspec/changes/cli-first-mcp/tasks.md +++ b/openspec/changes/cli-first-mcp/tasks.md @@ -38,16 +38,16 @@ One PR per domain. Each adds the commands from the parity table in design.md, on ## 5. Completion for agents -- [ ] 5.1 Split the completers in `lib/complete.ts` into completers over a data source: the SQLite cache with a live API fallback on stdio, the live API with the memory cache on hosted. Verify the shell completion tests pass unchanged. +- [ ] 5.1 Split the completers in `lib/complete.ts` into completers over a data source: the SQLite cache with a live API fallback on stdio, the live API, uncached, on hosted. Verify the shell completion tests pass unchanged. - [ ] 5.2 Wire them into `search_sentry`'s `complete` mode and `execute_sentry_command`'s validation errors, returning candidates with descriptions. Verify tests for slugs, issue IDs, dataset fields and aggregates, and query keys and values. - [ ] 5.3 Document the query syntax in the `search_sentry` entries for the search commands, replacing the embedded search agents' prompts. ## 6. The CLI SDK on the hosted MCP - [ ] 6.1 Spike: bundle `createSentrySDK()` with the exposed commands for workerd under `nodejs_compat` and a current compatibility date. Record what fails to bundle or run, by module and command, and the bundle size against the Workers script limit, in design.md. Mark blocked commands stdio-only. -- [ ] 6.2 Split the CLI's SQLite state by purpose behind interfaces (credentials, defaults, region cache, completion cache, DSN cache, telemetry state), with the SQLite implementations unchanged. Verify the CLI unit tests pass. If the spike shows this is too large, add a Durable Object SQLite adapter behind `lib/db/sqlite.ts` instead, with one object per OAuth grant. -- [ ] 6.3 Move per-user module state (the database handle, env-token snapshots, caches) into the invocation context with `AsyncLocalStorage`. Verify a test that runs two identities concurrently in one isolate and checks that neither sees the other's credentials, constraints, or cache entries. -- [ ] 6.4 Add the Workers backends: credentials and defaults from the OAuth grant and session constraints, `CacheStore` in memory, region and completion caches in memory. Verify Workers tests for an exposed read command, a write command, and constraint pinning. +- [ ] 6.2 Add a no-persistence mode to the SDK: `getDatabase()` callers get a null store (reads miss, writes do nothing), and the response cache, `.sentryclirc` and config-directory reads, custom CA and header files, version checks, and telemetry state are off. Hosted rejects the `next`/`prev` cursor keywords; agents pass the raw `nextCursor`. Verify tests that run an exposed read command, a write command, and a paginated list with persistence off and no config directory. +- [ ] 6.3 Audit module-level state on the exposed commands' import graph (for example `regionCache` in `lib/region.ts`, `loginTrustAnchor` in `lib/token-host.ts`, cached headers in `lib/custom-headers.ts`), and scope each item to the invocation or turn it off with persistence. Verify a test that runs two identities concurrently in one isolate and checks that neither sees the other's credentials, constraints, or results. +- [ ] 6.4 In `mcp-cloudflare`'s Hono app, create one SDK instance per request with the OAuth grant's token and the session constraints, persistence off. Verify Workers tests for an exposed read command, a write command, and constraint pinning. - [ ] 6.5 Serve `search_sentry` and `execute_sentry_command` from `mcp-cloudflare` on the SDK, next to the catalog, listing only commands the hosted build can run. Verify the Workers tests and the #1675 harness against the command surface. - [ ] 6.6 Map hosted consent to permission classes: API Read, API Write, and the existing skills as command groups. Verify a test per grant. - [ ] 6.7 For any domain the spike blocks entirely, split its commands into input resolution and a portable operation (formerly `unify-sentry-api-layer` 11.1), for that domain only. @@ -55,7 +55,7 @@ One PR per domain. Each adds the commands from the parity table in design.md, on ## 7. Slim stdio bundle -- [ ] 7.1 Bundle the exposed command handlers and the SDK into `@sentry/mcp-server`, keeping the guard against stdio-only and hidden commands, Ink, the terminal renderer, and SQLite WASM. Re-measure the size budget, and decide whether the package stays or installs point at `sentry mcp`. Verify the bundle test (`initialize`, `tools/list`, an `execute_sentry_command` call against mocks). +- [ ] 7.1 Bundle the exposed command handlers and the SDK into `@sentry/mcp-server`, keeping the guard against stdio-only and hidden commands, Ink, the terminal renderer, and SQLite WASM. Re-measure the size budget. The package stays. Verify the bundle test (`initialize`, `tools/list`, an `execute_sentry_command` call against mocks). ## 8. Eval gate and cutover diff --git a/openspec/changes/unify-sentry-api-layer/design.md b/openspec/changes/unify-sentry-api-layer/design.md index beb9d77ae..8d172a04a 100644 --- a/openspec/changes/unify-sentry-api-layer/design.md +++ b/openspec/changes/unify-sentry-api-layer/design.md @@ -65,7 +65,7 @@ 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). - Calling `createSentrySDK()` from the hosted MCP. `cli-first-mcp` takes - that on, with Workers storage backends for the SDK. + that on, running the SDK with persistence off. - 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 @@ -454,7 +454,7 @@ This section planned to move each MCP tool into the CLI as a portable `sentry/mcp` tool definition. `cli-first-mcp` replaces it: the MCP exposes the CLI's commands through search and execute tools, discovered through CLI introspection, and the hosted MCP runs the CLI SDK -with Workers storage backends. The resolve/operate/render split survives +with persistence off. The resolve/operate/render split survives there only as a fallback for domains Workers cannot run. ### `@sentry/mcp-server` is a slim bundle of `sentry mcp` diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index df670a84b..c70684238 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -40,7 +40,7 @@ ## 7. Hosted cache -Off the critical path since `cli-first-mcp`. The memory store (7.1, #1833) can land on its own. The R2 tier (7.3, 7.4, #1842) is parked until traffic on the command surface shows whether cross-isolate reuse pays; the cache policy itself moves into `@sentry/api` with the rest of the client. +Off the critical path since `cli-first-mcp`. The memory store (7.1, #1833) can land on its own for the catalog tools; the hosted SDK path runs uncached (`cli-first-mcp` 6.2). The R2 tier (7.3, 7.4, #1842) is parked until traffic on the command surface shows whether cross-isolate reuse pays; the cache policy itself moves into `@sentry/api` with the rest of the client. - [ ] 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`. From 610f023b6078c27b2e1b19e0bf16b267f210f00b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:57:24 +0000 Subject: [PATCH 33/35] docs(openspec): Split execute by permission class and link the implementation issues MCP hosts allowlist and approve by tool name (#1254), so execute becomes execute_sentry_read and execute_sentry_write. The release now follows the @sentry/api switch (8.10). Each task links its tracking issue. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- openspec/changes/cli-first-mcp/design.md | 46 +++++++++---- openspec/changes/cli-first-mcp/proposal.md | 10 +-- .../specs/mcp-command-surface/spec.md | 23 ++++++- openspec/changes/cli-first-mcp/tasks.md | 68 +++++++++---------- .../changes/unify-sentry-api-layer/tasks.md | 11 +-- 5 files changed, 99 insertions(+), 59 deletions(-) diff --git a/openspec/changes/cli-first-mcp/design.md b/openspec/changes/cli-first-mcp/design.md index 7f0f88a8f..a96a638d2 100644 --- a/openspec/changes/cli-first-mcp/design.md +++ b/openspec/changes/cli-first-mcp/design.md @@ -99,10 +99,11 @@ when its command lands): About 20 tools have no command yet, and a few more are partial. Each gap is a command PR, grouped by domain. -### Two tools: `search_sentry` and `execute_sentry_command` +### Search mode: `search_sentry`, `execute_sentry_read`, `execute_sentry_write` -The MCP hides the command tree behind two tools, so the tool list stays -small however many commands the CLI has (well over 100 leaf commands): +The MCP hides the command tree behind one search tool and two execute +tools, so the tool list stays small however many commands the CLI has +(well over 100 leaf commands): - **`search_sentry`** - `query`: free text. Returns ranked commands with path, brief, @@ -112,7 +113,10 @@ small however many commands the CLI has (well over 100 leaf commands): and output fields. - `complete`: a command, a parameter, the value typed so far, and the other parameters. Returns completion candidates (see below). -- **`execute_sentry_command`** takes `{ command, params }`: +- **`execute_sentry_read`** and **`execute_sentry_write`** take + `{ command, params }`. The read tool runs only read commands and carries + `readOnlyHint: true`; the write tool runs write and destructive commands + and carries `destructiveHint: true`. Each: 1. validates `params` against the command's schema; an invalid slug, enum value, or query key fails with completion candidates in the error; @@ -126,10 +130,19 @@ small however many commands the CLI has (well over 100 leaf commands): Commands that yield more than once are buffered into one result, as the SDK does today. Streaming flags (`--follow`, tail) are rejected. +Execute is split by permission class because MCP hosts allowlist and +render approvals by tool name (#1254). With one dispatcher, allowlisting it +allowlists every mutation, and denying it blocks every read. With the split, +a host can auto-run `execute_sentry_read` and ask before +`execute_sentry_write`, and `command` is a top-level argument, so the +approval card shows which command runs. A read-only connection does not +list the write tool at all. + A second mode lists every exposed command as its own tool (`sentry_issue_list`, with the same schema and description). It is for harnesses that search large tool lists themselves. It comes from the same -metadata, so both modes always agree. The two-tool mode is the default. +metadata, so both modes always agree, and each tool carries its own +command's annotations. Search mode is the default. #### How this differs from Code Mode @@ -138,14 +151,14 @@ execute, but its execute tool takes a program: the model writes JavaScript that calls several operations, loops, and filters, and only the program's result goes back into context. That code is arbitrary, so it needs a sandbox. -`execute_sentry_command` runs one known command per call. The agent cannot +The execute tools run one known command per call. The agent cannot run anything that is not a command, and the parameters are validated data, so there is nothing to sandbox. The cost is that multi-step work takes one model turn per call, and intermediate results pass through context. Command flags (`--limit`, `--fields`, queries) and `format` keep results small. If evals show multi-step tasks need it, Code Mode can be added later as a -third tool whose `sentry` object calls the same `execute_sentry_command` -path, permission checks included. +tool whose `sentry` object calls the same execute path, permission checks +included. ### What is exposed @@ -204,7 +217,10 @@ state is not, for example `regionCache` in `lib/region.ts` (keyed by org slug only), `loginTrustAnchor` in `lib/token-host.ts`, and the cached headers in `lib/custom-headers.ts`. With persistence off, the work is to audit module-level state on the exposed commands' import graph and either -scope each item to the invocation or turn it off on hosted. A test runs +scope each item to the invocation or turn it off on hosted. The +sequential token reuse in #1391 is already fixed on `main` (token memos +are keyed by the invocation's env); #1427 gives the remaining state +invocation, request, and session lifetimes and is the starting point. A test runs two identities concurrently in one isolate and checks that neither sees the other's credentials, constraints, or results. @@ -233,7 +249,7 @@ exposure), and the hosted MCP reads that. A test fails when the two differ. ### API routes through `sentry api` and `sentry schema` `sentry schema` and `sentry api` are exposed commands, so agents search the -published API with `execute_sentry_command({ command: "schema", ... })` and +published API with `execute_sentry_read({ command: "schema", ... })` and call any published operation with `sentry api`. The permission class of a `sentry api` call comes from its method, and hosted checks the path against the published operation list with #1807's shared matcher, as #1670's broker @@ -249,7 +265,7 @@ split into completers over a data source: - **stdio:** the SQLite cache, as for the shell, with a live API fallback. - **hosted:** the live API, uncached. -`search_sentry`'s `complete` mode and `execute_sentry_command`'s validation +`search_sentry`'s `complete` mode and the execute tools' validation errors return candidates with descriptions for the parameter in question: slugs, issue IDs, enum values, dataset fields and aggregates, and search query keys and values. This is how agents build valid queries without the @@ -294,7 +310,7 @@ move to `npx sentry mcp` later is out of scope here. The command surface ships next to the catalog: 1. Extend #1675's harness to run the full `mcp-server-evals` suite on the - catalog, the two-tool mode, and the all-tools mode. + catalog, search mode, and the all-tools mode. 2. Make the command surface the default when it matches or beats the catalog on task success, and record token cost and turn count next to it. Run more than one client and model. @@ -325,9 +341,11 @@ The command surface ships next to the catalog: ## Migration Plan +0. Move the shared client into `@sentry/api` and switch toolkit to it + (`unify-sentry-api-layer` 8.5, 8.6), then release (8.10). 1. Command metadata (permission class, exposure, generated catalog) and the parity commands, by domain. -2. The two tools on stdio over `createSentrySDK()`, behind a flag, with +2. Search mode on stdio over `createSentrySDK()`, behind a flag, with completion. 3. The Workers spike, then a no-persistence mode and the module-state audit, then hosted on the SDK. @@ -338,7 +356,7 @@ that, a revert of the deletion PR restores it. ## Open Questions -- Which mode each client should default to: two tools, or every command as +- Which mode each client should default to: search mode, or every command as a tool for harnesses that search tools themselves. - Whether `get_sentry_mcp_info` and `onboarding_status_update` survive. - When to simplify hosted consent from skill checkboxes to API Read, API diff --git a/openspec/changes/cli-first-mcp/proposal.md b/openspec/changes/cli-first-mcp/proposal.md index 511fade80..d93bb59ed 100644 --- a/openspec/changes/cli-first-mcp/proposal.md +++ b/openspec/changes/cli-first-mcp/proposal.md @@ -24,9 +24,11 @@ introspection and run through the CLI SDK. capabilities are added as CLI commands only; no new hand-written MCP tools. Commands call the API through `@sentry/api` (`unify-sentry-api-layer`). -2. **The MCP exposes the CLI's commands.** By default it lists two tools: - `search_sentry` finds commands, and `execute_sentry_command` runs one - command by name with JSON parameters through `createSentrySDK()`. No +2. **The MCP exposes the CLI's commands.** By default it lists three tools: + `search_sentry` finds commands, and `execute_sentry_read` and + `execute_sentry_write` run one command by name with JSON parameters + through `createSentrySDK()`. Execute is split by permission class so MCP + hosts can allowlist reads and ask before writes (#1254). No sandbox is involved: the agent can only run commands that exist, with parameters their schema accepts. Harnesses that search large tool lists themselves can instead list every command as its own tool. @@ -34,7 +36,7 @@ introspection and run through the CLI SDK. come from the CLI's route tree (`lib/introspect.ts`) on stdio and from metadata generated from it at build time on hosted. 4. **Every published API route.** `sentry api` and `sentry schema` are - commands like any other, so `search_sentry` and `execute_sentry_command` + commands like any other, so search and execute reach every published `@sentry/api` operation with no extra tool. 5. **Completion for agents.** The shell completion system (`lib/complete.ts`, `lib/completion-target.ts`) answers completion diff --git a/openspec/changes/cli-first-mcp/specs/mcp-command-surface/spec.md b/openspec/changes/cli-first-mcp/specs/mcp-command-surface/spec.md index ece815008..ac26da574 100644 --- a/openspec/changes/cli-first-mcp/specs/mcp-command-surface/spec.md +++ b/openspec/changes/cli-first-mcp/specs/mcp-command-surface/spec.md @@ -35,13 +35,13 @@ generated catalog, and SHALL list only commands its build can run. ### Requirement: Execution runs only exposed commands -`execute_sentry_command` SHALL run only commands marked exposed, with +The execute tools SHALL run only commands marked exposed, with parameters that validate against the command's schema, through the CLI SDK. It SHALL NOT run arbitrary code. #### Scenario: Hidden command -- **WHEN** an agent calls `execute_sentry_command` with `monitor run` +- **WHEN** an agent calls `execute_sentry_write` with `monitor run` - **THEN** the call fails without running anything #### Scenario: Invalid parameter value @@ -49,6 +49,25 @@ It SHALL NOT run arbitrary code. - **WHEN** an agent passes an organization slug that does not exist - **THEN** the call fails with completion candidates for that parameter +### Requirement: Execute tools split by permission class + +Search mode SHALL offer one execute tool for read commands, annotated +read-only, and one for write and destructive commands, annotated +destructive. Each SHALL take the command path as a top-level argument. A +read-only connection SHALL NOT list the write tool. + +#### Scenario: A host allowlists reads + +- **WHEN** a host allowlists `execute_sentry_read` +- **THEN** read commands run without approval +- **WHEN** the agent calls `execute_sentry_read` with `issue resolve` +- **THEN** the call is refused and names `execute_sentry_write` + +#### Scenario: Read-only connection lists tools + +- **WHEN** a read-only connection lists tools +- **THEN** the list has `search_sentry` and `execute_sentry_read` and no write tool + ### Requirement: Permission classes Every command SHALL have a permission class (read, write, or destructive) diff --git a/openspec/changes/cli-first-mcp/tasks.md b/openspec/changes/cli-first-mcp/tasks.md index df0a4318f..cbb51b83d 100644 --- a/openspec/changes/cli-first-mcp/tasks.md +++ b/openspec/changes/cli-first-mcp/tasks.md @@ -6,64 +6,64 @@ there. ## 1. Command metadata for agents -- [ ] 1.1 Add a permission class (read, write, destructive) to every command, derived from its operations in `command-api.json` and its `destructive` flag, with a lint check that fails when a class is missing or disagrees with the operations. Verify the check fails on a planted GET-only command marked write and a write command marked read. -- [ ] 1.2 Add an exposure (exposed, stdio-only, hidden) to every command, with a lint check that fails when one is missing. Mark session management, CLI maintenance, `local`, and `monitor run` hidden, and local-file commands stdio-only. -- [ ] 1.3 Generate `command-catalog.json` from the route tree: path, brief, JSON Schema for parameters, completion kinds, examples, output fields, API operations, permission class, and exposure. Verify a test that it matches `lib/introspect.ts` output for the full tree. +- [ ] 1.1 (#1880) Add a permission class (read, write, destructive) to every command, derived from its operations in `command-api.json` and its `destructive` flag, with a lint check that fails when a class is missing or disagrees with the operations. Verify the check fails on a planted GET-only command marked write and a write command marked read. +- [ ] 1.2 (#1880) Add an exposure (exposed, stdio-only, hidden) to every command, with a lint check that fails when one is missing. Mark session management, CLI maintenance, `local`, and `monitor run` hidden, and local-file commands stdio-only. +- [ ] 1.3 (#1881) Generate `command-catalog.json` from the route tree: path, brief, JSON Schema for parameters, completion kinds, examples, output fields, API operations, permission class, and exposure. Verify a test that it matches `lib/introspect.ts` output for the full tree. ## 2. CLI parity with the MCP catalog One PR per domain. Each adds the commands from the parity table in design.md, on `@sentry/api` operations, with unit tests, and ticks its rows. Pending MCP tool PRs for the same capability are redirected here. -- [ ] 2.1 Projects and teams: `project edit`, `team create`, `project team add/remove`. Supersedes `refine-project-management-tools`. -- [ ] 2.2 DSNs: `dsn create`, `dsn edit`. -- [ ] 2.3 Issues: `issue assign`, `issue comment`, `issue activity`; breadcrumbs and user reports where `event view` and `feedback list` fall short. -- [ ] 2.4 Events: `event attachment list/download`; dropped events (#1812). -- [ ] 2.5 Monitors: `monitor view`, `monitor env delete`, `monitor uptime list/view/create/edit/delete` (the detectors work in #1837 moves here). -- [ ] 2.6 Profiles: `profile list/view`. -- [ ] 2.7 Snapshots, alert options, and the metrics dataset in `explore`: check against the MCP tools and fill the gaps. -- [ ] 2.8 `sentry open `: resolve any Sentry URL to the matching resource and command. -- [ ] 2.9 Mark every row in the parity table done or dropped, with the reason. +- [ ] 2.1 (#1883) Projects and teams: `project edit`, `team create`, `project team add/remove`. Supersedes `refine-project-management-tools`. +- [ ] 2.2 (#1884) DSNs: `dsn create`, `dsn edit`. +- [ ] 2.3 (#1885) Issues: `issue assign`, `issue comment`, `issue activity`; breadcrumbs and user reports where `event view` and `feedback list` fall short. +- [ ] 2.4 (#1886) Events: `event attachment list/download`; dropped events (#1812). +- [ ] 2.5 (#1887) Monitors: `monitor view`, `monitor env delete`, `monitor uptime list/view/create/edit/delete` (the detectors work in #1837 moves here). +- [ ] 2.6 (#1381) Profiles: `profile list/view`. +- [ ] 2.7 (#1888) Snapshots, alert options, and the metrics dataset in `explore`: check against the MCP tools and fill the gaps. +- [ ] 2.8 (#1889) `sentry open `: resolve any Sentry URL to the matching resource and command. +- [ ] 2.9 (#1382) Mark every row in the parity table done or dropped, with the reason. ## 3. The command surface on stdio -- [ ] 3.1 Add `search_sentry` and `execute_sentry_command` to `mcp-core`: search by query, schema by command path, and the `complete` mode; execution with schema validation, the permission class, session constraints, a result size cap, cursors, and `format: "markdown"`. Verify `pnpm run measure-tokens`. -- [ ] 3.2 Serve them from `sentry mcp --commands`, next to the catalog, running commands through `createSentrySDK()` with the session's credentials. Reject streaming flags; buffer multi-yield commands. Add `--read-only`. Verify tests for each permission class, constraint pinning, an invalid parameter returning candidates, a hidden command refused, and `/mcp-qa` over stdio. -- [ ] 3.3 Add the all-tools mode, listing each exposed command as its own tool from the same metadata. Verify both modes list the same commands with the same schemas. -- [ ] 3.4 Add `format: "markdown"` for commands with a plain markdown renderer. Verify a test for `issue view` and `trace view`. +- [ ] 3.1 (#1890) Add `search_sentry`, `execute_sentry_read`, and `execute_sentry_write` to `mcp-core`: search by query, schema by command path, and the `complete` mode; execution split by permission class (read-only and destructive hints, `command` as a top-level argument, no write tool on read-only connections, #1254), with schema validation, the permission class, session constraints, a result size cap, cursors, and `format: "markdown"`. Verify `pnpm run measure-tokens`. +- [ ] 3.2 (#1890) Serve them from `sentry mcp --commands`, next to the catalog, running commands through `createSentrySDK()` with the session's credentials. Reject streaming flags; buffer multi-yield commands. Add `--read-only`. Verify tests for each permission class, constraint pinning, an invalid parameter returning candidates, a hidden command refused, and `/mcp-qa` over stdio. +- [ ] 3.3 (#1891) Add the all-tools mode, listing each exposed command as its own tool from the same metadata. Verify both modes list the same commands with the same schemas. +- [ ] 3.4 (#1892) Add `format: "markdown"` for commands with a plain markdown renderer. Verify a test for `issue view` and `trace view`. ## 4. API routes -- [ ] 4.1 Expose `sentry api` and `sentry schema`. A `sentry api` call's permission class comes from its method, and hosted checks the path against the published operation list with #1807's shared matcher. Verify a raw read, a raw write denied under `--read-only`, and an unpublished path refused on hosted. -- [ ] 4.2 Move `sentry schema`'s index onto `@sentry/api`'s operation catalog. Verify `sentry schema` output is unchanged. +- [ ] 4.1 (#1893) Expose `sentry api` and `sentry schema`. A `sentry api` call's permission class comes from its method, and hosted checks the path against the published operation list with #1807's shared matcher. Verify a raw read, a raw write denied under `--read-only`, and an unpublished path refused on hosted. +- [ ] 4.2 (#1894) Move `sentry schema`'s index onto `@sentry/api`'s operation catalog. Verify `sentry schema` output is unchanged. ## 5. Completion for agents -- [ ] 5.1 Split the completers in `lib/complete.ts` into completers over a data source: the SQLite cache with a live API fallback on stdio, the live API, uncached, on hosted. Verify the shell completion tests pass unchanged. -- [ ] 5.2 Wire them into `search_sentry`'s `complete` mode and `execute_sentry_command`'s validation errors, returning candidates with descriptions. Verify tests for slugs, issue IDs, dataset fields and aggregates, and query keys and values. -- [ ] 5.3 Document the query syntax in the `search_sentry` entries for the search commands, replacing the embedded search agents' prompts. +- [ ] 5.1 (#1895) Split the completers in `lib/complete.ts` into completers over a data source: the SQLite cache with a live API fallback on stdio, the live API, uncached, on hosted. Verify the shell completion tests pass unchanged. +- [ ] 5.2 (#1895) Wire them into `search_sentry`'s `complete` mode and the execute tools' validation errors, returning candidates with descriptions. Verify tests for slugs, issue IDs, dataset fields and aggregates, and query keys and values. +- [ ] 5.3 (#1896) Document the query syntax in the `search_sentry` entries for the search commands, replacing the embedded search agents' prompts. ## 6. The CLI SDK on the hosted MCP -- [ ] 6.1 Spike: bundle `createSentrySDK()` with the exposed commands for workerd under `nodejs_compat` and a current compatibility date. Record what fails to bundle or run, by module and command, and the bundle size against the Workers script limit, in design.md. Mark blocked commands stdio-only. -- [ ] 6.2 Add a no-persistence mode to the SDK: `getDatabase()` callers get a null store (reads miss, writes do nothing), and the response cache, `.sentryclirc` and config-directory reads, custom CA and header files, version checks, and telemetry state are off. Hosted rejects the `next`/`prev` cursor keywords; agents pass the raw `nextCursor`. Verify tests that run an exposed read command, a write command, and a paginated list with persistence off and no config directory. -- [ ] 6.3 Audit module-level state on the exposed commands' import graph (for example `regionCache` in `lib/region.ts`, `loginTrustAnchor` in `lib/token-host.ts`, cached headers in `lib/custom-headers.ts`), and scope each item to the invocation or turn it off with persistence. Verify a test that runs two identities concurrently in one isolate and checks that neither sees the other's credentials, constraints, or results. -- [ ] 6.4 In `mcp-cloudflare`'s Hono app, create one SDK instance per request with the OAuth grant's token and the session constraints, persistence off. Verify Workers tests for an exposed read command, a write command, and constraint pinning. -- [ ] 6.5 Serve `search_sentry` and `execute_sentry_command` from `mcp-cloudflare` on the SDK, next to the catalog, listing only commands the hosted build can run. Verify the Workers tests and the #1675 harness against the command surface. -- [ ] 6.6 Map hosted consent to permission classes: API Read, API Write, and the existing skills as command groups. Verify a test per grant. +- [ ] 6.1 (#1882) Spike: bundle `createSentrySDK()` with the exposed commands for workerd under `nodejs_compat` and a current compatibility date. Record what fails to bundle or run, by module and command, and the bundle size against the Workers script limit, in design.md. Mark blocked commands stdio-only. +- [ ] 6.2 (#1897) Add a no-persistence mode to the SDK: `getDatabase()` callers get a null store (reads miss, writes do nothing), and the response cache, `.sentryclirc` and config-directory reads, custom CA and header files, version checks, and telemetry state are off. Hosted rejects the `next`/`prev` cursor keywords; agents pass the raw `nextCursor`. Verify tests that run an exposed read command, a write command, and a paginated list with persistence off and no config directory. +- [ ] 6.3 (#1898) Audit module-level state on the exposed commands' import graph (for example `regionCache` in `lib/region.ts`, `loginTrustAnchor` in `lib/token-host.ts`, cached headers in `lib/custom-headers.ts`), and scope each item to the invocation or turn it off with persistence. Verify a test that runs two identities concurrently in one isolate and checks that neither sees the other's credentials, constraints, or results. +- [ ] 6.4 (#1899) In `mcp-cloudflare`'s Hono app, create one SDK instance per request with the OAuth grant's token and the session constraints, persistence off. Verify Workers tests for an exposed read command, a write command, and constraint pinning. +- [ ] 6.5 (#1899) Serve search mode from `mcp-cloudflare` on the SDK, next to the catalog, listing only commands the hosted build can run. Verify the Workers tests and the #1675 harness against the command surface. +- [ ] 6.6 (#1907) Map hosted consent to permission classes: API Read, API Write, and the existing skills as command groups. Verify a test per grant. - [ ] 6.7 For any domain the spike blocks entirely, split its commands into input resolution and a portable operation (formerly `unify-sentry-api-layer` 11.1), for that domain only. -- [ ] 6.8 Compare the CLI's OAuth scopes with the MCP `SCOPES` and widen the CLI's if needed. Verify a test that the CLI's scopes cover every exposed command's operations. (Formerly 11.5.) +- [ ] 6.8 (#1908) Compare the CLI's OAuth scopes with the MCP `SCOPES` and widen the CLI's if needed. Verify a test that the CLI's scopes cover every exposed command's operations. (Formerly 11.5.) ## 7. Slim stdio bundle -- [ ] 7.1 Bundle the exposed command handlers and the SDK into `@sentry/mcp-server`, keeping the guard against stdio-only and hidden commands, Ink, the terminal renderer, and SQLite WASM. Re-measure the size budget. The package stays. Verify the bundle test (`initialize`, `tools/list`, an `execute_sentry_command` call against mocks). +- [ ] 7.1 (#1909) Bundle the exposed command handlers and the SDK into `@sentry/mcp-server`, keeping the guard against stdio-only and hidden commands, Ink, the terminal renderer, and SQLite WASM. Re-measure the size budget. The package stays. Verify the bundle test (`initialize`, `tools/list`, an `execute_sentry_read` call against mocks). ## 8. Eval gate and cutover -- [ ] 8.1 Run the full `mcp-server-evals` suite on the catalog, the two-tool mode, and the all-tools mode, with at least two clients or models. Record task success, tokens, and turns in `docs/testing/`. -- [ ] 8.2 Make the command surface the default on stdio and hosted once it matches the catalog. Keep the catalog behind `--catalog` and a hosted skill for one release. -- [ ] 8.3 Delete the catalog tools, their `SentryApiService` endpoint wrappers, the embedded search agents, and the agent provider configuration from `mcp-core`. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. -- [ ] 8.4 Update mcp.sentry.dev setup, `docs/` architecture and operations pages, and the agent skills for the command surface. +- [ ] 8.1 (#1910) Run the full `mcp-server-evals` suite on the catalog, search mode, and the all-tools mode, with at least two clients or models. Record task success, tokens, and turns in `docs/testing/`. +- [ ] 8.2 (#1911) Make the command surface the default on stdio and hosted once it matches the catalog. Keep the catalog behind `--catalog` and a hosted skill for one release. +- [ ] 8.3 (#1911) Delete the catalog tools, their `SentryApiService` endpoint wrappers, the embedded search agents, and the agent provider configuration from `mcp-core`. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. +- [ ] 8.4 (#1911) Update mcp.sentry.dev setup, `docs/` architecture and operations pages, and the agent skills for the command surface. ## 9. Later: Code Mode over the SDK (optional) -- [ ] 9.1 If 8.1 shows multi-step tasks losing to Code Mode (#1670), add `execute_sentry_code` whose `sentry` object calls the `execute_sentry_command` path, permission checks included, in #1670's sandbox on hosted. Decide the stdio sandbox then. +- [ ] 9.1 If 8.1 shows multi-step tasks losing to Code Mode (#1670), add `execute_sentry_code` whose `sentry` object calls the same execute path, permission checks included, in #1670's sandbox on hosted. Decide the stdio sandbox then. diff --git a/openspec/changes/unify-sentry-api-layer/tasks.md b/openspec/changes/unify-sentry-api-layer/tasks.md index c70684238..0ea075541 100644 --- a/openspec/changes/unify-sentry-api-layer/tasks.md +++ b/openspec/changes/unify-sentry-api-layer/tasks.md @@ -56,10 +56,11 @@ The plan used to finish the whole client in `toolkit-core` and copy it into `@se - [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. - [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. - [x] 8.4 Request core and the first shared domains: `createApiClientCore` with `SentryApiService` on it (#1737), the organizations domain (#1741), and the idempotent-write list both products use for retries (#1820). **Dropped:** moving the remaining MCP domains onto shared wrappers. -- [ ] 8.5 Move `toolkit-core/src/api-client/` into getsentry/sentry-api-schema's `lib/`, reconciling it with what is already there (`browser-client.ts`, `sentry-errors.ts`, `sentry-pagination.ts`) so the package has one error type, one pagination helper, and one transport. Keep the injected interfaces (credentials, `CacheStore`, fresh-read scope, logger, span hook) and the portability rules. Publish a new `@sentry/api`. Verify the package's tests, and that its browser entry builds and makes a request with a mocked `fetch`. (Formerly 9.1.) -- [ ] 8.6 Bump `@sentry/api` in the pnpm catalog, switch the CLI, `mcp-core`, and `mcp-cloudflare` to it, and delete the `toolkit-core` copies in the same PR. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. (Formerly 9.2.) -- [ ] 8.7 Move the CLI's endpoint wrappers (`cli/src/lib/api/`) onto the `@sentry/api` client, domain by domain, and delete the CLI's own API errors and pagination. 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. Wrappers that only reshape one generated operation are replaced by the operation. Verify the CLI unit tests pass. (Formerly 8.5.) -- [ ] 8.8 Inventory the endpoints the CLI calls by raw path rather than through a generated operation (about 85 call sites in `cli/src/lib/api/` and 55 in `mcp-core/src/api-client/` by a path grep, many duplicates), and get each one into `@sentry/api`: publish it in getsentry/sentry's OpenAPI schema so it generates, or record why it stays raw. Verify the inventory is checked in and each new operation is used. +- [ ] 8.5 (#1875) Move `toolkit-core/src/api-client/` into getsentry/sentry-api-schema's `lib/`, reconciling it with what is already there (`browser-client.ts`, `sentry-errors.ts`, `sentry-pagination.ts`) so the package has one error type, one pagination helper, and one transport. Keep the injected interfaces (credentials, `CacheStore`, fresh-read scope, logger, span hook) and the portability rules. Publish a new `@sentry/api`. Verify the package's tests, and that its browser entry builds and makes a request with a mocked `fetch`. (Formerly 9.1.) +- [ ] 8.6 (#1876) Bump `@sentry/api` in the pnpm catalog, switch the CLI, `mcp-core`, and `mcp-cloudflare` to it, and delete the `toolkit-core` copies in the same PR. Verify `pnpm run tsc && pnpm run lint && pnpm run test`. (Formerly 9.2.) +- [ ] 8.10 (#1877) Release once 8.6 lands, before 8.7: the first toolkit release since `cli@0.47.0` (`sentry` and `@sentry/mcp-server`), which also validates Craft 3.8 `versioning.files` (#1825). Verify both packages install from npm and `sentry mcp` and `npx @sentry/mcp-server` answer `tools/list`. +- [ ] 8.7 (#1878) Move the CLI's endpoint wrappers (`cli/src/lib/api/`) onto the `@sentry/api` client, domain by domain, and delete the CLI's own API errors and pagination. 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. Wrappers that only reshape one generated operation are replaced by the operation. Verify the CLI unit tests pass. (Formerly 8.5.) +- [ ] 8.8 (#1879) Inventory the endpoints the CLI calls by raw path rather than through a generated operation (about 85 call sites in `cli/src/lib/api/` and 55 in `mcp-core/src/api-client/` by a path grep, many duplicates), and get each one into `@sentry/api`: publish it in getsentry/sentry's OpenAPI schema so it generates, or record why it stays raw. Verify the inventory is checked in and each new operation is used. - [ ] 8.9 In every PR, keep behavior the same and call out each intentional change in the PR body. Update `docs/contributing/api-patterns.md` and the `@sentry/api` README for the new layering, and verify `pnpm run docs:check` passes. Ongoing: #1737, #1741, and #1820 updated the toolkit docs for their layers. (Formerly 8.7.) - Old 8.6 **dropped:** merging the wrappers both products call. After the catalog is deleted, only the CLI calls endpoint wrappers; the MCP calls CLI operations. @@ -76,7 +77,7 @@ Moving the client into `@sentry/api` (old 9.1, 9.2) is now 8.5 and 8.6. **Replaced by `cli-first-mcp`.** The MCP exposes the CLI's commands through search and execute tools instead of one ported tool per command. - [x] 11.6 `@sentry/mcp-server` as a dependency-free bundle of the server built from CLI source. Done in #1731. Under `cli-first-mcp` it carries the exposed command handlers instead of catalog tools (7.1 there). -- 11.1, 11.2, 11.4: replaced by `cli-first-mcp` group 6, which runs the CLI SDK itself on Workers with pluggable storage backends. The per-domain split into portable operations (11.1) survives only as a fallback for domains Workers cannot run (6.7 there). +- 11.1, 11.2, 11.4: replaced by `cli-first-mcp` group 6, which runs the CLI SDK itself on Workers, one instance per request with persistence off. The per-domain split into portable operations (11.1) survives only as a fallback for domains Workers cannot run (6.7 there). - 11.3 **Dropped:** moving each MCP tool into `sentry/mcp`. The command surface exposes commands generically. - 11.5 moved to `cli-first-mcp` 6.8 (OAuth scope comparison). - 11.7 Partly done in #1731: an oxlint rule bans `sentry` in `mcp-core` and `toolkit-core`, and `lint:deps` fails on workspace cycles. `mcp-cloudflare` now imports the CLI SDK, so the remaining rule is set when `cli-first-mcp` 6.5 lands. From f4aaef0d89c0d20370c1a03773b3ded657ac149d Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 21:38:16 +0000 Subject: [PATCH 34/35] docs(openspec): Record the CLI SDK on workerd spike results The SDK runs on workerd once five small load-time issues are fixed and the hosted worker moves to a 2026 compatibility date. The bundle fits the 64 MiB limit, but it is the whole CLI because the route loaders import every command. The spike also found that the SDK inherits the worker's process.env, so the worker's own SENTRY_DSN redirected an untargeted command. Records the stdio-only additions and ranks what is left. Refs #1882 Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- openspec/changes/cli-first-mcp/design.md | 94 +++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/openspec/changes/cli-first-mcp/design.md b/openspec/changes/cli-first-mcp/design.md index a96a638d2..a5f49dc96 100644 --- a/openspec/changes/cli-first-mcp/design.md +++ b/openspec/changes/cli-first-mcp/design.md @@ -227,7 +227,8 @@ the other's credentials, constraints, or results. **Node built-ins.** `nodejs_compat` provides `path`, `crypto`, `util`, `zlib`, `async_hooks`, and timers. It does not provide `child_process`, `tty`, or an HTTP server, and its `fs` is an in-memory file system that -likely needs a newer compatibility date than the hosted MCP uses (to confirm in the spike). What matters is +needs a newer compatibility date than the hosted MCP uses (see the spike +results below). What matters is what the exposed commands import transitively, which only a build answers. So the first task is a spike: bundle the SDK for workerd with the exposed commands, and list what fails to bundle or run, and the bundle size against @@ -238,6 +239,97 @@ until fixed. If a whole domain is blocked, that domain's commands split into input resolution and a portable operation (the old `unify-sentry-api-layer` 11.1), for that domain only. +#### Spike results (#1882) + +A throwaway worker imported `createSentrySDK()` and ran 12 command calls +against a mocked Sentry API with a synthetic token, under `wrangler dev` +(wrangler 4.151.3, workerd 1.20261019.1, `nodejs_compat` and +`nodejs_compat_populate_process_env`). SQLite was swapped for a null store +at bundle time. The same harness passes every call on Node. + +**It runs.** With the small fixes in the table, `org list`, `project list`, +`issue list`, `issue view`, `event view`, `trace view`, `api`, `schema`, +`issue resolve`, and `run("issue", "list")` all pass, on a fresh isolate +and on a warm one. + +**Bundling.** wrangler alone fails on one module: `lib/init/ui/ink-ui.ts` +imports the Ink sidecar `with { type: "file" }`, which esbuild rejects. +`ROUTE_LOADERS` imports every command, so `init` is in the graph even when +only `issue list` is called. Bundling first with the CLI's esbuild plugins +(text imports, `_require` aliasing, zod stubs) and then with wrangler works. +The result is the whole CLI. + +**Size.** 6,971 KiB raw, 1,989 KiB gzip (`wrangler deploy --dry-run +--minify`). The hosted MCP today is 5,092 KiB raw, 975 KiB gzip. The +Workers limit is 64 MiB uncompressed, with no compressed limit, so size is +not a blocker. Code hosted never runs is about half the bundle: +highlight.js and parse5 (~1.2 MB), `sentry mcp`'s server stack (~1.2 MB, +partly shared with the hosted worker), `init` (Mastra, AI SDK, Clack, +~0.75 MB), and `@sentry/node` with its dependencies (~0.5 MB). + +**Startup.** `wrangler check startup` measured about 65 ms of startup CPU +locally, mostly parse and compile; the limit is 1 s. Modules initialize on +first use, so the first call in a fresh isolate took 155-215 ms against the +mock, and the same calls warm took 6-28 ms. + +**Compatibility date.** Every call fails on 2025-03-21 (hosted today) and on +every date tried up to 2026-01-01: pngjs, reached from `lib/sixel-image.ts` +through the chart formatters, calls `util.inherits` on a `stream` class the +older polyfills lack. Calls pass from 2026-01-28 on. Hosted moves to a 2026 +compatibility date, or the image code loads lazily. + +| Failure | Where | Hit by | Fix | Size | +| ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------- | ----- | +| `createRequire(import.meta.url)` throws; workerd leaves `import.meta.url` undefined | 11 modules at load (`db/*`, `telemetry`, `custom-ca`, `formatters/*`, `dif`, `sea-assets`, `init/ui`) | every call | define `import.meta.url` at build | small | +| `Dynamic require of "util" is not supported` | CommonJS dependencies in the ESM output | every call | a `createRequire` banner, as the npm ESM bundle has | small | +| top-level `readFileSync` of `assets/spleen-8x16.bin` | `formatters/spleen-font.ts` | every call | load the glyphs on first use, or embed them | small | +| a null store whose reads return `null` breaks schema setup (`row.count`) | `db/schema.ts` through `getDatabase()` | calls that open the database (`api` first) | no-persistence mode skips database setup, not only reads (#1897) | small | +| `superCtor.prototype` must be an object | pngjs on dates before 2026 | every call | 2026 compatibility date, or lazy image code | small | + +**Other findings.** + +- `withEnv()`'s `AsyncLocalStorage` works on workerd. Two tokens, three + concurrent `issue list` calls each, in one isolate: every request + carried only its own token. +- Environment inheritance is a correctness bug on hosted. + `buildIsolatedEnv()` copies `process.env`, which on Workers holds the + worker's vars and secrets (`SENTRY_DSN`, `SENTRY_CLIENT_SECRET`, + `COOKIE_SECRET`, provider keys). With the worker's `SENTRY_DSN` set, + `issue list` with no target resolved to that DSN's org and project. The + SDK needs an option to start from an empty env; small. +- Library-mode telemetry initializes `@sentry/node` and posts envelopes to + the CLI's own DSN (18 for 12 calls). Hosted turns it off; small. +- `node:fs` is workerd's virtual file system. `homedir()` and `tmpdir()` + are `/tmp`, which lasts one request, so config-directory writes succeed + and vanish. The current directory is `/bundle`, the worker's own code: + `api --input /bundle/