modelcontextprotocol / modelcontextprotocol/go-sdk

A response is still written for a request after notifications/cancelled

Open
#1,235 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

P2
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.