From 958dfcc532d56e3aa4a5215d69bae2941995a384 Mon Sep 17 00:00:00 2001 From: dev_Hakaze Date: Wed, 5 Aug 2026 20:36:22 +0700 Subject: [PATCH] mcp: limit SSE discover supportedVersions to SSE-defined revisions (#1121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `NewSSEHandler` sessions were advertising `2026-07-28` in `server/discover` `supportedVersions`. That revision only defines stdio and Streamable HTTP, not the deprecated HTTP+SSE binding, so Modern-first clients could negotiate a revision the transport cannot honour. ## Changes - Implement `ProtocolVersionSupporter` on `SSEServerTransport` so `filterSupportedVersions` excludes `>= 2026-07-28` (matching the streamable transport's version gate pattern). - In `Server.discover`, only set session `InitializeParams` when the requested version is actually supported by the transport. Without this, a discover probe that returns only legacy versions still marked the session initialized, and the client's fallback `initialize` failed with `duplicate "initialize" received`. - Tests: unit coverage for filtered SSE versions, plus an end-to-end Modern client → SSE fallback-to-initialize path. Fixes #1112 ## Test plan - [x] `go test ./...` - [x] `go test ./mcp/ -run 'TestSSE|TestInMemory_E2E_Discover|TestStreamableStateful_AcceptsDiscover|TestStreamableHTTP_E2E_Discover|TestClientConnectDiscover'` Assistance: human-reviewed AI assistance used for implementation. --------- Co-authored-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- mcp/sse.go | 7 ++++++ mcp/sse_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/mcp/sse.go b/mcp/sse.go index 7efaa95..fb32756 100644 --- a/mcp/sse.go +++ b/mcp/sse.go @@ -189,6 +189,13 @@ func (t *SSEServerTransport) Connect(context.Context) (Connection, error) { return &sseServerConn{t: t}, nil } +// SupportsProtocolVersion reports whether the HTTP+SSE transport can serve the +// given protocol version. MCP 2026-07-28 defines only the stdio and Streamable +// HTTP bindings, so this (deprecated) transport does not advertise that revision. +func (t *SSEServerTransport) SupportsProtocolVersion(version string) bool { + return version < protocolVersion20260728 +} + func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // DNS rebinding protection: auto-enabled for localhost servers. // See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise diff --git a/mcp/sse_test.go b/mcp/sse_test.go index 86b2cf1..c0ff445 100644 --- a/mcp/sse_test.go +++ b/mcp/sse_test.go @@ -12,12 +12,76 @@ import ( "net" "net/http" "net/http/httptest" + "slices" "sync/atomic" "testing" "github.com/google/go-cmp/cmp" ) +// TestSSEServerTransport_SupportedVersions verifies that the deprecated +// HTTP+SSE transport does not advertise protocol revisions that only define +// stdio and Streamable HTTP (see #1112). +func TestSSEServerTransport_SupportedVersions(t *testing.T) { + var tr SSEServerTransport + versions := filterSupportedVersions(&tr) + if slices.Contains(versions, protocolVersion20260728) { + t.Errorf("filterSupportedVersions(SSEServerTransport) = %v, must not include %q", + versions, protocolVersion20260728) + } + if !slices.Contains(versions, protocolVersion20241105) { + t.Errorf("filterSupportedVersions(SSEServerTransport) = %v, want %q (SSE-defined revision)", + versions, protocolVersion20241105) + } + if len(versions) == 0 { + t.Error("filterSupportedVersions(SSEServerTransport) is empty; want legacy SSE revisions") + } + for _, v := range supportedProtocolVersions { + got := tr.SupportsProtocolVersion(v) + want := v < protocolVersion20260728 + if got != want { + t.Errorf("SupportsProtocolVersion(%q) = %v, want %v", v, got, want) + } + } +} + +// TestSSEHandler_DiscoverFallsBackToInitialize checks that a Modern-first +// client connecting over HTTP+SSE does not stick on 2026-07-28: discover +// advertises only SSE-defined revisions, and Connect falls back to legacy +// initialize successfully. +func TestSSEHandler_DiscoverFallsBackToInitialize(t *testing.T) { + ctx := context.Background() + server := NewServer(testImpl, nil) + sseHandler := NewSSEHandler(func(*http.Request) *Server { return server }, nil) + httpServer := httptest.NewServer(sseHandler) + defer httpServer.Close() + + client := NewClient(testImpl, nil) + // Default Client.Connect probes with the latest protocol version (2026-07-28). + cs, err := client.Connect(ctx, &SSEClientTransport{Endpoint: httpServer.URL}, nil) + if err != nil { + t.Fatalf("Connect over SSE with modern client: %v", err) + } + defer cs.Close() + + ir := cs.InitializeResult() + if ir == nil { + t.Fatal("InitializeResult is nil after Connect") + } + if ir.ProtocolVersion == protocolVersion20260728 { + t.Errorf("InitializeResult.ProtocolVersion = %q; HTTP+SSE must not negotiate that revision", + ir.ProtocolVersion) + } + if ir.ProtocolVersion != protocolVersion20251125 { + // Client.Connect falls back to protocolVersion20251125 for legacy initialize. + t.Errorf("InitializeResult.ProtocolVersion = %q, want %q (legacy fallback)", + ir.ProtocolVersion, protocolVersion20251125) + } + if err := cs.Ping(ctx, nil); err != nil { + t.Errorf("Ping after SSE fallback initialize: %v", err) + } +} + func TestSSEServer(t *testing.T) { for _, closeServerFirst := range []bool{false, true} { t.Run(fmt.Sprintf("closeServerFirst=%t", closeServerFirst), func(t *testing.T) {