mcp: return json-rpc error on stateful server serving protocolVersion20260728 requests (#1143)

Fixes #1130
This commit is contained in:
Guglielmo Colombo 2026-08-05 12:16:39 +02:00 • committed by GitHub
commit 6b1518cb0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3504,6 +3504,96 @@ func TestStreamableStateful_RejectsNewProtocol(t *testing.T) {
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400; body = %s", resp.StatusCode, respBody)
}
msg, err := jsonrpc.DecodeMessage(respBody)
if err != nil {
t.Fatalf("decoding response body as JSON-RPC: %v; body = %s", err, respBody)
}
jresp, ok := msg.(*jsonrpc.Response)
if !ok || jresp.Error == nil {
t.Fatalf("response is not a JSON-RPC error: %s", respBody)
}
var jerr *jsonrpc.Error
if !errors.As(jresp.Error, &jerr) {
t.Fatalf("response error is not *jsonrpc.Error: %v", jresp.Error)
}
if jerr.Code != CodeUnsupportedProtocolVersion {
t.Errorf("error code = %d, want %d (CodeUnsupportedProtocolVersion)", jerr.Code, CodeUnsupportedProtocolVersion)
}
var data UnsupportedProtocolVersionData
if err := json.Unmarshal(jerr.Data, &data); err != nil {
t.Fatalf("decoding error data: %v; data = %s", err, jerr.Data)
}
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 the rejected version %q",
data.Supported, protocolVersion20260728)
}
if len(data.Supported) == 0 {
t.Errorf("data.Supported is empty; expected at least one legacy version")
}
}
// TestStreamableStateful_RejectsNewProtocol_LegacyPlainText verifies that
// MCPGODEBUG=plaintextstatefulrejection=1 restores the pre-v1.8.0 plain-text
// 400 body for the stateful/new-protocol rejection.
func TestStreamableStateful_RejectsNewProtocol_LegacyPlainText(t *testing.T) {
prev := plaintextstatefulrejection
plaintextstatefulrejection = "1"
t.Cleanup(func() { plaintextstatefulrejection = prev })
server := NewServer(testImpl, nil)
AddTool(server, &Tool{Name: "noop"},
func(ctx context.Context, req *CallToolRequest, args struct{}) (*CallToolResult, any, error) {
return &CallToolResult{Content: []Content{&TextContent{Text: "ok"}}}, nil, nil
})
handler := NewStreamableHTTPHandler(func(*http.Request) *Server { return server }, nil)
httpServer := httptest.NewServer(handler)
defer httpServer.Close()
initBody := strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}`)
initReq, err := http.NewRequest(http.MethodPost, httpServer.URL, initBody)
if err != nil {
t.Fatal(err)
}
initReq.Header.Set("Content-Type", "application/json")
initReq.Header.Set("Accept", "application/json, text/event-stream")
initResp, err := http.DefaultClient.Do(initReq)
if err != nil {
t.Fatal(err)
}
io.Copy(io.Discard, initResp.Body)
initResp.Body.Close()
sessionID := initResp.Header.Get(sessionIDHeader)
if sessionID == "" {
t.Fatalf("initialize response missing %s header", sessionIDHeader)
}
body := newProtocolBody(t, "noop", struct{}{})
req, err := http.NewRequest(http.MethodPost, httpServer.URL, bytes.NewReader(body))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set(sessionIDHeader, sessionID)
req.Header.Set(protocolVersionHeader, protocolVersion20260728)
req.Header.Set(methodHeader, "tools/call")
req.Header.Set(nameHeader, "noop")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400; body = %s", resp.StatusCode, respBody)
}
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
t.Errorf("Content-Type = %q, want text/plain; body = %s", ct, respBody)
}
if !strings.Contains(string(respBody), "stateless") {
t.Errorf("body = %q, want a message mentioning 'stateless'", respBody)
}