mcp: preserve single-request batch responses (#1214)

## What does this PR do?

Preserve JSON-RPC batch response arrays when a batch contains a single
request.

## Why is this needed?

A single-request legacy batch must be returned as a one-element JSON
array, but the JSON response path currently unwraps it into a single
JSON object.

## Changes

- Preserve the original batch state on the logical stream
- Keep single-request batch responses as arrays
- Add a regression test for legacy initialization batches

## Testing

- `go test ./mcp -count=1`
- `go test ./... -count=1`
- `git diff --check`

Fixes #1211
This commit is contained in:
Minghui Wang 2026-08-31 18:00:32 +08:00 • committed by GitHub
commit d19bfebab9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -4040,6 +4040,52 @@ func TestStreamableServerRejectsDuplicateInFlightRequestID(t *testing.T) {
}
}
// TestStreamableServerPreservesSingleRequestBatchResponse verifies that a
// single-request JSON-RPC batch is returned as a one-element JSON array.
func TestStreamableServerPreservesSingleRequestBatchResponse(t *testing.T) {
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
handler := NewStreamableHTTPHandler(
func(*http.Request) *Server { return server },
&StreamableHTTPOptions{JSONResponse: true},
)
defer handler.closeAll()
httpServer := httptest.NewServer(handler)
defer httpServer.Close()
body := []byte(`[{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0"}}}]`)
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")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want %d; body = %s", resp.StatusCode, http.StatusOK, data)
}
var messages []json.RawMessage
if err := json.Unmarshal(data, &messages); err != nil {
t.Fatalf("response is not a JSON array: %v; body = %s", err, data)
}
if len(messages) != 1 {
t.Fatalf("response contains %d messages, want 1; body = %s", len(messages), data)
}
if _, err := jsonrpc2.DecodeMessage(messages[0]); err != nil {
t.Fatalf("response item is not a JSON-RPC message: %v", err)
}
}
// TestStreamableMaxRequestBodyBytes verifies that the streamable HTTP handler
// enforces StreamableHTTPOptions.MaxRequestBodyBytes on incoming request
// bodies. The limit must apply uniformly regardless of transfer encoding: