fix: disable newline translation in stdio TextIOWrapper to prevent CRLF on Windows

Closed
MA
16d ago
+56-2

Fixes #2433

Summary

On Windows, TextIOWrapper without newline="" performs universal newline translation (\n → \r\n on write), corrupting the newline-delimited JSON wire format used by MCP's stdio transport.

Root Cause

mcp/server/stdio.py creates TextIOWrapper(sys.stdout.buffer, encoding="utf-8") without specifying newline="". On Windows, the default newline=None causes \n → \r\n translation, so every JSON-RPC message written to stdout ends with \r\n instead of \n.

The comment on line 43 even acknowledges: "Encoding of stdin/stdout as text streams on python is platform-dependent (Windows is particularly problematic)" — but the fix applied (re-wrap to ensure UTF-8) didn't also fix the newline translation mode.

Changes

src/mcp/server/stdio.py

  • Added newline="" to both the stdin and stdout TextIOWrapper calls
  • newline="" disables universal newline translation while still operating in text mode
  • This ensures bare \n is emitted on all platforms, matching the NDJSON wire format

tests/server/test_stdio.py

  • Added test_stdio_server_no_crlf_on_windows — exercises the default sys.stdout.buffer code path via monkeypatch, inspects raw bytes in the output buffer, and asserts no \r bytes are present

Verification

# With newline="":
buf = io.BytesIO()
wrapper = TextIOWrapper(buf, encoding="utf-8", newline="")
wrapper.write('{"jsonrpc":"2.0","result":"ok"}\n')
wrapper.flush()
repr(buf.getvalue())
# b'{"jsonrpc":"2.0","result":"ok"}\n'  ← correct on all platforms

Compatibility

  • No behavioral change on Linux/macOS (where \n was already preserved)
  • On Windows, fixes the \r\n corruption without any new dependencies
  • The stdin wrapper also gets newline="" to prevent future issues if a client sends strict LF