MCP Transports: stdio and Streamable HTTP
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
- Using standard print() to debug stdio servers: Because printing to standard output is the default debugging method taught in every tutorial, developers instinctively use it. Reality: In an MCP stdio server, stdout is the strictly formatted JSON-RPC pipe. Printing plain text into it corrupts the stream and crashes the client. Always log to stderr.
- Assuming remote servers use WebSockets: Because WebSockets are the usual choice for bidirectional communication, developers assume remote MCP uses them. Reality: remote MCP uses Streamable HTTP — the client POSTs JSON-RPC to one endpoint, and the server upgrades that response to a Server-Sent Events (SSE) stream when it needs to push data back. (You may still see a separate two-endpoint HTTP+SSE transport in pre-2025 servers and tutorials — that design is deprecated.)
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
- How does stdio communication differ from network transports in MCP?
- How does the Streamable HTTP transport handle server-to-client messages for remote servers?
- A developer adds `print('fetching data')` inside an MCP tool function to track execution. The client instantly throws a JSON parse error and disconnects upon tool execution. Why?
- A developer assumes they need to configure WebSockets on their API gateway to deploy a remote MCP server. Why is this incorrect?
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