Skip to content

Commit 73c70ca

Browse files
author
Nova
committed
fix: prevent stdio transport from closing real process stdin/stdout
The stdio server transport wraps sys.stdin.buffer and sys.stdout.buffer with TextIOWrapper for UTF-8 encoding. TextIOWrapper calls close() in __del__, which closes the underlying buffer. When the server exits and the AsyncFile wrappers are garbage collected, this closes the real process stdio file descriptors (fd 0 and fd 1). This causes ValueError on subsequent print() or input() calls in the parent process after the MCP server exits (e.g., Ctrl+D exit). Fix: use _NoCloseTextIOWrapper that overrides close() and __del__() to prevent closing the underlying buffer. The standard process handles should outlive the server. Fixes #1933
1 parent b478bff commit 73c70ca

1 file changed

Lines changed: 21 additions & 2 deletions

File tree

src/mcp/server/stdio.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,25 @@ async def run_server():
2929
from mcp.shared.message import SessionMessage
3030

3131

32+
class _NoCloseTextIOWrapper(TextIOWrapper):
33+
"""A TextIOWrapper that does not close the underlying buffer on garbage collection.
34+
35+
Standard TextIOWrapper calls close() in __del__, which closes the underlying
36+
buffer. When wrapping sys.stdin.buffer or sys.stdout.buffer, this causes the
37+
real process stdio to be closed after the server exits, breaking subsequent
38+
print() or input() calls in the parent process.
39+
"""
40+
41+
def close(self) -> None:
42+
# Intentionally not closing the underlying buffer.
43+
# The standard process handles should outlive the server.
44+
pass
45+
46+
def __del__(self) -> None:
47+
# Prevent TextIOWrapper.__del__ from calling close().
48+
pass
49+
50+
3251
@asynccontextmanager
3352
async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.AsyncFile[str] | None = None):
3453
"""Server transport for stdio: this communicates with an MCP client by reading
@@ -39,9 +58,9 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
3958
# python is platform-dependent (Windows is particularly problematic), so we
4059
# re-wrap the underlying binary stream to ensure UTF-8.
4160
if not stdin:
42-
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
61+
stdin = anyio.wrap_file(_NoCloseTextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
4362
if not stdout:
44-
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
63+
stdout = anyio.wrap_file(_NoCloseTextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
4564

4665
read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
4766
write_stream, write_stream_reader = create_context_streams[SessionMessage](0)

0 commit comments

Comments
 (0)