From adda1996553057ad69ecfbb12da0263bb0fc546a Mon Sep 17 00:00:00 2001 From: kaXianc2-gom Date: Thu, 18 Jun 2026 22:53:51 +0800 Subject: [PATCH] fix: stdio_server emits LF only on Windows stdout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add newline='' to TextIOWrapper for stdout to prevent \n → \r\n translation on Windows, which corrupts newline-delimited JSON messages. The MCP protocol uses \n as the line delimiter. Emitting \r\n is a protocol-level impurity that breaks clients parsing JSON lines. stdin is intentionally left with the default newline=None so that universal-newline behaviour normalises \r\n → \n on read. Fixes #2433 Co-Authored-By: Claude --- src/mcp/server/stdio.py | 4 +++- tests/server/test_stdio.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index 5c1459dff6..5fc758c5f6 100644 --- a/src/mcp/server/stdio.py +++ b/src/mcp/server/stdio.py @@ -41,7 +41,9 @@ async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio. if not stdin: stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")) if not stdout: - stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) + # newline="" prevents \n → \r\n translation on Windows, which would + # corrupt newline-delimited JSON messages (the MCP protocol uses \n). + 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 054a157b3b..32967cfc67 100644 --- a/tests/server/test_stdio.py +++ b/tests/server/test_stdio.py @@ -169,3 +169,24 @@ async def lifespan(server: MCPServer) -> AsyncIterator[None]: assert events == ["setup", "cleanup"] response = jsonrpc_message_adapter.validate_json(captured.getvalue().decode().strip()) assert response == JSONRPCResponse(jsonrpc="2.0", id=1, result={}) + + +def test_mcpserver_run_stdio_emits_lf_not_crlf(monkeypatch: pytest.MonkeyPatch) -> None: + """`MCPServer.run("stdio")` emits LF only, not CRLF, on Windows. + + The default stdout path in stdio_server() wraps sys.stdout.buffer with + newline="" so JSON lines end with \\n regardless of platform. + """ + ping = JSONRPCRequest(jsonrpc="2.0", id=1, method="ping") + stdin_bytes = io.BytesIO(ping.model_dump_json(by_alias=True, exclude_none=True).encode() + b"\n") + captured = _KeepOpenBytesIO() + # Simulate a "default" stdout WITHOUT newline="" — the fix in stdio_server + # must still produce LF-only output by wrapping sys.stdout.buffer itself. + monkeypatch.setattr(sys, "stdin", TextIOWrapper(stdin_bytes, encoding="utf-8")) + monkeypatch.setattr(sys, "stdout", TextIOWrapper(captured, encoding="utf-8")) + + _run_stdio_bounded(MCPServer(name="LfStdioServer")) + + raw = captured.getvalue() + assert b"\r\n" not in raw, f"stdout should not contain CRLF: {raw!r}" + assert raw.endswith(b"\n"), f"stdout should end with LF: {raw!r}"