From d5a828dc142a3a4f205cb3fdd384932b5cc4ac9d Mon Sep 17 00:00:00 2001 From: I528580 Date: Tue, 6 Oct 2026 14:47:29 +0530 Subject: [PATCH 1/4] mcp: reject legacy-format requests on 2026-only servers A server configured with SupportedProtocolVersions containing only versions >= 2026-07-28 should not silently serve legacy clients that use the initialize/initialized handshake protocol. Previously, such clients would receive a successful (if confusing) response: stateful servers would accept any post-initialize method call because ephemeral session setup bypassed the version check, and stateless servers would always appear initialized due to ephemeral connect opts synthesizing fake InitializeParams. Fix the stateful path by detecting a 2026-only server configuration in the uninitialized branch of handle() and returning CodeUnsupportedProtocolVersion before falling through to the generic "method invalid during initialization" error. Fix the stateless path by checking the legacy flag in serveStateless() immediately after ephemeralConnectOpts(), before any method dispatch. Both paths include an UnsupportedProtocolVersionData payload listing the server's supported versions so clients can adapt. Fixes #1313 --- mcp/server.go | 12 +++++++ mcp/server_test.go | 81 +++++++++++++++++++++++++++++++++++++++++- mcp/streamable.go | 14 ++++++++ mcp/streamable_test.go | 47 ++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) diff --git a/mcp/server.go b/mcp/server.go index 16aad6a9..66d4a5be 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -2065,6 +2065,18 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, // to wait for, and the request itself says which protocol it speaks. if !initialized && !validatedMeta.usesNewProtocol && req.IsCall() && req.Method != methodInitialize && req.Method != methodPing { + if !slices.ContainsFunc(ss.server.protocolVersions, func(v string) bool { + return v < protocolVersion20260728 + }) { + data, _ := json.Marshal(UnsupportedProtocolVersionData{ + Supported: ss.server.protocolVersions, + }) + return nil, &jsonrpc.Error{ + Code: CodeUnsupportedProtocolVersion, + Message: "legacy-format requests are not supported by this server", + Data: data, + } + } ss.server.opts.Logger.Error("method invalid during initialization", "method", req.Method) return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method) } diff --git a/mcp/server_test.go b/mcp/server_test.go index 64e7de0d..9a9d9220 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -2199,7 +2199,86 @@ func TestServerSupportedProtocolVersions_NewProtocol(t *testing.T) { } } -// TestNotifySessionsIsolatesStalledPeer verifies that a session whose write +// TestServerSupportedProtocolVersions_InitializeLegacy verifies that a legacy +// initialize request succeeds on a 2026-only server configured with +// [ServerOptions.SupportedProtocolVersions], with the server responding with +// protocolVersion20251125 as a courtesy (per negotiatedVersion semantics). +func TestServerSupportedProtocolVersions_InitializeLegacy(t *testing.T) { + ctx := context.Background() + server := NewServer(testImpl, &ServerOptions{ + SupportedProtocolVersions: []string{protocolVersion20260728}, + }) + ct, st := NewInMemoryTransports() + ss, err := server.Connect(ctx, st, nil) + if err != nil { + t.Fatalf("server.Connect: %v", err) + } + defer ss.Close() + _ = ct + + params, err := json.Marshal(&InitializeParams{ + ProtocolVersion: protocolVersion20251125, + Capabilities: &ClientCapabilities{}, + ClientInfo: &Implementation{Name: "test", Version: "1"}, + }) + if err != nil { + t.Fatal(err) + } + res, err := ss.handle(ctx, &jsonrpc.Request{ + ID: jsonrpc2.Int64ID(1), + Method: methodInitialize, + Params: json.RawMessage(params), + }) + if err != nil { + t.Fatalf("handle returned unexpected error: %v", err) + } + initRes, ok := res.(*InitializeResult) + if !ok { + t.Fatalf("handle returned %T, want *InitializeResult", res) + } + if initRes.ProtocolVersion != protocolVersion20251125 { + t.Errorf("ProtocolVersion = %q, want %q", initRes.ProtocolVersion, protocolVersion20251125) + } +} + +// TestServerSupportedProtocolVersions_LegacyDispatch verifies that a legacy +// request (no _meta) is rejected with [CodeUnsupportedProtocolVersion] when +// the server only supports protocol versions >= 2026-07-28 via +// [ServerOptions.SupportedProtocolVersions]. +func TestServerSupportedProtocolVersions_LegacyDispatch(t *testing.T) { + ctx := context.Background() + server := NewServer(testImpl, &ServerOptions{ + SupportedProtocolVersions: []string{protocolVersion20260728}, + }) + ct, st := NewInMemoryTransports() + ss, err := server.Connect(ctx, st, nil) + if err != nil { + t.Fatalf("server.Connect: %v", err) + } + defer ss.Close() + _ = ct + + _, err = ss.handle(ctx, &jsonrpc.Request{ + ID: jsonrpc2.Int64ID(1), + Method: methodListTools, + Params: json.RawMessage(`{}`), + }) + var jerr *jsonrpc.Error + if !errors.As(err, &jerr) { + t.Fatalf("handle returned %v, want a *jsonrpc.Error", err) + } + if jerr.Code != CodeUnsupportedProtocolVersion { + t.Fatalf("error code = %d, want %d", jerr.Code, CodeUnsupportedProtocolVersion) + } + var data UnsupportedProtocolVersionData + if err := json.Unmarshal(jerr.Data, &data); err != nil { + t.Fatalf("unmarshal error data: %v", err) + } + if diff := cmp.Diff([]string{protocolVersion20260728}, data.Supported); diff != "" { + t.Errorf("UnsupportedProtocolVersionData.Supported mismatch (-want +got):\n%s", diff) + } +} + // stalls — here a peer that never reads its end of the pipe — does not delay // or fail delivery to the other sessions in the same broadcast. func TestNotifySessionsIsolatesStalledPeer(t *testing.T) { diff --git a/mcp/streamable.go b/mcp/streamable.go index 6e28051b..15759572 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -472,6 +472,20 @@ func (h *StreamableHTTPHandler) serveStateless(w http.ResponseWriter, req *http. return } + if !info.usesNewProtocol && !slices.ContainsFunc(server.protocolVersions, func(v string) bool { + return v < protocolVersion20260728 + }) { + data, _ := json.Marshal(UnsupportedProtocolVersionData{ + Supported: server.protocolVersions, + }) + writeJSONRPCError(w, http.StatusBadRequest, jsonrpc.ID{}, &jsonrpc.Error{ + Code: CodeUnsupportedProtocolVersion, + Message: "legacy-format requests are not supported by this server", + Data: data, + }) + return + } + var sessionID string if legacySessions && !info.usesNewProtocol { sessionID = req.Header.Get(sessionIDHeader) diff --git a/mcp/streamable_test.go b/mcp/streamable_test.go index a481e7b7..030c79b2 100644 --- a/mcp/streamable_test.go +++ b/mcp/streamable_test.go @@ -1756,6 +1756,53 @@ func TestStreamableStateless(t *testing.T) { testClientCompatibility(t, statelessHandler) } +// TestStreamableStateless_RejectsLegacyOn2026OnlyServer verifies that a +// stateless server configured with [ServerOptions.SupportedProtocolVersions] +// set to only ["2026-07-28"] rejects legacy-format requests (no _meta, no +// Mcp-Protocol-Version header) with [CodeUnsupportedProtocolVersion]. +func TestStreamableStateless_RejectsLegacyOn2026OnlyServer(t *testing.T) { + server := NewServer(testImpl, &ServerOptions{ + SupportedProtocolVersions: []string{protocolVersion20260728}, + }) + handler := NewStreamableHTTPHandler(func(*http.Request) *Server { return server }, &StreamableHTTPOptions{ + Stateless: true, + JSONResponse: true, + }) + defer handler.closeAll() + + ts := httptest.NewServer(handler) + defer ts.Close() + + body, _ := jsonrpc2.EncodeMessage(req(1, methodListTools, &ListToolsParams{})) + httpReq, err := http.NewRequest(http.MethodPost, ts.URL, bytes.NewReader(body)) + if err != nil { + t.Fatalf("NewRequest: %v", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Accept", "application/json, text/event-stream") + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + t.Fatalf("POST failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusBadRequest) + } + got, _ := io.ReadAll(resp.Body) + var errResp struct { + Error struct { + Code int64 `json:"code"` + } `json:"error"` + } + if err := json.Unmarshal(got, &errResp); err != nil { + t.Fatalf("unmarshal response: %v\nbody: %s", err, got) + } + if errResp.Error.Code != CodeUnsupportedProtocolVersion { + t.Errorf("error code = %d, want %d", errResp.Error.Code, CodeUnsupportedProtocolVersion) + } +} + func textContent(t *testing.T, res *CallToolResult) string { t.Helper() if len(res.Content) != 1 { From 54e759594631e300e29b70b62432f445c3252249 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Mon, 12 Oct 2026 07:42:54 +0000 Subject: [PATCH 2/4] refactor: reject legacy protocol requests with UnsupportedProtocolVersion when no compatible handshake version exists --- mcp/server.go | 28 ++++++++++++++++------------ mcp/server_test.go | 39 ++++++++++++++++++--------------------- mcp/shared.go | 12 ++++-------- mcp/streamable.go | 32 ++++++++++++++------------------ 4 files changed, 52 insertions(+), 59 deletions(-) diff --git a/mcp/server.go b/mcp/server.go index bc73b166..a305aae1 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -2038,6 +2038,22 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, } } + // A server that supports only 2026-07-28 and later serves no legacy call, + // initialize included. + if !validatedMeta.usesNewProtocol && req.IsCall() && + !slices.ContainsFunc(ss.server.protocolVersions, func(v string) bool { + return v < protocolVersion20260728 + }) { + data, _ := json.Marshal(UnsupportedProtocolVersionData{ + Supported: ss.server.protocolVersions, + }) + return nil, &jsonrpc.Error{ + Code: CodeUnsupportedProtocolVersion, + Message: "legacy protocol version requests are not supported by this server", + Data: data, + } + } + switch req.Method { case methodInitialize, methodPing, notificationInitialized, notificationRootsListChanged, methodSetLevel, methodSubscribe, methodUnsubscribe: if validatedMeta.usesNewProtocol { @@ -2075,18 +2091,6 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, // to wait for, and the request itself says which protocol it speaks. if !initialized && !validatedMeta.usesNewProtocol && req.IsCall() && req.Method != methodInitialize && req.Method != methodPing { - if !slices.ContainsFunc(ss.server.protocolVersions, func(v string) bool { - return v < protocolVersion20260728 - }) { - data, _ := json.Marshal(UnsupportedProtocolVersionData{ - Supported: ss.server.protocolVersions, - }) - return nil, &jsonrpc.Error{ - Code: CodeUnsupportedProtocolVersion, - Message: "legacy-format requests are not supported by this server", - Data: data, - } - } ss.server.opts.Logger.Error("method invalid during initialization", "method", req.Method) return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method) } diff --git a/mcp/server_test.go b/mcp/server_test.go index f5c110d7..adcd1d9d 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -2128,16 +2128,6 @@ func TestServerSupportedProtocolVersions(t *testing.T) { client: protocolVersion20241105, want: protocolVersion20250326, }, - { - // Nothing the server supports can carry an initialize - // handshake, so the answer names a handshake-era version the - // client can act on rather than one it could mistake for the - // new protocol. - name: "server with no handshake version", - supported: []string{protocolVersion20260728}, - client: protocolVersion20250618, - want: protocolVersion20251125, - }, { name: "unrestricted server keeps the default behavior", supported: nil, @@ -2221,9 +2211,9 @@ func TestServerSupportedProtocolVersions_NewProtocol(t *testing.T) { } // TestServerSupportedProtocolVersions_InitializeLegacy verifies that a legacy -// initialize request succeeds on a 2026-only server configured with -// [ServerOptions.SupportedProtocolVersions], with the server responding with -// protocolVersion20251125 as a courtesy (per negotiatedVersion semantics). +// initialize request is rejected with [CodeUnsupportedProtocolVersion], naming +// the supported versions, when [ServerOptions.SupportedProtocolVersions] +// leaves the server no version that has an initialize handshake. func TestServerSupportedProtocolVersions_InitializeLegacy(t *testing.T) { ctx := context.Background() server := NewServer(testImpl, &ServerOptions{ @@ -2245,20 +2235,27 @@ func TestServerSupportedProtocolVersions_InitializeLegacy(t *testing.T) { if err != nil { t.Fatal(err) } - res, err := ss.handle(ctx, &jsonrpc.Request{ + _, err = ss.handle(ctx, &jsonrpc.Request{ ID: jsonrpc2.Int64ID(1), Method: methodInitialize, Params: json.RawMessage(params), }) - if err != nil { - t.Fatalf("handle returned unexpected error: %v", err) + var jerr *jsonrpc.Error + if !errors.As(err, &jerr) { + t.Fatalf("handle returned %v, want a *jsonrpc.Error", err) } - initRes, ok := res.(*InitializeResult) - if !ok { - t.Fatalf("handle returned %T, want *InitializeResult", res) + if jerr.Code != CodeUnsupportedProtocolVersion { + t.Fatalf("error code = %d, want %d", jerr.Code, CodeUnsupportedProtocolVersion) + } + var data UnsupportedProtocolVersionData + if err := json.Unmarshal(jerr.Data, &data); err != nil { + t.Fatalf("unmarshal error data: %v", err) + } + if diff := cmp.Diff([]string{protocolVersion20260728}, data.Supported); diff != "" { + t.Errorf("UnsupportedProtocolVersionData.Supported mismatch (-want +got):\n%s", diff) } - if initRes.ProtocolVersion != protocolVersion20251125 { - t.Errorf("ProtocolVersion = %q, want %q", initRes.ProtocolVersion, protocolVersion20251125) + if ss.InitializeParams() != nil { + t.Errorf("session initialized after a rejected initialize: %+v", ss.InitializeParams()) } } diff --git a/mcp/shared.go b/mcp/shared.go index f3b2a348..8cc11764 100644 --- a/mcp/shared.go +++ b/mcp/shared.go @@ -89,14 +89,10 @@ func negotiatedVersion(clientVersion string, supported []string) string { return v } } - // If the server was enforced to support only protocolVersion20260728 and - // later, the initialize function should not be called. - // If a client calls the initialize function, return the protocolVersion20251125. - // As according to spec (https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation): - // "If the server supports the requested protocol version, it MUST respond with the same version." - // "Otherwise, the server MUST respond with another protocol version it supports." - // "This SHOULD be the latest version supported by the server." - return protocolVersion20251125 + // A server that supports no version before protocolVersion20260728 has + // nothing to negotiate. [ServerSession.handle] rejects initialize on such + // a server with CodeUnsupportedProtocolVersion before it gets here. + return "" } // negotiateMutuallySupportedVersion returns a protocol version that is supported diff --git a/mcp/streamable.go b/mcp/streamable.go index 15759572..d7e15321 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -472,20 +472,6 @@ func (h *StreamableHTTPHandler) serveStateless(w http.ResponseWriter, req *http. return } - if !info.usesNewProtocol && !slices.ContainsFunc(server.protocolVersions, func(v string) bool { - return v < protocolVersion20260728 - }) { - data, _ := json.Marshal(UnsupportedProtocolVersionData{ - Supported: server.protocolVersions, - }) - writeJSONRPCError(w, http.StatusBadRequest, jsonrpc.ID{}, &jsonrpc.Error{ - Code: CodeUnsupportedProtocolVersion, - Message: "legacy-format requests are not supported by this server", - Data: data, - }) - return - } - var sessionID string if legacySessions && !info.usesNewProtocol { sessionID = req.Header.Get(sessionIDHeader) @@ -1226,10 +1212,10 @@ func (s *stream) keepAlive(ctx context.Context, interval time.Duration, committe // - InvalidParams (-32602), UnsupportedProtocolVersion (-32022) and // CodeMissingRequiredClientCapabilities (-32021) MUST // return HTTP 400. +// +// UnsupportedProtocolVersion is the one code mapped for every protocol +// version. func extractErrorStatus(ctx context.Context, msg jsonrpc.Message) int { - if protocolVersionFromContext(ctx) < protocolVersion20260728 { - return 0 - } resp, ok := msg.(*jsonrpc.Response) if !ok || resp.Error == nil { return 0 @@ -1238,10 +1224,20 @@ func extractErrorStatus(ctx context.Context, msg jsonrpc.Message) int { if !errors.As(resp.Error, &jerr) { return 0 } + // A legacy request gets this code only from a server that supports no + // version before 2026-07-28, which rejects it in ServerSession.handle + // before any handler runs, so nothing has been written to the response + // yet. + if jerr.Code == CodeUnsupportedProtocolVersion { + return http.StatusBadRequest + } + if protocolVersionFromContext(ctx) < protocolVersion20260728 { + return 0 + } switch jerr.Code { case jsonrpc.CodeMethodNotFound: return http.StatusNotFound - case jsonrpc.CodeInvalidParams, CodeUnsupportedProtocolVersion, CodeMissingRequiredClientCapabilities: + case jsonrpc.CodeInvalidParams, CodeMissingRequiredClientCapabilities: return http.StatusBadRequest } return 0 From a99dd5f27d9714a8a09a20b0b082aed4cb469100 Mon Sep 17 00:00:00 2001 From: I528580 Date: Wed, 14 Oct 2026 18:16:30 +0530 Subject: [PATCH 3/4] mcp: omit UnsupportedProtocolVersionData.Requested when empty --- mcp/protocol.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcp/protocol.go b/mcp/protocol.go index 223a9ea7..845daffa 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -2490,8 +2490,8 @@ const ( type UnsupportedProtocolVersionData struct { // Supported is the list of protocol versions the server supports. Supported []string `json:"supported"` - // Requested is the protocol version the client asked for. - Requested string `json:"requested"` + // Requested is the protocol version the client asked for, if known. + Requested string `json:"requested,omitempty"` } // MissingRequiredClientCapabilityData is the SEP-2575 payload carried in the From b8a1396bda3744e910a5cf45804e47dd8dfcf262 Mon Sep 17 00:00:00 2001 From: I528580 Date: Wed, 14 Oct 2026 18:46:33 +0530 Subject: [PATCH 4/4] Revert "mcp: omit UnsupportedProtocolVersionData.Requested when empty" This reverts commit a99dd5f27d9714a8a09a20b0b082aed4cb469100. --- mcp/protocol.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcp/protocol.go b/mcp/protocol.go index abca0467..2d3ba3cc 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -2490,8 +2490,8 @@ const ( type UnsupportedProtocolVersionData struct { // Supported is the list of protocol versions the server supports. Supported []string `json:"supported"` - // Requested is the protocol version the client asked for, if known. - Requested string `json:"requested,omitempty"` + // Requested is the protocol version the client asked for. + Requested string `json:"requested"` } // MissingRequiredClientCapabilityData is the SEP-2575 payload carried in the