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. As of the 2026-07-28 spec revision, the protocol core is also stateless: capability negotiation moved from a connection-scoped initialize/initialized handshake to per-request metadata, which changes how session state is modeled on Streamable HTTP (see below). 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. Under the current spec, protocol version and capabilities travel in _meta fields on every request rather than a connection-scoped session; servers still speaking the 2025-03-26 through 2025-06-18 revisions assign a session ID at initialize time in the Mcp-Session-Id response header, and the client echoes it on every subsequent request. See mcp-streamable-http for both models.

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 retry the request, since the current spec has no connection-scoped session to re-establish (older servers require a fresh initialize instead).

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-request or per-user isolation in multi-tenant HTTP deployments

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

Under the current spec, derive the isolation key from the authenticated identity on each request (a bearer token subject, an mTLS client identity) rather than a connection-scoped session, since requests can land on any server instance. Validate it on every request. Store rate-limit counters and audit log entries under that key. Never allow one caller to read another’s state. Servers still on the 2025-03-26 through 2025-06-18 revisions use the Mcp-Session-Id assigned at initialize time as the isolation key instead.

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