Real-Time Data Streaming via MCP SSE and WebSockets
Architecture for asynchronous, non-blocking tool execution in Model Context Protocol (MCP) using Server-Sent Events (SSE), WebSockets, and JSON-RPC progress notifications.
Generate & Validate Multi-Client MCP Config
One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.
Real-Time Data Streaming via MCP SSE and WebSockets
In traditional synchronous API design, a client sends a request and blocks until the server computes and returns the complete response payload. In AI agent environments, this synchronous model breaks down immediately. Enterprise tool operations—such as building large container images, executing multi-table SQL analytic aggregations, or running automated regression test suites—frequently take 30 to 180 seconds.
If an MCP server blocks its main event loop or leaves an HTTP connection hanging without feedback, AI hosts (Cursor, Claude Code, Windsurf) will hit internal transport timeouts (typically 30s to 60s), abort the agent turn, and trigger hallucination cascades.
Solving this requires asynchronous, non-blocking streaming transports over Server-Sent Events (SSE) or WebSockets, coupled with MCP JSON-RPC 2.0 Progress Notifications (notifications/progress).
This guide covers the streaming architecture, wire payloads, event-loop preservation strategies, and a complete TypeScript/BullMQ implementation for streaming long-running worker tasks to MCP clients.
1. Streaming Architecture: Asynchronous Task Decoupling
To prevent long-running tasks from stalling the MCP server process, the server must decouple JSON-RPC request acceptance from actual task execution using a background worker queue (e.g., Redis + BullMQ).
sequenceDiagram
autonumber
participant Host as AI Host (Cursor / Claude Code)
participant Server as Remote MCP Server (SSE Endpoint)
participant Queue as Redis Task Queue (BullMQ)
participant Worker as Background Worker Daemon
Host->>Server: POST /messages [tools/call: build_and_deploy_service]
Note over Host,Server: Includes meta.progressToken: "job-991"
Server->>Queue: Enqueue Build Job
Server-->>Host: 202 Accepted / Initial Response
Queue->>Worker: Dequeue & Execute Build
loop During Execution (Every 2 seconds)
Worker->>Queue: Publish Progress Event
Queue->>Server: Progress Callback
Server->>Host: SSE Event: notifications/progress (35% - Compiling)
Server->>Host: SSE Event: notifications/progress (78% - Packaging)
end
Worker->>Queue: Job Complete (Artifact URL)
Queue->>Server: Result Ready
Server->>Host: SSE Event: tools/call Result [Completed]Protocol Advantages
- ▸Maintains Host Liveness: Periodic progress notifications tell the AI host that the tool is actively progressing, preventing connection timeouts.
- ▸Non-Blocking Node.js / Python Event Loop: The HTTP/SSE server remains fully responsive to incoming health checks, cancellation signals, and concurrent tool requests.
- ▸Interactive UI Rendering: Host IDEs render live progress bars and step counters directly in the chat interface.
2. Wire Protocol: JSON-RPC Progress Notifications
The official Model Context Protocol specification defines standard progress reporting via notifications/progress.
Step 1: Tool Invocation with Progress Token
The host client passes a progressToken inside the request metadata (_meta):
{
"jsonrpc": "2.0",
"id": "build-req-102",
"method": "tools/call",
"params": {
"name": "execute_cloud_build",
"arguments": {
"service_name": "billing-api",
"environment": "staging"
},
"_meta": {
"progressToken": "token-xyz-8812"
}
}
}Step 2: Intermediate Progress Notifications (SSE Stream)
As the task executes, the MCP server emits unprompted JSON-RPC notifications over the open SSE connection. Because these are notifications, they have no id field:
event: message
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"token-xyz-8812","progress":25,"total":100,"message":"Docker image build: Layer 3/8 compiled"}}
event: message
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"token-xyz-8812","progress":70,"total":100,"message":"Running database migration dry-run"}}Step 3: Final Execution Result
Once the task finishes, the server pushes the final JSON-RPC response matching the original request id:
event: message
data: {"jsonrpc":"2.0","id":"build-req-102","result":{"content":[{"type":"text","text":"Build and deployment successful: Version 2.14.0 deployed to staging.\nArtifact: registry.enterprise.internal/billing-api:2.14.0"}]}}3. Production Asynchronous Streaming Server (TypeScript & BullMQ)
Below is an MCP streaming server that accepts long-running tasks, schedules them on a background Redis queue, and streams real-time progress events back to connected clients:
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import { Queue, Worker, QueueEvents } from 'bullmq';
import Redis from 'ioredis';
const app = Fastify({ logger: true });
const redisConnection = new Redis(process.env.REDIS_URL || 'redis://');
// 1. Task Queue Definition
const taskQueue = new Queue('mcp-long-tasks', { connection: redisConnection });
const queueEvents = new QueueEvents('mcp-long-tasks', { connection: redisConnection });
// Track active client SSE response streams by sessionId
const activeStreams = new Map<string, FastifyReply>();
// 2. Persistent SSE Endpoint
app.get('/sse', async (request: FastifyRequest, reply: FastifyReply) => {
reply.raw.setHeader('Content-Type', 'text/event-stream');
reply.raw.setHeader('Cache-Control', 'no-cache');
reply.raw.setHeader('Connection', 'keep-alive');
reply.raw.flushHeaders();
const sessionId = 'sess_' + Math.random().toString(36).substring(2, 9);
activeStreams.set(sessionId, reply);
// Send initial endpoint registration event
reply.raw.write(`event: endpoint\ndata: /messages?sessionId=${sessionId}\n\n`);
request.raw.on('close', () => {
activeStreams.delete(sessionId);
console.log(`SSE session disconnected: ${sessionId}`);
});
});
// 3. Asynchronous Tool Dispatcher Route
app.post('/messages', async (request: FastifyRequest<{ Querystring: { sessionId: string } }>, reply: FastifyReply) => {
const { sessionId } = request.query;
const body = request.body as any;
if (body.method !== 'tools/call') {
return reply.send({ jsonrpc: '2.0', id: body.id, error: { code: -32601, message: 'Method not supported' } });
}
const sseReply = activeStreams.get(sessionId);
if (!sseReply) {
return reply.status(400).send({ error: 'SSE session stream not active' });
}
const progressToken = body.params?._meta?.progressToken;
// Enqueue long-running task to background worker
const job = await taskQueue.add('execute_long_task', {
toolName: body.params.name,
args: body.params.arguments,
requestId: body.id,
progressToken,
sessionId
});
// Acknowledge receipt immediately to HTTP caller
reply.status(202).send({ status: 'queued', jobId: job.id });
});
// 4. Listen for Background Worker Progress & Completion Events
queueEvents.on('progress', ({ jobId, data }: { jobId: string; data: any }) => {
const sseReply = activeStreams.get(data.sessionId);
if (sseReply && data.progressToken) {
const notification = {
jsonrpc: '2.0',
method: 'notifications/progress',
params: {
progressToken: data.progressToken,
progress: data.progress,
total: 100,
message: data.message
}
};
sseReply.raw.write(`event: message\ndata: ${JSON.stringify(notification)}\n\n`);
}
});
queueEvents.on('completed', ({ jobId, returnvalue }: { jobId: string; returnvalue: any }) => {
const sseReply = activeStreams.get(returnvalue.sessionId);
if (sseReply) {
const responsePayload = {
jsonrpc: '2.0',
id: returnvalue.requestId,
result: {
content: [{ type: 'text', text: returnvalue.resultText }]
}
};
sseReply.raw.write(`event: message\ndata: ${JSON.stringify(responsePayload)}\n\n`);
}
});
// 5. Worker Implementation (Runs in isolated process or thread pool)
const worker = new Worker('mcp-long-tasks', async (job) => {
const { toolName, args, progressToken, sessionId } = job.data;
// Step 1: 25% Progress
await new Promise((r) => setTimeout(r, 2000));
await job.updateProgress({ progress: 25, message: 'Validating dependencies', progressToken, sessionId });
// Step 2: 75% Progress
await new Promise((r) => setTimeout(r, 4000));
await job.updateProgress({ progress: 75, message: 'Compiling project artifacts', progressToken, sessionId });
// Step 3: Complete
await new Promise((r) => setTimeout(r, 2000));
return {
requestId: job.data.requestId,
sessionId,
resultText: `Task '${toolName}' completed successfully in 8.0s.`
};
}, { connection: redisConnection });
app.listen({ port: 8080, host: '0.0.0.0' }, () => {
console.log('Streaming MCP Server running on port 8080');
});4. SSE vs. WebSockets: Architectural Trade-Offs
When selecting the transport for streaming MCP:
| Feature | Server-Sent Events (SSE) + HTTP POST | Full Bidirectional WebSockets |
|---|---|---|
| Protocol Simplicity | Standard HTTP/1.1 or HTTP/2. Easy to proxy. | Requires WebSocket upgrade handshake. |
| Firewall & Proxy Compatibility | 100% compatible with corporate firewalls and standard Ingress. | Often blocked or intercepted by corporate deep packet inspection (DPI). |
| Load Balancing | Standard HTTP sticky sessions; request/response decoupled. | Stateful persistent socket pinning required. |
| Client Native Support | Default remote transport for Claude Desktop and Cursor. | Requires custom client transport wrappers. |
| Recommendation | Standard Choice for enterprise remote MCP. | Specialized for ultra-high-frequency bidirectional binary feeds. |
5. Client Keep-Alives & Heartbeat Resilience
To prevent intermediate NAT gateways and cloud load balancers (AWS ALB, Cloudflare) from closing idle SSE connections during pauses between tool invocations, the server must emit periodic heartbeat comments every 15 seconds:
// SSE Heartbeat Timer
setInterval(() => {
for (const [sessionId, reply] of activeStreams.entries()) {
// SSE comments start with a colon ':' and are ignored by JSON-RPC parsers
reply.raw.write(': ping keep-alive\n\n');
}
}, 15000);Related Streaming & Architecture Guides
Build your full agent toolstack in the Visual Generator
Combine Real-Time Data Streaming via MCP SSE and WebSockets with databases, search APIs, and memory graphs in a single configuration file.
Real-Time Data Streaming via MCP SSE and WebSockets FAQ
What is the Real-Time Data Streaming via MCP SSE and WebSockets?
Architecture for asynchronous, non-blocking tool execution in Model Context Protocol (MCP) using Server-Sent Events (SSE), WebSockets, and JSON-RPC progress notifications.
How do I configure Real-Time Data Streaming via MCP SSE and WebSockets in Claude Desktop or Cursor?
You can copy the configuration JSON from our guide or launch the interactive MCP Codex Config Generator at https://mcp-codex.com/generator to export valid configs in 1 click.
Can I use Real-Time Data Streaming via MCP SSE and WebSockets with the OpenAI Codex CLI?
Yes, OpenAI Codex CLI supports Model Context Protocol. You can add it directly to ~/.codex/config.toml or pass arguments to codex mcp add.
Specializing in Model Context Protocol (MCP) integrations, autonomous AI agent orchestration, and distributed developer toolchains. Researches and benchmarks production MCP client-server architectures across OpenAI Codex, Claude, and Cursor.
Related Guides
Deploying Remote MCP Servers on Kubernetes at Scale
Production guide to deploying containerized remote Model Context Protocol (MCP) servers on Kubernetes with Helm, JSON-RPC queue-based HPA, and secure Ingress.
EnterpriseHardening MCP for SOC 2 and HIPAA Enterprise Workflows
Compliance architecture for Model Context Protocol (MCP): Client-side PII masking, immutable JSON-RPC audit logging, and zero-knowledge data pipelines for SOC 2 and HIPAA.
EnterpriseSandboxing MCP Server Execution: Containers to MicroVMs
Isolating Model Context Protocol (MCP) server execution to neutralize arbitrary code execution, filesystem escapes, and credential exfiltration using Docker rootless, gVisor, and Firecracker microVMs.