Skip to content

Commit 5593617

Browse files
committed
fix: avoid stdio cleanup BrokenResourceError race
1 parent b33c811 commit 5593617

File tree

2 files changed

+36
-4
lines changed

2 files changed

+36
-4
lines changed

src/mcp/client/stdio.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,13 @@ async def stdin_writer():
205205
except ProcessLookupError: # pragma: no cover
206206
# Process already exited, which is fine
207207
pass
208-
await read_stream.aclose()
209-
await write_stream.aclose()
210-
await read_stream_writer.aclose()
211-
await write_stream_reader.aclose()
208+
# Stop background stream tasks before closing the memory streams they use.
209+
tg.cancel_scope.cancel()
210+
211+
await read_stream.aclose()
212+
await write_stream.aclose()
213+
await read_stream_writer.aclose()
214+
await write_stream_reader.aclose()
212215

213216

214217
def _get_executable_command(command: str) -> str:

tests/client/test_stdio.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,35 @@ async def test_stdio_client_universal_cleanup():
155155
)
156156

157157

158+
@pytest.mark.anyio
159+
async def test_stdio_client_cleanup_cancels_backpressured_stdout_reader():
160+
"""Regression test for issue #1960.
161+
162+
Exiting the client without consuming the read stream leaves stdout_reader
163+
blocked on a zero-buffer send. Cleanup must cancel the task before closing
164+
its memory stream.
165+
"""
166+
script_content = textwrap.dedent(
167+
"""
168+
import sys
169+
import time
170+
171+
sys.stdout.write('{"jsonrpc":"2.0","id":1,"result":{}}\\n')
172+
sys.stdout.flush()
173+
time.sleep(2.0)
174+
"""
175+
)
176+
177+
server_params = StdioServerParameters(
178+
command=sys.executable,
179+
args=["-c", script_content],
180+
)
181+
182+
with anyio.fail_after(5.0):
183+
async with stdio_client(server_params) as (_, _):
184+
await anyio.sleep(0.2)
185+
186+
158187
@pytest.mark.anyio
159188
@pytest.mark.skipif(sys.platform == "win32", reason="Windows signal handling is different")
160189
async def test_stdio_client_sigint_only_process(): # pragma: lax no cover

0 commit comments

Comments
 (0)