Skip to main content
MCP Deep Dive

Transport: stdio, SSE, HTTP

Ravinder··5 min read
MCPModel Context ProtocolAITransportstdioSSEHTTP
Share:
Transport: stdio, SSE, HTTP

Transport is the part of MCP that engineers underestimate until something breaks in production. The protocol itself is transport-agnostic — the same JSON-RPC messages work over a Unix pipe, a Server-Sent Events stream, or plain HTTP. But the choice of transport determines your latency profile, your operational complexity, your ability to stream partial results, and whether your server can run inside a container or must live on the same machine as the client.

What Transport Actually Does

MCP uses JSON-RPC 2.0 as its message format. Transport is purely the mechanism that carries those messages between client and server. The spec currently defines three:

graph LR subgraph "Same process / machine" C1["Client"] -- "stdin/stdout" --> S1["Server\n(stdio)"] end subgraph "Network — browser-friendly" C2["Client"] -- "HTTP POST\n(client→server)" --> S2["Server\n(SSE)"] S2 -- "SSE stream\n(server→client)" --> C2 end subgraph "Network — REST-style" C3["Client"] -- "HTTP POST" --> S3["Server\n(Streamable HTTP)"] S3 -- "HTTP response\n(chunked or full)" --> C3 end

stdio: The Right Default for Local Tools

stdio is the simplest transport. The client spawns the server as a child process and communicates over its standard input and output streams. There is no network stack, no auth negotiation, no port to expose.

// server side — TypeScript SDK
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 
const server = new McpServer({ name: "my-local-tool", version: "1.0.0" });
// ... register tools/resources/prompts ...
 
const transport = new StdioServerTransport();
await server.connect(transport);

The client spawns it:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 
const transport = new StdioClientTransport({
  command: "node",
  args: ["./dist/server.js"]
});
const client = new Client({ name: "my-client", version: "1.0.0" });
await client.connect(transport);

Latency: sub-millisecond — no network hop, no serialisation overhead beyond JSON.

When to use stdio:

  • Developer tooling running locally (CLI assistants, IDE plugins).
  • CI pipelines where the server is a one-shot subprocess.
  • Security-sensitive tools where network exposure is unacceptable.

When to avoid stdio:

  • Multi-client scenarios — one process, one client, that's the contract.
  • Containerised or serverless deployments where you cannot spawn child processes easily.

SSE: Browser-Compatible Streaming

SSE (Server-Sent Events) was the original network transport in MCP. The client sends requests over HTTP POST; the server pushes responses back over a persistent SSE stream. This makes it the natural fit for browser-based clients because SSE is a native browser API.

# server side — Python SDK with SSE
from mcp.server.fastmcp import FastMCP
 
mcp = FastMCP("analytics-server")
 
@mcp.tool()
async def run_report(query: str) -> str:
    """Run an analytics query and stream results."""
    result = await db.execute(query)
    return result.to_json()
 
# FastMCP handles the SSE plumbing automatically
mcp.run(transport="sse", port=8080)

The SSE endpoint typically lives at /sse and the POST endpoint at /messages. The client:

import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
 
const transport = new SSEClientTransport(
  new URL("http://localhost:8080/sse")
);

Latency: one round-trip to establish the SSE stream, then near-zero overhead for subsequent messages. Long-lived connection — good for sessions, bad for ephemeral lambdas.

When to use SSE:

  • Agents running in the browser.
  • Servers that need to push notifications or streaming tool output.
  • Teams already familiar with SSE-based architectures.

When to avoid SSE:

  • Load-balanced environments where sticky sessions are painful.
  • Serverless functions with aggressive connection timeouts.

Streamable HTTP: The Production-Grade Choice

Streamable HTTP (introduced in the MCP spec after SSE) collapses the two-endpoint model into a single POST endpoint. The server can respond with either a full JSON body or a chunked/streamed response, decided per-request. This is much friendlier to standard HTTP infrastructure — load balancers, API gateways, reverse proxies.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
 
const app = express();
app.use(express.json());
 
const server = new McpServer({ name: "saas-mcp", version: "1.0.0" });
// ... register capabilities ...
 
app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});
 
app.listen(3000);

Latency: one HTTP round-trip per request. Slightly higher than SSE for chatty sessions, but trivial in practice for tool calls that do real work.

When to use Streamable HTTP:

  • Any server deployed behind a load balancer or API gateway.
  • Multi-tenant SaaS where sessions are request-scoped, not connection-scoped.
  • Teams that want standard HTTP observability (access logs, metrics, tracing headers).

Comparison at a Glance

quadrantChart title Transport fit by deployment context x-axis "Local / same-machine" --> "Remote / network" y-axis "Stateful / long-lived" --> "Stateless / ephemeral" quadrant-1 Streamable HTTP quadrant-2 SSE quadrant-3 stdio quadrant-4 Streamable HTTP stdio: [0.15, 0.35] SSE: [0.65, 0.65] "Streamable HTTP": [0.75, 0.25]
Concern stdio SSE Streamable HTTP
Latency Lowest Low Low–medium
Multi-client No Yes Yes
Browser support No Yes Yes
Load-balancer friendly N/A Requires sticky Yes
Streaming output Yes Yes Yes (chunked)
Auth integration OS-level Header/cookie Header/cookie

Migrating Between Transports

The protocol messages are identical across transports. Migrating is usually a matter of swapping the transport constructor and updating your deployment config. If you built your server logic cleanly against the MCP SDK (no transport-specific leakage), you can switch from stdio to Streamable HTTP in under an hour.

Key Takeaways

  • stdio is the fastest and simplest option but locks you to single-client, same-machine deployments.
  • SSE suits browser clients and scenarios where server-to-client push matters, but complicates load balancing.
  • Streamable HTTP is the safest default for any server that will be deployed over a network.
  • Transport choice has zero impact on the protocol messages — swap freely as requirements evolve.
  • Match transport to your infrastructure: if your stack is API gateways and container orchestration, Streamable HTTP wins.
  • Latency differences between SSE and Streamable HTTP are negligible compared to the I/O of actual tool work.
Share: