Skip to content

Discussion: Where should MCP protocol version negotiation happen? #1861

@ketema

Description

@ketema

Discussion: Where should MCP protocol version negotiation happen?

Related Issues

Context

Multiple major MCP clients send comma-separated values in the mcp-protocol-version HTTP header:

Client Observed Header Value
Claude Code 2025-11-25, 2025-06-18
Codex 2025-11-25, 2025-06-18
Gemini 2025-11-25, 2025-06-18

The Python SDK's StreamableHTTPServerTransport._validate_protocol_version() treats this as a literal string and returns 400 Bad Request, even when supported versions are present in the list.

This raises a fundamental question: Is this a server bug, a client bug, or a spec ambiguity?


The Case FOR Comma-Separated Parsing (Server Should Be Lenient)

  1. Multiple major clients do it — Claude, Codex, and Gemini all send comma-separated versions. This suggests either a common interpretation or an undocumented convention.

  2. HTTP precedent — RFC 7230 allows comma-separated values for content negotiation (e.g., Accept-Language: en-US, en;q=0.9). Clients may be applying this pattern.

  3. Defensive interoperability — Servers that parse comma-separated headers will work with more clients, regardless of who's "correct."

  4. The initialize request succeeds — Clients send a single version initially, negotiation completes, but then subsequent requests fail. This creates a confusing UX where connection works, then breaks.


The Case AGAINST Comma-Separated Parsing (Clients Should Send One Version)

  1. Spec uses singular form — From MCP Specification 2025-03-26:

    MCP-Protocol-Version: <protocol-version>

    Note: <protocol-version> is singular, not <protocol-versions>.

  2. Spec example shows single version:

    MCP-Protocol-Version: 2025-11-25
    
  3. "The one negotiated" — Spec says:

    "The protocol version sent by the client SHOULD be the one negotiated during initialization."

    Key phrase: "the one" (singular).

  4. Negotiation already happened — Version negotiation occurs in the JSON body during initialize:

    {"method":"initialize","params":{"protocolVersion":"2025-11-25",...}}

    The HTTP header is for AFTER negotiation — the client already knows THE agreed version.

  5. Simplicity principle — Why add parsing complexity for a header that, per spec, should only ever contain one value?


The Core Question

Where is protocol version negotiation supposed to happen?

Interpretation Negotiation Location HTTP Header Purpose
A: Header-based HTTP header (like Accept-Language) Client offers versions, server picks
B: Body-based JSON body during initialize Client sends THE negotiated version

The spec appears to describe Interpretation B, but three major clients implement Interpretation A.


Evidence: Network Capture

Captured from a real MCP session:

Request 1: Initialize (SUCCESS)

POST /mcp HTTP/1.1
mcp-protocol-version: 2025-06-18
Content-Type: application/json

{"method":"initialize","params":{"protocolVersion":"2025-11-25",...},"jsonrpc":"2.0","id":0}

Request 2: Subsequent Request (FAILURE)

POST /mcp HTTP/1.1
mcp-protocol-version: 2025-11-25, 2025-06-18
mcp-session-id: <REDACTED>
Content-Type: application/json

{"method":"notifications/initialized","jsonrpc":"2.0"}
HTTP/1.1 400 Bad Request

{"jsonrpc":"2.0","error":{"code":-32600,"message":"Bad Request: Unsupported protocol version: 2025-11-25, 2025-06-18. Supported versions: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25"}}

Note the irony: Both 2025-11-25 and 2025-06-18 ARE in the supported list.


Questions for Discussion

  1. Is the spec intentionally singular? Should the HTTP header only ever contain one version (the negotiated one)?

  2. Should servers be lenient? Even if clients are "wrong," should servers parse comma-separated values defensively?

  3. Why do multiple clients send comma-separated? Is there an undocumented convention, or are they all misinterpreting the spec?

  4. Is every server expected to implement negotiation logic in initialize? Or should the HTTP header serve as a fallback negotiation mechanism?

  5. What's the intended behavior when a client reconnects? Should it send the previously negotiated version, or re-offer multiple versions?


Possible Resolutions

Resolution Action
Spec clarification Update spec to explicitly state singular vs comma-separated
Server leniency SDK parses comma-separated defensively (proposed fix available)
Client fix Claude/Codex/Gemini should send only the negotiated version
Both Clarify spec AND make servers lenient for backward compatibility

Environment

  • MCP Python SDK: v1.25.0 (latest as of 2026-01-15)
  • Observed Clients: Claude Code v2.1.7, Codex, Gemini
  • Transport: Streamable HTTP

I'd appreciate maintainer perspective on the intended design. Happy to submit a PR for server-side comma parsing if that's the desired direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions