Describe the bug
The legacy initialize handshake pins the version it proposes into the body but sends no Mcp-Protocol-Version header of its own (client.go#L376-L386):
// Use the latest legacy protocol version for the fallback initialize.
protocolVersion = protocolVersion20251125
...
params := &InitializeParams{ProtocolVersion: protocolVersion, ...}
req := &InitializeRequest{Session: cs, Params: params}
res, err := handleSend[*InitializeResult](ctx, methodInitialize, req)
handleSend gets the plain ctx, not the discoverCtx used for the probe above it. By the time setMCPHeaders runs, c.initializedResult is still nil — it is assigned about six lines later — and the message carries no _meta.protocolVersion, so the header is decided by whatever the request context happens to hold.
For a standalone client that context is empty and the header is absent. For a process that is both an MCP server and an MCP client, the server transport has recorded its inbound request's version on that context (streamable.go#L375) and that value goes out instead, contradicting the body it accompanies.
To Reproduce
Steps to reproduce the behavior:
- Run a Go SDK Streamable HTTP client against a server that does not implement
server/discover, so it falls back to the legacy initialize.
- Observe the header on that
initialize request: absent for a standalone client, or the inbound version if the caller's context carries protocolVersionContextKey.
The same disagreement can be produced directly against a Go SDK server, as in #963:
curl -i -sS -X POST "$ENDPOINT" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"repro","version":"0.1.0"}}}'
What did you see
initialize body="2025-11-25" header="" (standalone client)
initialize body="2025-11-25" header="2026-07-28" (server+client process)
What did you expect to see
The header stating the version the request proposes, matching params.protocolVersion, and never inherited from ambient context:
initialize body="2025-11-25" header="2025-11-25"
Omitting the header is the other reasonable option, and is what the package doc for the header currently describes ("Is omitted before initialization completes", streamable_client.go). Sending it seems preferable: it agrees with the body, it is what a server validating the header expects, and it removes the dependency on the context being empty rather than relying on it. Happy to switch the fix to omission if maintainers prefer that reading.
Logs
An upstream that validates the header rejects the handshake outright:
400 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)
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
This is the client-side mirror of #963, which tightened the server against exactly this mismatch — the SDK's server now rejects a disagreement its own client can still produce.
Separate from #1162: fixing the precedence there does not fix this, because initializedResult is nil at this point regardless of ordering, so control still reaches the context branch.
The fake server in streamable_client_test.go asserts wantProtocolVersion for other methods but for none of its sixteen initialize cases, so neither an absent nor a contradicting header is currently covered.
I have a fix and have opened #1165.
Describe the bug
The legacy
initializehandshake pins the version it proposes into the body but sends noMcp-Protocol-Versionheader of its own (client.go#L376-L386):handleSendgets the plainctx, not thediscoverCtxused for the probe above it. By the timesetMCPHeadersruns,c.initializedResultis still nil — it is assigned about six lines later — and the message carries no_meta.protocolVersion, so the header is decided by whatever the request context happens to hold.For a standalone client that context is empty and the header is absent. For a process that is both an MCP server and an MCP client, the server transport has recorded its inbound request's version on that context (streamable.go#L375) and that value goes out instead, contradicting the body it accompanies.
To Reproduce
Steps to reproduce the behavior:
server/discover, so it falls back to the legacyinitialize.initializerequest: absent for a standalone client, or the inbound version if the caller's context carriesprotocolVersionContextKey.The same disagreement can be produced directly against a Go SDK server, as in #963:
What did you see
What did you expect to see
The header stating the version the request proposes, matching
params.protocolVersion, and never inherited from ambient context:Omitting the header is the other reasonable option, and is what the package doc for the header currently describes ("Is omitted before initialization completes", streamable_client.go). Sending it seems preferable: it agrees with the body, it is what a server validating the header expects, and it removes the dependency on the context being empty rather than relying on it. Happy to switch the fix to omission if maintainers prefer that reading.
Logs
An upstream that validates the header rejects the handshake outright:
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
This is the client-side mirror of #963, which tightened the server against exactly this mismatch — the SDK's server now rejects a disagreement its own client can still produce.
Separate from #1162: fixing the precedence there does not fix this, because
initializedResultis nil at this point regardless of ordering, so control still reaches the context branch.The fake server in
streamable_client_test.goassertswantProtocolVersionfor other methods but for none of its sixteeninitializecases, so neither an absent nor a contradicting header is currently covered.I have a fix and have opened #1165.