·

MCP Transport Protocols Compared

MCP Transport Protocols Compared

stdio Transport: Local Process Communication for MCP

The stdio transport is the simplest and lowest-latency MCP transport mechanism. The host spawns the MCP server as a child process and communicates via the process's standard input (stdin) and standard output (stdout). Each JSON-RPC message is a newline-delimited JSON object. There is no network stack involved, no port allocation, no TLS, and no authentication infrastructure required.

This simplicity makes stdio the default choice for local development tools and for AI coding assistants like Claude Code, Cursor, and VS Code extensions. When you run claude mcp add my-server -- node ./my-mcp-server.js, Claude Code spawns node ./my-mcp-server.js as a child process and wires up its stdin/stdout as the transport. The server process lives for the duration of the agent session and is terminated when the session ends.

Wire format: Each message is a complete JSON object followed by a newline (\n). The server reads from stdin line by line. The client reads from the server's stdout line by line. There is no HTTP framing, no headers, no content-length prefix — just newline-delimited JSON.


Client → Server stdin:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"claude-code","version":"1.5.0"}}}\n

Server → Client stdout:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"my-server","version":"1.0.0"}}}\n

Client → Server stdin:
{"jsonrpc":"2.0","method":"notifications/initialized"}\n

Client → Server stdin:
{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n

Server → Client stdout:
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_status","description":"Get service status","inputSchema":{"type":"object","properties":{}}}]}}\n

Stderr handling: The MCP spec reserves stdout for protocol messages. All server logging and diagnostic output must go to stderr, not stdout. Mixing log output into stdout corrupts the message stream and is a common mistake when building MCP servers. The host typically captures the server's stderr and routes it to its own debug logging system.

// Node.js MCP server: correct logging practice
// WRONG: console.log("Starting server...") — goes to stdout, corrupts protocol
// CORRECT: write to stderr
process.stderr.write("Starting server...\n");

// Or use a logger configured to write to stderr
import { createLogger, transports } from "winston";
const logger = createLogger({
  transports: [new transports.Stream({ stream: process.stderr })]
});

Process lifecycle considerations: The MCP server process is isolated — it runs under the host process's credentials but in its own process space. Environment variables inherited by the process are set at spawn time. If the server needs credentials (e.g., a GitHub token), they should be passed as environment variables in the MCP server configuration.

// Claude Code MCP configuration with env vars (in ~/.claude/settings.json)
{
  "mcpServers": {
    "github": {
      "command": "node",
      "args": ["/home/user/.mcp/github-server/index.js"],
      "env": {
        "GITHUB_TOKEN": "${GITHUB_TOKEN}",
        "GITHUB_DEFAULT_ORG": "acme-corp"
      }
    }
  }
}

The ${GITHUB_TOKEN} interpolation is host-specific — Claude Code resolves these from the host process's environment. Always verify your host's exact interpolation syntax, as it varies.

Limitations of stdio: The stdio transport only supports local processes. You cannot use stdio to connect to a remote MCP server. It also means the MCP server must be installed on the same machine as the host — for distributed development environments (remote dev containers, cloud IDE), stdio is impractical and remote transports are required.

Tips
- Always test your stdio MCP server by running it manually in a terminal and sending raw JSON-RPC messages via stdin before integrating with a host. This isolates protocol issues from host-level issues instantly.
- Add startup validation in your stdio MCP server: check that required environment variables are present and that external dependencies are reachable before the initialize exchange completes. Return a clear error in the initialize response if validation fails rather than silently degrading.
- For Node.js MCP servers, use process.stdin.setEncoding('utf8') and readline.createInterface for robust line-by-line reading. Do not use process.stdin.on('data') directly — partial reads break the JSON parser.
- Monitor the child process exit code in your host implementation. A stdio MCP server that crashes returns a non-zero exit code. Hosts should detect this, log the server's stderr output, and report a useful error rather than leaving the agent in a broken state with no explanation.


Server-Sent Events (SSE): HTTP-Based Streaming for Remote MCP

Server-Sent Events (SSE) was the original HTTP-based transport in the MCP spec (2024-11-05). It addresses stdio's locality limitation by allowing the MCP server to run as a remote HTTP service. SSE is a web standard (part of the HTML spec) that enables a server to push a stream of text events to an HTTP client over a persistent connection.

How SSE works in MCP: The client opens a long-lived HTTP GET request to the server's SSE endpoint. The server keeps this connection open and sends JSON-RPC messages as SSE events over it. However, SSE is unidirectional by design — the client cannot send data over the SSE stream. This creates an architectural asymmetry: the MCP client needs a separate mechanism for sending requests to the server.

The SSE transport in MCP solves this with a two-channel model:
1. SSE stream (GET /sse): Server-to-client channel. The server pushes responses and notifications here.
2. HTTP POST endpoint (POST /message): Client-to-server channel. The client sends requests via individual HTTP POST requests.

SSE Transport Architecture:

Client                          Server
  |                               |
  |── GET /sse ──────────────────>|  (long-lived connection)
  |<── event: endpoint ───────────|  (server sends POST URL)
  |   data: {"uri":"/message?session=abc123"}
  |                               |
  |── POST /message?session=abc123 ──>|  (initialize request)
  |<── 202 Accepted ───────────────|  (async acknowledgment)
  |                               |
  |<── event: message ─────────────|  (response arrives on SSE stream)
  |   data: {"jsonrpc":"2.0","id":1,"result":{...}}
  |                               |
  |── POST /message?session=abc123 ──>|  (tools/call request)
  |<── 202 Accepted ───────────────|
  |<── event: message ─────────────|  (tool result)
  |   data: {"jsonrpc":"2.0","id":2,"result":{...}}

The server first sends a special endpoint event that tells the client which URL to POST to. This URL includes a session identifier that associates the POST requests with the correct SSE stream. This session correlation is where many SSE implementations have bugs — if the session token is not properly isolated per client, responses can be delivered to the wrong connection.

Problems with SSE transport:

The two-channel model introduces operational complexity that the protocol designers recognized was suboptimal. Specific issues:

  1. Load balancer affinity: The SSE connection and POST requests must reach the same server instance. If your load balancer distributes them to different instances, session correlation breaks. You must configure sticky sessions, which complicates horizontal scaling.

  2. Firewall and proxy compatibility: Some corporate firewalls and HTTP proxies buffer SSE streams, introduce delays, or close long-lived connections after a timeout. This causes silent disconnections that are hard to diagnose.

  3. Reconnection complexity: If the SSE connection drops, the client must re-establish it and re-negotiate the session. There is no built-in message replay — tool results that arrived on the old connection may be lost.

  4. Testing difficulty: Testing an SSE-based MCP server requires simulating the two-channel dance, which is more cumbersome than testing HTTP request/response.

Despite these limitations, SSE is widely implemented because it was the only remote transport in the original spec. Many existing MCP servers in the community still use SSE, and you will encounter it regularly. Understanding its mechanics is necessary for diagnosing issues with these servers.


curl -N -H "Accept: text/event-stream" http://localhost:3000/sse &

curl -X POST http://localhost:3000/message?session=SESSION_ID \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}'

Tips
- If you are deploying an SSE MCP server behind a reverse proxy (Nginx, Caddy, AWS ALB), configure it to disable response buffering for the /sse endpoint. Nginx: proxy_buffering off;. ALB: use target group attribute deregistration_delay.connection_termination.enabled.
- Implement a heartbeat ping from the server every 15-30 seconds on the SSE stream. This prevents idle connection timeouts from proxies and load balancers and gives the client a mechanism to detect silent disconnections.
- Use the SSE transport for legacy compatibility when you must, but do not build new remote MCP servers on it. Streamable HTTP is strictly superior and is now the spec-recommended transport for remote deployments.
- When debugging SSE transport issues, use curl -N to manually open the SSE stream and observe raw event data. This lets you verify the server's SSE output format before involving any MCP client library.


Streamable HTTP: The New Standard for Remote MCP Connections

Streamable HTTP was introduced in the 2025-03-26 MCP specification revision as a replacement for the SSE two-channel model. It addresses the fundamental architectural problem of SSE: the need for two separate channels by using a single bidirectional HTTP connection where both request and response bodies can be streamed.

How Streamable HTTP works: The client sends HTTP POST requests to a single endpoint. The response to a POST can be either a standard synchronous JSON response (for simple tool calls that complete quickly) or an SSE stream in the response body (for long-running operations, streaming results, or server-initiated notifications). This means the client only needs one HTTP connection direction — POST requests carrying client messages, responses carrying server messages — with the flexibility to stream response data when needed.

Streamable HTTP — Request/Response patterns:

Pattern 1: Synchronous response (simple tool call)
  Client POST /mcp  ──────────────────────────────>
  {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{...}}

  Response: 200 OK
  Content-Type: application/json
  {"jsonrpc":"2.0","id":1,"result":{"content":[...]}}

Pattern 2: Streaming response (long-running or batched results)
  Client POST /mcp  ──────────────────────────────>
  {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"run_tests"}}

  Response: 200 OK
  Content-Type: text/event-stream

  event: message
  data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Running test suite..."}],"isError":false}}

  event: message
  data: {"jsonrpc":"2.0","id":2,"method":"notifications/progress","params":{"progress":45,"total":100}}

  event: message
  data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Tests complete: 47 passed, 0 failed"}],"isError":false}}

