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)
-
Multiple major clients do it — Claude, Codex, and Gemini all send comma-separated versions. This suggests either a common interpretation or an undocumented convention.
-
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.
-
Defensive interoperability — Servers that parse comma-separated headers will work with more clients, regardless of who's "correct."
-
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)
-
Spec uses singular form — From MCP Specification 2025-03-26:
MCP-Protocol-Version: <protocol-version>
Note: <protocol-version> is singular, not <protocol-versions>.
-
Spec example shows single version:
MCP-Protocol-Version: 2025-11-25
-
"The one negotiated" — Spec says:
"The protocol version sent by the client SHOULD be the one negotiated during initialization."
Key phrase: "the one" (singular).
-
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.
-
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
-
Is the spec intentionally singular? Should the HTTP header only ever contain one version (the negotiated one)?
-
Should servers be lenient? Even if clients are "wrong," should servers parse comma-separated values defensively?
-
Why do multiple clients send comma-separated? Is there an undocumented convention, or are they all misinterpreting the spec?
-
Is every server expected to implement negotiation logic in initialize? Or should the HTTP header serve as a fallback negotiation mechanism?
-
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.
Discussion: Where should MCP protocol version negotiation happen?
Related Issues
Context
Multiple major MCP clients send comma-separated values in the
mcp-protocol-versionHTTP header:2025-11-25, 2025-06-182025-11-25, 2025-06-182025-11-25, 2025-06-18The 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)
Multiple major clients do it — Claude, Codex, and Gemini all send comma-separated versions. This suggests either a common interpretation or an undocumented convention.
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.Defensive interoperability — Servers that parse comma-separated headers will work with more clients, regardless of who's "correct."
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)
Spec uses singular form — From MCP Specification 2025-03-26:
Note:
<protocol-version>is singular, not<protocol-versions>.Spec example shows single version:
"The one negotiated" — Spec says:
Key phrase: "the one" (singular).
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.
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?
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)
Request 2: Subsequent Request (FAILURE)
Note the irony: Both
2025-11-25and2025-06-18ARE in the supported list.Questions for Discussion
Is the spec intentionally singular? Should the HTTP header only ever contain one version (the negotiated one)?
Should servers be lenient? Even if clients are "wrong," should servers parse comma-separated values defensively?
Why do multiple clients send comma-separated? Is there an undocumented convention, or are they all misinterpreting the spec?
Is every server expected to implement negotiation logic in
initialize? Or should the HTTP header serve as a fallback negotiation mechanism?What's the intended behavior when a client reconnects? Should it send the previously negotiated version, or re-offer multiple versions?
Possible Resolutions
Environment
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.