Skip to content

Serve /mcp on dual protocol lanes: stateless 2026-07-28 + instrumented legacy, with CIMD and tool metadata - #1237

Merged
kody-bot merged 2 commits into
mainfrom
cursor/mcp-2026-stateless-lane-5bfa
Aug 5, 2026
Merged

kody-bot merged 2 commits into
mainfrom
cursor/mcp-2026-stateless-lane-5bfa

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

Intent

MCP protocol revision 2026-07-28 made the protocol stateless, and Cloudflare deprecated and feature-froze McpAgent — the Durable Object that hosts kody's /mcp today. Adopt the new revision without breaking a single existing client, and make retirement of the old path a metrics decision instead of a guess: instrument which lane every request uses so we know exactly when legacy traffic is gone.

Summary

  • Dual-lane /mcp behind the existing route and auth: after bearer-token validation, handleMcpRequest classifies each request with the SDK's own isLegacyRequest predicate. 2025-era requests keep the sessionful McpAgent Durable Object lane byte-identically; 2026-07-28 envelope requests are served by a per-request stateless SDK v2 server (packages/worker/src/mcp/stateless-lane.ts) with legacy: 'reject' so exactly one lane owns each era. Both lanes deliberately stay inside withAccountWriteLease — that lease is the deletion-safety boundary every kody surface takes (MCP, app, jobs, package invocations), not MCP session state; "stateless" here means no MCP session Durable Object.
  • Lane metrics, deliberately not in D1: every authenticated request writes one non-blocking data point (lane, JSON-RPC method, protocol revision, client name/version, user id) to a new MCP_PROTOCOL_EVENTS Analytics Engine dataset (packages/worker/src/mcp/protocol-metrics.ts). The retirement readout is a pure aggregate; the query is documented in the module header. No-op without the binding, never throws.
  • One tool registration for both server generations: registerTools now takes a shared registration surface (McpToolServer); the SDK v1/v2 generic mismatch is bridged at one documented seam (asMcpToolServer). The DO lane forwards state/setState live so session niceties (search preamble dedup, raw-fetch nudge accumulation) are unchanged; the stateless lane degrades those to per-call behavior, which the runners already handle.
  • Tool metadata with graceful degradation: search/execute now advertise a deliberately loose outputSchema (all fields optional, compound values unknown — server-side output validation can never reject a real response; covered by tests) on both lanes, and icons (2025-11-25 SEP-973) on the modern lane (SDK v1 ignores the config key).
  • CIMD: clientIdMetadataDocumentEnabled: true on the OAuth provider — clients may present an HTTPS URL as client_id with no registration step; clients that do not use CIMD register via DCR (/oauth/register), which stays enabled per spec. A failed CIMD fetch returns invalid_client; any DCR retry is the client's own recovery. The required global_fetch_strictly_public compat flag was already set.
  • Instructions off the hot path: the stateless lane assembles server instructions (3 D1 reads) only for server/discover, never for tools/call.
  • Docs: ADR 0005 (dual-lane + metrics-driven retirement policy), request-lifecycle, authentication (CIMD), data-storage (new dataset).

