Introduction
MCP supports multiple transport mechanisms, and choosing the right one depends on where your server runs and how clients connect to it. This lesson walks through the decision process, explains the legacy SSE transport, and covers authentication for HTTP-based transports.
Key Concepts
- SSE transport (legacy): The original remote transport that used a dedicated SSE endpoint for server-to-client messages and a separate POST endpoint for client-to-server messages.
- Transport decision: stdio for local servers, Streamable HTTP for remote servers. This is the primary decision axis.
- OAuth 2.1: The recommended authentication mechanism for MCP servers exposed over HTTP. Supports PKCE for public clients.
- API keys: A simpler alternative to OAuth for server-to-server authentication, passed as HTTP headers.
Real World Context
If you are building an MCP server for personal use in Claude Desktop, stdio is all you need. But if you are deploying a shared MCP server for your engineering team to query production databases, you need Streamable HTTP with authentication. Understanding when to use which transport saves you from over-engineering local tools or under-securing remote ones.
Deep Dive
The Legacy SSE Transport
Before Streamable HTTP, the only remote transport was Server-Sent Events (SSE). It used two separate communication channels:
- SSE endpoint (e.g.,
GET /sse) — A long-lived connection where the server pushed messages to the client. - POST endpoint (e.g.,
POST /messages) — Where the client sent requests to the server.
This dual-endpoint design had several drawbacks:
textLegacy SSE Transport: Client --POST--> /messages (client-to-server) Client <--SSE--- /sse (server-to-client) Streamable HTTP Transport: Client --POST--> /mcp (bidirectional, single endpoint) Client <--GET--- /mcp (optional SSE for server push)
The comparison above shows how Streamable HTTP simplifies the architecture by consolidating to a single endpoint.
Why Streamable HTTP Replaced SSE
Streamable HTTP improves on the legacy SSE transport in several ways:
- Single endpoint: All communication goes through one URL, simplifying deployment behind load balancers and reverse proxies.
- Session management: The
MCP-Session-Idheader provides built-in session tracking, which SSE lacked. - Flexible responses: The server can return immediate JSON for simple requests or switch to SSE streaming for long operations, on a per-request basis.
- Stateless option: Servers that do not need sessions can operate without
MCP-Session-Id, enabling truly stateless deployments.
The legacy SSE transport is still supported for backward compatibility, but all new servers should use Streamable HTTP.
Decision Guide
Use this decision tree to pick the right transport:
textWhere does the server run? │ ├─ Same machine as client? │ └─ Use stdio │ - Simplest setup │ - No network config needed │ - Process-level isolation │ └─ Remote / cloud / shared? └─ Use Streamable HTTP - Single /mcp endpoint - Session management built in - Add OAuth 2.1 or API key auth
This decision tree covers the vast majority of scenarios. The only reason to use the legacy SSE transport is when connecting to an older server that has not been updated.
Authentication with HTTP Transports
Remote MCP servers need authentication. The MCP specification recommends OAuth 2.1 with PKCE for user-facing applications:
typescriptimport { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const transport = new StreamableHTTPClientTransport( new URL("https://api.example.com/mcp"), { requestInit: { headers: { "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..." } } } );
The transport accepts custom headers through requestInit, allowing you to pass OAuth tokens or API keys. The SDK includes these headers in every request automatically.
For simpler server-to-server scenarios, API keys work well:
typescriptconst transport = new StreamableHTTPClientTransport( new URL("https://internal.example.com/mcp"), { requestInit: { headers: { "X-API-Key": "sk-abc123def456" } } } );
API keys are passed as custom headers. This approach is simpler than OAuth but only suitable for trusted server-to-server communication.
Common Pitfalls
- Using Streamable HTTP for local tools — HTTP adds unnecessary overhead for local communication. If the server runs on the same machine, stdio is faster and simpler with no ports to manage.
- Deploying remote servers without authentication — An unauthenticated MCP server exposed to the internet lets anyone invoke your tools. Always add OAuth 2.1 or API key authentication for remote deployments.
- Relying on legacy SSE for new projects — The SSE transport has known limitations around session management and load balancing. New servers should always use Streamable HTTP.
Best Practices
- Default to stdio for local, Streamable HTTP for remote — This covers almost every use case. Only consider legacy SSE when connecting to servers you cannot update.
- Use OAuth 2.1 with PKCE for user-facing servers — This is the MCP specification's recommended authentication flow. It supports token refresh and works with standard OAuth providers.
- Support both transports in your server — If your server might be used both locally and remotely, implement both stdio and Streamable HTTP transports. The SDK makes it straightforward to support multiple transports.
Summary
- The legacy SSE transport used two endpoints (GET for SSE, POST for messages) and lacked session management.
- Streamable HTTP replaced SSE with a simpler single-endpoint design, built-in sessions, and flexible response types.
- Use stdio for local servers and Streamable HTTP for remote servers.
- Authenticate remote servers with OAuth 2.1 (PKCE) for user-facing apps or API keys for server-to-server communication.
- Support both transports in your server when it may be used in both local and remote scenarios.
Code Examples
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const server = new McpServer({
name: "my-server",
version: "1.0.0"
});
// Register tools, resources, prompts...
// Option 1: stdio for local use
if (process.env.TRANSPORT === "stdio") {
const transport = new StdioServerTransport();
await server.connect(transport);
}
// Option 2: Streamable HTTP for remote use
if (process.env.TRANSPORT === "http") {
const app = express();
const transport = new StreamableHTTPServerTransport({ endpoint: "/mcp" });
app.use("/mcp", transport.requestHandler);
app.listen(3001);
await server.connect(transport);
}