Skip to content

Update session.py#2149

Open
DenisStefanAndrei wants to merge 2 commits intomodelcontextprotocol:mainfrom
DenisStefanAndrei:patch-1
Open

Update session.py#2149
DenisStefanAndrei wants to merge 2 commits intomodelcontextprotocol:mainfrom
DenisStefanAndrei:patch-1

Conversation

@DenisStefanAndrei
Copy link

@DenisStefanAndrei DenisStefanAndrei commented Feb 26, 2026

Fix: propagate CancelScope.exit return value in RequestResponder.exit

RequestResponder.__exit__ calls self._cancel_scope.__exit__() but did not
return its result. This prevented the cancel scope from suppressing
CancelledError when a request is cancelled via notifications/cancelled.

Motivation and Context

When a client sends a notifications/cancelled notification (e.g. after a
request timeout), the server calls RequestResponder.cancel() which cancels the
inner CancelScope. This raises CancelledError in the handler task, which is
a BaseException and not caught by the except Exception handlers in
_handle_request.

The CancelScope.__exit__ correctly identifies this as its own cancellation and
returns True to suppress it. However, RequestResponder.__exit__ discarded
that return value, implicitly returning None. Python treats a falsy return from
__exit__ as "do not suppress", so the CancelledError leaked out of the
with responder: block in _handle_message, crashed the server.run() task
group, and caused the session to tear down.

After teardown, the stdin_reader task in stdio_server remains alive (blocked
waiting for input on stdin). On the next incoming request, it attempts to write
to the now-closed read_stream, raising BrokenResourceError and crashing the
server process.

This bug is masked on Python 3.11 because asyncio's looser cancellation counter
tracking tolerates an unsuppressed CancelledError after task.uncancel() has
been called. Python 3.12 tightened this behavior, exposing the issue.

How Has This Been Tested?

Reproduced by connecting a Node.js MCP client (stdio transport) to a Python
FastMCP server. Triggering a request timeout causes the client SDK to send
notifications/cancelled. Without the fix, the next request to the server
crashes the process. With the fix, the server correctly suppresses the
CancelledError and continues serving subsequent requests.

Breaking Changes

None. This is a bugfix that restores the intended behavior of request
cancellation.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The fix is a single return keyword added to RequestResponder.__exit__ in
src/mcp/shared/session.py:

Before (broken):

self._cancel_scope.exit(exc_type, exc_val, exc_tb)

After (fixed):

return self._cancel_scope.exit(exc_type, exc_val, exc_tb)

@DenisStefanAndrei
Copy link
Author

@maxisbey thoughts on this?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant