Skip to content

Commit 7307434

Browse files
fix: add newline="" to stdio TextIOWrapper to prevent CRLF on Windows
Closes #2433
1 parent 5cbd259 commit 7307434

File tree

2 files changed

+36
-2
lines changed

2 files changed

+36
-2
lines changed

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,40 @@ async def test_stdio_server():
6363
assert received_responses[1] == JSONRPCResponse(jsonrpc="2.0", id=4, result={})
6464

6565

66+
@pytest.mark.anyio
67+
async def test_stdio_server_no_crlf(monkeypatch: pytest.MonkeyPatch):
68+
"""Raw bytes written to stdout must use LF (\\n), never CRLF (\\r\\n).
69+
70+
On Windows, TextIOWrapper with the default newline=None translates \\n to
71+
\\r\\n on write, which corrupts NDJSON framing for JSON-RPC. The fix is to
72+
pass newline="" to TextIOWrapper so no translation occurs.
73+
"""
74+
raw_stdout = io.BytesIO()
75+
# Wrap with newline="" so we can inspect the exact bytes that
76+
# stdio_server writes. The key assertion is that the raw bytes
77+
# contain \n and never \r\n.
78+
stdout_wrapper = TextIOWrapper(raw_stdout, encoding="utf-8", newline="")
79+
stdin_wrapper = TextIOWrapper(io.BytesIO(b""), encoding="utf-8")
80+
81+
message = JSONRPCRequest(jsonrpc="2.0", id=1, method="ping")
82+
83+
with anyio.fail_after(5):
84+
async with stdio_server(
85+
stdin=anyio.AsyncFile(stdin_wrapper),
86+
stdout=anyio.AsyncFile(stdout_wrapper),
87+
) as (read_stream, write_stream):
88+
async with write_stream:
89+
await write_stream.send(SessionMessage(message))
90+
async with read_stream:
91+
pass
92+
93+
stdout_wrapper.flush()
94+
raw_bytes = raw_stdout.getvalue()
95+
assert len(raw_bytes) > 0, "expected output bytes"
96+
assert raw_bytes.endswith(b"\n"), "output must end with LF"
97+
assert b"\r\n" not in raw_bytes, "output must not contain CRLF"
98+
99+
66100
@pytest.mark.anyio
67101
async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):
68102
"""Non-UTF-8 bytes on stdin must not crash the server.

0 commit comments

Comments
 (0)