MCP Transport Options

Three ways MCP servers communicate -- stdio for local, SSE (deprecated), and Streamable HTTP for remote. When to use each, how sessions work, and testing with the MCP Inspector.

April 1, 20268 min read

Everything you've built so far uses stdio -- standard input/output. It runs locally on your machine and communicates by piping text between processes. That's perfect for development.

For production -- when you want a company to run an MCP server that anyone can connect to -- you need a remote transport. This post covers all three options.


The Three Transports

Diagram

stdio -- The Local Transport

You've been using this all along. The process starts, hangs on stdin, and your MCP client pipes messages in and reads responses out.

Bash
node mcp.js # Process starts, waits for input on stdin # Client sends: { "jsonrpc": "2.0", "id": 1, "method": "tools/list", ... } # Server responds to stdout # Client reads the response

When to use stdio:

  • Local development and testing
  • Tools that need file system access (can't do remotely)
  • Claude Desktop, Tome, Cursor local integrations
  • Anything that can't or shouldn't be on the network

Why it'll never go away: If you have a tool that creates, reads, or deletes files on your computer, it has to run locally. You can't delegate file system access to a remote server. stdio is the only option for these cases.


SSE -- Server-Sent Events (Deprecated)

SSE was the first attempt at a remote MCP transport, released November 2024. Deprecated March 2025 -- a 4-month lifespan.

The idea was good: use the web's existing SSE technology (one-way real-time push from server to client) to bridge the gap between stdio and the internet.

Diagram

Why it was deprecated:

  • Stateful -- session state lived in memory on one server
  • Not horizontally scalable -- can't load-balance across multiple instances
  • Network fragility -- dropped connection = lost session, no recovery
  • Two endpoints -- had a /messages endpoint AND a SSE stream endpoint to manage

Do you need to know it? Just know it exists. You'll encounter legacy servers that still use it (Neon supports it for backward compatibility). Don't build new ones.


Streamable HTTP -- The Modern Remote Transport

Released after SSE's deprecation, Streamable HTTP is the right way to run a remote MCP server.

Diagram

Key improvements over SSE:

FeatureSSEStreamable HTTP
EndpointsTwo (/messages + SSE stream)One (/mcp)
StateIn-memory, one serverSession ID in HTTP header
Network dropsSession lostResumable via session ID
Horizontal scalingHardEasy -- store sessions in Redis
SerializationMust be serializableMust be serializable

Building a Streamable HTTP Server

Here's the complete implementation using Express:

JavaScript
// streamable.js import express from "express"; import { randomUUID } from "crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import registerJobsTools from "./jobs-based-tools.js"; const app = express(); app.use(express.json()); // In-memory session store (use Redis in production) const sessions = {}; function createMcpServer() { const server = new McpServer({ name: "issue-server", version: "1.0.0" }); registerJobsTools(server); return server; } app.post("/mcp", async (req, res) => { const sessionId = req.headers["mcp-session-id"]; if (req.body.method === "initialize") { // New session const newSessionId = randomUUID(); const transport = new StreamableHTTPServerTransport({ sessionId: newSessionId }); const server = createMcpServer(); await server.connect(transport); sessions[newSessionId] = { transport, server }; res.setHeader("Mcp-Session-Id", newSessionId); await transport.handleRequest(req, res); } else if (sessionId && sessions[sessionId]) { // Existing session const { transport } = sessions[sessionId]; await transport.handleRequest(req, res); } else { res.status(400).json({ error: "No valid session" }); } }); // Cleanup on close app.delete("/mcp", (req, res) => { const sessionId = req.headers["mcp-session-id"]; if (sessionId && sessions[sessionId]) { delete sessions[sessionId]; } res.status(200).end(); }); app.listen(3001, () => { console.log("MCP server running on http://localhost:3001/mcp"); });

Install Express:

Bash
npm install express

Run it:

Bash
node streamable.js # MCP server running on http://localhost:3001/mcp

DNS Rebinding Protection

The code includes this flag:

JavaScript
new StreamableHTTPServerTransport({ sessionId: newSessionId, // ⚠️ Only disable in development: // dangerouslyDisableDnsRebindingProtection: true });

DNS rebinding is an attack where a malicious site hijacks your session by serving responses from a different origin than the one your browser established the connection with.

🚨 Never disable DNS rebinding protection in production. The only reason to disable it is to use the MCP Inspector during development (the inspector needs it disabled to connect). Re-enable it before deploying.

The MCP Inspector

The MCP Inspector is an official debugging tool for MCP servers -- a web interface that lets you call tools, read resources, and inspect messages without needing a full client.

Bash
# Run the inspector (connects to your running Streamable HTTP server) npx @modelcontextprotocol/inspector # Choose transport type: Streamable HTTP # URL: http://localhost:3001/mcp # Click Connect
Diagram

What you can do with it:

  • See all registered tools with their schemas
  • Call any tool directly with test inputs
  • See the raw request and response
  • View the full message history
  • Test resources and prompts

This is far more convenient than writing raw echo commands. Use it for all Streamable HTTP development.


Production Session Management

The in-memory sessions object works for development. For production with multiple server instances, move sessions to Redis:

JavaScript
// Production pattern (conceptual) import Redis from "ioredis"; const redis = new Redis(); // On session create: await redis.set(`mcp-session:${sessionId}`, JSON.stringify(sessionData), "EX", 3600); // On each request: const sessionData = await redis.get(`mcp-session:${sessionId}`);

This makes your MCP server horizontally scalable -- any instance can handle any request from any client.


Lab -- Understand the Session Flow

JavaScript · Live Editor
Loading editor...
✅ Session ID = continuity. The client remembers the session ID and sends it with every request. The server uses it to look up the connection state. Drop the session ID → start over. This is what makes Streamable HTTP resumable and scalable.

Lab 2 -- Pick the Right Transport

JavaScript · Live Editor
Loading editor...

Key Takeaways

  • stdio -- local only. Will always exist. Use for local tools, file access, development.
  • SSE -- deprecated March 2025. Stateful, fragile, not scalable. Don't build new ones.
  • Streamable HTTP -- modern remote standard. One endpoint, session IDs, resumable, scalable.
  • Session IDs live in the Mcp-Session-Id HTTP header -- any server instance can handle any request
  • MCP Inspector is your best debugging tool for Streamable HTTP servers
  • Disable DNS rebinding protection only in dev, never in production

What's Next

You know how to build and deploy MCP servers. Now let's put them to work in a real coding workflow -- installing pre-built MCP servers into Claude Code and Cursor, and seeing what "vibe coding" actually looks like.