-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
Discussion: Where should MCP protocol version negotiation happen?
Related Issues
- Related to Support multiple MCP protocol versions #1746 (Support multiple MCP protocol versions)
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.