Skip to content

Commit 2781b8f

Browse files
jan-xyzcharmcrush
andauthored
mcp: derive Mcp-Protocol-Version header from the outgoing message (#1107)
`streamableClientConn.setMCPHeaders` sets `Mcp-Protocol-Version` only from `c.initializedResult`, which is populated exclusively through the unexported `clientConnection.sessionUpdated` hook. `Client.Connect` reaches that hook via a type assertion on the `Connection` returned by the `Transport`: ```go if hc, ok := cs.mcpConn.(clientConnection); ok { hc.sessionUpdated(cs.state) } ``` That assertion silently fails whenever the `Connection` is wrapped by another type that exposes only the exported `Connection` interface. `c.initializedResult` then stays `nil` for the life of the session, and every `SEP-2575` (>= `2026-07-28`) request sent through that connection is missing the header. Servers that validate the header per spec reject the request with `400 Bad Request`, closing the connection. Every SEP-2575 request already carries its own `_meta.protocolVersion` field via `injectRequestMeta`. `setMCPHeaders` now reads the protocol version from the outgoing message first, falling back to `c.initializedResult` and then the request context. This is currently breaking the GitHub MCP with the 1.7-pre releases of this module. Fixes #1109 Co-authored-by: eu.anthropic.claude-sonnet-5 via Crush <charmcrush@users.noreply.github.com>
1 parent 827f90b commit 2781b8f

2 files changed

Lines changed: 97 additions & 7 deletions

File tree

mcp/streamable.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,7 +2249,7 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e
22492249
req.Header.Set("Content-Type", "application/json")
22502250
req.Header.Set("Accept", "application/json, text/event-stream")
22512251

2252-
if err := c.setMCPHeaders(req); err != nil {
2252+
if err := c.setMCPHeaders(req, msg); err != nil {
22532253
// Failure to set headers means that the request was not sent.
22542254
// Wrap with ErrRejected so the jsonrpc2 connection doesn't set writeErr
22552255
// and permanently break the connection.
@@ -2373,7 +2373,7 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e
23732373
return nil
23742374
}
23752375

2376-
func (c *streamableClientConn) setMCPHeaders(req *http.Request) error {
2376+
func (c *streamableClientConn) setMCPHeaders(req *http.Request, msg jsonrpc.Message) error {
23772377
c.mu.Lock()
23782378
defer c.mu.Unlock()
23792379

@@ -2403,10 +2403,13 @@ func (c *streamableClientConn) setMCPHeaders(req *http.Request) error {
24032403
}
24042404
}
24052405
}
2406-
if c.initializedResult != nil {
2406+
switch {
2407+
case protocolVersionFromMessage(msg) != "":
2408+
req.Header.Set(protocolVersionHeader, protocolVersionFromMessage(msg))
2409+
case c.initializedResult != nil:
24072410
req.Header.Set(protocolVersionHeader, c.initializedResult.ProtocolVersion)
2408-
} else if v := protocolVersionFromContext(req.Context()); v != "" {
2409-
req.Header.Set(protocolVersionHeader, v)
2411+
case protocolVersionFromContext(req.Context()) != "":
2412+
req.Header.Set(protocolVersionHeader, protocolVersionFromContext(req.Context()))
24102413
}
24112414
if c.sessionID != "" {
24122415
req.Header.Set(sessionIDHeader, c.sessionID)
@@ -2415,6 +2418,23 @@ func (c *streamableClientConn) setMCPHeaders(req *http.Request) error {
24152418
return nil
24162419
}
24172420

2421+
// protocolVersionFromMessage recovers the SEP-2575 `_meta.protocolVersion`
2422+
// value from an outgoing JSON-RPC request, if present. It returns "" for
2423+
// notifications, responses, requests without a `_meta.protocolVersion`, or a
2424+
// nil msg.
2425+
func protocolVersionFromMessage(msg jsonrpc.Message) string {
2426+
req, ok := msg.(*jsonrpc.Request)
2427+
if !ok {
2428+
return ""
2429+
}
2430+
meta := extractRequestMeta(req.Params)
2431+
if meta == nil {
2432+
return ""
2433+
}
2434+
v, _ := meta[MetaKeyProtocolVersion].(string)
2435+
return v
2436+
}
2437+
24182438
func (c *streamableClientConn) handleJSON(requestSummary string, resp *http.Response) {
24192439
body, err := io.ReadAll(resp.Body)
24202440
resp.Body.Close()
@@ -2676,7 +2696,7 @@ func (c *streamableClientConn) connectSSE(ctx context.Context, lastEventID strin
26762696
if err != nil {
26772697
return nil, err
26782698
}
2679-
if err := c.setMCPHeaders(req); err != nil {
2699+
if err := c.setMCPHeaders(req, nil); err != nil {
26802700
return nil, err
26812701
}
26822702
if lastEventID != "" {
@@ -2712,7 +2732,7 @@ func (c *streamableClientConn) Close() error {
27122732
if err != nil {
27132733
c.closeErr = err
27142734
} else {
2715-
if err := c.setMCPHeaders(req); err != nil {
2735+
if err := c.setMCPHeaders(req, nil); err != nil {
27162736
c.closeErr = err
27172737
} else if resp, err := c.client.Do(req); err != nil {
27182738
c.closeErr = err

mcp/streamable_client_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,76 @@ func TestStreamableClientConnect_DiscoverSuccess(t *testing.T) {
13641364
}
13651365
}
13661366

1367+
// TestStreamableClientConnSetMCPHeaders_ProtocolVersion covers
1368+
// streamableClientConn.setMCPHeaders' selection of the Mcp-Protocol-Version
1369+
// header value.
1370+
//
1371+
// Ordinarily initializedResult is populated by sessionUpdated, called
1372+
// through a type assertion to the unexported clientConnection interface
1373+
// (see Client.Connect). That assertion silently fails, leaving
1374+
// initializedResult nil for the life of the session, whenever the
1375+
// Connection returned by a Transport is wrapped by another type exposing
1376+
// only the exported Connection interface (a real pattern for transports
1377+
// that intercept traffic, e.g. to filter notifications): Go does not
1378+
// promote unexported interface methods across an embedded interface
1379+
// boundary. Every SEP-2575 (>= 2026-07-28) request already carries its own
1380+
// `_meta.protocolVersion` field, so setMCPHeaders falls back to reading it
1381+
// from the outgoing message when initializedResult is unset.
1382+
func TestStreamableClientConnSetMCPHeaders_ProtocolVersion(t *testing.T) {
1383+
tests := []struct {
1384+
name string
1385+
initializedResult *InitializeResult
1386+
msg jsonrpc.Message
1387+
want string
1388+
}{
1389+
{
1390+
name: "message meta wins when initializedResult unset",
1391+
initializedResult: nil,
1392+
msg: req(1, methodListTools, &ListToolsParams{Meta: Meta{MetaKeyProtocolVersion: protocolVersion20260728}}),
1393+
want: protocolVersion20260728,
1394+
},
1395+
{
1396+
name: "initializedResult used when message has no meta",
1397+
initializedResult: &InitializeResult{ProtocolVersion: protocolVersion20251125},
1398+
msg: req(1, methodListTools, &ListToolsParams{}),
1399+
want: protocolVersion20251125,
1400+
},
1401+
{
1402+
name: "initializedResult used for nil message (GET/DELETE)",
1403+
initializedResult: &InitializeResult{ProtocolVersion: protocolVersion20251125},
1404+
msg: nil,
1405+
want: protocolVersion20251125,
1406+
},
1407+
{
1408+
name: "message meta preferred over stale initializedResult",
1409+
initializedResult: &InitializeResult{ProtocolVersion: protocolVersion20251125},
1410+
msg: req(1, methodListTools, &ListToolsParams{Meta: Meta{MetaKeyProtocolVersion: protocolVersion20260728}}),
1411+
want: protocolVersion20260728,
1412+
},
1413+
{
1414+
name: "no header when neither source is set",
1415+
initializedResult: nil,
1416+
msg: req(1, methodListTools, &ListToolsParams{}),
1417+
want: "",
1418+
},
1419+
}
1420+
for _, tt := range tests {
1421+
t.Run(tt.name, func(t *testing.T) {
1422+
conn := &streamableClientConn{initializedResult: tt.initializedResult}
1423+
httpReq, err := http.NewRequest(http.MethodPost, "http://test.invalid", nil)
1424+
if err != nil {
1425+
t.Fatal(err)
1426+
}
1427+
if err := conn.setMCPHeaders(httpReq, tt.msg); err != nil {
1428+
t.Fatalf("setMCPHeaders: %v", err)
1429+
}
1430+
if got := httpReq.Header.Get(protocolVersionHeader); got != tt.want {
1431+
t.Errorf("Mcp-Protocol-Version header = %q, want %q", got, tt.want)
1432+
}
1433+
})
1434+
}
1435+
}
1436+
13671437
// TestStreamableClientConnect_DiscoverMethodNotFound verifies that Client.Connect
13681438
// falls back to the legacy initialize handshake when the server responds to
13691439
// server/discover with a JSON-RPC "Method not found" error.

0 commit comments

Comments
 (0)