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
3 changes: 3 additions & 0 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,9 @@ func (s *Server) discover(_ context.Context, req *ServerRequest[*DiscoverParams]
// is never surfaced to the client via Mcp-Session-Id; leaving
// InitializeParams nil lets serveStatefulPOST's safety-net cleanup
// close it instead of leaking.
// With SSE ProtocolVersionSupporter filtering out 2026-07-28, negotiate
// returns a legacy revision so InitializeParams stays nil and Modern-first
// clients can fall back to the legacy initialize handshake (#1112).
if supportedVersion := negotiateMutuallySupportedVersion(versions); supportedVersion >= protocolVersion20260728 {
req.Session.updateState(func(state *ServerSessionState) {
state.InitializeParams = init
Expand Down
7 changes: 7 additions & 0 deletions mcp/sse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions mcp/sse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading