Skip to content

Commit bd2e378

Browse files
author
Christian-Sidak
committed
fix: disable CRLF translation in stdio_server TextIOWrapper on Windows
Add newline="" to both TextIOWrapper calls in stdio_server() to prevent platform-dependent LF→CRLF translation on Windows, ensuring LF-only line endings in the NDJSON wire format on all platforms. Fixes #2433
1 parent d5b9155 commit bd2e378

File tree

2 files changed

+28
-2
lines changed

2 files changed

+28
-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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,32 @@ 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_uses_lf_newlines(monkeypatch: pytest.MonkeyPatch):
68+
"""stdio_server() must not emit CRLF on Windows (newline='' disables translation)."""
69+
70+
class UnclosableBuffer(io.BytesIO):
71+
"""BytesIO that ignores close() so we can read its value after the wrapper closes it."""
72+
73+
def close(self) -> None:
74+
pass # prevent TextIOWrapper from closing our buffer
75+
76+
raw_stdout = UnclosableBuffer()
77+
monkeypatch.setattr(sys, "stdin", TextIOWrapper(io.BytesIO(b""), encoding="utf-8"))
78+
monkeypatch.setattr(sys, "stdout", TextIOWrapper(raw_stdout, encoding="utf-8"))
79+
80+
with anyio.fail_after(5):
81+
async with stdio_server() as (read_stream, write_stream):
82+
await read_stream.aclose()
83+
async with write_stream:
84+
session_message = SessionMessage(JSONRPCRequest(jsonrpc="2.0", id=1, method="ping"))
85+
await write_stream.send(session_message)
86+
87+
raw_bytes = raw_stdout.getvalue()
88+
assert raw_bytes.endswith(b"\n"), "output must end with LF"
89+
assert b"\r\n" not in raw_bytes, "output must not contain CRLF"
90+
91+
6692
@pytest.mark.anyio
6793
async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):
6894
"""Non-UTF-8 bytes on stdin must not crash the server.

0 commit comments

Comments
 (0)