Session management: Streamable HTTP introduces an optional session ID mechanism. On the first request (which must be an initialize), the server may return an Mcp-Session-Id header. The client should include this header on all subsequent requests in the same session. This enables stateful server-side session tracking for servers that need it, while remaining optional for stateless servers.

POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2025-03-26

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}

HTTP/1.1 200 OK
Content-Type: application/json
Mcp-Session-Id: sess_7f3a9b2c1d4e

{"jsonrpc":"2.0","id":1,"result":{...}}

POST /mcp HTTP/1.1
Content-Type: application/json
Mcp-Session-Id: sess_7f3a9b2c1d4e

{"jsonrpc":"2.0","id":2,"method":"tools/list"}

Batch request support: The 2025-03-26 spec also supports sending an array of JSON-RPC messages in a single POST body. This enables the client to batch multiple independent requests into one HTTP round-trip, reducing connection overhead for workflows that need to enumerate tools and resources simultaneously at session start.

// Batch request: initialize + tools/list in one POST
[
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {...} }
  },
  {
    "jsonrpc": "2.0",
    "method": "notifications/initialized"
  },
  {
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list"
  }
]

OAuth 2.1 authentication: For publicly accessible or multi-tenant MCP servers, the 2025-03-26 spec formalizes OAuth 2.1 as the authentication mechanism. The server publishes its OAuth metadata at /.well-known/oauth-authorization-server. The client performs the authorization code flow with PKCE to obtain an access token, then presents it as a Bearer token in all requests.

OAuth 2.1 discovery and token flow:

1. Client fetches: GET /.well-known/oauth-authorization-server
   Response: { "authorization_endpoint": "...", "token_endpoint": "...", "scopes_supported": [...] }

2. Client generates PKCE code_verifier and code_challenge

3. Client redirects user to authorization_endpoint with:
   client_id, redirect_uri, scope, code_challenge, code_challenge_method=S256

4. User authenticates and approves. Server redirects to redirect_uri with authorization code.

5. Client exchanges code at token_endpoint with code_verifier.
   Response: { "access_token": "...", "expires_in": 3600, "scope": "tools:read tools:write" }

6. Client includes token in all MCP requests:
   Authorization: Bearer <access_token>

Tips
- When building a new remote MCP server, use Streamable HTTP from the start. The implementation is simpler than SSE (one endpoint instead of two), scales better (no sticky session requirement), and is the forward-looking standard.
- For internal MCP servers not exposed to the internet, a simple API key in the Authorization header is acceptable. For any externally accessible MCP server, implement OAuth 2.1 — do not ship basic auth or static tokens in a public deployment.
- Implement request body size limits on your Streamable HTTP server. Malformed clients or prompt injection attempts can send arbitrarily large request bodies — enforce a maximum (e.g., 1MB) and return 413 for oversized requests.
- Test Streamable HTTP servers with both the synchronous and streaming response paths explicitly. Many implementations handle the simple JSON response path but have bugs in the SSE streaming response path that only surface with long-running tools.


Choosing the Right Transport Protocol for Your MCP Server

The choice of transport protocol is a deployment architecture decision, not an implementation preference. The right choice is determined by where the server runs, who accesses it, and what your operational constraints are.

Decision framework:

Requirement stdio SSE Streamable HTTP
Server runs on same machine as host Best Possible Possible
Server runs on remote host/cloud Not possible Yes Yes (preferred)
Multiple hosts share one server No Yes Yes
Zero infrastructure overhead Yes Partial Partial
Horizontal scaling No Complex (sticky sessions) Yes
Production deployment Not recommended Legacy support only Recommended
Legacy host compatibility Universal Widely supported Growing (2025+)
OAuth / enterprise auth Not applicable Custom Built into spec
Testing simplicity Very easy Moderate Easy

Use stdio when: You are building an MCP server for local development tooling that runs on the same machine as the developer's AI coding assistant. This covers the majority of CLI-based developer workflow tools: code linters, local database inspection, file system operations, Docker container management. Stdio is the right choice for 80% of tools that developers build for their own use or their immediate team.

Use SSE when: You need remote connectivity but the MCP hosts you are targeting (Cursor, older Claude Code versions) only support SSE for remote servers. Check the host's documentation for supported transports before deciding. As of May 2026, most actively developed hosts support Streamable HTTP, but verify for your specific deployment targets.

Use Streamable HTTP when: You are building a remote MCP server for production use — whether internal (shared across a team or CI environment) or external (public or SaaS). This is the recommended path for anything beyond local development.

Multi-transport MCP servers: For servers that need to support both local and remote access, implement both stdio and Streamable HTTP and let the deployment configuration determine which is active. This is achievable with a clean adapter pattern:

// Multi-transport MCP server (TypeScript pattern)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const server = new McpServer({ name: "my-server", version: "1.0.0" });

// Register tools and resources once — transport-agnostic
server.tool("my_tool", "Does something useful", { param: z.string() }, async ({ param }) => {
  return { content: [{ type: "text", text: await doSomething(param) }] };
});

// Choose transport based on invocation context
if (process.env.MCP_TRANSPORT === "http") {
  const transport = new StreamableHTTPServerTransport({ port: 8080 });
  await server.connect(transport);
  console.error("MCP server running on HTTP :8080");
} else {
  // Default to stdio for local use
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

Operational considerations for remote transports: Remote MCP servers are network services and inherit all associated operational requirements:

  • Health checks: Expose a /health endpoint that returns 200 when the server is ready to accept MCP connections. Use this for load balancer health checks and deployment readiness probes.
  • Graceful shutdown: Handle SIGTERM by completing in-flight tool calls and closing connections cleanly. Abrupt termination mid-tool-call leaves the host in an unrecoverable state.
  • Rate limiting: Implement per-session or per-client rate limiting. An agent in a loop can easily generate hundreds of tool calls per minute. Rate limit at the MCP server layer independently of the upstream API's own rate limits.
  • Observability: Log all tool calls with duration, input argument shapes (not values — they may contain secrets), and output size. Export these as metrics for alerting on latency and error rate regressions.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: github-mcp-server
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: github-mcp
        image: your-registry/github-mcp:1.4.2
        ports:
        - containerPort: 8080
        env:
        - name: GITHUB_TOKEN
          valueFrom:
            secretKeyRef:
              name: github-mcp-secrets
              key: token
        - name: MCP_TRANSPORT
          value: "http"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 2
          periodSeconds: 5

Tips
- Decide on transport before writing server code, not after. Adding HTTP transport to a server originally written for stdio is straightforward with adapter patterns, but retrofitting auth and session management is significant work.
- For team-shared MCP servers, deploy them as proper services with versioned Docker images and a deployment pipeline. Treat an MCP server the same as any other internal microservice — it has the same reliability requirements.
- If you maintain MCP servers for multiple transport types, use integration tests that exercise the full transport layer for each variant. Transport bugs are invisible to unit tests that mock the transport.
- Document the exact transport configuration required for your MCP server in the server's README. Include copy-paste configuration snippets for Claude Code, Cursor, and any other hosts your team uses. This saves every new team member from figuring out transport configuration independently.