MP-301e · Module 3
WebSocket Bridges
4 min read
WebSocket connections deliver server-pushed data with minimal latency — stock prices, chat messages, IoT sensor readings. An MCP server that bridges a WebSocket feed translates the continuous stream into discrete MCP resources. The bridge maintains a persistent WebSocket connection to the data source, buffers incoming messages, and serves the latest state (or a recent window) when an MCP client reads the resource. Subscription notifications are emitted when new messages arrive, letting the client decide when to re-read.
Connection lifecycle management is the hardest part of a WebSocket bridge. The bridge must handle initial connection, authentication, automatic reconnection on disconnect, exponential backoff on repeated failures, and graceful shutdown. A production bridge also monitors connection health — if the WebSocket has not received a message in longer than the expected interval, it should emit a heartbeat probe or reconnect. All of this runs independently of MCP client connections; the bridge maintains the WebSocket regardless of how many (or few) MCP clients are subscribed.
State buffering determines what the MCP client sees when it reads the resource. A "latest value" buffer holds only the most recent message — ideal for current price, current temperature, or current status. A "sliding window" buffer holds the last N messages or the last T seconds — ideal for recent activity feeds. A "snapshot + delta" buffer holds a periodic full snapshot plus incremental changes — ideal for complex state that is expensive to recompute. The buffer type should match how the AI model will use the data.
import WebSocket from "ws";
class WebSocketBridge {
private ws: WebSocket | null = null;
private buffer: { data: unknown; timestamp: number }[] = [];
private maxBufferSize = 100;
private reconnectDelay = 1000;
private maxReconnectDelay = 30_000;
constructor(private url: string, private authToken: string) {
this.connect();
}
private connect(): void {
this.ws = new WebSocket(this.url, {
headers: { Authorization: `Bearer ${this.authToken}` },
});
this.ws.on("open", () => {
this.reconnectDelay = 1000; // Reset on successful connect
});
this.ws.on("message", (raw) => {
const data = JSON.parse(raw.toString());
this.buffer.push({ data, timestamp: Date.now() });
if (this.buffer.length > this.maxBufferSize) {
this.buffer.shift(); // Drop oldest
}
// Notify subscribed MCP clients
subscriptionManager.onChange(this.resourceUri);
});
this.ws.on("close", () => this.reconnect());
this.ws.on("error", () => this.reconnect());
}
private reconnect(): void {
setTimeout(() => {
this.connect();
// Exponential backoff with cap
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
}, this.reconnectDelay);
}
/** MCP resource read — return current buffer state */
getSnapshot(): { items: unknown[]; lastUpdate: number; connected: boolean } {
return {
items: this.buffer.map((b) => b.data),
lastUpdate: this.buffer.length > 0
? this.buffer[this.buffer.length - 1].timestamp
: 0,
connected: this.ws?.readyState === WebSocket.OPEN,
};
}
private get resourceUri(): string {
return `ws://${new URL(this.url).host}/stream`;
}
}
- Implement reconnection logic Use exponential backoff starting at 1 second, capped at 30 seconds. Reset the delay on successful connection. Log every reconnection attempt for debugging.
- Choose a buffer strategy Latest-value for point-in-time data (prices, status). Sliding window for activity feeds (last 100 messages). Snapshot+delta for complex state (order books).
- Include connection status in responses Every resource read should include a connected boolean and lastUpdate timestamp. The AI model needs to know if the data is live or stale due to a disconnection.