Commit 157a3e8
committed
fix(stdio): resolve BrokenResourceError race condition on quick exit
ROOT CAUSE:
The stdio_client async context manager had a race condition when exiting
quickly (before subprocess finished outputting data). The cleanup code in
the finally block closed memory streams while background tasks (stdout_reader
and stdin_writer) were still using them, resulting in BrokenResourceError.
Timeline of the bug:
1. User code exits the async with stdio_client(...) context
2. The finally block executes
3. Streams are closed immediately
4. Background tasks are still running and trying to send/receive data
5. Tasks encounter closed streams → BrokenResourceError
CHANGES:
1. Added BrokenResourceError to exception handlers in both client and server
- src/mcp/client/stdio.py: Lines 161, 177 (stdout_reader, stdin_writer)
- src/mcp/server/stdio.py: Lines 67, 77 (stdin_reader, stdout_writer)
- This allows tasks to exit gracefully if streams close during operation
2. Added task cancellation before stream closure in client transport
- src/mcp/client/stdio.py: Line 210 (after process cleanup)
- tg.cancel_scope.cancel() sends cancellation signal to background tasks
- Tasks receive signal and finish their current operation
- Then streams are closed (tasks aren't using them anymore)
3. Added regression test
- tests/client/test_stdio.py: test_stdio_client_quick_exit_race_condition
- Verifies that quick context exits don't cause ExceptionGroup crashes
IMPACT:
- No more ExceptionGroup crashes when exiting quickly
- Graceful task shutdown with proper cancellation
- Backward compatible - all existing tests pass
- Better resource cleanup - tasks finish before streams close
TECHNICAL NOTES:
- Server transport only needed exception handler changes (not task cancellation)
because it doesn't manage subprocess lifecycle
- The fix uses defense-in-depth: both proper coordination AND graceful handling
- anyio.BrokenResourceError is raised when operations are attempted on closed
resources, distinct from ClosedResourceError (resource already closed)
FILES MODIFIED:
- src/mcp/client/stdio.py
- src/mcp/server/stdio.py
- tests/client/test_stdio.py1 parent 2fe56e5 commit 157a3e8
File tree
3 files changed
+56
-4
lines changed- src/mcp
- client
- server
- tests/client
3 files changed
+56
-4
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
| 161 | + | |
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| |||
174 | 174 | | |
175 | 175 | | |
176 | 176 | | |
177 | | - | |
| 177 | + | |
178 | 178 | | |
179 | 179 | | |
180 | 180 | | |
| |||
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
208 | 213 | | |
209 | 214 | | |
210 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
| 67 | + | |
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
77 | | - | |
| 77 | + | |
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
620 | 620 | | |
621 | 621 | | |
622 | 622 | | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
0 commit comments