import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; /** * Bridge MCP Transport * * High-performance bridge transport for MCP communication within the same process. * Uses direct callbacks with microtask scheduling to avoid serialization overhead * and minimize event loop impact. * * ## Design * * - **Zero serialization**: Messages are passed as JavaScript objects by reference * - **Microtask scheduling**: Uses `queueMicrotask` to avoid deep recursion while * maintaining message ordering * - **Direct callbacks**: No Web API overhead (EventTarget, MessageChannel, etc.) * * ## Usage * * ```ts * import { createBridgeTransportPair } from "@decocms/mcp-utils"; * import { Client } from "@modelcontextprotocol/sdk/client/index.js"; * import { Server } from "@modelcontextprotocol/sdk/server/index.js"; * * const { client: clientTransport, server: serverTransport } = * createBridgeTransportPair(); * * const client = new Client({ name: "test-client", version: "1.0.0" }); * const server = new Server({ name: "test-server", version: "1.0.0" }); * * await server.connect(serverTransport); * await client.connect(clientTransport); * * // Now client and server can communicate via bridge * ``` */ type TransportSide = "client" | "server"; /** * Maximum number of messages that can be queued before throwing an error. * This prevents unbounded memory growth if one side sends faster than the other processes. */ const MAX_QUEUE_SIZE = 10_000; /** * Internal channel that manages bidirectional message queues between client and server. */ class BridgeChannel { private clientQueue: JSONRPCMessage[] = []; private serverQueue: JSONRPCMessage[] = []; private clientClosed = false; private serverClosed = false; private clientFlushScheduled = false; private serverFlushScheduled = false; // Use type-only forward reference to avoid circular dependency private clientTransport?: { started: boolean; deliverMessage(message: JSONRPCMessage): void; }; private serverTransport?: { started: boolean; deliverMessage(message: JSONRPCMessage): void; }; /** * Register transports with the channel and link them to each other. * This sets up both message delivery and close notifications. */ registerTransports( client: BridgeClientTransport, server: BridgeServerTransport, ): void { this.clientTransport = client; this.serverTransport = server; // Link transports to each other for close notifications client.setOppositeTransport(server); server.setOppositeTransport(client); } /** * Get the queue for a specific side (opposite of sender) */ private getQueue(side: TransportSide): JSONRPCMessage[] { return side === "client" ? this.clientQueue : this.serverQueue; } /** * Check if the target side is closed */ isClosed(side: TransportSide): boolean { return side === "client" ? this.clientClosed : this.serverClosed; } /** * Mark a side as closed */ close(side: TransportSide): void { if (side === "client") { this.clientClosed = true; this.clientQueue = []; } else { this.serverClosed = true; this.serverQueue = []; } } /** * Enqueue a message to the target side's queue * @throws Error if queue size exceeds MAX_QUEUE_SIZE */ enqueue(side: TransportSide, message: JSONRPCMessage): void { if (this.isClosed(side)) { // Silent no-op when target is closed (matches stdio transport behavior) return; } const queue = this.getQueue(side); // Prevent unbounded memory growth if (queue.length >= MAX_QUEUE_SIZE) { throw new Error( `BridgeTransport: ${side} queue overflow (max ${MAX_QUEUE_SIZE} messages). ` + "The receiver may not be processing messages fast enough.", ); } queue.push(message); // Schedule flush if not already scheduled if (side === "client" && !this.clientFlushScheduled) { this.scheduleFlush("client"); } else if (side === "server" && !this.serverFlushScheduled) { this.scheduleFlush("server"); } } /** * Schedule a flush operation for the given side using microtask scheduling. * This frees the event loop and prevents deep recursion. */ private scheduleFlush(side: TransportSide): void { if (side === "client") { this.clientFlushScheduled = true; } else { this.serverFlushScheduled = true; } queueMicrotask(() => { this.flush(side); }); } /** * Flush all messages from the queue for a specific side * and deliver them to the appropriate transport */ flush(side: TransportSide): void { const queue = this.getQueue(side); // Reset scheduled flag if (side === "client") { this.clientFlushScheduled = false; } else { this.serverFlushScheduled = false; } // If closed, clear queue and return if (this.isClosed(side)) { queue.length = 0; return; } // Get the transport for this side const transport = side === "client" ? this.clientTransport : this.serverTransport; if (!transport) { // Transport not registered yet, messages will be processed when it starts return; } if (!transport.started) { // Transport not started yet — leave messages in queue so start() / onmessage setter // can flush them once the transport is ready to receive. return; } // Drain queue in FIFO order and deliver to transport const batch = queue.splice(0); for (const message of batch) { transport.deliverMessage(message); } } /** * Close both sides of the channel */ closeBoth(): void { this.close("client"); this.close("server"); } } /** * Base transport implementation shared by client and server transports */ abstract class BaseBridgeTransport implements Transport { protected channel: BridgeChannel; protected side: TransportSide; started = false; protected closed = false; private _onmessage?: (message: JSONRPCMessage) => void; private _onerror?: (error: Error) => void; private _onclose?: () => void; constructor(channel: BridgeChannel, side: TransportSide) { this.channel = channel; this.side = side; } get onmessage(): ((message: JSONRPCMessage) => void) | undefined { return this._onmessage; } set onmessage(fn: ((message: JSONRPCMessage) => void) | undefined) { this._onmessage = fn; // If transport is started and onmessage is set, flush any queued messages if (fn && this.started && !this.closed) { this.channel.flush(this.side); } } get onerror(): ((error: Error) => void) | undefined { return this._onerror; } set onerror(fn: ((error: Error) => void) | undefined) { this._onerror = fn; } get onclose(): (() => void) | undefined { return this._onclose; } set onclose(fn: (() => void) | undefined) { this._onclose = fn; } /** * Start the transport. For bridge transports, this is a no-op * but required by the Transport interface. */ async start(): Promise { if (this.started) { throw new Error( `${this.side === "client" ? "BridgeClientTransport" : "BridgeServerTransport"} already started! If using Client/Server class, note that connect() calls start() automatically.`, ); } this.started = true; // Process any messages that were queued before start // If onmessage is already set, flush immediately; otherwise it will flush when onmessage is set if (this._onmessage && !this.closed) { this.channel.flush(this.side); } } /** * Send a message to the opposite side */ async send(message: JSONRPCMessage): Promise { if (this.closed) { // Silent no-op when transport is closed (matches stdio transport behavior) return Promise.resolve(); } const targetSide: TransportSide = this.side === "client" ? "server" : "client"; this.channel.enqueue(targetSide, message); // Resolve immediately - message delivery is async via microtask return Promise.resolve(); } /** * Close the transport */ async close(): Promise { if (!this.started || this.closed) { return; } this.closed = true; this.channel.close(this.side); // Notify opposite side that we closed const oppositeTransport = this.getOppositeTransport(); if (oppositeTransport && !oppositeTransport.closed) { oppositeTransport._onclose?.(); } this._onclose?.(); } /** * Get reference to the opposite transport (set by factory) */ protected abstract getOppositeTransport(): BaseBridgeTransport | undefined; /** * Set reference to opposite transport (called by factory) */ abstract setOppositeTransport(transport: BaseBridgeTransport): void; /** * Internal method to deliver a message to this transport * Called by the channel during flush operations */ deliverMessage(message: JSONRPCMessage): void { if (!this.started || this.channel.isClosed(this.side)) { return; } try { this._onmessage?.(message); } catch (error) { this._onerror?.(error as Error); } } } /** * Client-side bridge transport */ export class BridgeClientTransport extends BaseBridgeTransport { private oppositeTransport?: BridgeServerTransport; constructor(channel: BridgeChannel) { super(channel, "client"); } protected getOppositeTransport(): BaseBridgeTransport | undefined { return this.oppositeTransport; } setOppositeTransport(transport: BaseBridgeTransport): void { if (!(transport instanceof BridgeServerTransport)) { throw new Error("Opposite transport must be BridgeServerTransport"); } this.oppositeTransport = transport; } override async start(): Promise { await super.start(); // Callbacks will be set by MCP SDK after start() // We use property setters to sync them with internal handlers } override async send(message: JSONRPCMessage): Promise { await super.send(message); } } /** * Server-side bridge transport */ export class BridgeServerTransport extends BaseBridgeTransport { private oppositeTransport?: BridgeClientTransport; constructor(channel: BridgeChannel) { super(channel, "server"); } protected getOppositeTransport(): BaseBridgeTransport | undefined { return this.oppositeTransport; } setOppositeTransport(transport: BaseBridgeTransport): void { if (!(transport instanceof BridgeClientTransport)) { throw new Error("Opposite transport must be BridgeClientTransport"); } this.oppositeTransport = transport; } override async start(): Promise { await super.start(); // Callbacks will be set by MCP SDK after start() // We use property setters to sync them with internal handlers } override async send(message: JSONRPCMessage): Promise { await super.send(message); } } /** * Result of creating a bridge transport pair */ export interface BridgeTransportPair { /** * Client-side transport (for MCP Client) */ client: BridgeClientTransport; /** * Server-side transport (for MCP Server) */ server: BridgeServerTransport; /** * Internal channel (for advanced use cases) */ channel: BridgeChannel; } /** * Create a pair of bridge transports for client-server communication. * * Uses microtask scheduling for message delivery, which frees the event loop * and prevents deep recursion while maintaining message ordering. * * @returns A pair of transports connected via a bridge channel * * @example * ```ts * const { client, server } = createBridgeTransportPair(); * * const mcpClient = new Client({ name: "test", version: "1.0.0" }); * const mcpServer = new Server({ name: "test", version: "1.0.0" }); * * await mcpServer.connect(server); * await mcpClient.connect(client); * * // Now client and server can communicate via bridge * ``` */ export function createBridgeTransportPair(): BridgeTransportPair { const channel = new BridgeChannel(); const client = new BridgeClientTransport(channel); const server = new BridgeServerTransport(channel); // Register transports with channel (also links them for close notifications) channel.registerTransports(client, server); return { client, server, channel }; }