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:
dev_Hakaze 2026-08-05 20:36:22 +07:00 • committed by GitHub
commit 958dfcc532
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 0 deletions

View file

@ -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

View file

@ -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) {