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`;
  }
}
  1. 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.
  2. 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).
  3. 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.