Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6455c5a
feat(mcp): add HTTP Code Mode with explicit API permissions
betegon Oct 26, 2026
ee90de2
fix(mcp): bound Code Mode inputs and align test runtime
betegon Oct 26, 2026
7dacf6b
refactor(mcp): separate agent evals from Code Mode runtime
betegon Oct 27, 2026
cdf3615
docs(mcp): clarify Code Mode lifecycle and token boundary
betegon Oct 27, 2026
4e625e9
fix(mcp): close Code Mode execution and pending API calls
betegon Oct 27, 2026
c50537e
fix(mcp): retain consent preferences and report unawaited failures
betegon Oct 30, 2026
3dfa43b
ref(mcp): ship the Code Mode schema and use one composition
claude Nov 2, 2026
08af7b0
ref(mcp): pin the Code Mode schema to the CLI's @sentry/api release
claude Nov 2, 2026
0771c55
Merge branch 'main' into bt/code-mode-http
claude Nov 2, 2026
00dcbb8
fix(mcp): keep Code Mode's output shape on internal failures
claude Nov 2, 2026
ceb85c4
ref(mcp): Drop OAuth token scope changes from Code Mode
claude Nov 2, 2026
282de49
Merge branch 'main' into bt/code-mode-http
claude Nov 2, 2026
048ba5a
ref(mcp): Reuse toolkit-core's bounded reader in Code Mode
claude Nov 2, 2026
8c5d120
ref(mcp): Route Code Mode requests through SentryApiService
claude Nov 2, 2026
1261de9
ref(mcp): Authorize Code Mode against the operation list
claude Nov 2, 2026
fb855c7
ref(mcp): Generate Code Mode tool definitions with the others
claude Nov 2, 2026
01f37b4
ref(mcp): Derive Code Mode skills from hostedOnly; drop hidden API pr…
claude Nov 2, 2026
f31aa3d
test(mcp): Consolidate Code Mode tests
claude Nov 2, 2026
45919a7
docs(mcp): Move Code Mode docs to architecture and trim them
claude Nov 2, 2026
d10ff0f
fix(mcp): List Code Mode skills explicitly; address review findings
claude Nov 2, 2026
f4150e1
ref(mcp): Stop forwarding conditional headers the sandbox cannot set
claude Nov 2, 2026
cc37b12
fix(mcp): Keep HTTP details when a failed response is not JSON
claude Nov 2, 2026
7ac6e60
ref(mcp): Trim Code Mode tool descriptions under the 2,048 budget
claude Nov 2, 2026
929dbcc
fix(mcp): Keep tools.listChanged off after registering Code Mode
claude Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ toolkit/
**MCP architecture and operations:**

- docs/architecture/overview.md — MCP system design
- docs/architecture/code-mode.md — Hosted HTTP Code Mode: sandbox, permissions, telemetry
- docs/operations/security.md — Authentication and security patterns
- docs/operations/stdio-auth.md — stdio auth: shared CLI login and static tokens
- docs/operations/oauth-signout-playbook.md — Remote OAuth diagnostic runbook
Expand Down
197 changes: 197 additions & 0 deletions docs/architecture/code-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# HTTP Code Mode

HTTP Code Mode lets an external MCP agent discover published API operations,
write JavaScript, and run authenticated HTTP requests without a dedicated tool
for each operation. It is hosted-only. This version uses HTTP requests; an SDK
interface is deferred.

## Tools

- `search_sentry_api({code})` runs an async arrow function against the shipped
OpenAPI document, available as `spec`. Network access is disabled.
- `execute_sentry_code({code})` runs an async arrow function with
`sentry.request({method?, path, query?, body?})`, defaulting to GET. Requests
return `{status, headers, body}`; non-2xx responses throw with HTTP details.
Query arrays become repeated parameters; `headers.link` supports pagination.

The client's model discovers the API, writes the code, corrects failures and
writes the answer. The server runs the code and never calls a model. Programs
can paginate, combine responses and return selected fields without sending every
intermediate response to the model. Both are ordinary MCP tools (`tools/list`,
`tools/call`, `structuredContent`, `isError`), not a protocol capability.

## Execution

