Title: fix: auto-send notifications/cancelled on anyio task cancellation by cubaseuser123 · Pull Request #2479 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: fix: auto-send notifications/cancelled on anyio task cancellation by cubaseuser123 · Pull Request #2479 · modelcontextprotocol/python-sdk
X Title: fix: auto-send notifications/cancelled on anyio task cancellation by cubaseuser123 · Pull Request #2479 · modelcontextprotocol/python-sdk
Description: Fixes #1410. Motivation and Context The friction here is pretty specific: when you use high-level request wrappers like client.call_tool(), the generated request_id stays hidden internally inside send_request. So if you need to abort that task from the outside using standard Python async patterns (like anyio.fail_after or CancelScope.cancel()), the client aborts locally, but it has no way to tell the server. The server continues executing an orphaned request that the client has already stopped waiting for. So I moved the cancellation hook into BaseSession.send_request. The idea is simple: intercept the anyio cancellation before it bubbles up, read the request_id we just generated, and automatically dispatch a notifications/cancelled straight to the peer. How Has This Been Tested? I made sure it runs cleanly end-to-end. Added test_anyio_cancel_scope_sends_cancelled_notification to tests/server/test_cancel_handling.py which: Kicks off call_tool Triggers an outer CancelScope.cancel() Asserts the server handler actually got aborted (Side note: Verified locally against the full regression suite using anyio 4.10.0 on Windows.) Breaking Changes None. 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 A few quick implementation details: It uses a request_sent flag: Cancels are only dispatched if the request actually hit the wire (so we don't spam the server for requests that failed on _write_stream.send()). It's shielded: The dispatch runs inside with anyio.CancelScope(shield=True) so it survives the very cancellation it's reacting to. It has a deadlock timeout: Added with anyio.move_on_after(2.0) inside the shield. If the transport buffer is completely full, we don't want to hang the user's cancellation handler forever. It's bidirectional: Since this lives in BaseSession, it automatically works for both client→server and server→client directions. Happy to tweak the timeout value or the notification reason string if there's a preference. Let me know if I should squash anything. Just a heads-up for reviewers — the CI failures here are pre-existing and not related to the changes in this PR. The pyright hook in .pre-commit-config.yaml runs against the whole repo. Currently, the upstream main branch (commit 3d7b311) has 21 Pyright errors located entirely in src/mcp/os/win32/utilities.py. Because pre-commit is configured with fail_fast: true, it aborts early and cascades all 20 test matrix jobs (checks / test) to fail with exit code 2 before Pytest even gets a chance to run. (I noticed this is happening on other recent PRs like #2475 as well). I have verified the test suite locally using parallel workers (pytest -n auto): 1170 passed, 101 skipped, 1 xfailed, exit 0. uv run pyright src/mcp/shared/session.py returns 0 errors. Happy to rebase to trigger a clean run once the upstream Pyright issue on main is resolved, or if a maintainer wants to merge this as-is!
Open Graph Description: Fixes #1410. Motivation and Context The friction here is pretty specific: when you use high-level request wrappers like client.call_tool(), the generated request_id stays hidden internally inside s...
X Description: Fixes #1410. Motivation and Context The friction here is pretty specific: when you use high-level request wrappers like client.call_tool(), the generated request_id stays hidden internally inside s...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/2479
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:9ed9a030-3907-abdb-8716-f0dc86470172 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | B0CE:258B52:523252:6DBE8F:6A5B8F08 |
| html-safe-nonce | 0e9f33fb454fb2933b8afb662719e7e61ede61c4f27ab7a838e698d880bcaaff |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCMENFOjI1OEI1Mjo1MjMyNTI6NkRCRThGOjZBNUI4RjA4IiwidmlzaXRvcl9pZCI6IjM1ODYwOTEyODY0MDYwNzQxMjAiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | f290eac258c2f393d137d7d52d44fae41f74793abe9ad1f2236f41f8d0a19b6a |
| hovercard-subject-tag | pull_request:3554767822 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/2479/files |
| twitter:image | https://avatars.githubusercontent.com/u/71630194?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/71630194?s=400&v=4 |
| og:image:alt | Fixes #1410. Motivation and Context The friction here is pretty specific: when you use high-level request wrappers like client.call_tool(), the generated request_id stays hidden internally inside s... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width