Skip to content

setMCPHeaders prefers the request context over the negotiated protocol version since #1115 #1162

Description

@gwyszynski-sa

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:

  1. 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 */)
  1. 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.

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