Skip to content

fix(server): route pre-2026 _meta protocol-version claims to legacy serving - #3160

Open
tomwangcn wants to merge 2 commits into
modelcontextprotocol:mainfrom
tomwangcn:fix/legacy-envelope-claim-routing
Open

tomwangcn wants to merge 2 commits into
modelcontextprotocol:mainfrom
tomwangcn:fix/legacy-envelope-claim-routing

Conversation

@tomwangcn

Copy link
Copy Markdown

Fixes #3159.

Problem

On a dual-era Streamable HTTP entry (createMcpHandler with legacy serving, e.g. the default legacy: 'stateless'), a non-initialize request whose params._meta carries a pre-2026 io.modelcontextprotocol/protocolVersion claim was routed to the modern path and rejected:

  • full envelope claim → 400 -32022 "Unsupported protocol version: 2025-06-18" — even though the endpoint serves 2025-06-18 fine through its legacy leg;
  • version-key-only claim → 400 -32602, because validateEnvelopeMeta applies the 2026-07-28 envelope schema to a claim that names a 2025 revision;
  • notifications and batch elements carrying such a claim hit the same wall (-32022, and -32600 via batch-with-modern-element).

Reproduced on current main (2.4.0) with the exact requests from the issue before writing the fix.

Fix

classifyRequestBody and classifyNotificationBody now classify a claim naming a pre-2026 revision as legacy-era traffic — the same precedence initialize already gets via carriesValidModernEnvelopeClaim — with requestedVersion set to the claimed revision, and skip the modern envelope validation for those claims (that schema belongs to the modern revision). classifyBatch keeps arrays whose elements claim pre-2026 revisions as legacy batches.

Deliberately unchanged:

  • claims naming a modern revision, including the malformed-envelope -32602 rejection;
  • a MCP-Protocol-Version header that disagrees with the claim → -32020;
  • modern-only endpoints (legacy: 'reject') still answer 2025-era claims with -32022 naming the supported revisions, via modernOnlyStrictRejection.

Note: this flips one pinned classifier cell, which previously routed legacy-claimed requests to the modern leg "rather than being silently re-routed". The issue argues that behavior breaks dual-era interop (the endpoint rejects a version it actually serves); the mismatch handoff is preserved where it is the only answer available, on modern-only endpoints. Happy to reshape if you prefer a different split.

Tests

  • classifier: legacy-claim request and notification route legacy with requestedVersion; a legacy claim is not validated against the modern envelope schema; a disagreeing header is still -32020; a batch with a legacy-claiming element stays legacy while a malformed claim is still rejected.
  • entry: dual-era handler serves the full claim (200), the version-only claim (200), a claimed notification (202) and a claimed batch element; a modern-only endpoint still answers -32022.
  • Full core-internal (1855) and server (679) suites pass; typecheck clean; changeset included.

…erving

A non-initialize request whose params._meta carries a pre-2026
io.modelcontextprotocol/protocolVersion claim was routed to serveModern
and rejected: -32022 for a full envelope, -32602 for a version-only
claim, because handle() dispatches on outcome.kind and the envelope was
validated against the 2026-07-28 schema. On a dual-era endpoint this
breaks 2025-era clients the legacy leg would serve fine.

classifyRequestBody and classifyNotificationBody now classify a claim
naming a pre-2026 revision as legacy traffic (requestedVersion keeps the
claimed revision, so modern-only endpoints still answer -32022), and
skip the modern envelope validation for it. classifyBatch keeps arrays
with legacy-claiming elements as legacy batches; modern-claiming and
malformed claims are still rejected, and a disagreeing
MCP-Protocol-Version header is still a -32020 mismatch.

Fixes modelcontextprotocol#3159
@tomwangcn
tomwangcn requested a review from a team as a code owner November 7, 2026 19:41
@changeset-bot

changeset-bot Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d90a641

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

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/server Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/core-internal 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 Nov 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: d90a641

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

The changeset is not parseable at this head. .changeset/legacy-envelope-claim-routing.md starts directly with '@modelcontextprotocol/server': patch, so the opening --- delimiter is missing; the changeset bot consequently reports invalid frontmatter and cannot determine the package/version bump.

It should start with:

---
'@modelcontextprotocol/server': patch
---

The routing tests and hosted matrix are otherwise green at 9df5f546.

@tomwangcn

tomwangcn commented Nov 8, 2026 •

Copy link
Copy Markdown
Author

Good catch, thanks - the opening --- fence was indeed missing (the changeset status parser only saw the trailing delimiter). Fixed in d90a641; changeset status now parses the file and bumps @modelcontextprotocol/server at patch.

@koriyoshi2041

Copy link
Copy Markdown

Confirmed on d90a641: the opening fence is present, Changesets now parses the server patch entry, and the refreshed build/test/E2E/runtime/conformance matrix is green. Thanks for the quick fix.

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Nov 8, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP: pre-2026 _meta protocolVersion claim on non-initialize requests is routed to serveModern and rejected with 400

2 participants