Skip to content

feat(mcp): add HTTP Code Mode with explicit API permissions - #1670

Open
betegon wants to merge 24 commits into
mainfrom
bt/code-mode-http
Open

betegon wants to merge 24 commits into
mainfrom
bt/code-mode-http

Conversation

@betegon

@betegon betegon commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Code Mode is the MCP equivalent of sentry api: agents can use published API operations without a dedicated MCP tool.
The client's agent writes JavaScript; our Cloudflare sandbox executes authenticated HTTP calls and returns the result.

For example: “Copy this dashboard for two projects, preserving its widgets and filters.” The existing tool catalog cannot do this; Code Mode can.

search_sentry_api discovers the schema and execute_sentry_code runs the program. This version is HTTP-only; the SDK remains future work.

Server-side Code Mode over standard MCP: the client sends code through tools/call, Sentry executes it in a Cloudflare sandbox, and only the selected result returns to the model.

Prerequisite #1676 is merged. Stack: this PR → #1675 (agent evals). #1807 (shared path matcher with the CLI) is also stacked on this PR. Runtime and permission safety tests remain here.

sequenceDiagram
    participant Agent as Agent / MCP client
    participant Sandbox as Sandbox<br/>(no token, no network)
    participant Broker as Broker in the parent Worker<br/>(holds the token)
    participant API as Sentry API
    Agent->>Sandbox: execute_sentry_code(code)
    Sandbox->>Broker: sentry.request(...) via RPC
    Broker->>API: checks permissions, adds Authorization
    API-->>Broker: HTTP response
    Broker-->>Sandbox: response data
    Sandbox-->>Agent: program result
Loading

How it fits

  • One composition. The hosted handler calls registerCodeModeTools(server, env.LOADER, context) on the server it builds for each request. ApiAuthorization is always applied; there is no standalone worker or eval-only path. Tool definitions live in mcp-core/src/code-mode.ts and are generated into toolDefinitions.json with the other tools. The runtime lives in mcp-cloudflare/src/code-mode/.

  • Requests go through SentryApiService, like every other tool: the organization's regional host, MCP client attribution and the bearer header come from it, so the broker never handles the token itself.

  • The API data ships with the deploy, as in Cloudflare's Code Mode server: nothing fetches it at runtime. For the @sentry/api release in the pnpm catalog (0.428.0: 173 paths, 273 operations), the same release the CLI builds sentry api and sentry schema from:

    • generated/openapi.json.txt is the dereferenced OpenAPI document. It is a Workers Text module that each search sandbox parses.
    • generated/operations.json holds the method and path of each operation, which authorization matches. This is how the CLI ships its generated catalog.

    To move to a new release, bump the catalog and run pnpm --filter @sentry/mcp-cloudflare generate:openapi; a test fails if either file differs from the installed @sentry/api.

Permissions

The Sentry token authenticates API calls; its OAuth scopes and the user's Sentry permissions set the maximum access.
The permission screen controls which of those actions this MCP connection may perform, without changing the token or its scopes.

  • API Read permits reads. API Write includes reads and enables create/update/delete. It starts unchecked; explicitly remembered selections are preserved.
  • Permissions are additive: API Write can modify issues even with Triage Issues unchecked, and projects/teams with Manage Projects & Teams unchecked. The consent screen explains this before approval.
  • The screen shows the scope: /mcp covers organizations authorized by the Sentry token; /mcp/{org} is restricted to that organization. Project-scoped connections do not expose Code Mode.
  • Direct Sentry-Bearer connections default to all active skills, including API Write: the client already holds the token. skills / disable-skills can narrow access; token permissions and connection constraints still apply.
  • Existing grants gain no new permissions, and stdio (sentry mcp, @sentry/mcp-server) rejects the API skills. Shared upstream OAuth scopes stay unchanged; the broker checks the MCP grant on every call, and Sentry still enforces token, role and resource permissions. Credentials stay outside generated code.
Sentry MCP permissions: organization scope, API Write unchecked, and additive permissions warning

