Skip to content

fix(server): reject a modern POST without the required MCP-Protocol-Version header - #2590

Open
davidpavlovschi wants to merge 2 commits into
modelcontextprotocol:mainfrom
davidpavlovschi:fix/require-protocol-version-header-modern-post
Open

fix(server): reject a modern POST without the required MCP-Protocol-Version header#2590
davidpavlovschi wants to merge 2 commits into
modelcontextprotocol:mainfrom
davidpavlovschi:fix/require-protocol-version-header-modern-post

Conversation

@davidpavlovschi

Copy link
Copy Markdown

Fixes #2589.

The bug

A 2026-07-28 POST whose body carries a valid _meta envelope but whose MCP-Protocol-Version header is absent dispatched and answered HTTP 200. The issue's exact repro (server/discover, mcp-method present, version header omitted) reproduces on cc4b416.

Two halves of the ladder each assumed the other covered it:

  • classifyInboundRequest treats the protocol-version header as a cross-check onlyclassifyRequestBody compares it against the envelope claim under headerVersion !== undefined, so an absent header is simply not compared.
  • validateStandardRequestHeaders (the standard-header-validation rung) checks presence of Mcp-Method and, where applicable, Mcp-Name — but not of MCP-Protocol-Version. The entry did not even pass the header into it.

The 2026-07-28 Streamable HTTP spec requires the header on every POST, and a missing required standard header must be answered 400 / HeaderMismatch (-32020).

The fix

  • validateStandardRequestHeaders rejects an absent MCP-Protocol-Version on a modern-classified request via the existing crossCheckMismatch constructor — same rung, same -32020, same HTTP 400, same { mismatch: { header, body } } data shape, request id echoed. New cell id: version-header-missing. No new error constructor, no new code.
  • createMcpHandler passes mcp-protocol-version into that call (it previously passed only mcp-method / mcp-name).
  • Rung doc/rationale updated to name the header.

Scope

The presence rung runs only on a modern route and returns early for notifications, so:

  • legacy-era POSTs (initialize, no-claim bodies) are untouched;
  • body-less GET / DELETE are still method-routed to legacy serving before any header validation;
  • a present but disagreeing header is still answered earlier, by the classifier's edge header-body-version-mismatch cell.

Test-fixture updates

Several existing in-repo fixtures built modern POSTs from the body envelope alone, without the header a conformant client sends. They now send it — in packages/server/test/server/createMcpHandler.test.ts this is one line in the existing bodyDerivedStandardHeaders helper, whose stated job is "the SEP-2243 standard headers a conformant client derives from the body it sends". Same one-line addition in the listen / capability-gate / e2e-subscriptions / e2e-hosting-entry-session / integration fixtures. No assertion was weakened.

Tests added

  • packages/core-internal/test/shared/standardHeaderValidation.test.ts: missing header rejects version-header-missing (400 / -32020); the version rung outranks the Mcp-Method presence rung; present-and-matching passes; present-and-mismatched still answered by the classifier's header-body-version-mismatch cell; a notification without the header is still never enforced.
  • packages/server/test/server/stdHeaderValidation.test.ts: end-to-end through createMcpHandler — missing header → 400 / -32020 with the id echoed; present-and-matching → 200; mismatched → 400 / -32020; a body-less GET is never header-validated. The existing legacy-untouched cell stays.

This change was developed with AI assistance; the tests listed below were run locally.

Validation run locally

Repro asserted before the change (HTTP 200) and after (HTTP 400 / -32020).

  • packages/server — 42 files, 472 tests passed
  • packages/core-internal — 69 files, 1438 tests passed
  • packages/client — 33 files, 797 tests passed
  • packages/server-legacy (11/168), packages/codemod (19/629), packages/core (1/2) — passed
  • test/integration — 19 files, 371 tests passed
  • test/e2e — 42/44 files passed; the only 2 failing files (6 tests: protocol:progress:callback, typescript:protocol:progress:token-injected, tools:call:progress, all sse arms) fail identically on unmodified cc4b416 and are unrelated to this change
  • test/conformance: test:conformance:server:2026114 passed, 0 failed (http-header-validation 13/13, http-custom-header-server-validation 9/9); test:conformance:server:all — baseline check passed
  • Lint + typecheck: packages/server, packages/core-internal, test/e2e, test/integration all clean (prettier included)

A changeset is included (core-internal + server, patch).

…ersion header

A 2026-07-28 POST carrying a valid _meta envelope but no
MCP-Protocol-Version header dispatched and answered HTTP 200: the
standard-header rung validated only Mcp-Method / Mcp-Name presence, and
the classifier cross-checks the version header only when it is present.

validateStandardRequestHeaders now rejects the absent header on the
standard-header-validation rung with the existing HeaderMismatch (-32020)
constructor on HTTP 400, and createMcpHandler passes the header through
for that check. The rung still runs only on a modern-classified request,
so notifications, legacy POSTs, and GET/DELETE are unchanged.

Fixes modelcontextprotocol#2589
@davidpavlovschi
davidpavlovschi requested a review from a team as a code owner July 30, 2026 21:10
@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4643bbc

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

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core-internal Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/codemod Patch

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 Jul 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2590

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2590

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2590

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2590

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2590

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2590

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2590

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2590

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2590

commit: 4643bbc

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.

[v2] createMcpHandler accepts modern POST without required MCP-Protocol-Version

1 participant