Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/mcp/client/stdio.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ async def stdout_reader():

session_message = SessionMessage(message)
await read_stream_writer.send(session_message)
except anyio.ClosedResourceError: # pragma: lax no cover
except (anyio.ClosedResourceError, anyio.BrokenResourceError): # pragma: lax no cover
await anyio.lowlevel.checkpoint()

async def stdin_writer():
Expand All @@ -174,7 +174,7 @@ async def stdin_writer():
errors=server.encoding_error_handler,
)
)
except anyio.ClosedResourceError: # pragma: no cover
except (anyio.ClosedResourceError, anyio.BrokenResourceError): # pragma: no cover
await anyio.lowlevel.checkpoint()

async with anyio.create_task_group() as tg, process:
Expand Down
38 changes: 38 additions & 0 deletions tests/client/test_stdio.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,44 @@ def handle_term(sig, frame):
pass


@pytest.mark.anyio
@pytest.mark.filterwarnings("ignore::ResourceWarning")
async def test_stdio_client_no_broken_resource_error_on_shutdown():
"""Test that exiting stdio_client without consuming the read stream does not
raise BrokenResourceError.

Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/1960.
The race condition occurs when stdout_reader is blocked on send() into a
zero-buffer memory stream and the finally block closes the receiving end,
causing BrokenResourceError instead of ClosedResourceError.
"""
# Server sends a JSON-RPC message and then sleeps, keeping stdout open.
# The client exits without consuming the read stream, triggering the race.
server_script = textwrap.dedent(
"""
import sys
import time

sys.stdout.write('{"jsonrpc":"2.0","id":1,"result":{}}\\n')
sys.stdout.flush()
time.sleep(5.0)
"""
)

server_params = StdioServerParameters(
command=sys.executable,
args=["-c", server_script],
)

# This should exit cleanly without raising an ExceptionGroup
# containing BrokenResourceError.
with anyio.fail_after(10.0):
async with stdio_client(server_params) as (_read_stream, _write_stream):
# Give stdout_reader time to read the message and block on send()
await anyio.sleep(0.3)
# Exit without consuming read_stream - this triggers the race


@pytest.mark.anyio
async def test_stdio_client_graceful_stdin_exit():
"""Test that a process exits gracefully when stdin is closed,
Expand Down