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

@ -988,6 +988,10 @@ type stream struct {
// Note: if we remove support for batching, this could just be a bool.
pendingJSONMessages []json.RawMessage
// isBatch reports whether the request that opened the stream was a JSON-RPC
// batch. Batch responses must remain arrays even when they contain one item.
isBatch bool
// w is the HTTP response writer for this stream. A non-nil w indicates
// that the stream is claimed by an HTTP request (the hanging POST or GET);
// it is set to nil when the request completes.
@ -1138,7 +1142,7 @@ func (s *stream) deliverLocked(data []byte, eventID string, responseTo jsonrpc.I
if done {
// Flush all pending messages as JSON response.
var toWrite []byte
if len(s.pendingJSONMessages) == 1 {
if len(s.pendingJSONMessages) == 1 && !s.isBatch {
toWrite = s.pendingJSONMessages[0]
} else {
toWrite, err = json.Marshal(s.pendingJSONMessages)
@ -1691,6 +1695,7 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques
return
}
stream.isListen = isSubscriptionsListen
stream.isBatch = isBatch
// subscriptions/listen is inherently a long-lived SSE endpoint (SEP-2575):
// it has no synchronous result, the response stream stays open until the