Skip to content

Commit 4193e2f

Browse files
shenpeng.sp0916claude
andcommitted
fix(server): duplicate stdio fds to prevent closing real stdio
When using transport="stdio", the async wrappers around sys.stdin/sys.stdout would close the underlying file descriptors on server exit, causing subsequent operations like print() to fail with ValueError. Use os.dup() to duplicate the file descriptors before wrapping, so closing the async wrappers does not affect the original stdio streams. Fall back to the original behavior when the stream lacks a real fd (e.g. BytesIO in tests). Fixes #1933 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e8e6484 commit 4193e2f

1 file changed

Lines changed: 18 additions & 7 deletions

File tree

src/mcp/server/stdio.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ async def run_server():
1717
```
1818
"""
1919

20+
import os
2021
import sys
2122
from contextlib import asynccontextmanager
22-
from io import TextIOWrapper
23+
from io import TextIOWrapper, UnsupportedOperation
2324

2425
import anyio
2526
import anyio.lowlevel
@@ -34,14 +35,24 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
3435
"""Server transport for stdio: this communicates with an MCP client by reading
3536
from the current process' stdin and writing to stdout.
3637
"""
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.
38+
# Duplicate the underlying file descriptors so that closing the async
39+
# wrappers does not close the real stdio, which would break subsequent
40+
# operations (e.g. print) after the server exits. When the stream does
41+
# not have a real fd (e.g. in tests), fall back to the original stream.
4142
if not stdin:
42-
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
43+
try:
44+
stdin_fd = os.dup(sys.stdin.fileno())
45+
stdin_bin = os.fdopen(stdin_fd, "rb", closefd=True)
46+
stdin = anyio.wrap_file(TextIOWrapper(stdin_bin, encoding="utf-8", errors="replace"))
47+
except (OSError, UnsupportedOperation):
48+
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
4349
if not stdout:
44-
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
50+
try:
51+
stdout_fd = os.dup(sys.stdout.fileno())
52+
stdout_bin = os.fdopen(stdout_fd, "wb", closefd=True)
53+
stdout = anyio.wrap_file(TextIOWrapper(stdout_bin, encoding="utf-8"))
54+
except (OSError, UnsupportedOperation):
55+
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
4556

4657
read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
4758
write_stream, write_stream_reader = create_context_streams[SessionMessage](0)

0 commit comments

Comments
 (0)