Server-Sent Events Basics
Understand the SSE format, event structure, and event IDs.
What are Server-Sent Events?
Server-Sent Events (SSE) is a standard for pushing updates from a server to a client over HTTP. Unlike WebSockets, SSE is unidirectional — data flows only from server to client — which makes it simpler and works through most proxies and firewalls.
SSE is perfect for MCP because the client sends requests via POST (one direction) and receives responses via SSE (other direction). This clean separation is simpler than bidirectional WebSockets.
SSE Event Format
SSE uses a simple text-based format. Each event consists of one or more fields, with each field on its own line:
event: message
id: 1
data: {"jsonrpc":"2.0","id":1,"result":{"tools":[]}}
event: message
id: 2
data: {"jsonrpc":"2.0","id":2,"result":{"resources":[]}}
Event Fields
| Field | Purpose | Required |
|---|---|---|
event: |
Event type name (e.g., "message") | Optional (defaults to "message") |
data: |
The event payload (can span multiple lines) | Required |
id: |
Unique event identifier for reconnection | Optional |
retry: |
Reconnection time in milliseconds | Optional |
Critical Formatting Rules
SSE has strict formatting requirements:
- Each event ends with a blank line (two newlines:
\n\n) - Fields are separated by a single newline (
\n) - The
data:field contains your JSON payload - Multi-line data requires multiple
data:lines
def format_sse_event(data, event_type="message", event_id=None):
"""Format data as a Server-Sent Event."""
lines = []
# Add event type
if event_type:
lines.append(f"event: {event_type}")
# Add event ID (useful for reconnection)
if event_id is not None:
lines.append(f"id: {event_id}")
# Add data (must be a single line for JSON)
lines.append(f"data: {data}")
# Join with newlines and add blank line to end event
return "\n".join(lines) + "\n\n"
# Example usage:
import json
response = {
"jsonrpc": "2.0",
"id": 1,
"result": {"tools": []}
}
sse_event = format_sse_event(
data=json.dumps(response),
event_type="message",
event_id=1
)
print(sse_event)
# Output:
# event: message
# id: 1
# data: {"jsonrpc":"2.0","id":1,"result":{"tools":[]}}
#
# (blank line)
Event IDs and Reconnection
Event IDs enable clients to resume after a disconnection. When a client
reconnects, it sends the last received ID in the Last-Event-ID
header:
┌─────────────────────────────────────────────────────────────┐
│ SSE RECONNECTION │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Client receives events with IDs │
│ ◄── id: 1, data: {...} │
│ ◄── id: 2, data: {...} │
│ ◄── id: 3, data: {...} │
│ │
│ 2. Connection drops after id: 3 │
│ ✕ DISCONNECTED │
│ │
│ 3. Client reconnects with Last-Event-ID │
│ ──► GET /events │
│ Last-Event-ID: 3 │
│ │
│ 4. Server resumes from id: 4 │
│ ◄── id: 4, data: {...} │
│ ◄── id: 5, data: {...} │
│ │
└─────────────────────────────────────────────────────────────┘
MCP-Specific SSE Usage
In MCP, SSE is used for streaming JSON-RPC responses. Each event contains a complete JSON-RPC message:
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{}}}
event: message
data: {"jsonrpc":"2.0","method":"notifications/initialized"}
Each SSE data: line should contain a complete, single-line
JSON object. Don't pretty-print your JSON when sending SSE events!
HTTP Headers for SSE
When sending SSE responses, include these headers:
def send_sse_headers(self):
"""Send the required headers for SSE responses."""
self.send_response(200)
# Required: Identifies this as an SSE stream
self.send_header("Content-Type", "text/event-stream")
# Prevent caching of the stream
self.send_header("Cache-Control", "no-cache")
# Keep the connection open
self.send_header("Connection", "keep-alive")
# Allow cross-origin requests (if needed)
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
Now that you understand SSE format, let's implement the POST request handler that receives JSON-RPC requests from clients.