Skip to content

Commit f4090bd

Browse files
committed
Fix stdio_client BrokenResourceError race condition during shutdown
During shutdown, the finally block closes the receiving end of the read_stream memory channel while stdout_reader may still be blocked on send(). This causes BrokenResourceError (not ClosedResourceError) because the *other* end of the stream was closed. The existing except clause only caught ClosedResourceError, letting BrokenResourceError propagate into an ExceptionGroup. Catch BrokenResourceError alongside ClosedResourceError in both stdout_reader and stdin_writer to handle this race gracefully. Closes #1960 Github-Issue:#1960 Reported-by:maxisbey
1 parent 62eb08e commit f4090bd

File tree

2 files changed

+40
-2
lines changed

2 files changed

+40
-2
lines changed

src/mcp/client/stdio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ async def stdout_reader():
158158

159159
session_message = SessionMessage(message)
160160
await read_stream_writer.send(session_message)
161-
except anyio.ClosedResourceError: # pragma: lax no cover
161+
except (anyio.ClosedResourceError, anyio.BrokenResourceError): # pragma: lax no cover
162162
await anyio.lowlevel.checkpoint()
163163

164164
async def stdin_writer():
@@ -174,7 +174,7 @@ async def stdin_writer():
174174
errors=server.encoding_error_handler,
175175
)
176176
)
177-
except anyio.ClosedResourceError: # pragma: no cover
177+
except (anyio.ClosedResourceError, anyio.BrokenResourceError): # pragma: no cover
178178
await anyio.lowlevel.checkpoint()
179179

180180
async with anyio.create_task_group() as tg, process:

tests/client/test_stdio.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,44 @@ def handle_term(sig, frame):
508508
pass
509509

510510

511+
@pytest.mark.anyio
512+
@pytest.mark.filterwarnings("ignore::ResourceWarning")
513+
async def test_stdio_client_no_broken_resource_error_on_shutdown():
514+
"""Test that exiting stdio_client without consuming the read stream does not
515+
raise BrokenResourceError.
516+
517+
Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/1960.
518+
The race condition occurs when stdout_reader is blocked on send() into a
519+
zero-buffer memory stream and the finally block closes the receiving end,
520+
causing BrokenResourceError instead of ClosedResourceError.
521+
"""
522+
# Server sends a JSON-RPC message and then sleeps, keeping stdout open.
523+
# The client exits without consuming the read stream, triggering the race.
524+
server_script = textwrap.dedent(
525+
"""
526+
import sys
527+
import time
528+
529+
sys.stdout.write('{"jsonrpc":"2.0","id":1,"result":{}}\\n')
530+
sys.stdout.flush()
531+
time.sleep(5.0)
532+
"""
533+
)
534+
535+
server_params = StdioServerParameters(
536+
command=sys.executable,
537+
args=["-c", server_script],
538+
)
539+
540+
# This should exit cleanly without raising an ExceptionGroup
541+
# containing BrokenResourceError.
542+
with anyio.fail_after(10.0):
543+
async with stdio_client(server_params) as (_read_stream, _write_stream):
544+
# Give stdout_reader time to read the message and block on send()
545+
await anyio.sleep(0.3)
546+
# Exit without consuming read_stream - this triggers the race
547+
548+
511549
@pytest.mark.anyio
512550
async def test_stdio_client_graceful_stdin_exit():
513551
"""Test that a process exits gracefully when stdin is closed,

0 commit comments

Comments
 (0)