Skip to content

Commit 56c0fed

Browse files
committed
Fix #1933: Prevent closing real stdio when server exits
When using transport="stdio", the server was closing sys.stdin.buffer and sys.stdout.buffer when exiting, causing subsequent stdio operations to fail with ValueError: I/O operation on closed file. This fix uses os.dup() to create duplicate file descriptors, so the original stdin/stdout remain open when the wrapper streams are closed. Fixes #1933
1 parent d5b9155 commit 56c0fed

File tree

1 file changed

+21
-13
lines changed

1 file changed

+21
-13
lines changed

src/mcp/server/stdio.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
"""Stdio Server Transport Module
22
33
This module provides functionality for creating an stdio-based transport layer
4-
that can be used to communicate with an MCP client through standard input/output
5-
streams.
4+
that can be used to communicate with an MCP client through standard input/output streams.
65
76
Example:
8-
```python
9-
async def run_server():
10-
async with stdio_server() as (read_stream, write_stream):
11-
# read_stream contains incoming JSONRPCMessages from stdin
12-
# write_stream allows sending JSONRPCMessages to stdout
13-
server = await create_my_server()
14-
await server.run(read_stream, write_stream, init_options)
7+
```python
8+
async def run_server():
9+
async with stdio_server() as (read_stream, write_stream):
10+
# read_stream contains incoming JSONRPCMessages from stdin
11+
# write_stream allows sending JSONRPCMessages to stdout
12+
server = await create_my_server()
13+
await server.run(read_stream, write_stream, init_options)
1514
16-
anyio.run(run_server)
17-
```
15+
anyio.run(run_server)
16+
```
1817
"""
1918

19+
import os
2020
import sys
2121
from contextlib import asynccontextmanager
2222
from io import TextIOWrapper
@@ -38,10 +38,18 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
3838
# standard process handles. Encoding of stdin/stdout as text streams on
3939
# python is platform-dependent (Windows is particularly problematic), so we
4040
# re-wrap the underlying binary stream to ensure UTF-8.
41+
#
42+
# Fix #1933: Use os.dup() to avoid closing the original stdin/stdout
43+
# when the wrapper is closed, preventing "I/O operation on closed file" errors.
4144
if not stdin:
42-
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
45+
stdin_fd = os.dup(sys.stdin.fileno())
46+
stdin_bin = os.fdopen(stdin_fd, "rb", closefd=True)
47+
stdin = anyio.wrap_file(TextIOWrapper(stdin_bin, encoding="utf-8", errors="replace"))
48+
4349
if not stdout:
44-
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
50+
stdout_fd = os.dup(sys.stdout.fileno())
51+
stdout_bin = os.fdopen(stdout_fd, "wb", closefd=True)
52+
stdout = anyio.wrap_file(TextIOWrapper(stdout_bin, encoding="utf-8"))
4553

4654
read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
4755
write_stream, write_stream_reader = create_context_streams[SessionMessage](0)

0 commit comments

Comments
 (0)