Skip to content

mcp: keep a delivered response when the caller's context is cancelled - #1370

Merged
guglielmo-san merged 3 commits into
modelcontextprotocol:mainfrom
vbcherepanov:fix/call-cancel-delivered-response
Oct 13, 2026
Merged

guglielmo-san merged 3 commits into
modelcontextprotocol:mainfrom
vbcherepanov:fix/call-cancel-delivered-response

Conversation

@vbcherepanov

@vbcherepanov vbcherepanov commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

A client call could return context.Canceled for a request whose response had already arrived. call in mcp/transport.go checked ctx.Err() before it checked whether Await had succeeded, so a context that was cancelled while the result was being decoded discarded the result, retired the call and sent notifications/cancelled for a request that was already complete.

This change adds a case err == nil at the top of the switch in call, so a successful Await returns the result. Everything else is unchanged: a request that is genuinely in flight is retired eagerly and the notification is sent off the caller's return path, as introduced for #1150.

The specification permits cancellation notifications only for requests "believed to still be in-progress" (2025-11-25 and 2026-07-28, Cancellation, Behavior Requirements).

Fixes #1368

Tested with go test -race ./..., go vet and staticcheck v0.6.1. Following review, the PR has no test of its own: the race is hard to reproduce without a custom transport.

Comment thread mcp/transport.go
Comment on lines +304 to +309
case err == nil:
// The response arrived and decoded. It is returned even if ctx has
// been cancelled in the meantime: the request is complete, so there
// is nothing to cancel, and the spec permits notifications/cancelled
// only for requests believed to still be in progress.
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the only change i would keep. This PR is aiming to fix a very niche issue and I would prefer to keep the code less convoluted

@vbcherepanov vbcherepanov Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, done. The PR now only adds the case err == nil branch in call, plus the regression test. The Await change and its tests are gone. The test still fails on main and passes with the branch, so the one case covers it.

@vbcherepanov
vbcherepanov force-pushed the fix/call-cancel-delivered-response branch 2 times, most recently from 1e49108 to 9fc4206 Compare October 12, 2026 06:19
call checked ctx.Err() before it checked whether Await had succeeded, so a context cancelled while the result was being decoded discarded the result and sent notifications/cancelled for a completed request. Return the result when Await succeeds; a request still in flight is retired and notified as before.

Fixes modelcontextprotocol#1368
@vbcherepanov
vbcherepanov force-pushed the fix/call-cancel-delivered-response branch from 9fc4206 to ba70c87 Compare October 12, 2026 12:02
Comment thread mcp/transport_test.go Outdated
Comment on lines +370 to +377
// cancelAfterResponseTransport wraps a Transport so that a test can cancel a
// context at a point the client cannot observe from the outside: the first
// Read after the response to the given method has been delivered. The
// jsonrpc2 read loop dispatches a message before it reads the next one, so
// by then the call has been retired with its result while the caller may
// still be decoding it. The transport also records the methods written to
// it, so a test can check that no cancelled notification followed.
type cancelAfterResponseTransport struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we can also drop entirely this test and leave the code change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the test is removed. The PR is now only the case err == nil branch in call.

@guglielmo-san
guglielmo-san merged commit b04b49e into modelcontextprotocol:main Oct 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants