Repository navigation
Windows: TextIOWrapper in stdio_server() emits CRLF instead of LF, corrupting newline-delimited JSON messages #2433
Description
Activity
Hi! I'd like to work on this. The fix is straightforward — adding newline="" to the TextIOWrapper calls in stdio.py to disable platform-specific line translation, ensuring LF-only output per the NDJSON spec. I'll include a test to verify. Targeting v1.x. Can I be assigned?
Reacted by Elijah Donovan- added a commit that references this issue
on Apr 14, 2026 - added 4 commits that reference this issue
on Apr 14, 2026 /assign @faridun-ag2
confirmed in source on both
mainandv1.x.src/mcp/server/stdio.pylines 42 and 44 createTextIOWrapperwithoutnewline="". Python docs: defaultnewline=Nonetranslates\n→os.linesepon write; on Windowsos.linesep == "\r\n", so every JSON-RPC message gets CRLF line endings, violating the NDJSON wire format.workaround: pass
newline=""to bothTextIOWrappercalls (disables translation, as the reporter's fix shows). the fix needs to land on bothmainandv1.x.repro script, output, and code path
repro.py (run from repo root:
uv run python repro.py):import io, subprocess, sys, pathlib from io import TextIOWrapper print(f"Python {sys.version}") print(f"Platform: {sys.platform}") script_buggy = r''' import sys from io import TextIOWrapper stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8") stdout.write('{"jsonrpc":"2.0","result":"ok"}\n') stdout.flush() ''' script_fixed = r''' import sys from io import TextIOWrapper stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline="") stdout.write('{"jsonrpc":"2.0","result":"ok"}\n') stdout.flush() ''' proc_buggy = subprocess.Popen([sys.executable, "-c", script_buggy], stdout=subprocess.PIPE) out_buggy, _ = proc_buggy.communicate(timeout=5) print(f"current (no newline=''): {repr(out_buggy)}") proc_fixed = subprocess.Popen([sys.executable, "-c", script_fixed], stdout=subprocess.PIPE) out_fixed, _ = proc_fixed.communicate(timeout=5) print(f"fixed (newline=''): {repr(out_fixed)}") stdio_src = pathlib.Path("src/mcp/server/stdio.py").read_text() print("bug in source:", 'newline=""' not in stdio_src)
output (Linux — CRLF not emitted here, but code defect is confirmed):
subprocess (current, no newline=''): b'{"jsonrpc":"2.0","result":"ok"}\n' subprocess (fixed, newline=''): b'{"jsonrpc":"2.0","result":"ok"}\n' bug in source: TrueOn Windows,
subprocess (current)would emitb'...\r\n'becausestdout=subprocess.PIPEon Windows opens in text mode and theTextIOWrapperdefault appliesos.lineseptranslation.code path:
src/mcp/server/stdio.py:42— stdin wrapper missingnewline=""src/mcp/server/stdio.py:44— stdout wrapper missingnewline=""src/mcp/server/stdio.py:69—await stdout.write(json + "\n")—\ngets translated to\r\nvia the wrapper on Windows- Python docs: "If newline is None … on output, any
\ncharacters written are translated to the system default line separator,os.linesep."
suggested fix
# src/mcp/server/stdio.py - 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=""))
test to verify: spy on
mcp.server.stdio.TextIOWrappervia monkeypatch, confirm the stdout wrapper is constructed withnewline=""(seetests/server/test_stdio.py::test_stdio_server_stdout_no_crlf).- addedbugSomething isn't workingSomething isn't workingready for workEnough information for someone to start working onEnough information for someone to start working onP2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable featurefix proposedBot has a verified fix diff in the commentBot has a verified fix diff in the comment
on Apr 17, 2026 - added a commit that references this issue
on Apr 19, 2026 Opened a fix in #2470 -- disables CRLF translation in
stdio_serveron Windows by addingnewline=""to bothTextIOWrappercalls, preventing\nfrom being silently converted to\r\n.- added a commit that references this issue
on May 1, 2026 3 remaining items
- added a commit that references this issue
on May 28, 2026 Hope I was able to help guys.
I've submitted PR #2908 for this — added
newline=""to the stdout TextIOWrapper and included a targeted test that verifies LF-only output on Windows.Tested on Windows 11: 629/629 passed + new CRLF-specific test. Ruff format and lint both clean.
Happy to adjust anything — feedback welcome.
Reproduced this natively on Windows 11 (Python 3.13, current
main): a raw-bytes client reading the server's stdout gets frames terminated with\r\n. Root cause is the stdoutTextIOWrapperinstdio_server()using default newline translation.#2552 has the right idea but has been stale since May with failing pre-commit, and its regression test asserts
endswith(b"\n"), which passes even when the bug is present (b"}\r\n"also ends withb"\n").Opening a PR with the stdout-only fix, a byte-level regression test verified red/green on native Windows, and clean lint.
I'd like to take this one if it's still open — the existing PR (#2552) looks stale (no updates since early May).
The cause is in
src/mcp/server/stdio.py: stdout is wrapped withTextIOWrapper(sys.stdout.buffer, encoding="utf-8")withoutnewline="". On Windows, text-mode newline translation rewrites the\nmessage terminator to\r\n, which corrupts the newline-delimited-JSON framing that the stdio transport relies on. The fix is to passnewline=""so the wrapper writes bytes through unchanged (matching how the read side is handled).I can reproduce and test this on Windows directly (no API key or cloud needed), and I'll add a regression test. Could you assign it to me? Disclosure: I'll use AI assistance in preparing the change; I understand the fix and will own it through review.
- added a commit that references this issue
on Jul 20, 2026 - added 6 commits that reference this issue
on Aug 22, 2026 - added 2 commits that reference this issue
on Sep 11, 2026 Reproduced the Windows CRLF framing issue conceptually:
TextIOWrapper(sys.stdout.buffer, encoding='utf-8')withoutnewline=''translates\n→\r\non Windows, which is impure for NDJSON MCP framing.Existing PRs (#2552, and related stdio work in #3090) already target this. One nuance worth preserving in whatever lands: set
newline=''on stdout (and ideally leave stdin behavior documented), and keep a raw-bytes assertion in CI on Windows so regression is caught outside of text-mode readers that normalize CRLF.(AI-assisted note after reading the issue + open PRs; not requesting assignment.)
Summary
mcp/server/stdio.pycreatesTextIOWrapper(sys.stdout.buffer, encoding="utf-8")without specifyingnewline="". On Windows, the defaultnewline=Nonecauses\n→\r\ntranslation, so every JSON-RPC message written to stdout ends with\r\ninstead of\n.The MCP spec uses newline-delimited JSON with
\nas the delimiter. Emitting\r\nis a protocol-level impurity.Affected file
mcp/server/stdio.pylines 46–49Reproduction
On Windows, spawn a Python subprocess that uses this code and read the raw bytes:
Verified on:
Why this matters
While the current JS MCP SDK client (
StdioClientTransport) strips trailing\rvia.replace(/\r$/, "")before parsing, this is a server-side bug that:stdio_clientsends bare\n, but the Pythonstdio_serverresponds with\r\nsplit("\n")and then fails toJSON.parsethe line with a trailing\rThe 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) doesn't also fix the newline translation mode.Fix
Add
newline=""to bothTextIOWrappercalls.newline=""disables translation while still operating in text mode:With this fix:
The same fix should be applied to the
stdinwrapper so that incoming messages with bare\nare not translated either (avoiding any future issues if a client sends strict LF).Context
This was discovered while debugging Windows MCP tool timeouts with
mem0-mcp-selfhosted. The eager-init approach fixed the actual timeout, but this CRLF emission was identified as a secondary protocol-level issue during investigation.