Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions mcp/streamable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2136,14 +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
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
}

Expand Down Expand Up @@ -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)
}
Expand Down
59 changes: 59 additions & 0 deletions mcp/streamable_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading