MCP Transports: stdio and Streamable HTTP

RoadmapsAI Engineering

Scenario

Your local MCP server is working perfectly. You want to see exactly when a tool is called, so you add a quick `print('fetching data')` inside the function. The next time the LLM calls the tool, the client instantly crashes with a JSON parse error and disconnects.

A developer adds a simple print statement to track execution, and it instantly crashes the client. What fundamental rule of the stdio transport did they break?

Mental model

stdio is like whispering directly into a pipe; Streamable HTTP is like a phone line that can switch to a live stream mid-call.

MCP needs a wire to send JSON-RPC messages across. It uses two main wires (transports). For local servers running on your machine, it uses `stdio`—the Host literally runs your server script and talks to it through standard input/output. For remote servers, it uses Streamable HTTP: the client POSTs to one endpoint, and the server can upgrade its response into a live SSE stream when it needs to push messages back. Because `stdio` uses standard output as the strict data pipe, any stray text printed to stdout corrupts the data stream and breaks the connection.

Deep dive

When you configure a local MCP Server (like in Claude Desktop or Cursor), you usually use the `stdio` transport. The Host application spawns your server script as a child subprocess. It writes JSON-RPC requests to your script's `stdin`, and reads the JSON-RPC responses from your script's `stdout`.

The stdio transport runs the server locally, communicating via stdin and stdout.

This creates a massive gotcha for logging. If you use a standard `print()` or `console.log()` statement in a `stdio` server, that plain text goes directly into `stdout`. The Host, expecting a strict JSON-RPC object, receives `fetching data{...}`. It fails to parse the JSON and instantly kills the connection.

Printing to stdout in a stdio server corrupts the JSON-RPC stream and crashes the client.

To safely log in a `stdio` server, you MUST direct all logs to standard error (`stderr`). The Host ignores `stderr` for protocol parsing but will often capture it for your debugging logs. The official MCP SDKs handle this automatically if you use their built-in logging methods.

All debugging logs in a stdio server must be routed to stderr, never stdout.

For remote servers, MCP uses **Streamable HTTP**. The client POSTs its JSON-RPC messages to a single MCP endpoint. For a quick call the server just answers with JSON. When it needs to stream — progress updates, or requests of its own back to the client — it upgrades that same response into a Server-Sent Events (SSE) stream. So SSE still exists here, but as a streaming mode *inside* one endpoint, not as a separate transport.

Remote MCP uses Streamable HTTP: one endpoint, with SSE as an optional streaming upgrade inside it.

You will still meet an older design in tutorials and older servers: the 2024 spec had a separate **HTTP+SSE** transport with TWO endpoints — a long-lived `/sse` stream to receive on, plus a different POST endpoint to send to. The 2025-03-26 spec revision replaced it with Streamable HTTP, and it is now deprecated. If a blog post tells you to open a persistent SSE connection *first*, before you can send anything, you are reading pre-2025 material. Either way, deploying remotely means your gateway or load balancer must tolerate long-lived streaming responses, not just quick request/response pairs.

HTTP+SSE was the pre-2025 two-endpoint transport — deprecated now, but still all over old tutorials.

Code examples

The fatal error: Corrupting the stdio stream

def search_database():
    # WRONG: This writes to stdout, corrupting the JSON-RPC stream
    print('Executing search query...') 
    
    # CORRECT: Write to stderr so it doesn't break the protocol
    import sys
    print('Executing search query...', file=sys.stderr)
    
    return 'Results...'

Because stdio uses standard output exclusively for protocol messaging, a single stray print statement is enough to instantly crash the host connection.

Client Configuration: stdio vs Streamable HTTP

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamablehttp_client

# 1. Local stdio connection
server_params = StdioServerParameters(command="python", args=["server.py"])
async with stdio_client(server_params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()

# 2. Remote Streamable HTTP connection
async with streamablehttp_client("https://api.example.com/mcp") as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()

The Python SDK provides distinct context managers (`stdio_client` and `streamablehttp_client`) to handle the transports, while `ClientSession` remains the same. (API shape verified against modelcontextprotocol.io/docs/develop)

Common mistakes

Glossary

stdio transport
A local transport method where the Host boots the Server as a subprocess and they communicate directly via standard input (stdin) and standard output (stdout).
Streamable HTTP transport
The current remote transport for MCP: the client POSTs JSON-RPC to a single endpoint, and the server may upgrade its response into a Server-Sent Events (SSE) stream when it needs to stream messages back.

Recall questions

Questions & answers

What transports does MCP use today?

MCP primarily uses two transports: `stdio` for local, subprocess-based connections (where communication happens over standard input/output), and Streamable HTTP for remote, network-based connections. Streamable HTTP uses a single endpoint, which the server can upgrade to a Server-Sent Events (SSE) stream when it needs to push messages back.

Approach: Name the two modern transports. Explicitly mention that Streamable HTTP uses one endpoint, with SSE as an optional streaming upgrade — not a separate transport.

Why is adding a print statement dangerous in a local MCP server?

Local MCP servers use the `stdio` transport, meaning they send their JSON-RPC protocol messages over standard output. If you print plain text to stdout for debugging, it corrupts the JSON stream and causes the Host client to immediately crash with a parse error. All debugging logs must be written to stderr.

Approach: Explain the mechanics of the `stdio` transport. Emphasize that stdout is strictly for protocol data, and stderr is for logging.

Continue learning

Previous: MCP Architecture: Hosts, Clients, and Servers

Next: Observability: Inspecting MCP Traffic

Return to AI Engineering Roadmap