feat: SEP-2243 standard half — std-header server validation; http-header-validation 13/0#2328
feat: SEP-2243 standard half — std-header server validation; http-header-validation 13/0#2328felixweinberger wants to merge 8 commits into
Conversation
🦋 Changeset detectedLatest commit: 9e9ede2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
17f10d2 to
98117b6
Compare
507001d to
08c9f8b
Compare
98117b6 to
356eac6
Compare
08c9f8b to
e12c5d9
Compare
356eac6 to
d85240a
Compare
43ffc63 to
87ce6c5
Compare
bf03f18 to
78b289d
Compare
78b289d to
5779aab
Compare
3678bf9 to
1bed404
Compare
ea9fe3a to
a085951
Compare
a085951 to
1e780e2
Compare
1e780e2 to
6e8a521
Compare
6bf980e to
ce53141
Compare
6e8a521 to
0de8fe9
Compare
There was a problem hiding this comment.
All of my earlier inline feedback (the Object.hasOwn guard, the dedicated standard-header-validation rung, the example client headers, and the test/comment cleanups) has been addressed in the latest commits, and I found no new issues — but since this PR adds new request-rejection behavior on the modern serving path (a spec-level enforcement change spanning core, server, conformance baselines, and e2e), it should still get a human maintainer's review.
Extended reasoning...
Overview
This PR implements the server-side half of SEP-2243 standard request-header validation: validateStandardRequestHeaders in packages/core/src/shared/inboundClassification.ts (Mcp-Method presence, Mcp-Name presence/decoding/cross-check), wired into createMcpHandler's modern serving path after the supported-revision gate. It adds a new standard-header-validation ladder rung, updates conformance expected-failure baselines (http-header-validation now expected to pass 13/0), adds unit/over-HTTP/e2e test coverage, updates hand-built test fixtures across server/integration/e2e suites and the json-response example, and adds migration-doc prose plus a changeset.
Security risks
Low. The validation tightens what the server accepts (rejecting modern requests with missing/mismatched standard headers with 400/-32001) rather than loosening anything. The peer-controlled method-string lookup is guarded with Object.hasOwn (addressing my earlier inline comment), so prototype-pollution-style key collisions are handled. The decoded Mcp-Name value is only compared, never executed or stored. No auth, crypto, or session logic is touched.
Level of scrutiny
This deserves a real human review despite the clean bug-hunt result: it changes which requests the modern (2026-07-28) serving path accepts — a behavioral/spec-compliance decision (e.g., evaluating the standard-header rung after the supported-revision gate but before dispatch, treating notifications as exempt, and the choice of -32001 for all four cells) that a maintainer should sign off on. The changeset/release impact (minor for @modelcontextprotocol/server, with the stacked series triggering major bumps for the adapter packages per changeset-bot) is also a maintainer call.
Other factors
All five of my earlier review threads have been addressed by follow-up commits in this PR (the hasOwn guard, the dedicated rung + ladder renumbering with precedence caveat, the rung name in the call-site comment and test header, the era-gating test title, and the missing mcp-method header in examples/json-response/client.ts). Test coverage is solid: a 16-case core unit corpus, a 9-case over-HTTP corpus, an e2e mismatch cell, and the conformance baseline burn-down. The PR is stacked on #2327, so review ordering matters for the maintainers.
0de8fe9 to
a271dc7
Compare
ce53141 to
f6680bd
Compare
…cp-Name presence and Mcp-Name cross-check Adds `validateStandardRequestHeaders`, evaluated by the HTTP entry on a modern-classified request immediately after `classifyInboundRequest` returns a modern route. Rejects `400`/`-32001` (`HeaderMismatch`) — the same shape and rung the classifier already emits for the `MCP-Protocol-Version` and `Mcp-Method` mismatch cells — when the required `Mcp-Method` header is absent, when the required `Mcp-Name` header is absent on a `tools/call`/`prompts/get`/`resources/read` request, when `Mcp-Name` carries an invalid Base64 sentinel, and when its (decoded) value disagrees with `params.name`/`params.uri`. Kept separate from `classifyInboundRequest` so a body-only call to the classifier keeps routing a modern request unchanged: the classifier remains a pure body-primary router, and this function is the presence/`Mcp-Name` half of the standard-header rung the entry layers on top. `InboundHttpRequest` gains an additive optional `mcpNameHeader` field.
…dler entry `createMcpHandler` now reads `Mcp-Name` and runs `validateStandardRequestHeaders` on a modern-classified request immediately after the body-primary classifier returns a modern route. The four hand-written test fixtures that build modern requests directly (`postRequest`/`nodeRequestResponse` in createMcpHandler.test.ts, `listenRequest` in createMcpHandlerListen.test.ts, `postEcho` in createMcpHandlerCapabilityGate.test.ts, `call` in mcpParamValidation.test.ts) now derive the SEP-2243 standard headers from the body they send, exactly as a conformant client does — fixture-only, no assertion changes. Legacy test cells stay byte-untouched (the derivation only emits for a body carrying a modern envelope claim).
…ndard headers to raw-request fixtures Removes `http-header-validation` from both server expected-failures baselines (the scenario is now 13/0). The four hand-written raw-request sites in test/integration and test/e2e that POST a modern envelope directly at `createMcpHandler` (the subscriptions/listen ack-first cell, the capacity-guard cell, the honored-filter integration check, and the hosting-entry-session router probe) now carry the SEP-2243 standard headers a conformant client sends — fixture-only, no assertion changes.
The body method string is peer-controlled; a bare plain-object lookup returns inherited Object.prototype members for names like 'constructor' or 'toString', so the off-table early-return would not fire and an invalid Mcp-Name sentinel on such a request was answered 400/-32001 instead of reaching dispatch's -32601. Match the established idiom in requiredClientCapabilitiesForRequest.
…l on the entryModern arm Adds the sep-2243:std-header:mismatch-rejected requirement (entryModern, 2026-07-28) and its scenario body: a raw envelope-carrying tools/call POSTed with an Mcp-Method: tools/list header is rejected by the createMcpHandler entry with HTTP 400 and the -32001 HeaderMismatch JSON-RPC error code.
…ch ladder rung The four validateStandardRequestHeaders rejection cells (method-header-missing, name-header-missing, name-header-invalid-encoding, name-header-mismatch) were stamped 'era-classification', whose ladder descriptor says evaluatedAt: 'edge' — but they are evaluated by the HTTP entry's serveModern path after the supported-revision gate, not by the edge classifier. Add a 'standard-header-validation' rung (order 8, evaluatedAt: 'pre-dispatch', sitting between client-capabilities and the param-header rung, which moves to order 9) and re-stamp the four cells with it. The classifier's own header-mismatch cells (protocol-version, Mcp-Method mismatch) stay on the edge era-classification rung. crossCheckMismatch grows an optional rung parameter so the shared shape stays single-source.
…header; add precedence caveat The serveModern call-site comment and the server stdHeaderValidation test header still attributed the presence/Mcp-Name rejections to the edge era-classification rung after 5779aab re-stamped them onto the dedicated standard-header-validation rung. Both now name the rung explicitly (and note the classifier's mismatch cells stay on era-classification). The new rung's documented order (8) is also not the observed precedence — serveModern evaluates it immediately after the supported-revision gate, before the dispatch rungs (5-6) and the capability gate (7). Rather than renumber (wider blast across the ladder table and the param-header rung), add a precedence caveat to its rationale, mirroring the client-capabilities entry's caveat.
…apability gate; align era-gating test title with its initialize body The standard-header-validation ladder entry is moved to order 7 (ahead of client-capabilities, now 8, and param-header-validation, now 9), and its rationale carries an explicit precedence caveat: serveModern evaluates this rung immediately after the supported-revision gate, so a request that also fails a dispatch rung (method-registry 5, request-params 6) is answered here first — the documented order is not the observed precedence relative to those. The era-gating test's title said it posts a 2025-era tools/list without standard headers, but the body POSTs initialize (the 2025 handshake, which is the right body for era-gating); aligned the title to match.
a271dc7 to
9e9ede2
Compare
f6680bd to
5d68be8
Compare
| --- | ||
| '@modelcontextprotocol/server': minor | ||
| --- | ||
|
|
||
| SEP-2243 standard-header server-side validation (protocol revision 2026-07-28). On the modern (2026-07-28) serving path, `createMcpHandler` now enforces the required `Mcp-Method` and `Mcp-Name` standard request headers in addition to the existing `MCP-Protocol-Version` and `Mcp-Method` cross-checks: a modern request without an `Mcp-Method` header, a `tools/call` / `prompts/get` / `resources/read` request without an `Mcp-Name` header, an `Mcp-Name` header carrying an invalid `=?base64?…?=` sentinel, and an `Mcp-Name` header whose (decoded) value disagrees with the body's `params.name` / `params.uri` are all rejected with `400 Bad Request` and JSON-RPC `-32001` (`HeaderMismatch`). The 2025-era serving paths are unchanged. |
There was a problem hiding this comment.
🟡 The PR adds new public surface to @modelcontextprotocol/core (validateStandardRequestHeaders, MCP_NAME_HEADER_SOURCE, the mcpNameHeader field, and the standard-header-validation rung — all re-exported by the core barrel and imported by createMcpHandler), but the only changeset bumps just @modelcontextprotocol/server, so core is never versioned for this change and its CHANGELOG won't document the new exports. Core is bundled into the server dist (tsdown noExternal) and is private: true, so there is no runtime/publish breakage — this is purely a changeset/changelog-completeness nit; consider adding '@modelcontextprotocol/core': minor (matching the SEP-1865 precedent in .changeset/subscriptions-listen-server.md).
Extended reasoning...
What's missing. This PR adds genuinely new public API to @modelcontextprotocol/core: the exported validateStandardRequestHeaders() function and MCP_NAME_HEADER_SOURCE table in packages/core/src/shared/inboundClassification.ts, the new mcpNameHeader field on InboundHttpRequest, and the new 'standard-header-validation' member of InboundValidationRung (with the client-capabilities/param-header-validation entries renumbered 8/9). The core barrel (packages/core/src/index.ts line 7, export * from './shared/inboundClassification.js') re-exports all of it, and packages/server/src/server/createMcpHandler.ts imports validateStandardRequestHeaders from '@modelcontextprotocol/core'. Yet the only changeset added by this PR (.changeset/sep-2243-std-header-server.md) names only '@modelcontextprotocol/server': minor — the changeset-bot summary confirms core is not in this PR's release set (server minor + dependent express/fastify/hono/node bumps only, and updateInternalDependencies: 'patch' only propagates bumps to dependents, never to dependencies).\n\nWhat this is NOT (addressing the refutation). A verifier refuted the originally-claimed failure mode — a published server resolving to a published core that lacks validateStandardRequestHeaders and failing at import time — and that refutation is correct. Two facts make that scenario impossible: (1) packages/server/package.json lists '@modelcontextprotocol/core': 'workspace:^' only under devDependencies (runtime dependencies contain only zod), and packages/server/tsdown.config.ts has noExternal: ['@modelcontextprotocol/core', ...], so core is bundled into the published server dist from the workspace source at build time; (2) packages/core/package.json is \"private\": true, so core is never published to npm at all — there is no published core release for anything to resolve against. Whatever core code exists in the repo at publish time ships inside the server bundle. This comment therefore does not claim any breakage risk; it is filed as a nit.\n\nWhat remains actionable. Core is still versioned and changelogged through changesets even though it is private — packages/core/CHANGELOG.md exists and is actively maintained, ~44 pending changesets in .changeset/ name @modelcontextprotocol/core, and the repo's convention when core gains exports consumed by a serving entry is to name core in the changeset and describe the new surface. The closest precedent, .changeset/subscriptions-listen-server.md (SEP-1865), bumps both '@modelcontextprotocol/core' and '@modelcontextprotocol/server' and explicitly lists the new core exports under "New public surface". The sibling Mcp-Param half of SEP-2243 also got its core additions documented — the companion .changeset/sep-2243-mcp-param-client.md carries '@modelcontextprotocol/core': minor — whereas this PR's core additions appear in no changeset at all, so validateStandardRequestHeaders / MCP_NAME_HEADER_SOURCE / the new ladder rung will never show up in core's CHANGELOG.\n\nStep-by-step. (1) changeset version runs against the pending changesets; this PR contributes only a server: minor entry, so core's version and CHANGELOG are untouched by this PR's changeset (core still moves because of the ~44 unrelated pending changesets, but none of them describes this PR's exports). (2) packages/core/CHANGELOG.md for the next 2.0.0-alpha.x therefore lists the SEP-1865 listen exports, the Mcp-Param codec, etc., but not validateStandardRequestHeaders or the standard-header-validation rung. (3) Anyone tracing where the new -32001 presence/Mcp-Name cells came from via core's changelog finds nothing; the server changelog entry mentions the behavior but not the new core API that other in-repo consumers (e.g. serveStdio or future entries) could pick up.\n\nFix. One line: add '@modelcontextprotocol/core': minor to .changeset/sep-2243-std-header-server.md (or add a small separate core changeset) with a sentence naming the new exports — validateStandardRequestHeaders, MCP_NAME_HEADER_SOURCE, the mcpNameHeader field on InboundHttpRequest, and the standard-header-validation ladder rung — mirroring how subscriptions-listen-server.md documents its core additions. No code change needed; non-blocking.
SEP-2243 standard-header server-side validation —
Mcp-Method/Mcp-Namepresence andMcp-Namecross-check (protocol revision 2026-07-28).Stacked on #2327.
Motivation and Context
SEP-2243 requires servers to validate the standard
Mcp-MethodandMcp-Namerequest-metadata headers against the body, and to reject a modern request whose required standard header is missing, malformed, or disagrees with the body. TheMCP-Protocol-VersionandMcp-Methodmismatch cells were already answered by the inbound classifier; this adds the presence andMcp-Namecross-check half on the same rung.How Has This Been Tested?
validateStandardRequestHeaders(16 cases, including Base64-sentinel decode, OWS, theparams.urisource, off-table methods includingObject.prototype-colliding names, and notification/legacy passes).createMcpHandler(9 cases).http-header-validationconformance scenario goes from 7 passed / 3 failed to 13 passed / 0 failed.Breaking Changes
None on the 2025-era serving paths. On the modern (2026-07-28) serving path, a hand-built HTTP request must now carry the
Mcp-Method(and where applicableMcp-Name) header; the SDK client already sends them.Types of changes
Checklist
Additional context
validateStandardRequestHeadersis kept separate fromclassifyInboundRequestso the classifier stays a pure body-primary router (a body-only call keeps routing a modern request unchanged); the entry layers the presence/Mcp-Namehalf on top inserveModern, after the supported-revision gate so an envelope naming a revision the endpoint does not serve is still answered-32004with the supported list.The
MCP_NAME_HEADER_SOURCElookup is guarded withObject.hasOwn(the body method string is peer-controlled), matching the established idiom for the client-capability table. The hand-built modern fixture requests across the server/integration/e2e suites were updated to send theMcp-Method/Mcp-Nameheaders a conformant client sends; the cell-sheet and inbound-classification core unit tests, the integration-32004cell, and theentryModerne2e arm (driven through the realStreamableHTTPClientTransport) are unchanged.