Skip to content

Commit 6cf8179

Browse files
sherlock-488claudemaxisbey
committed
fix: align Context logging methods with MCP spec data type (#2366)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
1 parent 3d7b311 commit 6cf8179

1 file changed

Lines changed: 36 additions & 10 deletions

File tree

src/mcp/server/stdio.py

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

20+
import io
21+
import os
2022
import sys
2123
from contextlib import asynccontextmanager
2224
from 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

Comments
 (0)