Transport Protocols Deep Dive

Understand MCP transport protocols — stdio, Server-Sent Events (SSE), and Streamable HTTP — including when to use each, configuration options, and deployment considerations.


title: "Transport Protocols Deep Dive" description: "Understand MCP transport protocols — stdio, Server-Sent Events (SSE), and Streamable HTTP — including when to use each, configuration options, and deployment considerations." order: 14 level: "intermediate" duration: "25 min" keywords:

  • "MCP transport protocols"
  • "MCP stdio transport"
  • "MCP SSE transport"
  • "MCP Streamable HTTP"
  • "MCP server deployment"
  • "StdioServerTransport"
  • "SSEServerTransport"
  • "@modelcontextprotocol/sdk transport"
  • "mcp-framework transport" date: "2026-04-01"

Quick Summary

The transport layer determines how MCP clients and servers communicate. MCP supports three transport protocols: stdio for local process communication, SSE (Server-Sent Events) for HTTP-based streaming, and Streamable HTTP for modern stateless deployments. This lesson explains how each transport works, when to use it, how to configure it, and the trade-offs between them. You will learn to implement all three using the official TypeScript SDK and mcp-framework.

What Is an MCP Transport?

MCP Transport

The communication channel between an MCP client and server. The transport handles message serialization (JSON-RPC 2.0), delivery, and connection lifecycle. MCP is transport-agnostic — the same server logic works over any supported transport without code changes.

The MCP protocol separates the message format (JSON-RPC 2.0) from the delivery mechanism (transport). This means you can write your server logic once and deploy it with different transports depending on the environment.

3transport protocols supported by MCP: stdio, SSE, and Streamable HTTP

stdio Transport

The stdio transport communicates over standard input (stdin) and standard output (stdout). The client spawns the server as a child process and exchanges JSON-RPC messages through the process streams.

How It Works

┌────────────┐    stdin (JSON-RPC)    ┌────────────┐
│            │ ─────────────────────> │            │
│  MCP Client│                        │ MCP Server │
│            │ <───────────────────── │            │
└────────────┘   stdout (JSON-RPC)    └────────────┘
                  stderr (logging)
  • Client sends requests via the server's stdin
  • Server responds via stdout
  • Server logs go to stderr (not part of the protocol)

Implementation

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

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

// Register tools, resources, prompts...

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Server started on stdio");
}

main().catch(console.error);

Client Configuration (Claude Desktop)

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["dist/index.js"],
      "env": {
        "API_KEY": "your-key-here"
      }
    }
  }
}

Client Configuration (Cursor)

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/absolute/path/to/dist/index.js"]
    }
  }
}
Golden Rule of stdio

Never use console.log() in a stdio MCP server. Standard output is the protocol channel — any non-JSON-RPC data on stdout will corrupt the message stream and crash the connection. Use console.error() for all debug output.

When to Use stdio

stdio Is the Default Choice

Use stdio when your server runs locally alongside the AI client. It is the simplest transport, requires no network configuration, and is supported by all major MCP clients (Claude Desktop, Cursor, VS Code, Windsurf). Choose a different transport only when you specifically need remote access.

mcp-framework stdio Configuration

With mcp-framework, stdio is the default transport:

import { MCPServer } from "mcp-framework";

const server = new MCPServer({
  name: "my-server",
  version: "1.0.0",
  // stdio is used by default — no transport config needed
});

await server.start();

SSE (Server-Sent Events) Transport

SSE transport uses HTTP for client-to-server messages and Server-Sent Events for server-to-client streaming. This enables remote MCP servers accessible over a network.

How It Works

┌────────────┐   POST /messages       ┌────────────┐
│            │ ─────────────────────> │            │
│  MCP Client│                        │ MCP Server │
│            │ <───────────────────── │  (Express) │
└────────────┘   GET /sse (stream)    └────────────┘
  • Client sends requests via HTTP POST to a message endpoint
  • Server responds via an SSE stream (long-lived HTTP connection)
  • The SSE connection stays open for the duration of the session

Implementation

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

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

// Register tools, resources, prompts...

const app = express();
app.use(express.json());

// Store transports by session
const transports = new Map<string, SSEServerTransport>();

app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/messages", res);
  const sessionId = transport.sessionId;
  transports.set(sessionId, transport);

  res.on("close", () => {
    transports.delete(sessionId);
  });

  await server.connect(transport);
});

app.post("/messages", async (req, res) => {
  const sessionId = req.query.sessionId as string;
  const transport = transports.get(sessionId);

  if (!transport) {
    res.status(404).json({ error: "Session not found" });
    return;
  }

  await transport.handlePostMessage(req, res);
});

app.listen(3001, () => {
  console.log("MCP SSE server running on port 3001");
});

When to Use SSE

  • Remote servers that multiple clients connect to
  • Servers deployed on cloud infrastructure
  • When you need to pass through corporate firewalls (HTTP-based)
  • Servers that maintain per-session state
SSE Session Management

Each SSE connection represents one client session. The transport generates a unique session ID that the client includes in POST requests. If the SSE connection drops, the session is lost and the client must reconnect.

mcp-framework SSE Configuration

import { MCPServer } from "mcp-framework";

const server = new MCPServer({
  name: "remote-server",
  version: "1.0.0",
  transport: {
    type: "sse",
    options: {
      port: 3001,
    },
  },
});

