From babfbb3f803ebad0c5031cc440f6ba55dc393879 Mon Sep 17 00:00:00 2001 From: Grzegorz Wyszynski Date: Thu, 13 Aug 2026 13:58:39 +0200 Subject: [PATCH] mcp: send the proposed protocol version in the initialize header The legacy initialize handshake pins params.protocolVersion but sends no Mcp-Protocol-Version header of its own: setMCPHeaders finds c.initializedResult still nil, since it is assigned a few lines later, and the message carries no _meta.protocolVersion, so the header falls to the request context. For a standalone client that context is empty and the header is absent; for a process that is both a server and a client it holds that process's inbound version, which then contradicts the body it accompanies. This is the client-side mirror of #963. Declares the proposed version on the context of the initialize send, the same way the server/discover probe immediately above already does, so the header always states what the body proposes. The fake server asserted wantProtocolVersion for other methods but for none of its sixteen initialize cases, so neither an absent nor a contradicting header was covered. Asserts header/body agreement centrally instead, since it holds for every initialize unconditionally. Fixes #1164 --- mcp/client.go | 5 ++++- mcp/streamable_client_test.go | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/mcp/client.go b/mcp/client.go index 74037990..fff196f3 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -383,7 +383,10 @@ func (c *Client) Connect(ctx context.Context, t Transport, opts *ClientSessionOp Capabilities: c.capabilities(protocolVersion), } req := &InitializeRequest{Session: cs, Params: params} - res, err := handleSend[*InitializeResult](ctx, methodInitialize, req) + // The header must agree with params.protocolVersion: no version has been + // negotiated yet, so nothing else identifies what this request proposes. + initializeCtx := context.WithValue(ctx, protocolVersionContextKey{}, protocolVersion) + res, err := handleSend[*InitializeResult](initializeCtx, methodInitialize, req) if err != nil { _ = cs.Close() return nil, err diff --git a/mcp/streamable_client_test.go b/mcp/streamable_client_test.go index a5957bf6..9c519b7f 100644 --- a/mcp/streamable_client_test.go +++ b/mcp/streamable_client_test.go @@ -145,6 +145,18 @@ func (s *fakeStreamableServer) ServeHTTP(w http.ResponseWriter, req *http.Reques if v := req.Header.Get(protocolVersionHeader); v != resp.wantProtocolVersion && resp.wantProtocolVersion != "" { s.t.Errorf("%v: bad protocol version header: got %q, want %q", key, v, resp.wantProtocolVersion) } + // On initialize no version has been negotiated yet, so the header carries + // what the request proposes and must agree with the body. Checked for every + // initialize rather than per-response, since it holds unconditionally. + if key.jsonrpcMethod == methodInitialize && jsonrpcReq != nil { + var params InitializeParams + if err := json.Unmarshal(jsonrpcReq.Params, ¶ms); err != nil { + s.t.Errorf("%v: unmarshal initialize params: %v", key, err) + } else if got := req.Header.Get(protocolVersionHeader); got != params.ProtocolVersion { + s.t.Errorf("%v: protocol version header %q disagrees with body protocolVersion %q", + key, got, params.ProtocolVersion) + } + } w.Write([]byte(body)) rc.Flush() // flush response