Skip to content

MCP handlers on an auth: true mount receive the caller's verified principal - #1151

Merged
jimhoyd merged 2 commits into
mainfrom
claude/mcp-tool-principal-trj2bt
Oct 7, 2026
Merged

jimhoyd merged 2 commits into
mainfrom
claude/mcp-tool-principal-trj2bt

Conversation

@jimhoyd

@jimhoyd jimhoyd commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Jimmy · project thread

Closes #1139.

Problem and change

Before: an auth: true MCP mount admitted only a signed-in caller, but its tool, resource and prompt handlers got just env, kind, progress, requestId, server, signal and tool. They couldn't tell who called, so a per-user tool had nothing to scope its work by.

After: on a mount whose route names a principal-providing policy (auth: true), every handler gets context.principal. This is the same frozen {id, provider} that core carries on the request (RIM-EXT-PRINCIPAL-001). With the bundled auth extension, id is the signed-in user id, the same value a function route reads as context.capabilities.auth.identity.userId. A mount without such a policy, or a request whose provider set no principal, gets no principal key.

How: createMcpExtension records each server's mount against the activation's principalMounts, and invocation() adds request.principal to the handler context only for those mounts.

Verification

  • test/account-composition.test.ts is the [Request]: per-user remote MCP for non-browser clients: protected mcp mounts refuse headless callers and handlers get no caller identity #1139 repro. It now checks that Alice and Bob each get their own frozen principal, matching what /api/me returns, and that a second mount without auth: true gets no principal even when a valid session cookie is sent.
  • test/feature-plan.test.ts is updated for the new mount authoring text.
  • Locally, npm run lint, typecheck, typecheck:tooling, check (docs included) and build pass. The mcp and auth workspace tests pass (47 and 33). npm run audit:packages and npm run test:package:built pass.
  • npm test: 1435 pass and 2 fail. The 2 failures are disk-full.test.ts and hermetic-accounts.test.ts, and both fail only because this container's Node 22 ships SQLite 3.50.4, below the store's patched-SQLite floor. They don't touch this change.

Package budgets

The new docs pushed core's unpacked size 792 bytes over its budget, and auth's unpacked size over its budget too. Following the policy in scripts/package-audit.ts, every package was re-measured on Node 26 (npm pack --dry-run --json --ignore-scripts after the build) and each limit reset to measurement plus the fixed margin, rather than raised by the overshoot. Measured sizes (packed / unpacked / entries):

  • core: 1137219 / 4453214 / 571
  • auth: 33059 / 107069 / 14
  • mcp: 36888 / 147709 / 12
  • store: 161504 / 626654 / 36
  • store-schema: 12845 / 51053 / 7, which leaves its limits unchanged

Compatibility and security

  • This adds a key to the handler context and changes no YAML. Unprotected mounts behave exactly as before, which packages/mcp/test/context.test.ts already pins.

  • The principal is never taken from a header, cookie or tool argument. It comes only from core's principal slot, and only on a principalMounts mount.

  • Remaining for Make URLCode auth and MCP easier to adopt in existing apps #1138: admission is unchanged. With the bundled auth, a non-browser MCP client (no Origin, or a bearer token) is still refused 403 cross_origin_refused by the session gate. There is still no bearer, OAuth or headless principal provider, so remote MCP clients can't reach these per-user tools yet.

  • Docs: the mcp README ("Protecting a mount", the context list and the generated reference), the auth README cross-link, the mcp CHANGELOG.md and llms.txt, and the RIM-EXT-PRINCIPAL-001 card in docs/RUNTIME-IMPLEMENTATION.md.

  • Changes match the documented portable contract; docs/examples are updated where needed.

  • Relevant tests and package checks pass, or limitations are explained above.

  • No credentials, customer data or private project material are included.

  • Runtime/starter changes remain aligned where applicable.

🤖 Generated with Claude Code

https://claude.ai/code/session_013oBXtj4wmB5VHq2cNMWNeU

…1139)

A tool, resource or prompt handler on a mount whose route names a
principal-providing policy now receives the request's frozen
{id, provider} principal (RIM-EXT-PRINCIPAL-001) as context.principal.
Unprotected mounts, and requests whose provider set none, get no key.
Admission and the cross-origin gate are unchanged (#1138).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oBXtj4wmB5VHq2cNMWNeU
@jimhoyd jimhoyd self-assigned this Oct 7, 2026
The handler-principal docs pushed core's unpacked size 792 bytes over its
budget and auth's README past its unpacked budget. Per the policy in
scripts/package-audit.ts, every package is re-measured on Node 26 and each
limit reset to measurement plus the fixed margin, not raised by the overshoot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oBXtj4wmB5VHq2cNMWNeU
@jimhoyd
jimhoyd marked this pull request as ready for review October 7, 2026 22:30
@jimhoyd
jimhoyd merged commit 241a764 into main Oct 7, 2026
27 checks passed
@jimhoyd
jimhoyd deleted the claude/mcp-tool-principal-trj2bt branch October 7, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Request]: per-user remote MCP for non-browser clients: protected mcp mounts refuse headless callers and handlers get no caller identity

2 participants