modelcontextprotocol / modelcontextprotocol/python-sdk
Windows: TextIOWrapper in stdio_server() emits CRLF instead of LF, corrupting newline-delimited JSON messages
Nadie ha tomado este issue todavía.
- Lenguaje dominante
- Python
- Estrellas
- 24.3k
- Forks
- 4k
- Merge medio
- 1 d 1 h
- PR fusionados (30 d)
- 31
Descripción
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:
- Violates the NDJSON wire format (which specifies LF-only line endings)
- Creates an asymmetry: the Python
stdio_clientsends bare\n, but the Pythonstdio_serverresponds with\r\n - Could break any MCP client that does a strict
split("\n")and then fails toJSON.parsethe 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.
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Línea de trabajo
Comienza en src/mcp/server/stdio.py, en las llamadas a TextIOWrapper alrededor de las líneas 46–49, y luego reproduce la salida sin procesar del subproceso en Windows. Se considera terminado cuando tanto stdin como stdout conservan bytes LF sin adornos para JSON delimitado por saltos de línea en todas las plataformas.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- python
- Área
- backend-api-design
- Tipo de issue
- Error
- Dificultad
- 1/5
- Tiempo estimado
- Menos de una hora
- Estado de actividad
- Activo
- Claridad
- Bien especificado
- Aptitud para principiantes
- 88/100