Skip to content

Windows: TextIOWrapper in stdio_server() emits CRLF instead of LF, corrupting newline-delimited JSON messages #2433

Description

@DonovanDeHart

Summary

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 MCP spec uses newline-delimited JSON with \n as the delimiter. Emitting \r\n is a protocol-level impurity.

Affected file

mcp/server/stdio.py lines 46–49

# Current (buggy on Windows)
if not stdin:
    stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8"))
if not stdout:
    stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))

Reproduction

On Windows, spawn a Python subprocess that uses this code and read the raw bytes:

import subprocess, sys

script = 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()
'''

proc = subprocess.Popen([sys.executable, "-c", script], stdout=subprocess.PIPE)
out, _ = proc.communicate(timeout=5)
print(repr(out))
# Output on Windows: b'{"jsonrpc":"2.0","result":"ok"}\r\n'
# Output on Linux:   b'{"jsonrpc":"2.0","result":"ok"}\n'

Verified on:

  • OS: Windows 11 Pro (10.0.26200)
  • Python: 3.11
  • mcp: 1.26.0

Why this matters

While the current JS MCP SDK client (StdioClientTransport) strips trailing \r via .replace(/\r$/, "") before parsing, this is a server-side bug that:

  1. Violates the NDJSON wire format (which specifies LF-only line endings)
  2. Creates an asymmetry: the Python stdio_client sends bare \n, but the Python stdio_server responds with \r\n
  3. Could break any MCP client that does a strict split("\n") and then fails to JSON.parse the line with a trailing \r

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) doesn't also fix the newline translation mode.

Fix

Add newline="" to both TextIOWrapper calls. newline="" disables translation while still operating in text mode:

if not stdin:
    stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", newline=""))
if not stdout:
    stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8", newline=""))

With this fix:

# newline="" result:
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

The same fix should be applied to the stdin wrapper so that incoming messages with bare \n are 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.

Activity

  1. faridun-m commented on Apr 14, 2026

    @faridun-m

    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?

  2. added a commit that references this issue on Apr 14, 2026
    7307434
  3. DonovanDeHart commented on Apr 16, 2026

    @DonovanDeHart
    Author

    /assign @faridun-ag2

  4. mcp-claude commented on Apr 17, 2026

    @mcp-claude

    confirmed in source on both main and v1.x. src/mcp/server/stdio.py lines 42 and 44 create TextIOWrapper without newline="". Python docs: default newline=None translates \n → os.linesep on write; on Windows os.linesep == "\r\n", so every JSON-RPC message gets CRLF line endings, violating the NDJSON wire format.

    workaround: pass newline="" to both TextIOWrapper calls (disables translation, as the reporter's fix shows). the fix needs to land on both main and v1.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: True
    

    On Windows, subprocess (current) would emit b'...\r\n' because stdout=subprocess.PIPE on Windows opens in text mode and the TextIOWrapper default applies os.linesep translation.

    code path:

    • src/mcp/server/stdio.py:42 — stdin wrapper missing newline=""
    • src/mcp/server/stdio.py:44 — stdout wrapper missing newline=""
    • src/mcp/server/stdio.py:69 — await stdout.write(json + "\n") — \n gets translated to \r\n via the wrapper on Windows
    • Python docs: "If newline is None … on output, any \n characters 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.TextIOWrapper via monkeypatch, confirm the stdout wrapper is constructed with newline="" (see tests/server/test_stdio.py::test_stdio_server_stdout_no_crlf).

  5. added
    bugSomething isn't working
    ready for workEnough information for someone to start working on
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    fix proposedBot has a verified fix diff in the comment
    on Apr 17, 2026
  6. added a commit that references this issue on Apr 19, 2026
    bd2e378
  7. Christian-Sidak commented on Apr 19, 2026

    @Christian-Sidak

    Opened a fix in #2470 -- disables CRLF translation in stdio_server on Windows by adding newline="" to both TextIOWrapper calls, preventing \n from being silently converted to \r\n.

  8. 3 remaining items

  9. DonovanDeHart commented on Jun 8, 2026

    @DonovanDeHart
    Author

    Hope I was able to help guys.

  10. kaXianc2-gom commented on Jun 18, 2026

    @kaXianc2-gom

    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.

  11. bmdhodl commented on Jul 19, 2026

    @bmdhodl

    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 stdout TextIOWrapper in stdio_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 with b"\n").

    Opening a PR with the stdout-only fix, a byte-level regression test verified red/green on native Windows, and clean lint.

  12. Kaif10 commented on Jul 20, 2026

    @Kaif10

    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 with TextIOWrapper(sys.stdout.buffer, encoding="utf-8") without newline="". On Windows, text-mode newline translation rewrites the \n message terminator to \r\n, which corrupts the newline-delimited-JSON framing that the stdio transport relies on. The fix is to pass newline="" 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.

  13. added a commit that references this issue on Jul 20, 2026
    5a16bf0
  14. Amiirhosseini commented on Sep 14, 2026

    @Amiirhosseini

    Reproduced the Windows CRLF framing issue conceptually: TextIOWrapper(sys.stdout.buffer, encoding='utf-8') without newline='' translates \n → \r\n on 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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingfix proposedBot has a verified fix diff in the commentready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions