Skip to content

Commit 7591a78

Browse files
committed
fix: set newline='' on TextIOWrapper in stdio_server to prevent Windows CRLF corruption
On Windows, TextIOWrapper defaults to platform-native line endings (\r\n), which corrupts newline-delimited JSON-RPC messages over stdio. Add newline='' to both the stdin and stdout TextIOWrapper constructors in stdio_server(), matching the pattern from PR #2302. Fixes #2433
1 parent 161834d commit 7591a78

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

src/mcp/server/stdio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.
3939
# python is platform-dependent (Windows is particularly problematic), so we
4040
# re-wrap the underlying binary stream to ensure UTF-8.
4141
if not stdin:
42-
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
42+
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace", newline=""))
4343
if not stdout:
44-
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
44+
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline=""))
4545

4646
read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
4747
write_stream, write_stream_reader = create_context_streams[SessionMessage](0)

tests/server/test_stdio.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import io
22
import sys
3+
import unittest.mock
34
from io import TextIOWrapper
45

56
import anyio
@@ -92,3 +93,33 @@ async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):
9293
second = await read_stream.receive()
9394
assert isinstance(second, SessionMessage)
9495
assert second.message == valid
96+
97+
98+
@pytest.mark.anyio
99+
async def test_stdio_server_default_textiowrapper_newline(monkeypatch: pytest.MonkeyPatch):
100+
"""Default TextIOWrapper must use newline='' to prevent CRLF corruption on Windows.
101+
102+
On Windows, TextIOWrapper defaults to platform-native line endings (\r\n),
103+
which corrupts newline-delimited JSON-RPC messages over stdio. Verify that
104+
stdio_server() passes newline='' when creating the wrappers.
105+
"""
106+
mock_stdin = io.BytesIO()
107+
mock_stdout = io.BytesIO()
108+
monkeypatch.setattr(sys, "stdin", type("S", (), {"buffer": mock_stdin})())
109+
monkeypatch.setattr(sys, "stdout", type("S", (), {"buffer": mock_stdout})())
110+
111+
created_kwargs: list[dict] = []
112+
real_textiowrapper = TextIOWrapper
113+
114+
def capturing_textiowrapper(*args: object, **kwargs: object) -> TextIOWrapper:
115+
created_kwargs.append(kwargs)
116+
return real_textiowrapper(*args, **kwargs)
117+
118+
with unittest.mock.patch("mcp.server.stdio.TextIOWrapper", side_effect=capturing_textiowrapper):
119+
async with stdio_server() as (read_stream, write_stream):
120+
await write_stream.aclose()
121+
await read_stream.aclose()
122+
123+
assert len(created_kwargs) == 2
124+
assert created_kwargs[0].get("newline") == "", "stdin TextIOWrapper must set newline=''"
125+
assert created_kwargs[1].get("newline") == "", "stdout TextIOWrapper must set newline=''"

0 commit comments

Comments
 (0)