diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index 5c1459dff..5fc758c5f 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 054a157b3..32967cfc6 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}"