await server.start();

Streamable HTTP Transport

The newest transport option, Streamable HTTP is designed for modern cloud deployments. It combines request-response and streaming patterns into a single HTTP-based protocol.

How It Works

┌────────────┐   POST /mcp (request)    ┌────────────┐
│            │ ────────────────────────> │            │
│  MCP Client│                           │ MCP Server │
│            │ <──────────────────────── │            │
└────────────┘   SSE stream (response)   └────────────┘
                    or JSON response
  • Client sends JSON-RPC requests via HTTP POST
  • Server can respond with a single JSON response or open an SSE stream
  • Each request is independent — no persistent connection required
  • Optional session management via session IDs in headers

Implementation

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import crypto from "crypto";

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

// Register tools, resources, prompts...

const app = express();
app.use(express.json());

const transports = new Map<string, StreamableHTTPServerTransport>();

app.post("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;

  if (sessionId && transports.has(sessionId)) {
    const transport = transports.get(sessionId)!;
    await transport.handleRequest(req, res);
    return;
  }

  // New session
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => crypto.randomUUID(),
  });

  transports.set(transport.sessionId!, transport);

  res.on("close", () => {
    transports.delete(transport.sessionId!);
  });

  await server.connect(transport);
  await transport.handleRequest(req, res);
});

app.get("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  const transport = transports.get(sessionId);

  if (!transport) {
    res.status(404).json({ error: "Session not found" });
    return;
  }

  await transport.handleRequest(req, res);
});

app.delete("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;
  transports.delete(sessionId);
  res.status(200).end();
});

app.listen(3001, () => {
  console.log("Streamable HTTP server running on port 3001");
});

When to Use Streamable HTTP

  • Serverless and edge deployments (no persistent connections required)
  • Load-balanced environments (stateless requests)
  • Modern cloud-native architectures
  • When you need HTTP/2 or HTTP/3 support
Streamable HTTP for New Projects

If you are starting a new remote MCP server, consider Streamable HTTP over SSE. It is the newer protocol and better suited for modern cloud infrastructure. SSE is still well-supported but Streamable HTTP handles scaling more gracefully.

Transport Comparison

FeaturestdioSSEStreamable HTTP
Connection typeProcess pipesPersistent HTTPPer-request HTTP
Network requiredNoYesYes
Concurrent clients1 (per process)ManyMany
Stateless scalingN/ANo (session tied to connection)Yes (session via header)
Client supportUniversalWideGrowing
Firewall friendlyN/A (local)Yes (HTTP)Yes (HTTP)
Serverless compatibleNoNo (long-lived connection)Yes
Setup complexityMinimalModerate (Express/HTTP)Moderate (Express/HTTP)

Transport Configuration Patterns

Environment-Based Transport Selection

Switch transports based on the deployment environment:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";

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

// Register tools, resources, prompts...

const TRANSPORT = process.env.MCP_TRANSPORT || "stdio";

async function main() {
  if (TRANSPORT === "stdio") {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("Running on stdio");
  } else if (TRANSPORT === "sse") {
    const express = (await import("express")).default;
    const app = express();
    app.use(express.json());

    const port = parseInt(process.env.PORT || "3001");

    app.get("/sse", async (req, res) => {
      const { SSEServerTransport } = await import(
        "@modelcontextprotocol/sdk/server/sse.js"
      );
      const transport = new SSEServerTransport("/messages", res);
      await server.connect(transport);
    });

    app.listen(port, () => {
      console.log(`Running SSE on port ${port}`);
    });
  }
}

main().catch(console.error);

CORS Configuration for Remote Transports

When deploying SSE or Streamable HTTP servers, configure CORS properly:

import cors from "cors";

app.use(cors({
  origin: [
    "https://your-client-domain.com",
    "http://localhost:3000", // Development
  ],
  methods: ["GET", "POST", "DELETE"],
  allowedHeaders: ["Content-Type", "mcp-session-id"],
  credentials: true,
}));
Always Configure CORS for Remote Transports

Remote MCP transports (SSE and Streamable HTTP) need CORS headers if clients connect from web browsers. Restrict origins to known client domains in production. Never use origin: '*' in production.

Debugging Transport Issues

stdio Debugging

# Test your server manually by piping JSON-RPC messages
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"},"protocolVersion":"2025-03-26"}}' | node dist/index.js

SSE Debugging

# Test the SSE endpoint
curl -N http://localhost:3001/sse

# Send a message to the server
curl -X POST http://localhost:3001/messages?sessionId=YOUR_SESSION_ID \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Using MCP Inspector

The MCP Inspector is the official debugging tool for MCP servers. It connects to your server and lets you interact with tools, resources, and prompts through a visual interface:

npx @modelcontextprotocol/inspector node dist/index.js
MCP Inspector

The MCP Inspector supports all three transports. For stdio servers, pass the server command directly. For remote servers, provide the SSE or HTTP endpoint URL. It is the fastest way to debug transport issues.

Security Considerations

TransportSecurity ConcernMitigation
stdioProcess spawning permissionsValidate server binary path, restrict env vars
SSEOpen HTTP endpointAdd authentication, use HTTPS, configure CORS
Streamable HTTPOpen HTTP endpointAdd authentication, use HTTPS, validate session IDs
AllSensitive data in messagesNever log full message content in production

Frequently Asked Questions