modelcontextprotocol / modelcontextprotocol/go-sdk
A response is still written for a request after notifications/cancelled
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 5.1k
- Forks
- 543
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 37
Description
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in internal/jsonrpc2/conn.go at processResult and run the in-memory transport reproduction from the issue with LoggingTransport. Verify that after notifications/cancelled, the server writes no message for the cancelled request, including over stdio.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100