@@ -17,6 +17,8 @@ async def run_server():
1717 ```
1818"""
1919
20+ import io
21+ import os
2022import sys
2123from contextlib import asynccontextmanager
2224from io import TextIOWrapper
@@ -34,14 +36,32 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
3436 """Server transport for stdio: this communicates with an MCP client by reading
3537 from the current process' stdin and writing to stdout.
3638 """
37- # Purposely not using context managers for these, as we don't want to close
38- # standard process handles. Encoding of stdin/stdout as text streams on
39- # python is platform-dependent (Windows is particularly problematic), so we
40- # re-wrap the underlying binary stream to ensure UTF-8.
39+ # When stdin/stdout are not provided, duplicate the underlying file descriptors
40+ # so that closing the wrappers does not close the real sys.stdin/sys.stdout.
41+ # Encoding of stdin/stdout as text streams on Python is platform-dependent
42+ # (Windows is particularly problematic), so we re-wrap the underlying binary
43+ # stream to ensure UTF-8.
44+ _stdin_wrapper : TextIOWrapper | None = None
45+ _stdout_wrapper : TextIOWrapper | None = None
46+
4147 if not stdin :
42- stdin = anyio .wrap_file (TextIOWrapper (sys .stdin .buffer , encoding = "utf-8" , errors = "replace" ))
48+ try :
49+ stdin_fd = os .dup (sys .stdin .fileno ())
50+ _stdin_wrapper = TextIOWrapper (os .fdopen (stdin_fd , "rb" ), encoding = "utf-8" , errors = "replace" )
51+ stdin = anyio .wrap_file (_stdin_wrapper )
52+ except (AttributeError , io .UnsupportedOperation ):
53+ # sys.stdin has no real fd (e.g. BytesIO in tests) — wrap buffer directly.
54+ # Closing this wrapper also closes the buffer, but that is harmless in
55+ # that context because there is no real fd to leak.
56+ stdin = anyio .wrap_file (TextIOWrapper (sys .stdin .buffer , encoding = "utf-8" , errors = "replace" ))
4357 if not stdout :
44- stdout = anyio .wrap_file (TextIOWrapper (sys .stdout .buffer , encoding = "utf-8" ))
58+ try :
59+ stdout_fd = os .dup (sys .stdout .fileno ())
60+ _stdout_wrapper = TextIOWrapper (os .fdopen (stdout_fd , "wb" ), encoding = "utf-8" )
61+ stdout = anyio .wrap_file (_stdout_wrapper )
62+ except (AttributeError , io .UnsupportedOperation ):
63+ # sys.stdout has no real fd — wrap buffer directly (same reasoning as above).
64+ stdout = anyio .wrap_file (TextIOWrapper (sys .stdout .buffer , encoding = "utf-8" ))
4565
4666 read_stream_writer , read_stream = create_context_streams [SessionMessage | Exception ](0 )
4767 write_stream , write_stream_reader = create_context_streams [SessionMessage ](0 )
@@ -71,7 +91,13 @@ async def stdout_writer():
7191 except anyio .ClosedResourceError : # pragma: no cover
7292 await anyio .lowlevel .checkpoint ()
7393
74- async with anyio .create_task_group () as tg :
75- tg .start_soon (stdin_reader )
76- tg .start_soon (stdout_writer )
77- yield read_stream , write_stream
94+ try :
95+ async with anyio .create_task_group () as tg :
96+ tg .start_soon (stdin_reader )
97+ tg .start_soon (stdout_writer )
98+ yield read_stream , write_stream
99+ finally :
100+ if _stdout_wrapper is not None :
101+ _stdout_wrapper .close ()
102+ if _stdin_wrapper is not None :
103+ _stdin_wrapper .close ()
0 commit comments