Title: fix(stdio): handle BrokenResourceError in stdout_reader race (#1960) by ddullah · Pull Request #2450 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: fix(stdio): handle BrokenResourceError in stdout_reader race (#1960) by ddullah · Pull Request #2450 · modelcontextprotocol/python-sdk
X Title: fix(stdio): handle BrokenResourceError in stdout_reader race (#1960) by ddullah · Pull Request #2450 · modelcontextprotocol/python-sdk
Description: Summary Fixes #1960. Paired with #2449 (the matching [v1.x] backport). stdio_client has a shutdown race: read_stream_writer.aclose() in the context's finally block can close the receiver while the background stdout_reader task is mid-send. anyio raises BrokenResourceError, which the outer except does not cover (ClosedResourceError is the sibling class, raised on already-closed streams, not streams closed during an in-flight send). The exception propagates through the task group as ExceptionGroup and fails every caller that exits the context while the subprocess is still writing to stdout. Trigger Any MCP server that writes a burst of output (startup banners, log-level notifications/message frames, scheduler init messages, etc.) paired with a caller that opens a short-lived stdio_client per invocation. The task group exits before the subprocess drains, and the race fires. Reproduced against jules-mcp-server — which emits three notifications/message frames on init — driven by mcp2cli. Every tool call surfaced as: ``` ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) +-+---------------- 1 ---------------- | Traceback (most recent call last): | File '.../mcp/client/stdio/init.py', line 162, in stdout_reader | await read_stream_writer.send(session_message) | File '.../anyio/streams/memory.py', line 213, in send_nowait | raise BrokenResourceError | anyio.BrokenResourceError ``` Fix Wrap both read_stream_writer.send(...) sites in stdout_reader with try/except (ClosedResourceError, BrokenResourceError): return, and widen the outer except to the same union for defense in depth. stdout_reader now shuts down cleanly no matter how abruptly the caller exits the context. No API changes. Alternative considered #1960 also suggests tg.cancel_scope.cancel() before the stream closes in finally. That works but changes shutdown ordering (cancels user-defined notification handlers, etc.). The chosen fix is narrower: treat the shutdown race as a graceful exit from stdout_reader specifically, without altering the task group lifecycle. Tests Added test_stdio_client_exits_cleanly_while_server_still_writing in tests/client/test_stdio.py. Spawns a subprocess that writes a thousand valid JSONRPC notifications, exits the stdio_client context immediately, asserts no exception propagates. Wrapped in anyio.fail_after(5.0) per AGENTS.md. Fails before the fix (ExceptionGroup / BrokenResourceError), passes after. Closes #1960.
Open Graph Description: Summary Fixes #1960. Paired with #2449 (the matching [v1.x] backport). stdio_client has a shutdown race: read_stream_writer.aclose() in the context's finally block can close the receiver while ...
X Description: Summary Fixes #1960. Paired with #2449 (the matching [v1.x] backport). stdio_client has a shutdown race: read_stream_writer.aclose() in the context's finally block can close the receiver wh...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/2450
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:557f0fde-9213-0362-c998-3a6d81216a3b |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | AFF4:182F17:13FB2BF:1C4E9BF:6A5A347C |
| html-safe-nonce | 58975b2ce41112c8994ea1f0797f0f89f6fd0136fb8c574b98ef22554b427645 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJBRkY0OjE4MkYxNzoxM0ZCMkJGOjFDNEU5QkY6NkE1QTM0N0MiLCJ2aXNpdG9yX2lkIjoiNDU5NzQxMjUyOTYyNzA4MzkwMCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 7eb9caa36865a659013622fce81e2062868058a267ce8092f4725b39a315faab |
| hovercard-subject-tag | pull_request:3534396414 |
| 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/2450/files |
| twitter:image | https://avatars.githubusercontent.com/u/134097561?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/134097561?s=400&v=4 |
| og:image:alt | Summary Fixes #1960. Paired with #2449 (the matching [v1.x] backport). stdio_client has a shutdown race: read_stream_writer.aclose() in the context's finally block can close the receiver while ... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 19c67ad7579d2401ff06e88a32ef45460fbb40fab3191c7f7582d5e166f326fc |
| 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 | a3dea2706161c733403beb02e41d5765d358d44e |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width