Describe the bug
After a client sends notifications/cancelled for an in-flight request, the server still writes a response for that request once the handler returns. The stdio transport page for 2026-07-28 says a server "SHOULD stop work on a cancelled request as soon as practical and MUST NOT send any further messages for it", and the cancellation pattern page says the receiver should not send a response for a cancelled request.
The handler's context is cancelled correctly; what is missing is dropping the reply. internal/jsonrpc2/conn.go processResult forwards whatever the handler returned as the response regardless of whether the request was cancelled by the peer.
To Reproduce
Test against v1.7.0, in-memory transports, the server side wrapped in a LoggingTransport:
server := mcp.NewServer(&mcp.Implementation{Name: "slow", Version: "1"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "sleep", InputSchema: map[string]any{"type": "object"}},
func(ctx context.Context, _ *mcp.CallToolRequest, _ map[string]any) (*mcp.CallToolResult, any, error) {
<-ctx.Done()
return nil, nil, ctx.Err()
})
clientT, serverT := mcp.NewInMemoryTransports()
var wire bytes.Buffer
ss, _ := server.Connect(ctx, &mcp.LoggingTransport{Transport: serverT, Writer: &wire}, nil)
cs, _ := mcp.NewClient(&mcp.Implementation{Name: "c", Version: "1"}, nil).Connect(ctx, clientT, nil)
callCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
cs.CallTool(callCtx, &mcp.CallToolParams{Name: "sleep", Arguments: map[string]any{}})
time.Sleep(300 * time.Millisecond)
fmt.Println(wire.String())
The server wire, trimmed:
read: {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{...,"name":"sleep","arguments":{}}}
read: {"jsonrpc":"2.0","method":"notifications/cancelled","params":{"reason":"context deadline exceeded","requestId":2}}
write: {"jsonrpc":"2.0","id":2,"result":{"_meta":{...},"content":[{"type":"text","text":"context canceled"}],"isError":true,"resultType":"complete"}}
The same happens over stdio, where I first saw it, with the reply appearing about a second after the notification.
Expected behavior
No message with id: 2 after the cancellation notification. Clients are told to ignore such a response, so the impact is low, but it is a wire violation, and over stdio it is the one message the spec singles out as forbidden.
Additional context
go-sdk v1.7.0. Related but distinct from #1212, which was about the shape of the notification itself.
Describe the bug
After a client sends
notifications/cancelledfor an in-flight request, the server still writes a response for that request once the handler returns. The stdio transport page for 2026-07-28 says a server "SHOULD stop work on a cancelled request as soon as practical and MUST NOT send any further messages for it", and the cancellation pattern page says the receiver should not send a response for a cancelled request.The handler's context is cancelled correctly; what is missing is dropping the reply.
internal/jsonrpc2/conn.goprocessResultforwards whatever the handler returned as the response regardless of whether the request was cancelled by the peer.To Reproduce
Test against v1.7.0, in-memory transports, the server side wrapped in a
LoggingTransport:The server wire, trimmed:
The same happens over stdio, where I first saw it, with the reply appearing about a second after the notification.
Expected behavior
No message with
id: 2after the cancellation notification. Clients are told to ignore such a response, so the impact is low, but it is a wire violation, and over stdio it is the one message the spec singles out as forbidden.Additional context
go-sdk v1.7.0. Related but distinct from #1212, which was about the shape of the notification itself.