Skip to content

fix: enforce HTTP 400 and data.requested for per-request _meta and version errors#343

Merged
pcarleton merged 1 commit into
mainfrom
fweinberger/stateless-spec-must-fixes
Jun 16, 2026
Merged

fix: enforce HTTP 400 and data.requested for per-request _meta and version errors#343
pcarleton merged 1 commit into
mainfrom
fweinberger/stateless-spec-must-fixes

Conversation

@felixweinberger

Copy link
Copy Markdown
Collaborator

Three small, strictly spec-backed fixes around per-request _meta handling: the everything-server fixture returned its missing-_meta rejection with the wrong HTTP status and applied draft-only validation to legacy traffic, and the server-stateless scenario was not checking two members the spec requires. No error-code expectations change in this PR (those are tied to the open renumbering discussion in modelcontextprotocol/modelcontextprotocol#2907 and stay with #336).

Motivation and Context

Each change maps to a normative sentence in the current draft (2026-07-28) revision (quotes from 98c49d9):

  1. Missing required _meta fields ⇒ HTTP 400. basic/index.mdx#L313-L315:

    "A request missing any required field is malformed; the server MUST reject it with JSON-RPC error code -32602 (Invalid params). On HTTP, the response status MUST be 400 Bad Request."

    The everything-server returned this -32602 error with HTTP 200, and the sep-2575-request-meta-invalid-* checks only looked at the JSON-RPC code, so the suite could not catch the wrong status.

  2. Per-request _meta is a modern-revision requirement. basic/versioning.mdx#L34-L37:

    "Modern: protocol versions that convey version, identity, and capabilities as per-request metadata (revision 2026-07-28 and later). Legacy: protocol versions that establish a session with an initialize handshake (2025-11-25 and earlier)."

    The dual-era everything-server applied the per-request _meta validation to any sessionless request that carried an MCP-Protocol-Version header — including legacy-revision values — so a legacy client that sends the header before it has a session would be rejected with "missing _meta" for metadata its revision does not define.

  3. UnsupportedProtocolVersionError.data requires requested (and supported). schema/draft/schema.ts#L387-L402 defines data: { supported: string[]; requested: string } (both non-optional), and basic/versioning.mdx#L48-L52:

    "If the server does not implement the requested version (whether the version is unknown to the server, or is a known version the server has chosen not to support), it MUST respond with an UnsupportedProtocolVersionError listing the versions it does support"

    sep-2575-server-unsupported-version-error already validated data.supported, but never checked that data.requested is present and echoes the version the request asked for.

What changed

  • examples/servers/typescript/everything-server.ts
    • The missing-_meta-fields rejection now returns HTTP 400 (code stays -32602).
    • Sessionless requests that carry no _meta and a legacy session-era version in MCP-Protocol-Version (2024-11-05 … 2025-11-25) skip the per-request validation block and fall through to the existing session/initialize handling. Requests that carry _meta, the draft version, or an unknown version are validated exactly as before.
  • src/scenarios/server/stateless.ts
    • New companion check sep-2575-http-server-meta-invalid-400 (FAILURE severity), emitted once per _meta probe: rejections of requests missing required _meta fields must use HTTP 400. The existing sep-2575-request-meta-invalid-* checks keep their code-only meaning, mirroring how the scenario already splits error-shape vs HTTP-status checks elsewhere (e.g. sep-2575-server-unsupported-version-error / sep-2575-http-server-unsupported-version-400).
    • sep-2575-server-unsupported-version-error now also requires error.data.requested to echo the requested version.
  • src/scenarios/server/stateless.test.ts: two new negative tests — a server that returns the -32602 rejection with HTTP 200 keeps the per-field checks green but FAILs the new status check, and a server that omits data.requested from its unsupported-version error FAILs the negotiation check.

Deliberately not in this PR: any change to which JSON-RPC error codes the scenario expects. The -32602/-32001/-32004 questions interact with the error-code renumbering in modelcontextprotocol/modelcontextprotocol#2907 and remain with #336.

How Has This Been Tested?

  • npm run check, npm run build, npm test — all green (287 tests). all-scenarios.test.ts runs the draft and active suites against the modified everything-server in-process, which exercises the strengthened checks against the fixed fixture; the active (2025) scenarios are unaffected by the legacy-traffic gate because the SDK client never sends the version header before it has a session.
  • The two new negative tests prove the strengthened checks fail non-conformant servers (wrong HTTP status; missing data.requested).

Breaking Changes

None for conformant servers. Servers that return the missing-_meta rejection with a non-400 status, or an unsupported-version error without data.requested, will newly fail the corresponding checks — both are MUST/required-member violations of the draft revision — and may need an expected-failures entry until fixed.

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

  • This is the uncontroversial subset of what fix: correct server-stateless error-code expectations against the draft spec #336 touches. The remaining parts of fix: correct server-stateless error-code expectations against the draft spec #336 (which error code applies to a missing _meta/protocolVersion when a valid header is present, and HeaderMismatch vs UnsupportedProtocolVersion precedence when both rules trigger) depend on the error-code allocation/renumbering in Define error code allocation policy and renumber draft error codes modelcontextprotocol#2907 and on a precedence clarification, and are untouched here.
  • Side effect of the legacy-traffic gate worth knowing: the server-initialize scenario's raw probe (which sends a 2025-11-25 header with no session and no _meta) previously hit the _meta rejection on the everything-server and reported its session-id check as INFO; it now reaches the real initialize/session path, so that check reports SUCCESS against the fixture.
  • The fixture keeps its own LEGACY_SESSION_PROTOCOL_VERSIONS list rather than importing the suite's STATEFUL_VERSIONS because the example servers intentionally don't import from src/.
  • sep-2575-http-server-meta-invalid-400 is not added to sep-2575.yaml, matching the existing sep-2575-request-meta-invalid-* checks which aren't mapped there either; a follow-up could add rows for both halves of the missing-field requirement if we want them traced.

…rsion errors

The everything-server returned its -32602 missing-_meta rejection with
HTTP 200; the draft spec requires 400 Bad Request. It also applied the
per-request _meta validation to sessionless requests carrying a legacy
session-era version header, although per-request metadata is a
2026-07-28 requirement; such requests now fall through to the session
path.

The server-stateless scenario gains a companion check
(sep-2575-http-server-meta-invalid-400) requiring HTTP 400 on those
rejections, and sep-2575-server-unsupported-version-error now also
requires error.data.requested to echo the requested version. Two new
negative tests pin both.
@pkg-pr-new

pkg-pr-new Bot commented Jun 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@343

commit: 6e9c80f

@pkg-pr-new

pkg-pr-new Bot commented Jun 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@343

commit: 6e9c80f

@pcarleton pcarleton merged commit 1be8141 into main Jun 16, 2026
8 checks passed
@pcarleton pcarleton deleted the fweinberger/stateless-spec-must-fixes branch June 16, 2026 16:17
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.

2 participants