Skip to content

Bug: stdio transport fails to handle Content-Length header format from MCP clients #2546

@weishigao

Description

@weishigao

Bug: stdio transport fails to handle Content-Length header format from MCP clients

Description

The stdio_server transport in mcp/server/stdio.py reads stdin line-by-line and attempts to parse each line as JSON. This works for NDJSON (newline-delimited JSON) format but fails completely when MCP clients send messages using the Content-Length header format.

Several popular MCP clients (including those built on the TypeScript SDK @modelcontextprotocol/sdk) send stdio messages with Content-Length headers, following the pattern established by LSP (Language Server Protocol). This causes a compatibility gap where Python SDK-based servers cannot be initialized by these clients.

Steps to Reproduce

  1. Create a minimal MCP server using the Python SDK:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("test-server")
mcp.run(transport="stdio")
  1. Send an MCP initialize request using Content-Length header format (as used by TypeScript SDK-based clients):
Content-Length: 157\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}
  1. Observe the server response.

Expected Behavior

The server should either:

  • (a) Properly parse the Content-Length header and read the specified number of bytes as the message body, OR
  • (b) Gracefully skip non-JSON lines (like Content-Length headers) and process valid JSON messages

Actual Behavior

The server reads the input line-by-line via async for line in stdin: and tries to parse each line as JSON:

# mcp/server/stdio.py, line 60-68
async def stdin_reader():
    async with read_stream_writer:
        async for line in stdin:
            try:
                message = types.JSONRPCMessage.model_validate_json(line)
            except Exception as exc:
                await read_stream_writer.send(exc)
                continue

When receiving Content-Length header format:

  1. Line Content-Length: 157 → JSON parse error → exception sent to stream → error notification emitted
  2. Empty line `` → JSON parse error → exception sent to stream → error notification emitted
  3. JSON body without trailing newline → never read (stdin reader blocks waiting for \n)

The server outputs two error notifications:

{"method":"notifications/message","params":{"level":"error","logger":"mcp.server.exception_handler","data":"Internal Server Error"},"jsonrpc":"2.0"}
{"method":"notifications/message","params":{"level":"error","logger":"mcp.server.exception_handler","data":"Internal Server Error"},"jsonrpc":"2.0"}

And the actual initialize response is never sent, causing the client to timeout with:

failed to initialize MCP client: transport error: context deadline exceeded

Verification

I verified this with a Node.js test script that spawns the Python MCP server and sends messages in both formats:

Format Result
NDJSON (raw JSON + \n) ? Success - correct initialize response
Content-Length header + body (no trailing \n) ? Failed - 2 error notifications, no response (body never read)
Content-Length header + body + \n ?? Partial - 2 error notifications + correct response (error notifications may confuse client)

Environment

  • mcp package version: 1.27.0 (latest)
  • Python version: 3.14
  • OS: Windows 11
  • MCP client: Qoder IDE (uses TypeScript SDK)

Suggested Fix

The stdin_reader in mcp/server/stdio.py should be updated to handle both formats:

Option A: Skip non-JSON lines silently
Instead of sending parse exceptions to the stream, silently skip lines that don't parse as valid JSON-RPC messages. This would allow Content-Length header lines and empty lines to be ignored while processing the actual JSON body.

async def stdin_reader():
    async with read_stream_writer:
        async for line in stdin:
            stripped = line.strip()
            if not stripped:
                continue  # Skip empty lines
            try:
                message = types.JSONRPCMessage.model_validate_json(stripped)
            except Exception:
                continue  # Skip non-JSON lines (e.g., Content-Length headers)
            session_message = SessionMessage(message)
            await read_stream_writer.send(session_message)

Option B: Full Content-Length header support
Implement proper Content-Length header parsing (read header, extract length, read body of that length), falling back to NDJSON for backward compatibility.

Additional Context

  • The MCP specification (2025-03-26) states stdio messages are "delimited by newlines" (NDJSON format).
  • However, the TypeScript SDK (@modelcontextprotocol/sdk) uses Content-Length headers for stdio transport, creating a real-world compatibility gap.
  • Other projects (e.g., Switchboard) have documented this issue and implemented dual-format support: "Looks ahead to detect Content-Length headers, Falls back to line-delimited parsing if no header found, Skips non-JSON lines."
  • This affects any Python SDK-based MCP server when used with clients that send Content-Length headers (Qoder, and potentially others built on the TypeScript SDK).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions