Skip to content

fix(everything-server): classify by the wire's markers, not by the presence of _meta - #510

Closed
po-et wants to merge 1 commit into
modelcontextprotocol:mainfrom
po-et:fix/506-session-era-meta-classification
Closed

po-et wants to merge 1 commit into
modelcontextprotocol:mainfrom
po-et:fix/506-session-era-meta-classification

Conversation

@po-et

@po-et po-et commented Sep 21, 2026

Copy link
Copy Markdown

Fixes #506.

What's wrong

The fixture decides which wire a request is on by asking whether params._meta is present:

const isLegacySessionEraRequest =
  meta === undefined && reqVersion !== undefined && LEGACY_SESSION_PROTOCOL_VERSIONS.includes(reqVersion);
if (!sessionId && (reqVersion || meta) && !isLegacySessionEraRequest) { … }

But _meta is part of the base request shape in every revision — progress tokens, trace context, anything a client attaches — so a session-era request that carries it is routed to the 2026-07-28 path and rejected for metadata it never owed. Measured on 7169291:

initialize 2025-11-25 + "_meta": {}                      -> 400  -32020 Missing MCP-Protocol-Version header
initialize 2025-11-25 + "_meta": {} + era header         -> 400  -32602 Invalid params: missing _meta or required fields
initialize 2025-11-25 + "_meta": {"progressToken": "p1"} -> 400  -32602 (same line)
initialize 2025-11-25, no _meta                          -> 200

The second and third are the same root cause as the reported one, reached through the other branch. MCP Python SDK 2.x sends _meta: {} on initialize, which is what makes this fixture unusable as its upstream server.

What this changes

Two markers identify the request-meta wire, and neither is "there is a _meta key":

  • a MCP-Protocol-Version header naming a post-session revision, and
  • the per-request metadata envelope, whose own marker is io.modelcontextprotocol/protocolVersion inside _meta.

Classification uses those. Session-era traffic keeps the session path whether or not it carries _meta.

What did NOT change

The rejections the envelope exists to trigger still fire — a request-meta-era request that omits the header still answers -32020, and an envelope missing its protocol version still answers -32602:

server/discover + full envelope, no header               -> 400  -32020
server/discover + header, envelope without protocolVersion -> 400  -32602
server/discover + header + full envelope                 -> 200

Nothing in the suite depended on the old rule: every scenario probe builds its request through buildStandardHeaders, which always sets MCP-Protocol-Version (default DRAFT_PROTOCOL_VERSION), so the _meta-integrity probes in stateless.ts reach the modern path by header and keep failing as they should.

Verification

  • all-scenarios.test.ts: 55/55 before and after.
  • npm test: 627/627. npm run check: clean.
  • New vitest case (everything-server-request-classification.test.ts) pins both halves — the two session-era requests that were rejected, and the three request-meta-era outcomes that must not move.
  • Mutation-checked: reverting to presence-of-_meta fails 2 of the 5; dropping the envelope signal fails the -32020 case; treating a session-era header as modern fails the progress-token case.

…esence of _meta

`_meta` belongs to the base request shape in every revision — progress
tokens, trace context, whatever a client attaches — so the fixture cannot
read its presence as "2026-07-28 traffic". It did, and session-era requests
that carry it were rejected instead of served:

  initialize (2025-11-25) + `_meta: {}`, no header  -> 400 -32020 Missing MCP-Protocol-Version header
  initialize (2025-11-25) + `_meta: {}`, era header -> 400 -32602 Invalid params: missing _meta or required fields

MCP Python SDK 2.x sends `_meta: {}` on `initialize`, which is what makes
the fixture unusable as its upstream server (modelcontextprotocol#506).

Two markers identify the request-meta wire: a `MCP-Protocol-Version` header
naming a post-session revision, and the per-request metadata envelope,
whose own marker is `io.modelcontextprotocol/protocolVersion` inside
`_meta`. Classify on those. Session-era traffic keeps the session path
whether or not it carries `_meta`, and the rejections the envelope exists to
trigger are untouched: a request-meta-era request that omits the header
still answers -32020, and an envelope missing its protocol version still
answers -32602.

Every scenario probe reaches the modern path through `buildStandardHeaders`,
which always sets `MCP-Protocol-Version`, so the 55 server scenarios are
unaffected — verified, plus a vitest case pinning both halves.
@po-et

po-et commented Sep 23, 2026

Copy link
Copy Markdown
Author

Closing in favour of #507 — @lucarlig opened it three days before this, as the reporter, and it fixes the same thing. It routes a session-era initialize to the session path by its params.protocolVersion, which also covers the -32602 variant (session-era header plus _meta) I added here. I missed #507 when I checked the issue; sorry for the noise.

@po-et po-et closed this Sep 23, 2026
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.

Everything-server rejects valid _meta on stateful initialize

1 participant