This follows Cloudflare's [server-side search/execute pattern](https://blog.cloudflare.com/code-mode-mcp/).
Each call loads a fresh Dynamic Worker through the `LOADER` binding with
`globalOutbound: null`, so raw `fetch()` and other network access fail. The
sandbox receives an `ApiBroker` RPC capability; generated code is a separate
module and cannot reach it or the token.

```mermaid
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
```

`registerCodeModeTools()` (`packages/mcp-cloudflare/src/code-mode/tools.ts`)
adds both tools to the server that `mcp-handler.ts` builds for each
authenticated request, capturing that request's `ServerContext`. The parent
Worker owns one `ExecutionGate` per call, with a 30-second deadline covering
startup, execution and reading the result:

- Programs must await every `sentry.request()`. Returning with calls pending, or
leaving a rejection unhandled, fails the execution. Late catches and
`Promise.allSettled` are fine.
- Closing the gate aborts upstream HTTP and refuses new calls, checked again
after authorization, so a slow lookup cannot start a write after the program
ended. It does not stop JavaScript still running inside the isolate.
- A write Sentry already accepted cannot be rolled back. There are no retries
or transactions; failures tell the agent to inspect state before retrying
writes.

`LOADER.load()` creates a fresh Worker per execution, without ID caching, which
keeps executions isolated and affects
[Dynamic Workers costs](https://developers.cloudflare.com/dynamic-workers/pricing/).
Reusing Workers would have to keep credentials and authorization per request.

## Permissions

Two skills control Code Mode on a connection:

- **API Read** permits reads.
- **API Write** permits reads and writes. It starts unchecked on the consent
screen.

Code Mode never widens the token: the OAuth flow requests the same upstream
scopes, and Sentry still checks the token's scopes and the user's role and
resource access on every request. Code Mode reads neither `grantedScopes` nor
`upstreamScopes`. Permissions add up: API Write can change issues with **Triage
Issues** unchecked, or projects and teams with **Manage Projects & Teams**
unchecked. The consent screen says so and shows the organization scope. Existing
OAuth grants do not gain API skills, and stdio rejects them. Direct
`Sentry-Bearer` connections default to all active skills, API Write included,
because the client already holds the token; `skills` and `disable-skills`
narrow them.

Before each `sentry.request()`, the broker (`broker.ts`) checks:

1. The URL keeps the configured origin and `/api/0` prefix, with no encoded
separators.
2. Without API Write, only reads (GET and HEAD) reach Sentry.

Then its `ApiAuthorization` (`authorization.ts`, `access.ts`) checks:

3. The path and method are a published operation. HEAD may use a published
GET. Traversal segments, unknown paths and ambiguous paths fail.
4. On `/mcp/{org}`, the organization path parameter must be that organization.
Other organizations, numeric aliases and global object-ID routes fail;
query parameters cannot stand in for a scoped path.

Allowed requests go through `SentryApiService`, like every other tool: the
organization's regional host, MCP client attribution and the bearer header. Only
`accept` and `content-type` cross from the sandbox.
Redirects are not followed. A 401 invokes the unauthorized callback once.

Project-constrained connections (`/mcp/{org}/{project}`) do not get Code Mode.
A project URL does not prove isolation: release files can be shared across
projects, and release deploys can affect every project when `projects` is
omitted (`src/sentry/releases/endpoints/project_release_file_details.py` and
`release_deploys.py` in Sentry).

## API Schema

Code Mode ships its API data with the deploy; nothing fetches it at runtime.
`src/code-mode/generated/` holds, for the `@sentry/api` release in the pnpm
catalog (the release the CLI's `sentry api` and `sentry schema` use):

- `openapi.json.txt`: the published `openapi-derefed.json`, minified and
ASCII-escaped, as a Workers Text module. Each search sandbox parses it.
- `operations.json`: method and path of each operation, for authorization.
- `source.json`: the release.

To move to a new release, bump `@sentry/api` in the catalog, install, and
regenerate in the same pull request. `openapi.test.ts` fails when either file
differs from the installed release. Never edit the generated files by hand.

```bash
pnpm --filter @sentry/mcp-cloudflare generate:openapi
```

The tool descriptions in `packages/mcp-core/src/code-mode.ts` guide the agent:
search paths, operation IDs, summaries, descriptions and tags with the user's
terms; read path-level and operation-level parameters; and for writes, read the
current resource, send required fields and requested changes, preserve IDs in
nested collections, and verify the result. These instructions cannot enforce
endpoint-specific semantics.

## Limits and Failures

Each execution has a 20-call budget, a 30-second deadline, a 50,000-byte result
limit and a 2,000,000-byte limit per upstream response. The hosted MCP's shared
input limits (`@sentry/toolkit-core/mcp-limits`) apply before the tool runs.
Dynamic Worker CPU and subrequest limits are set in the `load()` call; local
workerd does not enforce them, and hosted enforcement is not yet verified.

HTTP errors keep the status, message and allowed headers such as
`WWW-Authenticate` and `Retry-After`. Upstream 5xx responses create a Sentry
issue through `logIssue`, even when the program catches them; the response
carries the event ID as `x-sentry-event-id`. Failures outside the program, such
as the Worker Loader failing, also create an issue and end the tool error with
the event ID. Program errors, 4xx responses and the deadline do not. Every
failure keeps the tools' `{error, stats}` output shape.

## Telemetry

Each `tools/call` span records the execution:

- `gen_ai.tool.call.arguments.code`: the generated program
- `app.code_mode.outcome`: `ok`, `error`, `timeout` or `internal_error`
- `app.code_mode.spec_version`: the `@sentry/api` release shipped
- `app.code_mode.api_calls`, `app.code_mode.writes`: calls the broker saw and
writes it sent (execute only)
- `app.code_mode.operations`: JSON list of `METHOD template status`, with
`denied` when this connection's policy refused the call (execute only)

Spans are sampled. Two metrics count every execution:

- `app.code_mode.execution` with `tool` and `outcome`
- `app.code_mode.request` with `method`, `operation` (the path template, or
`unknown`), `status` and `denied`

The broker records operations outside the sandbox, by path template. Apart from
the program text itself, concrete URLs, query strings and bodies are not
recorded.

## Testing

`src/code-mode/tools.integration.test.ts` drives the authenticated MCP handler
with real Dynamic Workers and the RPC broker, the shipped schema, and simulated
Sentry HTTP. `access.test.ts` covers path and organization matching.

```bash
pnpm --filter '@sentry/mcp-core...' --if-present run build
pnpm --filter @sentry/mcp-cloudflare test
```

To try a real account, start the hosted server with `pnpm run dev` and connect
with `Authorization: Sentry-Bearer <token>` and `?skills=api-read` or
`?skills=api-write`; this is the same code path.

## References

- Implementation: `packages/mcp-cloudflare/src/code-mode/` (`tools.ts`,
`authorization.ts`, `access.ts`, `broker.ts`, `sandbox.ts`, `openapi.ts`);
tool definitions in `packages/mcp-core/src/code-mode.ts`.
- [Cloudflare architecture](../cloudflare/architecture.md) and
[OAuth architecture](../cloudflare/oauth-architecture.md).
- [Stainless SDK Code Mode](https://www.stainless.com/blog/sdk-code-mode/), for
the deferred SDK interface.
9 changes: 9 additions & 0 deletions docs/cloudflare/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ const mcpClient = await experimental_createMCPClient({
- Prompt metadata endpoint
- Error handling with fallbacks

### Hosted MCP Code Mode

The authenticated hosted MCP also registers `search_sentry_api` and
`execute_sentry_code` for connections granted API Read or API Write. The
client's model writes the code; a Dynamic Worker from the `LOADER` binding runs
it without network access, and an RPC broker in the parent Worker sends the
allowed Sentry API requests with the user's token. See
[HTTP Code Mode](../architecture/code-mode.md).

### 4. AI Assistant

GPT-4 integration with Sentry-specific system prompt:
Expand Down
18 changes: 17 additions & 1 deletion docs/cloudflare/oauth-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,17 @@ remains valid RFC 8414 metadata.

When a constrained OAuth request includes `resource=/mcp/{org}` or
`resource=/mcp/{org}/{project}`, the approval page surfaces that scope to the
user as a "Session scope" banner before permissions are granted.
user before permissions are granted. Organization-only and unconstrained
connections show an "Organization scope" banner; project-constrained
connections retain the "Session scope" banner. The unconstrained banner
explicitly covers all organizations authorized by the upstream token.

New consent forms leave API Write unchecked, while signed remembered selections
remain preselected for the same client. API Write's warning explains that it is
an independent, additive permission: it allows API writes even when Triage Issues
or Manage Projects & Teams is unchecked, subject to Sentry permissions and the
connection's organization scope. Project-constrained sessions do not offer
Code Mode skills.

Note: These describe the MCP OAuth server, not Sentry's OAuth endpoints.

Expand All @@ -447,6 +457,12 @@ Direct mode intentionally does not:
The caller owns token lifetime and refresh. `Authorization: Bearer ...` remains
reserved for MCP OAuth access tokens.

Direct requests default to all active skills, including API Read and API Write:
the client already holds the upstream token. Sentry enforces that token's scopes
and resource permissions, and MCP connection constraints still apply. The
`skills` and `disable-skills` parameters can narrow access; the OAuth consent
screen's unchecked API Write default does not apply to direct tokens.

## Integration Between MCP OAuth and MCP Server

The MCP Server (stateless handler) receives context via closure capture:
Expand Down
8 changes: 5 additions & 3 deletions docs/integrations/claude-code-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,28 @@ The body below the frontmatter is the agent's system prompt.

## Keeping `allowedTools` in Sync

The `allowedTools` list in the agent frontmatter must match the generated default-grant direct MCP surface. The `generate-definitions` script handles this automatically:
The `allowedTools` list in the agent frontmatter must match the generated default-grant direct MCP surface, including the hosted `search_sentry_api` and `execute_sentry_code` tools. Their availability and write permissions still depend on the connection's existing grant and constraints. The `generate-definitions` script handles this automatically:

```bash
pnpm run --filter @sentry/mcp-core generate-definitions
```

The public `/.mcp/tools.json` metadata includes the same hosted definitions. Code Mode has no fixed tool-level API scopes (`requiredScopes: []`): each HTTP operation is authorized against the connection's permissions and organization constraint before the request is sent.

This script:

1. Imports all tools from `packages/mcp-core/src/tools/index.ts`
2. Imports all skills from `packages/mcp-core/src/skills.ts`
3. Writes `toolDefinitions.json` and `skillDefinitions.json` to `packages/mcp-core/src/`
4. Updates `allowedTools` in both `plugins/sentry-mcp/agents/sentry-mcp.md` and `plugins/sentry-mcp-experimental/agents/sentry-mcp.md`, using the default-grant direct surface for each plugin mode. Optional-skill tools remain reachable through `search_sentry_tools` and `execute_sentry_tool` when available.
4. Updates `allowedTools` in both `plugins/sentry-mcp/agents/sentry-mcp.md` and `plugins/sentry-mcp-experimental/agents/sentry-mcp.md`, using the default-grant direct surface for each plugin mode plus hosted Code Mode names from `packages/mcp-core/src/code-mode.ts`. Optional-skill tools remain reachable through `search_sentry_tools` and `execute_sentry_tool` when available.

The script runs automatically as a `prebuild` and `pretest` hook in `packages/mcp-core/package.json`. Run it explicitly after:

- Adding, removing, or renaming tools
- Changing tool skills assignments
- Modifying agent prompts (to verify frontmatter stays valid)

The script skips regeneration if all outputs are newer than all inputs (tool source files, `skills.ts`, and the script itself).
The script skips regeneration if all outputs are newer than all inputs (tool source files, hosted Code Mode definitions, `skills.ts`, and the script itself).

## Modifying the Agent Prompt

Expand Down
2 changes: 2 additions & 0 deletions docs/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ them. The [Toolkit index](../README.md) also links the CLI and shared guides.
[error handling](../contributing/error-handling.md) — MCP client conventions.
- [Testing](../testing/overview.md), [stdio](../testing/stdio.md), and
[remote](../testing/remote.md) — tests and transport QA.
- [HTTP Code Mode](../architecture/code-mode.md) — sandbox, permissions,
schema and telemetry of the hosted Code Mode tools.

## Run and Release

Expand Down
1 change: 1 addition & 0 deletions docs/operations/monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ These are Sentry MCP application attributes that are not part of the MCP semanti
- `app.consent.skill.<skill>.granted` - Per-skill boolean attributes for skills granted to the MCP request. Skill ids are normalized with `-` replaced by `_`
- `app.upstream.host` - Upstream Sentry host configured for the server
- `app.url.full` - MCP URL configured for stdio
- `app.code_mode.*` - Hosted Code Mode outcome, API calls, writes, operations by path template and `@sentry/api` release; see [HTTP Code Mode telemetry](../architecture/code-mode.md#telemetry)

### User Agent Tracking

Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import {
} from "../lib/mcp/args.js";

/**
* mcp-core's `ALL_SKILLS`, copied so the command tree does not import
* mcp-core: docs generation and `pnpm run cli` load every command without
* building it. A test fails if this list drifts.
* `STDIO_SKILLS` from lib/mcp/config.ts, copied so the command tree does not
* import mcp-core: docs generation and `pnpm run cli` load every command
* without building it. A test fails if this list drifts.
*/
export const MCP_SKILL_NAMES = [
"inspect",
Expand Down
42 changes: 29 additions & 13 deletions packages/cli/src/lib/mcp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ACTIVE_SKILLS,
ALL_SKILLS,
parseSkills,
SKILLS,
type Skill,
} from "@sentry/mcp-core/skills";
import {
Expand Down Expand Up @@ -69,12 +70,34 @@ function validated<T>(validate: () => T): T {
}
}

/** Skills stdio can serve: hosted-only skills (Code Mode) need the hosted MCP. */
export const STDIO_SKILLS = ALL_SKILLS.filter(
(skill) => !SKILLS[skill].hostedOnly,
);
const ACTIVE_STDIO_SKILLS = ACTIVE_SKILLS.filter(
(skill) => !SKILLS[skill].hostedOnly,
);

export function formatInvalidSkills(
invalid: string[],
source?: string,
): string {
const prefix = source ? `${source} provided` : "Invalid skills provided";
return `${prefix}: ${invalid.join(", ")}\nAvailable skills: ${ALL_SKILLS.join(", ")}`;
return `${prefix}: ${invalid.join(", ")}\nAvailable skills: ${STDIO_SKILLS.join(", ")}`;
}

function parseStdioSkills(input: string, source?: string): Set<Skill> {
const { valid, invalid } = parseSkills(input);
if (invalid.length > 0) {
throw new ValidationError(formatInvalidSkills(invalid, source));
}
const hosted = [...valid].filter((skill) => SKILLS[skill].hostedOnly);
if (hosted.length > 0) {
throw new ValidationError(
`These skills are available only on the hosted MCP: ${hosted.join(", ")}.`,
);
}
return valid;
}

/** Resolve the Sentry host from `--url` or `--host`, defaulting to sentry.io. */
Expand Down Expand Up @@ -105,34 +128,27 @@ function resolveSkills(input: McpMergedArgs, sentryHost: string): Set<Skill> {

let finalSkills: Set<Skill>;
if (input.allSkills) {
finalSkills = new Set<Skill>(ACTIVE_SKILLS);
finalSkills = new Set<Skill>(ACTIVE_STDIO_SKILLS);
} else if (input.skills) {
const { valid, invalid } = parseSkills(input.skills);
if (invalid.length > 0) {
throw new ValidationError(formatInvalidSkills(invalid));
}
const valid = parseStdioSkills(input.skills);
if (valid.size === 0) {
throw new ValidationError(
"Invalid skills provided. No valid skills found.",
);
}
finalSkills = valid;
} else {
finalSkills = new Set<Skill>(ACTIVE_SKILLS);
finalSkills = new Set<Skill>(ACTIVE_STDIO_SKILLS);
if (!isSentryHost(sentryHost)) {
finalSkills.delete("seer");
}
}

if (input.disableSkills) {
const { valid: skillsToDisable, invalid } = parseSkills(
const skillsToDisable = parseStdioSkills(
input.disableSkills,
"--disable-skills",
);
if (invalid.length > 0) {
throw new ValidationError(
formatInvalidSkills(invalid, "--disable-skills"),
);
}
for (const skill of skillsToDisable) {
finalSkills.delete(skill);
}
Expand Down
Loading
Loading