@@ -23,6 +23,7 @@ async def run_server():
2323
2424import anyio
2525import anyio .lowlevel
26+ from anyio .to_thread import run_sync
2627
2728from mcp import types
2829from mcp .shared ._context_streams import create_context_streams
@@ -38,8 +39,16 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
3839 # standard process handles. Encoding of stdin/stdout as text streams on
3940 # python is platform-dependent (Windows is particularly problematic), so we
4041 # re-wrap the underlying binary stream to ensure UTF-8.
41- if not stdin :
42- stdin = anyio .wrap_file (TextIOWrapper (sys .stdin .buffer , encoding = "utf-8" , errors = "replace" ))
42+ #
43+ # When no custom stdin is provided, use a blocking read on sys.stdin.buffer
44+ # instead of anyio.wrap_file(). anyio.wrap_file() wraps the underlying file
45+ # as an async iterator, which raises StopAsyncIteration when the client closes
46+ # its end of stdin between connection cycles — this looks like EOF and kills
47+ # the read loop. Blocking sys.stdin.buffer.readline() survives transient
48+ # client disconnects by waiting forever until real process EOF.
49+ _read_stdin_raw = stdin is None
50+ if _read_stdin_raw :
51+ raw_stdin = TextIOWrapper (sys .stdin .buffer , encoding = "utf-8" , errors = "replace" )
4352 if not stdout :
4453 stdout = anyio .wrap_file (TextIOWrapper (sys .stdout .buffer , encoding = "utf-8" ))
4554
@@ -49,15 +58,30 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
4958 async def stdin_reader ():
5059 try :
5160 async with read_stream_writer :
52- async for line in stdin :
53- try :
54- message = types .jsonrpc_message_adapter .validate_json (line , by_name = False )
55- except Exception as exc :
56- await read_stream_writer .send (exc )
57- continue
58-
59- session_message = SessionMessage (message )
60- await read_stream_writer .send (session_message )
61+ if _read_stdin_raw :
62+ # Blocking read — survives client stdin close between cycles
63+ while True :
64+ line = await run_sync (raw_stdin .readline )
65+ if not line : # real process EOF
66+ break
67+ line = line .strip ()
68+ if not line :
69+ continue
70+ try :
71+ message = types .jsonrpc_message_adapter .validate_json (line , by_name = False )
72+ except Exception as exc :
73+ await read_stream_writer .send (exc )
74+ continue
75+ await read_stream_writer .send (SessionMessage (message ))
76+ else :
77+ # Async iterator path — for custom stdin (e.g., tests)
78+ async for line in stdin :
79+ try :
80+ message = types .jsonrpc_message_adapter .validate_json (line , by_name = False )
81+ except Exception as exc :
82+ await read_stream_writer .send (exc )
83+ continue
84+ await read_stream_writer .send (SessionMessage (message ))
6185 except anyio .ClosedResourceError : # pragma: no cover
6286 await anyio .lowlevel .checkpoint ()
6387
0 commit comments