Repository navigation
Description
Describe the bug
streamableClientConn.setMCPHeaders resolves Mcp-Protocol-Version from the request context before c.initializedResult (streamable.go#L2447-L2453), so a connection sends a version other than the one it negotiated.
This appears unintended. #1107 established the order message -> initializedResult -> context and stated it in its description ("falling back to c.initializedResult and then the request context"); the code it replaced also consulted the context last. The two branches were transposed three days later by the switch-to-if rewrite in #1115, whose subject was an unrelated nil-pointer check:
- switch {
- case protocolVersionFromMessage(msg) != "":
- req.Header.Set(protocolVersionHeader, protocolVersionFromMessage(msg))
- case c.initializedResult != nil:
+ if pv := protocolVersionFromMessage(msg); pv != "" {
+ req.Header.Set(protocolVersionHeader, pv)
+ } else if pv := protocolVersionFromContext(req.Context()); pv != "" {
+ req.Header.Set(protocolVersionHeader, pv)
+ } else if c.initializedResult != nil {
req.Header.Set(protocolVersionHeader, c.initializedResult.ProtocolVersion)
- case protocolVersionFromContext(req.Context()) != "":
- req.Header.Set(protocolVersionHeader, protocolVersionFromContext(req.Context()))
}| order | |
|---|---|
| before #1107 | initializedResult -> context |
| #1107 (Jul 24) | message -> initializedResult -> context |
| #1115 (Jul 27) | message -> context -> initializedResult |
| main today | unchanged since #1115 |
To Reproduce
Steps to reproduce the behavior:
- Give a
streamableClientConna negotiated version, and put a different version on the outgoing request's context — which is what the server transport leaves there at streamable.go#L375 for a process that is both an MCP server and an MCP client:
conn := &streamableClientConn{
initializedResult: &InitializeResult{ProtocolVersion: protocolVersion20251125},
}
req, _ := http.NewRequest(http.MethodPost, "http://example.invalid", nil)
req = req.WithContext(context.WithValue(req.Context(), protocolVersionContextKey{}, protocolVersion20260728))
conn.setMCPHeaders(req, /* a request carrying no _meta.protocolVersion */)- Read back
req.Header.Get("Mcp-Protocol-Version").
What did you see
The header carries the context's version, 2026-07-28, rather than the 2025-11-25 the connection negotiated.
End to end, in a proxy: an agent speaking 2026-07-28 connects, the proxy's own upstream negotiated 2025-11-25, and every proxied tools/call is rejected by that upstream.
What did you expect to see
Mcp-Protocol-Version: 2025-11-25 — the version this connection negotiated. Once negotiation has happened it is the authoritative version for the connection, so c.initializedResult should outrank the context, as it did before #1115. _meta.protocolVersion from the outgoing message should stay on top so that #1109 remains fixed.
Logs
The upstream rejection this produces:
400 Bad Request: {"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Unsupported protocol version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)"},"id":null}
What version of the Go MCP SDK are you using
v1.7.0, and main at 64e454e.
What version of Go are you using
go version go1.26.5 darwin/arm64. Not Go-version dependent.
Additional context
The context branch exists for the pre-negotiation server/discover probe, which writes its own value at client.go#L323 and again on the negotiation retry at #L367. Those are the only client-side writers. The other writer is the server transport, whose value is meant for the server-side readers (streamable.go L489, L1060, L1167, L1260, L1458, L1529, L1912) and is meaningless to a client leg.
Why no existing test catches it: TestStreamableClientConnSetMCPHeaders_ProtocolVersion builds its request with http.NewRequest, whose context never carries protocolVersionContextKey, so protocolVersionFromContext returns "" in every case and the two orderings are indistinguishable to the table. The suite passes under either.
Fixing this alone does not fix the related problem in #1164: on initialize there is no negotiated version yet, so control reaches the context branch regardless of ordering.
I have a fix and have opened #1163.
Activity
mcp: prefer the negotiated protocol version over the request context
mcp: prefer the negotiated protocol version over the request context (m…
A gateway-proxied call carries the upstream's protocol revision, not …
mcp: clear the caller's protocol version for the initialize handshake
mcp: clear the caller's protocol version for the initialize handshake (…