Skip to content

feat: SEP-2243 standard half — std-header server validation; http-header-validation 13/0#2328

Open
felixweinberger wants to merge 8 commits into
fweinberger/sep2243-customfrom
fweinberger/sep2243-std
Open

feat: SEP-2243 standard half — std-header server validation; http-header-validation 13/0#2328
felixweinberger wants to merge 8 commits into
fweinberger/sep2243-customfrom
fweinberger/sep2243-std

Conversation

@felixweinberger

@felixweinberger felixweinberger commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

SEP-2243 standard-header server-side validation — Mcp-Method/Mcp-Name presence and Mcp-Name cross-check (protocol revision 2026-07-28).

Stacked on #2327.

Motivation and Context

SEP-2243 requires servers to validate the standard Mcp-Method and Mcp-Name request-metadata headers against the body, and to reject a modern request whose required standard header is missing, malformed, or disagrees with the body. The MCP-Protocol-Version and Mcp-Method mismatch cells were already answered by the inbound classifier; this adds the presence and Mcp-Name cross-check half on the same rung.

How Has This Been Tested?

  • Unit corpus for validateStandardRequestHeaders (16 cases, including Base64-sentinel decode, OWS, the params.uri source, off-table methods including Object.prototype-colliding names, and notification/legacy passes).
  • Over-HTTP corpus through createMcpHandler (9 cases).
  • The published http-header-validation conformance scenario goes from 7 passed / 3 failed to 13 passed / 0 failed.
  • Full workspace test suite green, e2e matrix unchanged at 2544 + 205 xfail.

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 applicable Mcp-Name) header; the SDK client already sends them.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

validateStandardRequestHeaders is kept separate from classifyInboundRequest so the classifier stays a pure body-primary router (a body-only call keeps routing a modern request unchanged); the entry layers the presence/Mcp-Name half on top in serveModern, after the supported-revision gate so an envelope naming a revision the endpoint does not serve is still answered -32004 with the supported list.

The MCP_NAME_HEADER_SOURCE lookup is guarded with Object.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 the Mcp-Method/Mcp-Name headers a conformant client sends; the cell-sheet and inbound-classification core unit tests, the integration -32004 cell, and the entryModern e2e arm (driven through the real StreamableHTTPClientTransport) are unchanged.

@felixweinberger felixweinberger requested a review from a team as a code owner June 18, 2026 22:11
@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9e9ede2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@modelcontextprotocol/server Minor
@modelcontextprotocol/express Major
@modelcontextprotocol/fastify Major
@modelcontextprotocol/hono Major
@modelcontextprotocol/node Major

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@2328

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/codemod@2328

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@2328

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server-legacy@2328

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@2328

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/fastify@2328

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@2328

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@2328

commit: 9e9ede2

@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from 17f10d2 to 98117b6 Compare June 18, 2026 22:15
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-custom branch from 507001d to 08c9f8b Compare June 18, 2026 22:15
Comment thread packages/core/src/shared/inboundClassification.ts
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from 98117b6 to 356eac6 Compare June 19, 2026 10:06
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-custom branch from 08c9f8b to e12c5d9 Compare June 19, 2026 10:06
Comment thread packages/core/src/shared/inboundClassification.ts Outdated
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from 356eac6 to d85240a Compare June 19, 2026 12:17
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-custom branch from 43ffc63 to 87ce6c5 Compare June 19, 2026 12:19
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch 2 times, most recently from bf03f18 to 78b289d Compare June 19, 2026 12:53
Comment thread packages/core/src/shared/inboundClassification.ts
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from 78b289d to 5779aab Compare June 19, 2026 13:26
Comment thread packages/core/src/shared/inboundClassification.ts Outdated
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-custom branch from 3678bf9 to 1bed404 Compare June 19, 2026 13:50
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from ea9fe3a to a085951 Compare June 19, 2026 13:51
Comment thread packages/server/test/server/stdHeaderValidation.test.ts
Comment thread packages/server/src/server/createMcpHandler.ts
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from 1e780e2 to 6e8a521 Compare June 19, 2026 14:24
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-custom branch from 6bf980e to ce53141 Compare June 19, 2026 16:46
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from 6e8a521 to 0de8fe9 Compare June 19, 2026 16:46

@claude claude 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.

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.

@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from 0de8fe9 to a271dc7 Compare June 19, 2026 17:23
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-custom branch from ce53141 to f6680bd Compare June 19, 2026 17:23
Comment thread packages/core/src/shared/inboundClassification.ts
…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.
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-std branch from a271dc7 to 9e9ede2 Compare June 22, 2026 11:41
@felixweinberger felixweinberger force-pushed the fweinberger/sep2243-custom branch from f6680bd to 5d68be8 Compare June 22, 2026 11:41
Comment on lines +1 to +5
---
'@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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

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.

1 participant