From 019954a3530c228bc5dea0680d8c7927d10e5b79 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Mon, 27 Jul 2026 11:33:05 +0200 Subject: [PATCH] mcp: emit UnsupportedProtocolVersionError for stateful servers rejecting SEP-2575 requests A stateful (Stateless == false) Streamable HTTP server rejected requests carrying `_meta."io.modelcontextprotocol/protocolVersion"` >= 2026-07-28 with a plain-text http.Error body, instead of the JSON-RPC UnsupportedProtocolVersionError the draft spec requires for a known version the server has chosen not to support. The body also leaked a Go-API hint ("set StreamableHTTPOptions.Stateless = true") meant for the server author, not the client. Reuse the existing CodeUnsupportedProtocolVersion (-32022) and UnsupportedProtocolVersionData shape already used by the discover and initialize paths, listing only the server's legacy-era supported versions. This lets Client.Connect's existing renegotiation logic recognize and handle the rejection instead of falling back on an unrecognized text/plain body. The developer hint moves to a server-side log line. Co-Authored-By: Claude Sonnet 5 --- mcp/streamable.go | 23 +++++++++++++++++++---- mcp/streamable_test.go | 26 ++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/mcp/streamable.go b/mcp/streamable.go index 1f07ea61..ca718a80 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -1514,10 +1514,25 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques // rejection as it should learn about the supported protocols from the // DiscoverResult response. if !c.stateless && jreq.Method != methodDiscover { - http.Error(w, fmt.Sprintf( - "Bad Request: protocol version %q is only supported on stateless HTTP servers (set StreamableHTTPOptions.Stateless = true)", - protocolVersion), - http.StatusBadRequest) + // This server has chosen not to serve >= 2026-07-28 requests + // unless configured as stateless (see StreamableHTTPOptions.Stateless). + c.logger.Warn("rejecting request for unsupported protocol version; set StreamableHTTPOptions.Stateless = true to serve it", + "protocolVersion", protocolVersion) + legacyVersions := make([]string, 0, len(supportedProtocolVersions)) + for _, v := range supportedProtocolVersions { + if v < protocolVersion20260728 { + legacyVersions = append(legacyVersions, v) + } + } + data, _ := json.Marshal(UnsupportedProtocolVersionData{ + Supported: legacyVersions, + Requested: protocolVersion, + }) + writeJSONRPCError(w, http.StatusBadRequest, jreq.ID, &jsonrpc.Error{ + Code: CodeUnsupportedProtocolVersion, + Message: fmt.Sprintf("protocol version %q is not supported by this server", protocolVersion), + Data: data, + }) return } if headerVersion == "" { diff --git a/mcp/streamable_test.go b/mcp/streamable_test.go index 85ee331f..fee1b220 100644 --- a/mcp/streamable_test.go +++ b/mcp/streamable_test.go @@ -3504,8 +3504,30 @@ func TestStreamableStateful_RejectsNewProtocol(t *testing.T) { if resp.StatusCode != http.StatusBadRequest { t.Fatalf("status = %d, want 400; body = %s", resp.StatusCode, respBody) } - if !strings.Contains(string(respBody), "stateless") { - t.Errorf("body = %q, want a message mentioning 'stateless'", respBody) + msg, err := jsonrpc2.DecodeMessage(respBody) + if err != nil { + t.Fatalf("failed to decode message: %v; body = %s", err, respBody) + } + wireResp, ok := msg.(*jsonrpc2.Response) + if !ok { + t.Fatalf("expected *jsonrpc2.Response, got %T", msg) + } + var wireErr *jsonrpc2.WireError + if !errors.As(wireResp.Error, &wireErr) { + t.Fatalf("expected *jsonrpc2.WireError, got %T", wireResp.Error) + } + if wireErr.Code != CodeUnsupportedProtocolVersion { + t.Errorf("error code = %d, want %d", wireErr.Code, CodeUnsupportedProtocolVersion) + } + var data UnsupportedProtocolVersionData + if err := json.Unmarshal(wireErr.Data, &data); err != nil { + t.Fatalf("error.Data is not UnsupportedProtocolVersionData: %v", err) + } + if data.Requested != protocolVersion20260728 { + t.Errorf("data.Requested = %q, want %q", data.Requested, protocolVersion20260728) + } + if slices.Contains(data.Supported, protocolVersion20260728) { + t.Errorf("data.Supported = %v, must not contain %q for a stateful server", data.Supported, protocolVersion20260728) } }