From 73074347952548d3e3476e6954aa34acc3cdb07e Mon Sep 17 00:00:00 2001 From: Christian Sidak Date: Mon, 13 Apr 2026 18:08:38 -0700 Subject: [PATCH] fix: add newline="" to stdio TextIOWrapper to prevent CRLF on Windows Closes #2433 --- src/mcp/server/stdio.py | 4 ++-- tests/server/test_stdio.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index 5c1459dff..b66c0a522 100644 --- a/src/mcp/server/stdio.py +++ b/src/mcp/server/stdio.py @@ -39,9 +39,9 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio. # python is platform-dependent (Windows is particularly problematic), so we # re-wrap the underlying binary stream to ensure UTF-8. if not stdin: - stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")) + stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace", newline="")) if not stdout: - stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) + stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline="")) read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0) write_stream, write_stream_reader = create_context_streams[SessionMessage](0) diff --git a/tests/server/test_stdio.py b/tests/server/test_stdio.py index 677a99356..713bb6758 100644 --- a/tests/server/test_stdio.py +++ b/tests/server/test_stdio.py @@ -63,6 +63,40 @@ async def test_stdio_server(): assert received_responses[1] == JSONRPCResponse(jsonrpc="2.0", id=4, result={}) +@pytest.mark.anyio +async def test_stdio_server_no_crlf(monkeypatch: pytest.MonkeyPatch): + """Raw bytes written to stdout must use LF (\\n), never CRLF (\\r\\n). + + On Windows, TextIOWrapper with the default newline=None translates \\n to + \\r\\n on write, which corrupts NDJSON framing for JSON-RPC. The fix is to + pass newline="" to TextIOWrapper so no translation occurs. + """ + raw_stdout = io.BytesIO() + # Wrap with newline="" so we can inspect the exact bytes that + # stdio_server writes. The key assertion is that the raw bytes + # contain \n and never \r\n. + stdout_wrapper = TextIOWrapper(raw_stdout, encoding="utf-8", newline="") + stdin_wrapper = TextIOWrapper(io.BytesIO(b""), encoding="utf-8") + + message = JSONRPCRequest(jsonrpc="2.0", id=1, method="ping") + + with anyio.fail_after(5): + async with stdio_server( + stdin=anyio.AsyncFile(stdin_wrapper), + stdout=anyio.AsyncFile(stdout_wrapper), + ) as (read_stream, write_stream): + async with write_stream: + await write_stream.send(SessionMessage(message)) + async with read_stream: + pass + + stdout_wrapper.flush() + raw_bytes = raw_stdout.getvalue() + assert len(raw_bytes) > 0, "expected output bytes" + assert raw_bytes.endswith(b"\n"), "output must end with LF" + assert b"\r\n" not in raw_bytes, "output must not contain CRLF" + + @pytest.mark.anyio async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch): """Non-UTF-8 bytes on stdin must not crash the server.