Overview

MCP defines two standard transports: stdio and Streamable HTTP. The choice is not cosmetic. stdio is the default for local servers and eliminates network configuration entirely. Streamable HTTP is required for remote servers, shared services, and multi-tenant deployments. Each transport has distinct lifecycle semantics, reconnection behavior, and security posture. Picking the wrong one adds operational overhead without benefit. The older HTTP+SSE transport (two endpoints: a long-lived SSE channel plus a separate POST endpoint) was deprecated in spec revision 2025-03-26 and is retained only for backwards compatibility; new servers use Streamable HTTP. See mcp-streamable-http for the single-endpoint design and mcp-protocol for how JSON-RPC messages ride on top of these transports.

Use stdio for local servers; it is the simplest deployment

In the stdio transport, the client launches the server as a child process and communicates over stdin and stdout. Each JSON-RPC message is a newline-delimited JSON string. There are no ports, no TLS certificates, and no authentication tokens beyond what the process inherits from its environment.

{
  "mcpServers": {
    "filesystem": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
    }
  }
}

Use stdio when the server and client run on the same machine, when the user is the sole consumer, and when the server does not need to persist state across client sessions. Filesystem servers, local database servers, and dev-tool servers belong here.

Use Streamable HTTP for remote and shared servers

Streamable HTTP runs the server behind a single HTTP endpoint. The client POSTs each JSON-RPC message to that endpoint; the server replies with a single JSON response or, when it needs to stream notifications and progress, upgrades the response to an SSE stream. There is no separate long-lived channel to open. The server assigns a session ID at initialize time in the Mcp-Session-Id response header, and the client echoes it on every subsequent request.

Use Streamable HTTP when the server is a SaaS integration (GitHub, Notion, Slack), when multiple clients share the same server, or when the server must be deployed independently of the client machines. Remote servers require auth at the transport layer: a bearer token or mTLS on every request.

Streamable HTTP is also the correct choice for servers behind a CDN or reverse proxy. Cloudflare Workers, for example, can host a lightweight MCP proxy layer. See cloudflare for deployment patterns and mcp-streamable-http for the full transport mechanics.

Handle reconnection explicitly in Streamable HTTP clients

Streams drop. Networks partition, servers restart, and load balancers time out idle connections. A production Streamable HTTP client must implement reconnection with exponential backoff. When a stream carried SSE events, resume it by replaying the Last-Event-ID header so the server can re-send only the events the client missed; otherwise re-send the initialize handshake on a fresh session.

async def connect_with_retry(url: str, max_attempts: int = 5):
    for attempt in range(max_attempts):
        try:
            async with streamablehttp_client(url) as (read, write, get_session_id):
                async with ClientSession(read, write) as client:
                    await client.initialize()
                    return client
        except ConnectionError:
            await asyncio.sleep(2 ** attempt)
    raise RuntimeError("MCP server unreachable after retries")

Reconnection is invisible in stdio because the client controls the server process lifecycle. In Streamable HTTP it is the client’s responsibility. Build it before shipping.

Manage server lifecycle: startup, shutdown, and health checks

For stdio servers, lifecycle is tied to the client process. The server starts when the client starts, receives SIGTERM or EOF when the client exits, and should flush logs and close connections in its shutdown handler.

For HTTP servers, treat startup and shutdown as first-class concerns. Provide a /health endpoint that returns 200 when the server is ready to accept connections. Wire a graceful shutdown handler that completes in-flight requests before stopping. Emit a structured startup log that records the server version, transport, and declared capabilities.

A server that starts slowly or crashes silently during development is invisible to the client until tool calls begin failing. Health checks surface problems earlier. Log the startup event with a timestamp so latency tracking can measure cold-start overhead.

Apply per-session isolation in multi-tenant HTTP deployments

When multiple agents share a single Streamable HTTP MCP server, each session must be isolated. Rate limits, credential scoping, and audit logs must be keyed to the session, not the server instance.

Use the Mcp-Session-Id assigned at initialize time as the isolation key. Validate it on every subsequent request. Store session state (rate-limit counters, audit log entries) under the session ID. Never allow one session to read another session’s state.

Multi-tenant deployments also require care with secrets. A server that serves multiple organizations must scope credentials per organization at the session level, not at the server level. Mixing credentials across sessions is a data-isolation violation.