mcp: limit SSE discover supportedVersions to SSE-defined revisions (#1121)
## 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>
This commit is contained in:
parent
d462fbd303
commit
958dfcc532
2 changed files with 71 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue