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
streamableClientConn a 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.
Describe the bug
streamableClientConn.setMCPHeadersresolvesMcp-Protocol-Versionfrom the request context beforec.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 -> contextand stated it in its description ("falling back toc.initializedResultand then the request context"); the code it replaced also consulted the context last. The two branches were transposed three days later by theswitch-to-ifrewrite in #1115, whose subject was an unrelated nil-pointer check:initializedResult-> contextinitializedResult-> contextinitializedResultTo Reproduce
Steps to reproduce the behavior:
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:req.Header.Get("Mcp-Protocol-Version").What did you see
The header carries the context's version,
2026-07-28, rather than the2025-11-25the connection negotiated.End to end, in a proxy: an agent speaking
2026-07-28connects, the proxy's own upstream negotiated2025-11-25, and every proxiedtools/callis 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, soc.initializedResultshould outrank the context, as it did before #1115._meta.protocolVersionfrom the outgoing message should stay on top so that #1109 remains fixed.Logs
The upstream rejection this produces:
What version of the Go MCP SDK are you using
v1.7.0, andmainat 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/discoverprobe, 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.goL489, L1060, L1167, L1260, L1458, L1529, L1912) and is meaningless to a client leg.Why no existing test catches it:
TestStreamableClientConnSetMCPHeaders_ProtocolVersionbuilds its request withhttp.NewRequest, whose context never carriesprotocolVersionContextKey, soprotocolVersionFromContextreturns""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
initializethere is no negotiated version yet, so control reaches the context branch regardless of ordering.I have a fix and have opened #1163.