Debugging

  • Each tools/call span records:
    • the executed code (gen_ai.tool.call.arguments.code, like every tool's arguments)
    • the outcome (ok, error, timeout, internal_error)
    • API calls and writes
    • the operations as METHOD template status, with denied when this connection's policy refused them
    • the schema release
  • Unsampled metrics count every execution (app.code_mode.execution) and every operation by path template (app.code_mode.request).
  • Failures outside the program (Worker Loader, sandbox crash, CPU limit) create an issue and return its event ID; program errors and deadlines do not. Upstream 5xx responses keep reporting the operation template.
  • Concrete URLs, query strings and bodies are never recorded.
  • Locally, pnpm run dev with Authorization: Sentry-Bearer <token> and ?skills=api-read runs the same code path against a real account.

Design, permissions and limits: docs/architecture/code-mode.md.

Validation and before production

Executions close pending RPC, abort in-flight HTTP and block requests whose authorization finishes after execution ends. Programs must await API calls before returning; accepted writes cannot be rolled back.

An independent review of the cleanup found nothing that widens what a request can reach. Its smaller findings are fixed in d10ff0f: Code Mode skills are listed explicitly instead of derived from hostedOnly, the 403-does-not-revoke check is back, and the docs are corrected.

Warden's findings:

  • 304: addressed in f4150e1. The sandbox cannot send conditional headers, so the broker no longer forwards them and a 304 cannot occur.
  • Non-JSON error bodies: addressed in cc37b12. A failed response whose body is not JSON keeps its HTTP details.
  • listChanged: initialize already advertised listChanged: false, because the SDK sets the capability only on the first registerTool. That is an SDK detail, so 929dbcc sets it explicitly after Code Mode registers, as buildServer() does.
  • Description length: addressed in 7ac6e60. Both descriptions now fit the repo's 2,048-character budget, with the same rules and the same published examples: search_sentry_api went from 2,310 to 2,045 characters and execute_sentry_code from 3,259 to 2,042. The length test now covers them.

Passed locally on 929dbcc:

  • 493 Cloudflare tests. Code Mode runs through the authenticated handler with real Dynamic Workers, the RPC broker and the shipped schema.
  • 1,899 core tests (six skipped), the toolkit-core tests, and the CLI sentry mcp tests.
  • Workspace typecheck, lint and documentation links.

The remaining CLI failures (upgrade, cli fix, db permissions and others) are the same 24 that fail on main in a root container.

Agent evals in #1675 passed 35/35 with claude-opus-5-5 on this composition before the cleanup and before the descriptions were shortened. After it, the harness tests pass against the cleaned branch (61 tests, real eval worker). The model runs were not repeated.

Pending before production: real hosted OAuth-to-API authorization and Cloudflare CPU/termination/concurrency enforcement. Local workerd tests do not establish these platform guarantees; the internal_error issues and metrics above will show whether hosted CPU limits trigger. No production deployment performed.

https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX


Generated by Claude Code

@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Nov 2, 2026 8:58pm UTC

Request Review

Comment thread packages/mcp-core/src/server.ts Resolved
@betegon
betegon changed the base branch from main to bt/cloudflare-test-runtime October 27, 2026 10:32
@betegon
betegon force-pushed the bt/cloudflare-test-runtime branch from 7fb178a to 49d10be Compare October 27, 2026 13:28
betegon added a commit that referenced this pull request Oct 27, 2026
The Cloudflare test plugin still used an older Miniflare/workerd than
Wrangler, so updating the direct dependency alone left different
runtimes in local and integration tests.

Switch to `@cloudflare/vitest-plugin` 1.4.6 and align Miniflare/workerd
with Wrangler. Miniflare 5 removed `fetchMock`; an Undici outbound
service preserves the existing fixtures and `disableNetConnect()`
policy.

This is the prerequisite for #1670 (HTTP Code Mode), followed by #1675
(agent evals). Only seven test/dependency files move here; the broker,
permissions and production integration remain in #1670.

Validated independently on Node 24: **443 Cloudflare tests across 34
files**, root typechecks, lint and frozen install. The earlier full
workspace run had unrelated CLI fixture/platform failures; CLI fixes are
excluded. No deployment performed.
Base automatically changed from bt/cloudflare-test-runtime to main October 27, 2026 13:42
@betegon
betegon requested a review from BYK October 28, 2026 12:27
@betegon
betegon marked this pull request as ready for review October 29, 2026 21:17
@github-actions github-actions Bot added the risk: high PR risk score: high label Oct 29, 2026

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Sentry's token endpoint always returns `scope` (process_token_details), so
the optional schema field and its upstreamScopes tests and docs are not
needed. Code Mode does not read upstreamScopes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
main moved the stdio server into the CLI (#1731), so the rejection of
hosted-only Code Mode skills moves from mcp-server's resolve.ts to the
CLI's `sentry mcp` config, and `buildServer` no longer takes `sdkVersion`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
Code Mode carried its own copy of the bounded body loop. Expose that loop
from toolkit-core as `readBoundedStream(stream, maxBytes)` and use it for
Sentry responses and the sandbox result. The configured API base comes from
server settings, so the extra URL validation in `http.ts` goes too; the
broker's origin and prefix check is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
Code Mode rebuilt the region host, attribution headers and bearer header by
hand. It now takes them from the same API service the other tools use:
`apiServiceForOrganization()` resolves the organization's region, and
`requestTarget()` returns that service's origin and headers. The broker no
longer handles the access token, and a malformed token fails the same way
as in every other tool.

This replaces mcp-core's `validate-region-url` export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
Authorization walked the 1.9 MB search schema, parsed on first use. It now
matches the method and path of each `@sentry/api` operation, which the
generator writes next to the schema, as the CLI does with its generated
catalog. The schema stays for `search_sentry_api`, and the drift test checks
that both files describe the catalog's release.

The defensive schema parsing and its tests go away, as do the access-test
fixtures for OpenAPI `security`, which authorization never read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
`/.mcp/tools.json` converted Code Mode's Zod schemas at runtime, and
`toolDefinitions.json` left them out, so the chat listed both tools without a
description. The definitions generator now emits them next to the registry
tools, and the route serves the generated file as it did before.

The generator also stops overriding `toolCount` for hosted-only skills: its
only reader, the consent screen, hides the count for Code Mode skills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
…eferences

`CODE_MODE_SKILLS` and the `hostedOnly` skill flag listed the same two
skills, and five call sites repeated the membership check. Derive the list
from `hostedOnly` and check it with `isCodeModeSkill()`.

The consent screen also kept remembered API Read/Write choices through a
project consent, where those skills are hidden. That threaded the OAuth
resource into the preference cookie for a rare case. A project consent now
saves what it shows, like every other consent, and API skills start
unchecked again on the next organization consent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
- Drop the request-size and nested-input limit tests: main's handler and
  toolkit-core tests own those limits.
- Drop the upstreamScopes scenarios (dashboard copy with an org:read token,
  recorded scopes on a 403): Code Mode never reads upstreamScopes.
- Replace the grant-visibility tests with one table, which also checks that
  API Read and API Write are listed and how they are annotated.
- Merge the API error, ignored-rejection and handled-rejection cases into
  fewer tests with the same assertions.
- Reduce the consent round trip to the case that matters: approving API
  Write requests no extra upstream scopes.
- Move the integration test next to the code it exercises.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
`docs/testing/code-mode.md` described the design and permissions, not how to
test, and carried the PR's investigation notes and validation status. Move it
to `docs/architecture/code-mode.md`, keep the design, permissions, schema,
limits and telemetry, and cut the narrative and stale notes.

Replace the 1.1 MB PNG with a Mermaid diagram, as the OAuth docs do, so it can
be edited in a pull request. Shorten the Cloudflare architecture section to a
summary and a link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
From an independent review of the cleanup:

- Deriving `CODE_MODE_SKILLS` from `hostedOnly` meant a future hosted-only
  skill would grant arbitrary API access by itself. List the two skills
  again, and test that each is `hostedOnly` so stdio never serves them.
- Every skill's generated `toolCount` matches its `tools` again, Code Mode
  skills included.
- Restore the check that a caught 403 does not revoke the grant.
- Docs: say which checks the broker and `ApiAuthorization` run, that reads
  are GET and HEAD, that direct `Sentry-Bearer` connections default to API
  Write, and that only the program text can contain concrete URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
Warden flagged that a 304 from a conditional request would come back as a
502. A 304 can't happen: `sentry.request()` has no `headers` option and raw
`fetch` is blocked, so no program can send `If-Match` or `If-None-Match`.
The broker now forwards only `accept` and `content-type`, and the 304
special case goes away; any 3xx is still refused before it can redirect the
token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
Warden: the sandbox parsed a JSON content type before checking the status,
so an error response with a body that was not JSON surfaced as "API response
was not valid JSON" without its status, path or headers. A failed request
now keeps the raw text as `data`; only a successful response with an
invalid JSON body is an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
`search_sentry_api` (2,310 characters) and `execute_sentry_code` (3,259)
exceeded the repository's description budget, which some clients enforce.
Both keep every rule and the published examples, in fewer words: 2,045 and
2,042 characters. The length test now covers the Code Mode definitions too,
since they are registered outside the tool registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX
`buildServer()` turns `listChanged` off after registering its tools because
each request's catalog is fixed. Code Mode registers two more tools
afterwards. Today the SDK only sets the capability on the first
registration, but that is an SDK detail, so set it the same way
`buildServer()` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017M6odW3kt5i65zATxfz9RX

This branch was successfully deployed

1 active and 1 inactive deployments
Preview — 929dbcc0 Deployed Nov 2, 2026 by vercel[bot]
Actions — 929dbcc0 Deployed Nov 2, 2026 by betegon via eval #1489
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants