Conversation
Groundwork for a remote MCP endpoint at /mcp: MCP-spec OAuth so clients like Claude Code connect with the standard one-click browser flow, no CLI install required. - auth.ts: four new HMAC token kinds behind the signValue chokepoints — mcp-consent (user-bound consent transaction), mcp-code (60s one-time, PKCE-bound), and audience-bound mcp-access (1h) / mcp-refresh (session TTL, non-rotating) — under an independent MCP_TOKEN_VERSION revocation lever; requireMcpUser resolves the /mcp bearer principal (mcp-access, or an existing session bearer as a capability-neutral fallback; never cookies). - mcp-clients.ts: RFC 7591 dynamic client registration in PrimitiveDb (90-day TTL, 1000-live cap, redirect-URI policy with RFC 8252 loopback port variance). - mcp-oauth-routes.ts: RFC 9728/8414 discovery metadata, /oauth/register, /oauth/authorize + consent page, /oauth/token (code + refresh grants, RFC 6749 error bodies), enumerated in MCP_OAUTH_ROUTES for the policy matrix; ACAO * only on the cookie-free endpoints. - app.ts: dispatch + canonical app-origin redirect for /oauth/* and /.well-known/*; access.ts reserves the mcp and oauth slugs. - api-routes.ts: one-time code burn extracted as burnOneTimeCode, shared by the CLI and MCP exchanges. - Tests: token corpus rows for all four kinds (cross-kind, bit-flip, audience/scope/lifecycle), full-loop + adversarial mcp-oauth.test.ts, policy-matrix rows (mcp-access denied API-wide; MCP OAuth route completeness and CORS posture). Threat review (invariants 3/5): codes burn before possession checks so replays and wrong-verifier races fail closed; PKCE is mandatory S256 with no plain path; unvalidated client_id/redirect_uri 400s and never redirects, loopback matching varies only the port; audience confusion is closed both directions and mechanized in the matrix; DCR spam is bounded by body/URI/registration caps; consent is same-origin, user-bound, and unframeable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First of two PRs adding a remote MCP server so agents (Claude Code) can publish Scratchwork projects with no CLI installed. This PR is the auth surface; the follow-up (#next) adds the
/mcpendpoint and tools.What this adds
MCP-spec OAuth 2.1 on the app origin, built on the existing chokepoints:
/mcppath-inserted forms).token_endpoint_auth_method: none), stored in PrimitiveDb with a 90-day TTL and a 1000-live-client cap; redirect URIs are https-or-loopback with RFC 8252 §7.3 loopback port variance.frame-ancestors 'none'.authorization_code(burn-before-verify one-time codes) andrefresh_tokengrants with RFC 6749 error bodies.auth.ts—mcp-consent,mcp-code,mcp-access(1h, audience-bound to<appBase>/mcp),mcp-refresh(session TTL) — under an independentMCP_TOKEN_VERSIONrevocation lever, all minted/verified through the privatesignValue/verifySignedValuechokepoints.requireMcpUserresolves the future/mcpbearer principal: anmcp-accesstoken, or an existing session bearer as a capability-neutral fallback (sessions already authorize the full/apisurface); cookies are never read on that path.Threat review (invariants 3/5)
mcp-code.idburns via conditional create before the possession check (sharedburnOneTimeCode, also adopted by the CLI exchange) — replays and wrong-verifier races fail closed.code_challengemandatory at authorize, method literalS256, noplainpath exists; verifier digests compare viatimingSafeEqual.client_id/redirect_urigets a 400 page, never a redirect; loopback matching varies only the port; the code pins the redirect and the token endpoint re-requires byte equality; oversizedstateis never echoed.kinddiscrimination and theaudclaim — mechanized in the policy matrix (anmcp-accessbearer is denied on every/apiroute) and the token corpus (9-kind cross-pairing)./oauth/*and/.well-known/*canonicalize to the app origin before any cookie/issuer derivation;mcpandoauthare now reserved slugs;Access-Control-Allow-Origin: *appears only on the four cookie-free endpoints.Accepted trade-offs
mcp-access/mcp-refreshtokens are replayable within TTL, like sessions; levers are the allow-list (re-checked at every mint and verify) andMCP_TOKEN_VERSION. The refresh grant does not rotate refresh tokens — stateless rotation cannot revoke the predecessor, so rotation would be theater.client_name.Tests
Token corpus rows for all four kinds (bit-flip, cross-kind, boundary expiry, audience/scope confusion, future issuance, lifecycle); full-loop
mcp-oauth.test.ts(discover → register → authorize → consent → token → principal) plus the negatives above; policy-matrix extensions (deny-allmcp-accessrow, MCP OAuth route completeness, CORS posture); redirect-URI rule units.bun run cigreen.🤖 Generated with Claude Code