fix(jsonrpc2): decode requests when method key is present (#1000)

## Summary

Fixes #976.

`DecodeMessage` used `msg.Method != ""` to classify messages. A wire
payload with `"method":""` was decoded as a **response**, so stdio
servers dropped it without a correlated JSON-RPC error.

## Approach

Decode into a `wireDecode` struct whose `method` field is
`json.RawMessage`. Treat the message as a **request** when the `method`
key is **present on the wire** (including `""`), which matches JSON-RPC
semantics.

This revision:

- Uses a **single JSON parse** per message (dedicated decode struct, no
secondary fields-map unmarshal)
- Documents why `json.RawMessage` is used for `method`
- Adds regression coverage so normal responses and id-only payloads
without a `method` key stay on the response path

## Test plan

- [x] `go test ./internal/jsonrpc2/... -v`
- [x] `go test ./mcp/... -run TestIOConnRead_EmptyMethod -v`
- [x] `go test ./... -count=1`
- [x] `go test -race ./internal/jsonrpc2/... -count=1`
- [x] Server conformance (`npx @modelcontextprotocol/conformance server
--url http://127.0.0.1:3000/mcp`) — 40 passed, 0 failed
- [x] Client conformance (`./scripts/client-conformance.sh`) — 217
passed, 0 failed

## Scope note

This PR addresses the primary repro in #976 (empty `method` with valid
`id`). Other cases listed in the issue (`id: null`, structurally invalid
envelopes) are unchanged.

Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
This commit is contained in:
Piyush Bag 2026-06-16 00:57:18 -07:00 • committed by GitHub
commit bcbfe0ce36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -124,3 +124,26 @@ func TestIOConnRead(t *testing.T) {
})
}
}
// go-sdk#976: stdio must surface empty-method calls as requests, not responses.
func TestIOConnRead_EmptyMethod(t *testing.T) {
tr := newIOConn(rwc{
rc: io.NopCloser(strings.NewReader(`{"jsonrpc":"2.0","id":5,"method":"","params":{}}`)),
})
t.Cleanup(func() { tr.Close() })
msg, err := tr.Read(context.Background())
if err != nil {
t.Fatalf("ioConn.Read() error = %v", err)
}
req, ok := msg.(*jsonrpc.Request)
if !ok {
t.Fatalf("message type = %T, want *jsonrpc.Request", msg)
}
if req.Method != "" {
t.Errorf("Method = %q, want empty string", req.Method)
}
if req.ID != jsonrpc2.Int64ID(5) {
t.Errorf("ID = %v, want 5", req.ID.Raw())
}
}