Deliberately not in scope: the tasks extension (SDK v2 ships vocabulary only, no runtime; execute's idempotency-key + run_get already covers it), sampling/roots/MCP-logging (deprecated in 2026-07-28; kody never adopted them), and legacy-lane removal itself (that waits on the metrics this PR adds).

Testing

  • npm run validate — all gates green except one pre-existing failure unrelated to this change: packages/worker/src/email/inbound-due-owners.workers.test.ts fails identically on unmodified main in the same VM (time-dependent assertion; main CI passed today at 14:00 UTC).
  • New MCP e2e (stateless-lane.mcp-e2e.test.ts): a real SDK v2 client pinned to 2026-07-28 completes OAuth, negotiates via server/discover, lists tools (asserting output schemas + icons), and runs a real search call. Existing v1-client e2e tests keep passing untouched, proving the legacy lane is unaffected.
  • New workers test in mcp-auth.workers.test.ts: legacy initialize routes to the DO lane, a modern envelope request is served statelessly without touching the DO, and both record the expected Analytics Engine data points.
  • New node tests: era classification (protocol-metrics.node.test.ts) and output-schema safety (output-schemas.node.test.ts runs every structured-response shape through the advertised schemas exactly as the SDK validates them).

System changes

System recap — extends mcp-server and mcp-oauth (medium risk)

Mode: recap · Base: main @ 865304b0 · Head: e806211c

Classification: extends — the /mcp surface gains a second (stateless 2026-07-28) serving lane and per-request lane metrics; MCP OAuth gains CIMD client registration. No new primitive; primitives.yaml unchanged.

Primitives touched

Primitive Group Impact
mcp-server surfaces extends — dual-lane era routing, stateless SDK v2 lane, shared tool registration, outputSchema + icons, lane metrics
mcp-oauth auth extends — lane classification added post-auth in mcp-auth.ts; CIMD enabled beside DCR
scheduled-cron surfaces composes — classifier matches index.ts by root only; the actual edit there is the OAuth provider CIMD option

System map

Authenticated /mcp traffic forks by protocol era into the existing Durable Object lane or the new stateless lane, and every request drops a lane data point into Analytics Engine.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	mcpOauth["mcp-oauth<br/>MCP OAuth"]:::extended
	mcpServer["mcp-server<br/>MCP endpoint (/mcp)"]:::extended
	capabilityRegistry["capability-registry<br/>Capability registry"]:::untouched
	analyticsEngine["MCP_PROTOCOL_EVENTS<br/>Analytics Engine dataset"]:::extended
	mcpOauth -->|"isLegacyRequest era routing after bearer auth"| mcpServer
	mcpOauth -->|"one data point per request: lane, method, version, client"| analyticsEngine
	mcpServer -->|"shared registerTools on both lanes (search/execute only)"| capabilityRegistry
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Change flow

sequenceDiagram
	participant C as MCP client
	participant A as mcp-auth.ts
	participant DO as McpAgent DO (SDK v1, 2025 era)
	participant SL as stateless-lane.ts (SDK v2, 2026-07-28)
	participant AE as Analytics Engine
	C->>A: POST /mcp (Bearer token)
	A->>A: unwrapToken + gates + classifyMcpProtocolRequest
	A--)AE: writeDataPoint(lane, method, version, client)
	alt 2025-era request (initialize / session ops)
		A->>DO: fetchMcp (unchanged sessionful lane)
	else 2026-07-28 envelope request
		A->>SL: per-request McpServer, legacy:'reject'
	end
Loading

Invariants

compact-mcp-surface upheld: both lanes register exactly the same two tools (search, execute) from one shared registration; no per-capability tools added.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added support for the modern 2026-07-28 stateless MCP protocol alongside legacy traffic.
    • Added OAuth Client ID Metadata Document support with dynamic registration fallback.
    • Added structured output schemas and Kody icons for MCP search and execute tools.
    • Added MCP protocol usage analytics for monitoring migration progress.
  • Documentation

    • Documented protocol routing, analytics, authentication options, and the migration strategy.
  • Tests

    • Added coverage for modern and legacy routing, analytics, OAuth connectivity, and tool outputs.

…etadata

- Route /mcp by protocol era after auth: 2025-era requests stay on the
  sessionful McpAgent Durable Object lane; 2026-07-28 envelope requests
  are served per-request by a stateless SDK v2 server (no DO, no session)
- Record one Analytics Engine data point per authenticated /mcp request
  (lane, method, protocol version, client info) so legacy-lane retirement
  is a metrics decision; deliberately not stored in the primary D1
- Share one tool registration across both server generations; advertise
  loose outputSchema on search/execute (both lanes) and tool icons
  (modern lane; SDK v1 ignores the key)
- Enable OAuth Client ID Metadata Documents alongside DCR
- ADR 0005 documents the dual-lane design and retirement policy

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The worker adds a stateless MCP lane for protocol revision 2026-07-28, preserves legacy sessionful routing, records protocol events, supports shared SDK v1/v2 tool registration, advertises schemas and icons, and enables OAuth Client ID Metadata Documents.

Changes

MCP dual-lane serving

Layer / File(s) Summary
Protocol classification and event recording
packages/worker/src/mcp/protocol-metrics.ts, packages/worker/src/mcp/protocol-metrics.node.test.ts, packages/worker/src/env-schema.ts, packages/worker/wrangler.jsonc, packages/worker/worker-configuration.d.ts, docs/contributing/architecture/data-storage.md
MCP requests are classified as legacy or modern. Authenticated request metadata is recorded in Analytics Engine with fallback and write-error handling.
Shared tool registration and metadata
packages/worker/src/mcp/mcp-registration-agent.ts, packages/worker/src/mcp/index.ts, packages/worker/src/mcp/tools/*, packages/worker/src/mcp/tools/output-schemas.node.test.ts
Tool registration supports SDK v1 and v2 servers. Search and execute tools advertise structured output schemas and optional icons.
Stateless lane and authenticated routing
packages/worker/src/mcp/stateless-lane.ts, packages/worker/src/mcp-auth.ts, packages/worker/src/mcp-auth.workers.test.ts, tools/mcp-test-support.ts, packages/worker/src/mcp/stateless-lane.mcp-e2e.test.ts, docs/contributing/architecture/request-lifecycle.md, docs/contributing/decisions/*
Modern requests use a per-request SDK v2 server. Legacy requests use the sessionful Durable Object lane. Tests cover routing, tool metadata, OAuth-backed clients, and search execution.
OAuth client metadata support
packages/worker/src/index.ts, docs/contributing/architecture/authentication.md
The OAuth provider enables Client ID Metadata Documents and retains dynamic registration fallback. The ADR index and ADR 0005 document the dual-lane migration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • kentcdodds/kody issue 1191: Covers the SDK v2 stateless MCP path and retention of the legacy McpAgent path.

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MCPAuth
  participant ProtocolMetrics
  participant StatelessLane
  participant McpAgent
  participant AnalyticsEngine

  Client->>MCPAuth: Authenticated /mcp request
  MCPAuth->>ProtocolMetrics: Classify request
  ProtocolMetrics-->>MCPAuth: Legacy or modern lane
  MCPAuth->>AnalyticsEngine: Record protocol event
  alt Modern 2026-07-28 request
    MCPAuth->>StatelessLane: Handle per-request MCP server
    StatelessLane-->>Client: MCP response
  else Legacy request
    MCPAuth->>McpAgent: Forward to sessionful lane
    McpAgent-->>Client: MCP response
  end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately describes the main change: dual protocol lanes for /mcp with stateless 2026-07-28 support, legacy instrumentation, CIMD, and tool metadata.
Description check ✅ Passed The description comprehensively covers all template sections with clear intent, detailed summary with bullet points, comprehensive testing details, and system changes with diagrams and risk assessment.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/mcp-2026-stateless-lane-5bfa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review August 5, 2026 14:51
@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1237.kody-a99.workers.dev

Worker: kody-pr-1237
D1: kody-pr-1237-db
KV: kody-pr-1237-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/index.ts`:
- Around line 430-432: Narrow the authentication wording so it does not promise
fallback after a failed CIMD fetch: update packages/worker/src/index.ts lines
430-432 and docs/contributing/architecture/authentication.md lines 476-478 to
state that clients may present a CIMD client_id, while clients that do not use
CIMD register through DCR; remove the claim that CIMD-capable clients skip
registration or that other clients fall back after CIMD failure.

In `@packages/worker/src/mcp-auth.ts`:
- Around line 273-293: The modern branch in the MCP dispatch must bypass
withAccountWriteLease and call handleStatelessMcpRequest directly; retain the
lease-wrapped path only for legacy requests, and enforce account-deletion
protection at the relevant mutating tool boundary. In
packages/worker/src/mcp-auth.ts:273-293, update the dispatch accordingly. In
docs/contributing/architecture/request-lifecycle.md:54-63 and
docs/contributing/decisions/0005-mcp-dual-lane-stateless-migration.md:32-34,
retain the no-Durable-Object claims only if this bypass is implemented;
otherwise revise them to document the account Durable Object hop.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 300f6de9-a2eb-4b0f-a2f1-d4fa1072888c

📥 Commits

Reviewing files that changed from the base of the PR and between 7ed1432 and 004986a.

📒 Files selected for processing (23)
  • docs/contributing/architecture/authentication.md
  • docs/contributing/architecture/data-storage.md
  • docs/contributing/architecture/request-lifecycle.md
  • docs/contributing/decisions/0005-mcp-dual-lane-stateless-migration.md
  • docs/contributing/decisions/index.md
  • packages/worker/src/env-schema.ts
  • packages/worker/src/index.ts
  • packages/worker/src/mcp-auth.ts
  • packages/worker/src/mcp-auth.workers.test.ts
  • packages/worker/src/mcp/index.ts
  • packages/worker/src/mcp/mcp-registration-agent.ts
  • packages/worker/src/mcp/protocol-metrics.node.test.ts
  • packages/worker/src/mcp/protocol-metrics.ts
  • packages/worker/src/mcp/stateless-lane.mcp-e2e.test.ts
  • packages/worker/src/mcp/stateless-lane.ts
  • packages/worker/src/mcp/tools/execute.ts
  • packages/worker/src/mcp/tools/output-schemas.node.test.ts
  • packages/worker/src/mcp/tools/search-register.ts
  • packages/worker/src/mcp/tools/search-tool-definition.ts
  • packages/worker/src/mcp/tools/tool-icons.ts
  • packages/worker/worker-configuration.d.ts
  • packages/worker/wrangler.jsonc
  • tools/mcp-test-support.ts

Comment thread packages/worker/src/index.ts Outdated
Comment on lines 273 to +293
return await withAccountWriteLease({
db: env.APP_DB,
stableUserId: mcpUser.userId,
holder: `mcp:${request.method} ${url.pathname}`,
env,
write: async () =>
await fetchMcp(
request,
env,
context as ExecutionContext<OAuthContextProps>,
),
classification.lane === 'legacy'
? await fetchMcp(
request,
env,
context as ExecutionContext<OAuthContextProps>,
)
: await handleStatelessMcpRequest({
request,
env,
ctx,
callerContext: props,
...(classification.parsedBody === undefined
? {}
: { parsedBody: classification.parsedBody }),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Remove the account Durable Object from the modern request path.

handleStatelessMcpRequest runs inside withAccountWriteLease. The supplied lease implementation calls acquireDoAccountWriteLeaseAndWrite. Every modern request therefore depends on an account Durable Object. This defeats the stated stateless, no-Durable-Object lane design and adds a lease availability dependency to tools/list and other read-only requests.

  • packages/worker/src/mcp-auth.ts#L273-L293: Dispatch the modern branch outside withAccountWriteLease. Apply any required account-deletion protection at the specific mutating tool boundary.
  • docs/contributing/architecture/request-lifecycle.md#L54-L63: Keep the no-Durable-Object statement only after the modern branch bypasses the lease. Otherwise document the account Durable Object hop.
  • docs/contributing/decisions/0005-mcp-dual-lane-stateless-migration.md#L32-L34: Keep the no-Durable-Object consequence only after the modern branch bypasses the lease. Otherwise revise the decision record.
📍 Affects 3 files
  • packages/worker/src/mcp-auth.ts#L273-L293 (this comment)
  • docs/contributing/architecture/request-lifecycle.md#L54-L63
  • docs/contributing/decisions/0005-mcp-dual-lane-stateless-migration.md#L32-L34
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/mcp-auth.ts` around lines 273 - 293, The modern branch in
the MCP dispatch must bypass withAccountWriteLease and call
handleStatelessMcpRequest directly; retain the lease-wrapped path only for
legacy requests, and enforce account-deletion protection at the relevant
mutating tool boundary. In packages/worker/src/mcp-auth.ts:273-293, update the
dispatch accordingly. In
docs/contributing/architecture/request-lifecycle.md:54-63 and
docs/contributing/decisions/0005-mcp-dual-lane-stateless-migration.md:32-34,
retain the no-Durable-Object claims only if this bypass is implemented;
otherwise revise them to document the account Durable Object hop.

… server fallback

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kody-bot
kody-bot merged commit 89185bf into main Aug 5, 2026
10 checks passed
@kody-bot
kody-bot deleted the cursor/mcp-2026-stateless-lane-5bfa branch August 5, 2026 15:12
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.

3 participants