feat(mcp): serve /mcp through the SDK envelope behind a flag, at parity - #9677
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-registry-sync-api | 64bf18b | Aug 06 2026, 12:11 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-data-api | 64bf18b | Aug 06 2026, 12:11 PM |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
JSONbored
force-pushed
the
feat/mcp-sdk-envelope-wiring
branch
from
August 6, 2026 12:06
8934b68 to
e691933
Compare
Routes /mcp through @modelcontextprotocol/sdk's Server and web-standard transport when MCP_SDK_ENVELOPE=1, default off. The SDK takes the envelope only -- JSON-RPC parse, batch fan-out, 202-on-notification, framing -- while every method still resolves through dispatchMessage, so the single instrumentation chokepoint is unchanged. Total delegation is what makes that possible: initialize and ping are reclaimed with removeRequestHandler, so no method is answered by the SDK. That keeps their telemetry (client attribution, session identity) and avoids setRequestHandler's zod re-validation, which would have rejected a handshake with no protocolVersion that this server negotiates today. Running the SDK rather than reading it turned up seven ways the swap would not have been behaviour-neutral. None became a response shim: _meta dropped total delegation returns our own result verbatim thrown-message leak dispatchMessage returns, never throws McpError rewrites JsonRpcFailure carries a code the SDK serialises as-is Accept 406 normalized on the rebuilt request; callers send */* headers lost MCP_HEADERS overlaid, keeping CORS and no-store malformed input gated away: the SDK never sees a bad message notification race pending dispatches drained before the response The malformed-input one loses data rather than relabelling it -- a batch with one bad member currently answers the valid ones, where the SDK rejects the batch whole. Ours is also the more spec-correct classification, since -32700 is reserved for JSON that did not parse. The notification one is invisible to a response comparison. The SDK's Protocol dispatches notifications fire-and-forget, so the 202 returns while the handler is still running and the request context is torn down with the telemetry write unfinished. Measured: at response time the dispatch had started and not completed. Left alone, notifications/initialized would simply have stopped appearing once the flag flipped. The batch ceiling moves out of the hand-rolled branch so both envelopes share it; the SDK transport fans out with no bound of its own, and a flag that swaps envelopes must not also remove a resource limit. mergeInitializeMeta is deleted rather than left behind: total delegation makes it unreachable. Parity is asserted against the real served path -- handleMcpRequest with the flag off, then on -- across every method, both failure shapes, malformed input, batches, headers, status codes and session minting, with a flag test so the comparison cannot pass vacuously. Closes #9676 Refs #9647
JSONbored
force-pushed
the
feat/mcp-sdk-envelope-wiring
branch
from
August 6, 2026 12:10
e691933 to
64bf18b
Compare
This was referenced Aug 6, 2026
Merged
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.
Summary
Routes
/mcpthrough@modelcontextprotocol/sdk'sServer+WebStandardStreamableHTTPServerTransportbehind a default-offMCP_SDK_ENVELOPEflag. Nothing changes in production until the flag is set; the point of shipping it dark is that flipping it — and flipping it back — is a config change rather than a deploy, on the surface with the most external callers.The SDK takes the envelope only: JSON-RPC parse, batch fan-out and correlation, 202-on-notification, JSON/SSE framing. Every method still resolves through
dispatchMessage, so the single instrumentation chokepoint (#8993, #8994, #9054, #9639, #9642) is untouched.Total delegation
initializeandpingare reclaimed withremoveRequestHandler, so no method is answered by the SDK. Three reasons, each of which was a real defect in the partial-delegation draft:dispatchMessage'sfinallyemits the protocol usage event for every method. A method the SDK answered internally is a method that silently stops being counted,initializemost of all.setRequestHandlerwraps a handler in a zod parse of the SDK's own schema. Registering one forinitializewould reject a handshake with noprotocolVersionthat we negotiate today, and answer with zod's text.Six divergences, zero response shims
Found by running the SDK, not reading it:
initialize._meta(registry backlink) dropped-32603dispatchMessagereturns, never throwsMcpErrorrewrites text toMCP error -N: …JsonRpcFailurecarries acodethe SDK serialises as-isAcceptlacking both media typescontent-type, nothing on 202MCP_HEADERSoverlaid — CORS andno-storesurvive400 -32700for the whole requestThe
Acceptone would have presented as "the migration broke every script" — most traffic here iscurl/python-requestssending*/*, which fails the transport's literal substring test.The malformed-input one loses data rather than relabelling it: a batch with one bad member currently answers the valid ones, where the SDK rejects the batch whole. Ours is also the more spec-correct classification, since
-32700is reserved for JSON that did not parse. So malformed input is routed away from the SDK rather than shimmed back.The batch ceiling moves out of the hand-rolled branch so both envelopes share it — the transport fans out with no bound of its own, and a flag that swaps envelopes must not also remove a resource limit.
mergeInitializeMetais deleted, not left behind: total delegation makes it unreachable.Verification
tests/mcp-sdk-parity.test.ts— 32 tests driving the real served path (handleMcpRequest, flag off then on) across every method, bothtools/callfailure shapes, malformed input, batches, headers and status codes. The previous version compared the adapter against the dispatcher through a stub, which proved nothing about the wiring around it.Acceptnormalization, and the header overlay each fail the harness.Not in scope
Deleting the hand-rolled envelope (#9647 step 4) stays open until the flag has been on through a full deploy cycle.
Closes #9676
Refs #9647
Update: a seventh divergence, found reviewing my own diff
The SDK dispatches notifications fire-and-forget.
Protocol._onnotificationruns the handler asPromise.resolve().then(() => handler(n))and never awaits it, so the 202 returns while the handler is mid-flight — and on Workers the request context is then torn down with the telemetry write unfinished.Measured: at response time the dispatch had started and not completed.
This is invisible to a response comparison — every parity test above passes either way, because the HTTP response is byte-identical. The only symptom would have been
notifications/initializedquietly disappearing from PostHog some time after the flag flipped, with nothing pointing at the cause.Fixed by draining pending notification dispatches before answering (
allSettled, so a failing telemetry write can never become the caller's response), with two tests: one asserting the funnel finished rather than merely started, one asserting a rejecting dispatch still yields a 202. Both sabotage-checked.And an eighth gap — in my own harness
observe()compared status, body,content-type, CORS andcache-control, but notmcp-session-id. Session minting reads the dispatched response, which the SDK path has to capture on its way past rather than parse back out of a serialized body — so getting it wrong would have cost every caller its identity (#9054) with nothing in the suite objecting.Now asserted three ways: that a successful
initializemints on both paths, that the id is well-formed ([\x21-\x7E]{1,128}), and that a refused request mints nothing — an id the client never received would be a session the hub holds for nobody. Sabotage-checked by breaking the capture: three tests fail.