SSE vs Streamable HTTP in MCP: Key Differences and Implementation Guide
While both transports use HTTP, SSE uses separate GET /sse and POST /message endpoints with unidirectional streaming and no message resumability, whereas Streamable HTTP offers a unified /mcp endpoint with bidirectional streaming, client-managed sessions via the mcp-session-id header, and automatic event replay through an in-memory event store.
The Model Context Protocol (MCP) provides flexible transport abstractions for connecting AI clients and servers. In the modelcontextprotocol/servers repository, developers can choose between Server-Sent Events (SSE) and Streamable HTTP transports depending on reliability requirements and session management needs. Understanding the difference between SSE and streamable HTTP transports in MCP helps you select the appropriate implementation for stateless updates versus long-running, resumable interactions.
Connection Model and Endpoint Structure
The fundamental architectural divergence begins with how each transport handles HTTP endpoints.
SSE Transport (SSEServerTransport) splits communication across two distinct routes. According to the source code in src/everything/transports/sse.ts (lines 25-48), the server creates a new transport instance when a client issues a GET request to /sse, which establishes a persistent connection for server-to-client streaming. Client-to-server messages must be sent separately via POST requests to /message (lines 58-71), requiring the client to include the sessionId as a query parameter.
Streamable HTTP Transport (StreamableHTTPServerTransport) consolidates all communication into a single /mcp endpoint that handles POST, GET, and DELETE methods. As implemented in src/everything/transports/streamableHttp.ts, this design enables true bidirectional communication over one connection: clients send JSON-RPC requests via POST and receive streamed responses on the same transport, while GET requests allow clients to subscribe to incremental server events.
Session Management and Identification
Session handling represents a critical operational difference between the two transports.
SSE generates the session identifier automatically during the initial GET request. The server stores the transport instance in a server-side map keyed by transport.sessionId, returning this ID to the client through the SSE stream itself. The client must capture this ID from the first event message to use in subsequent POST requests to the /message endpoint.
Streamable HTTP shifts session ownership to the client through the mcp-session-id HTTP header. When a client makes an initial POST request without this header (lines 59-84 in streamableHttp.ts), the server generates a new session ID and returns it in the response header. Clients include this header on all subsequent requests to maintain state. Explicit session termination occurs via DELETE requests (lines 64-98), which trigger cleanup hooks distinct from simple connection closure.
Resumability and Event Replay
The transports differ significantly in handling connection drops and message recovery.
The SSE transport provides no built-in event store—if the connection drops, the client must establish a fresh stream and loses any events transmitted during the outage. This makes SSE suitable for "fire-and-forget" scenarios like live UI updates where historical message loss is acceptable.
The Streamable HTTP transport implements an InMemoryEventStore (lines 11-36 in streamableHttp.ts) that assigns a monotonic eventId to every dispatched message. When resuming a connection, clients send the Last-Event-ID header with the ID of the last received message. The server then replays all events occurring after that ID from the in-memory store, ensuring reliable delivery for long-running tool calls and stateful interactions.
Lifecycle Hooks and Connection Management
Both transports register cleanup handlers, but Streamable HTTP offers more granular control.
In src/everything/transports/sse.ts, the transport removes itself from the active sessions map when the underlying SSE connection closes via server.server.onclose (lines 25-48). This passive cleanup works well for simple request-response cycles but offers no explicit termination mechanism.
The Streamable HTTP implementation in streamableHttp.ts registers both onsessioninitialized (lines 78-84) for setup and server.server.onclose (lines 87-96) for teardown. Additionally, the DELETE method provides explicit session termination, allowing clients or administrators to forcibly close sessions and free resources without waiting for connection timeout.
Implementation Examples
SSE Client Implementation
When using the SSE transport, clients must manage two separate connections and extract the session ID from the initial event:
// Subscribe to an SSE stream
const evtSource = new EventSource('http://localhost:3001/sse');
// The server sends a sessionId in the first event; store it for later POSTs
let sessionId: string | undefined;
evtSource.onmessage = (e) => {
if (!sessionId) {
// First message contains the sessionId (transport.sessionId)
sessionId = e.data;
}
console.log('Server event:', e.data);
};
// Send a client request over the complementary POST endpoint
async function sendMessage(payload: any) {
if (!sessionId) throw new Error('No session yet');
const resp = await fetch(`http://localhost:3001/message?sessionId=${sessionId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return resp.json();
}
Streamable HTTP Client Implementation
The Streamable HTTP transport simplifies client logic by using headers for session management and enabling connection resumption:
// Helper to start or resume a session
async function startSession(): Promise<string> {
const resp = await fetch('http://localhost:3001/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', method: 'ping', id: 1 }),
});
const sessionId = resp.headers.get('mcp-session-id');
if (!sessionId) throw new Error('No session ID returned');
return sessionId;
}
// Send a request within an existing session
async function sendRequest(sessionId: string, request: object) {
const resp = await fetch('http://localhost:3001/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'mcp-session-id': sessionId,
},
body: JSON.stringify(request),
});
return resp.json();
}
// Resume a lost connection, replaying missed events
async function resume(sessionId: string, lastEventId: string) {
const resp = await fetch('http://localhost:3001/mcp', {
method: 'GET',
headers: {
'mcp-session-id': sessionId,
'Last-Event-ID': lastEventId,
},
});
// The response body streams events that occurred after `lastEventId`
}
Summary
-
Endpoint Structure: SSE requires separate GET
/sseand POST/messageendpoints, while Streamable HTTP uses a unified/mcpendpoint supporting POST, GET, and DELETE methods. -
Session Management: SSE generates session IDs server-side during the initial connection, whereas Streamable HTTP uses the
mcp-session-idheader managed by the client. -
Resumability: SSE offers no message replay capability; Streamable HTTP provides an
InMemoryEventStorewith automatic event replay when clients specifyLast-Event-ID. -
Communication Direction: SSE is unidirectional from server to client (with client messages sent separately), while Streamable HTTP supports bidirectional streaming over a single connection.
-
Lifecycle Control: Streamable HTTP supports explicit session termination via DELETE requests, while SSE relies solely on connection closure detection.
Frequently Asked Questions
Can I switch from SSE to Streamable HTTP without changing my MCP server logic?
Yes, the transport layer is abstracted from the core MCP protocol implementation. Both SSEServerTransport and StreamableHTTPServerTransport in modelcontextprotocol/servers wrap the same McpServer instance from src/everything/server/index.ts, allowing you to swap transports without modifying tool definitions or request handlers.
How does Streamable HTTP handle session resumption after a network failure?
The transport maintains an InMemoryEventStore (lines 11-36 of streamableHttp.ts) that records every event with a unique ID. When reconnecting, the client sends the last received event ID via the Last-Event-ID header. The server then replays all messages with higher IDs from the in-memory buffer, ensuring no data loss during transient network interruptions.
Why does SSE require a separate POST endpoint for client messages?
SSE establishes a unidirectional server-to-client channel using the text/event-stream MIME type. Because the browser's EventSource API cannot send data over that connection, the MCP implementation requires a separate HTTP POST endpoint (/message) for client-to-server communication, with the session ID passed as a query parameter to correlate requests with the correct SSE stream.
Is there a performance difference between the two transports for high-frequency updates?
Streamable HTTP may introduce slightly higher memory usage due to the InMemoryEventStore buffering events for potential replays (lines 11-36 in streamableHttp.ts). However, for high-frequency scenarios where message loss is unacceptable, this overhead provides reliability guarantees that SSE cannot offer. For simple, stateless updates where dropped messages are tolerable, SSE provides lower overhead by eliminating the event store entirely.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →