Skip to content

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

Description

@ddromanidis

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues, valuable feature requests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions