From ae6ddf51e0ba93217d24a1ce34371d5dc3e8c018 Mon Sep 17 00:00:00 2001 From: Arpan Mondal Date: Tue, 1 Sep 2026 17:30:51 +0530 Subject: [PATCH 1/2] mcp: ignore Mcp-Session-Id on 2026-07-28 sessions 2026-07-28 removed protocol-level sessions (SEP-2567), so a client must not echo an Mcp-Session-Id even if a non-compliant server sends one. Drop the session ID once the negotiated version is known to be modern, so it is not sent on later requests or used to terminate the session on close. --- mcp/streamable.go | 17 ++++++++-- mcp/streamable_client_test.go | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/mcp/streamable.go b/mcp/streamable.go index f24badbb..42bf99a2 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -2138,6 +2138,13 @@ var _ clientConnection = (*streamableClientConn)(nil) func (c *streamableClientConn) sessionUpdated(state clientSessionState) { c.mu.Lock() c.initializedResult = state.InitializeResult + // Protocol-level sessions were removed in 2026-07-28 (SEP-2567): a client + // must not echo an Mcp-Session-Id, so drop one that the initial response + // adopted before the negotiated version was known. + if state.InitializeResult != nil && + state.InitializeResult.ProtocolVersion >= protocolVersion20260728 { + c.sessionID = "" + } c.mu.Unlock() // Under SEP-2575 (protocol version >= 2026-07-28) the standalone HTTP GET @@ -2371,12 +2378,18 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e if sessionID := resp.Header.Get(sessionIDHeader); sessionID != "" { c.mu.Lock() + // 2026-07-28 (SEP-2567) removed protocol-level sessions. Once the + // negotiated version is known to be modern, ignore an Mcp-Session-Id + // from a non-compliant server: don't adopt it, so it is never echoed on + // later requests or used to terminate the session with DELETE. + modern := c.initializedResult != nil && + c.initializedResult.ProtocolVersion >= protocolVersion20260728 hadSessionID := c.sessionID - if hadSessionID == "" { + if !modern && hadSessionID == "" { c.sessionID = sessionID } c.mu.Unlock() - if hadSessionID != "" && hadSessionID != sessionID { + if !modern && hadSessionID != "" && hadSessionID != sessionID { resp.Body.Close() return fmt.Errorf("mismatching session IDs %q and %q", hadSessionID, sessionID) } diff --git a/mcp/streamable_client_test.go b/mcp/streamable_client_test.go index 77ff81a2..db61abf0 100644 --- a/mcp/streamable_client_test.go +++ b/mcp/streamable_client_test.go @@ -1406,6 +1406,65 @@ func TestStreamableClientConnect_DiscoverSuccess(t *testing.T) { } } +// TestStreamableClientConnect_ModernIgnoresSessionID verifies that on a +// 2026-07-28 session the client ignores an Mcp-Session-Id from a non-compliant +// server: protocol-level sessions were removed (SEP-2567), so later requests +// must carry no session ID and Close must not send a session-terminating +// DELETE. +func TestStreamableClientConnect_ModernIgnoresSessionID(t *testing.T) { + ctx := context.Background() + + echoResult := func(result any) func(*jsonrpc.Request) (string, int) { + return func(r *jsonrpc.Request) (string, int) { + return jsonBody(t, &jsonrpc.Response{ID: r.ID, Result: mustMarshal(result)}), http.StatusOK + } + } + + fake := &fakeStreamableServer{ + t: t, + responses: fakeResponses{ + // The discover response wrongly carries a session ID. + {"POST", "", methodDiscover, ""}: { + header: header{ + "Content-Type": "application/json", + sessionIDHeader: "sess-1", + }, + wantProtocolVersion: protocolVersion20260728, + responseFunc: echoResult(discoverResult), + }, + // The follow-up tools/list must arrive with no session ID. Had the + // client echoed "sess-1", this key would not match and the server + // would fail with "missing response". A DELETE carrying the session + // ID is likewise never registered, so Close sending one would fail. + {"POST", "", methodListTools, ""}: { + header: header{"Content-Type": "application/json"}, + wantProtocolVersion: protocolVersion20260728, + responseFunc: echoResult(&ListToolsResult{}), + }, + }, + } + + httpServer := httptest.NewServer(fake) + defer httpServer.Close() + + transport := &StreamableClientTransport{Endpoint: httpServer.URL} + client := NewClient(testImpl, nil) + session, err := client.Connect(ctx, transport, &ClientSessionOptions{ProtocolVersion: protocolVersion20260728}) + if err != nil { + t.Fatalf("Connect: %v", err) + } + if _, err := session.ListTools(ctx, nil); err != nil { + t.Fatalf("ListTools: %v", err) + } + if err := session.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + if missing := fake.missingRequests(); len(missing) > 0 { + t.Errorf("missing expected requests: %v", missing) + } +} + // TestStreamableClientConnSetMCPHeaders_ProtocolVersion covers // streamableClientConn.setMCPHeaders' selection of the Mcp-Protocol-Version // header value. From db6b06510dbad0b1f0e5c8086ab9282670c65061 Mon Sep 17 00:00:00 2001 From: Arpan Mondal Date: Tue, 1 Sep 2026 22:35:02 +0530 Subject: [PATCH 2/2] mcp: reuse the version check in sessionUpdated instead of repeating it --- mcp/streamable.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mcp/streamable.go b/mcp/streamable.go index 42bf99a2..07261a7d 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -2136,21 +2136,21 @@ type streamableClientConn struct { var _ clientConnection = (*streamableClientConn)(nil) func (c *streamableClientConn) sessionUpdated(state clientSessionState) { + // 2026-07-28 removed both protocol-level sessions (SEP-2567) and the + // standalone HTTP GET SSE stream (SEP-2575). + modern := state.InitializeResult != nil && + state.InitializeResult.ProtocolVersion >= protocolVersion20260728 + c.mu.Lock() c.initializedResult = state.InitializeResult - // Protocol-level sessions were removed in 2026-07-28 (SEP-2567): a client - // must not echo an Mcp-Session-Id, so drop one that the initial response - // adopted before the negotiated version was known. - if state.InitializeResult != nil && - state.InitializeResult.ProtocolVersion >= protocolVersion20260728 { + if modern { + // A client must not echo an Mcp-Session-Id, so drop one that the initial + // response adopted before the negotiated version was known. c.sessionID = "" } c.mu.Unlock() - // Under SEP-2575 (protocol version >= 2026-07-28) the standalone HTTP GET - // SSE stream is removed. - if state.InitializeResult == nil || - state.InitializeResult.ProtocolVersion >= protocolVersion20260728 { + if state.InitializeResult == nil || modern { return }