MCP Transports: STDIO vs. Streamable HTTP Explained
TL;DR
- The Model Context Protocol (MCP) uses transports to send JSON-RPC 2.0 messages between clients and servers
- The STDIO transport is designed for local development, where a client launches the server as a subprocess and communicates via standard input/output
- The Streamable HTTP transport enables communication with remote servers over HTTP, using Server-Sent Events (SSE) for real-time, bidirectional messaging
- Streamable HTTP includes features for session management, message redelivery, and OAuth 2.1-based authorization
- Configuration flags like
stateless_httpandjson_responsecan modify Streamable HTTP behavior for use cases like horizontal scaling or simpler integrations - Custom transports can be developed, but they must follow the core MCP message format and lifecycle rules
The Model Context Protocol (MCP) relies on transport mechanisms to connect clients and servers so they can exchange messages. Because all communication follows the JSON-RPC 2.0 specification, MCP itself is transport-agnostic, and clients and servers can communicate over different protocols depending on their needs.
MCP defines two standard transports: STDIO and Streamable HTTP.
STDIO transport
The STDIO (standard input/output) transport is the simplest way to connect an MCP client and server, and a popular choice for development and testing. The client launches the MCP server as a subprocess; the server then reads JSON-RPC messages from its standard input (stdin) and writes responses to its standard output (stdout). Messages are individual JSON-RPC requests, notifications, or responses, delimited by newlines with no embedded line breaks. Either side can send a message at any time, so communication runs in both directions, and the server can also write UTF-8 strings to standard error (stderr) for logging.
This transport is ideal for development and testing when the client and server run on the same machine. You can even test an MCP server directly from your terminal by pasting JSON messages into stdin and watching the responses come back on stdout. Because everything stays local, STDIO implementations should skip the OAuth 2.1 authorization spec and pull credentials from the environment instead.
Streamable HTTP transport
The Streamable HTTP transport lets MCP clients connect to remotely hosted servers over HTTP, replacing the older HTTP+SSE transport and making public MCP servers reachable by anyone. It runs on standard HTTP POST and GET requests and can optionally use Server-Sent Events (SSE) to stream multiple server messages, which enables richer interactions like server-to-client notifications and requests. Plain HTTP only goes client-to-server, though, so features like sampling, notifications, and logging that need the server to initiate a request need a workaround, which is what the rest of this section covers.
A connection starts with three steps: the client sends an Initialize request, the server replies with an Initialize result containing an mcp-session-id header that identifies the client, and the client confirms with an Initialized notification carrying that same session ID. From then on, every request from the client must include the session ID.
To get messages moving the other way, from server to client, the client makes an HTTP GET request to the MCP endpoint and keeps it open as a long-lived SSE connection. When a client then makes a tool call over POST, the system can open two separate SSE connections: a primary one that stays open indefinitely for server-initiated requests and progress notifications, and a tool-specific one that closes automatically once the tool's result is sent, carrying log messages and the result itself.
Connections drop, so servers can attach an id field to each SSE event, and a reconnecting client can send the Last-Event-ID header to say where it left off, letting the server replay whatever was missed on that stream. Session state works the same way: a server assigns a session ID at initialization via the Mcp-Session-Id header, which clients must then include on every request. Omit it and you get a 400; use one after the session is gone and you get a 404. A client can end a session explicitly with an HTTP DELETE request that includes the session ID. Clients must also send an MCP-Protocol-Version header (e.g., MCP-Protocol-Version: 2025-06-18) on every request so the server knows which protocol version was negotiated.
On the authorization side, HTTP-based transports should follow the MCP authorization spec, which is built on OAuth 2.1. Servers must validate the Origin header, should bind only to localhost (127.0.0.1) when running locally, and should implement proper authentication. Together, these measures stop attacks like DNS rebinding. The authorization spec goes further into token theft prevention, requiring HTTPS, PKCE for authorization codes, and blocking open redirects.
Two flags change how Streamable HTTP behaves, and both can limit what a server can do. Setting stateless_http=True turns off session IDs, server-to-client requests (sampling, progress reports, subscriptions), and client initialization. In exchange, servers can scale horizontally behind a load balancer without coordinating session state across instances, and clients can skip the initial handshake entirely. It's the right call when you need that kind of scaling and don't need server-to-client communication or model sampling. Setting json_response=True disables streaming on POST responses, so a client gets only the final result as plain JSON, with no progress messages or logs along the way, useful when you don't need streaming or you're integrating with something that expects a simple, non-streaming HTTP response. Whichever flags you use, test with the same transport configuration you plan to run in production: stateful and stateless modes can behave quite differently.
Custom transports
MCP allows custom transport mechanisms beyond STDIO and Streamable HTTP. Implementers just need to preserve the JSON-RPC message format and lifecycle rules MCP defines, and document their own connection setup and message exchange patterns so other implementations can interoperate.