Skip to content

initialize sends no Mcp-Protocol-Version of its own, so the header can contradict params.protocolVersion #1164

Description

@gwyszynski-sa

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:

  1. Run a Go SDK Streamable HTTP client against a server that does not implement server/discover, so it falls back to the legacy initialize.
  2. 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.

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