@mcp-web/bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +311 -0
  3. package/dist/adapters/bun.d.ts +95 -0
  4. package/dist/adapters/bun.d.ts.map +1 -0
  5. package/dist/adapters/bun.js +286 -0
  6. package/dist/adapters/bun.js.map +1 -0
  7. package/dist/adapters/deno.d.ts +89 -0
  8. package/dist/adapters/deno.d.ts.map +1 -0
  9. package/dist/adapters/deno.js +249 -0
  10. package/dist/adapters/deno.js.map +1 -0
  11. package/dist/adapters/index.d.ts +21 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +21 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/node.d.ts +112 -0
  16. package/dist/adapters/node.d.ts.map +1 -0
  17. package/dist/adapters/node.js +309 -0
  18. package/dist/adapters/node.js.map +1 -0
  19. package/dist/adapters/partykit.d.ts +153 -0
  20. package/dist/adapters/partykit.d.ts.map +1 -0
  21. package/dist/adapters/partykit.js +372 -0
  22. package/dist/adapters/partykit.js.map +1 -0
  23. package/dist/bridge.d.ts +38 -0
  24. package/dist/bridge.d.ts.map +1 -0
  25. package/dist/bridge.js +1004 -0
  26. package/dist/bridge.js.map +1 -0
  27. package/dist/core.d.ts +75 -0
  28. package/dist/core.d.ts.map +1 -0
  29. package/dist/core.js +1508 -0
  30. package/dist/core.js.map +1 -0
  31. package/dist/index.d.ts +38 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +42 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/runtime/index.d.ts +11 -0
  36. package/dist/runtime/index.d.ts.map +1 -0
  37. package/dist/runtime/index.js +9 -0
  38. package/dist/runtime/index.js.map +1 -0
  39. package/dist/runtime/scheduler.d.ts +69 -0
  40. package/dist/runtime/scheduler.d.ts.map +1 -0
  41. package/dist/runtime/scheduler.js +88 -0
  42. package/dist/runtime/scheduler.js.map +1 -0
  43. package/dist/runtime/types.d.ts +144 -0
  44. package/dist/runtime/types.d.ts.map +1 -0
  45. package/dist/runtime/types.js +82 -0
  46. package/dist/runtime/types.js.map +1 -0
  47. package/dist/schemas.d.ts +6 -0
  48. package/dist/schemas.d.ts.map +1 -0
  49. package/dist/schemas.js +6 -0
  50. package/dist/schemas.js.map +1 -0
  51. package/dist/types.d.ts +130 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +2 -0
  54. package/dist/types.js.map +1 -0
  55. package/package.json +28 -0
  56. package/src/adapters/bun.ts +354 -0
  57. package/src/adapters/deno.ts +282 -0
  58. package/src/adapters/index.ts +28 -0
  59. package/src/adapters/node.ts +385 -0
  60. package/src/adapters/partykit.ts +482 -0
  61. package/src/bridge.test.ts +64 -0
  62. package/src/core.ts +2176 -0
  63. package/src/index.ts +90 -0
  64. package/src/limits.test.ts +436 -0
  65. package/src/remote-mcp.test.ts +770 -0
  66. package/src/runtime/index.ts +24 -0
  67. package/src/runtime/scheduler.ts +130 -0
  68. package/src/runtime/types.ts +229 -0
  69. package/src/schemas.ts +6 -0
  70. package/src/session-naming.test.ts +443 -0
  71. package/src/types.ts +180 -0
  72. package/tsconfig.json +12 -0
@@ -0,0 +1,282 @@
1
+ /**
2
+ * MCPWebBridgeDeno - Deno adapter for the MCP Web Bridge.
3
+ *
4
+ * Uses Deno.serve() with WebSocket upgrade for a single-port server.
5
+ * Handles both HTTP requests and WebSocket connections on the same port.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // main.ts
10
+ * import { MCPWebBridgeDeno } from '@mcp-web/bridge';
11
+ *
12
+ * const bridge = new MCPWebBridgeDeno({
13
+ * name: 'My App',
14
+ * description: 'My awesome app',
15
+ * port: 3001,
16
+ * });
17
+ *
18
+ * // Bridge is now listening on ws://localhost:3001 and http://localhost:3001
19
+ * ```
20
+ *
21
+ * @example Deploy to Deno Deploy
22
+ * ```typescript
23
+ * // main.ts - Entry point for Deno Deploy
24
+ * import { MCPWebBridgeDeno } from '@mcp-web/bridge';
25
+ *
26
+ * new MCPWebBridgeDeno({
27
+ * name: 'My Production App',
28
+ * description: 'Production bridge server',
29
+ * // Port is typically provided by Deno Deploy via environment
30
+ * port: Number(Deno.env.get('PORT')) || 8000,
31
+ * });
32
+ * ```
33
+ *
34
+ * @remarks
35
+ * This adapter requires Deno runtime. It uses:
36
+ * - `Deno.serve()` for the HTTP server
37
+ * - `Deno.upgradeWebSocket()` for WebSocket connections
38
+ *
39
+ * For Deno Deploy, ensure your entry point is at the root of your repository
40
+ * or configure the entry point in your deployment settings.
41
+ *
42
+ * @see https://docs.deno.com/deploy/
43
+ * @see https://deno.land/api?s=Deno.serve
44
+ */
45
+
46
+ import type { MCPWebConfig } from '@mcp-web/types';
47
+ import { MCPWebBridge } from '../core.js';
48
+ import { TimerScheduler } from '../runtime/scheduler.js';
49
+ import type { HttpRequest, WebSocketConnection } from '../runtime/types.js';
50
+ import { isSSEResponse } from '../runtime/types.js';
51
+
52
+ /**
53
+ * Configuration for the Deno bridge adapter.
54
+ */
55
+ export interface MCPWebBridgeDenoConfig extends Omit<MCPWebConfig, 'bridgeUrl'> {
56
+ /** Port to listen on (default: 3001, or PORT env var on Deno Deploy) */
57
+ port?: number;
58
+
59
+ /** Hostname to bind to (default: '0.0.0.0') */
60
+ hostname?: string;
61
+ }
62
+
63
+ /**
64
+ * Wraps a Deno WebSocket in our runtime-agnostic interface.
65
+ */
66
+ function wrapDenoWebSocket(socket: WebSocket): WebSocketConnection {
67
+ const messageHandlers = new Set<(data: string) => void>();
68
+
69
+ socket.onmessage = (event) => {
70
+ const data = typeof event.data === 'string' ? event.data : event.data.toString();
71
+ for (const handler of messageHandlers) {
72
+ handler(data);
73
+ }
74
+ };
75
+
76
+ return {
77
+ send(data: string): void {
78
+ if (socket.readyState === WebSocket.OPEN) {
79
+ socket.send(data);
80
+ }
81
+ },
82
+
83
+ close(code?: number, reason?: string): void {
84
+ socket.close(code, reason);
85
+ },
86
+
87
+ get readyState(): 'CONNECTING' | 'OPEN' | 'CLOSING' | 'CLOSED' {
88
+ switch (socket.readyState) {
89
+ case WebSocket.CONNECTING:
90
+ return 'CONNECTING';
91
+ case WebSocket.OPEN:
92
+ return 'OPEN';
93
+ case WebSocket.CLOSING:
94
+ return 'CLOSING';
95
+ case WebSocket.CLOSED:
96
+ default:
97
+ return 'CLOSED';
98
+ }
99
+ },
100
+
101
+ onMessage(handler: (data: string) => void): void {
102
+ messageHandlers.add(handler);
103
+ },
104
+
105
+ offMessage(handler: (data: string) => void): void {
106
+ messageHandlers.delete(handler);
107
+ },
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Wraps a Deno Request in our runtime-agnostic HttpRequest interface.
113
+ */
114
+ function wrapDenoRequest(req: Request): HttpRequest {
115
+ return {
116
+ method: req.method,
117
+ url: req.url,
118
+ headers: {
119
+ get(name: string): string | null {
120
+ return req.headers.get(name);
121
+ },
122
+ },
123
+ text(): Promise<string> {
124
+ return req.text();
125
+ },
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Deno adapter for MCPWebBridge.
131
+ * Provides a single-port server using Deno.serve() with WebSocket upgrade.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const bridge = new MCPWebBridgeDeno({
136
+ * name: 'My App',
137
+ * description: 'My app',
138
+ * port: 3001,
139
+ * });
140
+ * ```
141
+ */
142
+ export class MCPWebBridgeDeno {
143
+ #core: MCPWebBridge;
144
+ #controller: AbortController;
145
+ #port: number;
146
+ #hostname: string;
147
+
148
+ constructor(config: MCPWebBridgeDenoConfig) {
149
+ // biome-ignore lint/suspicious/noExplicitAny: Deno global is runtime-specific
150
+ const Deno = (globalThis as any).Deno;
151
+ const envPort = Deno?.env?.get?.('PORT');
152
+ this.#port = config.port ?? (envPort ? Number(envPort) : 3001);
153
+ this.#hostname = config.hostname ?? '0.0.0.0';
154
+ this.#controller = new AbortController();
155
+
156
+ // Create the core with a timer-based scheduler
157
+ const scheduler = new TimerScheduler();
158
+ this.#core = new MCPWebBridge(config, scheduler);
159
+ const handlers = this.#core.getHandlers();
160
+
161
+ // Start Deno server
162
+ Deno.serve(
163
+ {
164
+ port: this.#port,
165
+ hostname: this.#hostname,
166
+ signal: this.#controller.signal,
167
+ onListen: ({ port, hostname }: { port: number; hostname: string }) => {
168
+ console.log(`šŸŒ‰ MCP Web Bridge (Deno) listening on ${hostname}:${port}`);
169
+ console.log(` WebSocket: ws://${hostname === '0.0.0.0' ? 'localhost' : hostname}:${port}`);
170
+ console.log(` HTTP/MCP: http://${hostname === '0.0.0.0' ? 'localhost' : hostname}:${port}`);
171
+ },
172
+ },
173
+ async (req: Request): Promise<Response> => {
174
+ const url = new URL(req.url);
175
+
176
+ // Handle WebSocket upgrade
177
+ if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
178
+ const sessionId = url.searchParams.get('session');
179
+
180
+ if (!sessionId) {
181
+ return new Response('Missing session parameter', { status: 400 });
182
+ }
183
+
184
+ const { socket, response } = Deno.upgradeWebSocket(req);
185
+ const wrapped = wrapDenoWebSocket(socket);
186
+
187
+ socket.onopen = () => {
188
+ handlers.onWebSocketConnect(sessionId, wrapped, url);
189
+ };
190
+
191
+ socket.onmessage = (event: MessageEvent) => {
192
+ const data = typeof event.data === 'string' ? event.data : event.data.toString();
193
+ handlers.onWebSocketMessage(sessionId, wrapped, data);
194
+ };
195
+
196
+ socket.onclose = () => {
197
+ handlers.onWebSocketClose(sessionId);
198
+ };
199
+
200
+ socket.onerror = (error: Event) => {
201
+ console.error(`WebSocket error for session ${sessionId}:`, error);
202
+ };
203
+
204
+ return response;
205
+ }
206
+
207
+ // Handle regular HTTP requests
208
+ const wrappedReq = wrapDenoRequest(req);
209
+ const httpResponse = await handlers.onHttpRequest(wrappedReq);
210
+
211
+ // Check if this is an SSE response
212
+ if (isSSEResponse(httpResponse)) {
213
+ // Create a ReadableStream for SSE
214
+ const stream = new ReadableStream({
215
+ start(controller) {
216
+ // Create writer function that sends SSE-formatted data
217
+ const writer = (data: string): void => {
218
+ controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`));
219
+ };
220
+
221
+ // Set up the SSE stream
222
+ httpResponse.setup(writer, () => {
223
+ controller.close();
224
+ });
225
+
226
+ // Keep connection alive with periodic comments
227
+ const keepAlive = setInterval(() => {
228
+ try {
229
+ controller.enqueue(new TextEncoder().encode(': keepalive\n\n'));
230
+ } catch {
231
+ clearInterval(keepAlive);
232
+ }
233
+ }, 30000);
234
+ },
235
+ });
236
+
237
+ return new Response(stream, {
238
+ status: httpResponse.status,
239
+ headers: httpResponse.headers,
240
+ });
241
+ }
242
+
243
+ return new Response(httpResponse.body, {
244
+ status: httpResponse.status,
245
+ headers: httpResponse.headers,
246
+ });
247
+ }
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Get the underlying MCPWebBridge core instance.
253
+ */
254
+ get core(): MCPWebBridge {
255
+ return this.#core;
256
+ }
257
+
258
+ /**
259
+ * Get the bridge handlers for custom integrations.
260
+ */
261
+ getHandlers() {
262
+ return this.#core.getHandlers();
263
+ }
264
+
265
+ /**
266
+ * Get the port the server is listening on.
267
+ */
268
+ get port(): number {
269
+ return this.#port;
270
+ }
271
+
272
+ /**
273
+ * Gracefully shut down the bridge.
274
+ */
275
+ async close(): Promise<void> {
276
+ // Abort the server
277
+ this.#controller.abort();
278
+
279
+ // Close the core (cleans up sessions, timers)
280
+ await this.#core.close();
281
+ }
282
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Bridge adapters for different JavaScript runtimes.
3
+ *
4
+ * Each adapter wraps the core MCPWebBridge class and provides
5
+ * runtime-specific I/O implementations.
6
+ *
7
+ * Available adapters:
8
+ * - `MCPWebBridgeNode` - Node.js (production ready)
9
+ * - `MCPWebBridgeDeno` - Deno / Deno Deploy
10
+ * - `MCPWebBridgeBun` - Bun runtime
11
+ * - `MCPWebBridgeParty` / `createPartyKitBridge` - PartyKit / Cloudflare
12
+ */
13
+
14
+ // Node.js adapter (production ready)
15
+ export { Bridge, MCPWebBridgeNode } from './node.js';
16
+ export type { MCPWebBridgeNodeConfig, MCPWebBridgeNodeSSLConfig } from './node.js';
17
+
18
+ // Deno adapter
19
+ export { MCPWebBridgeDeno } from './deno.js';
20
+ export type { MCPWebBridgeDenoConfig } from './deno.js';
21
+
22
+ // Bun adapter
23
+ export { MCPWebBridgeBun } from './bun.js';
24
+ export type { MCPWebBridgeBunConfig } from './bun.js';
25
+
26
+ // PartyKit adapter
27
+ export { AlarmScheduler, createPartyKitBridge, MCPWebBridgeParty } from './partykit.js';
28
+ export type { MCPWebBridgePartyConfig } from './partykit.js';
@@ -0,0 +1,385 @@
1
+ /**
2
+ * MCPWebBridgeNode - Node.js adapter for the MCP Web Bridge.
3
+ *
4
+ * Uses a single port for both HTTP and WebSocket connections.
5
+ * WebSocket connections are upgraded from HTTP via the `upgrade` event.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { MCPWebBridgeNode } from '@mcp-web/bridge';
10
+ *
11
+ * const bridge = new MCPWebBridgeNode({
12
+ * name: 'My App',
13
+ * description: 'My awesome app',
14
+ * port: 3001,
15
+ * });
16
+ *
17
+ * // Bridge is now listening on ws://localhost:3001 and http://localhost:3001
18
+ * ```
19
+ */
20
+
21
+ import {
22
+ createServer as createHttpServer,
23
+ type IncomingMessage,
24
+ type Server,
25
+ type ServerResponse,
26
+ } from 'node:http';
27
+ import { createServer as createHttpsServer } from 'node:https';
28
+ import { WebSocket, WebSocketServer } from 'ws';
29
+ import type { MCPWebConfig } from '@mcp-web/types';
30
+ import { MCPWebBridge } from '../core.js';
31
+ import { TimerScheduler } from '../runtime/scheduler.js';
32
+ import type { HttpRequest, WebSocketConnection } from '../runtime/types.js';
33
+ import { readyStateToString, isSSEResponse } from '../runtime/types.js';
34
+
35
+ /**
36
+ * Configuration for the Node.js bridge adapter.
37
+ */
38
+ /**
39
+ * SSL/TLS configuration for secure connections.
40
+ */
41
+ export interface MCPWebBridgeNodeSSLConfig {
42
+ /** Private key in PEM format (string content, not file path) */
43
+ key: string | Buffer;
44
+
45
+ /** Certificate in PEM format (string content, not file path) */
46
+ cert: string | Buffer;
47
+ }
48
+
49
+ /**
50
+ * Configuration for the Node.js bridge adapter.
51
+ */
52
+ export interface MCPWebBridgeNodeConfig extends Omit<MCPWebConfig, 'bridgeUrl'> {
53
+ /** Port to listen on (default: 3001) */
54
+ port?: number;
55
+
56
+ /** Host to bind to (default: '0.0.0.0') */
57
+ host?: string;
58
+
59
+ /**
60
+ * SSL/TLS configuration for HTTPS/WSS support.
61
+ * When provided, the bridge will use secure connections.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * import { readFileSync } from 'node:fs';
66
+ *
67
+ * const bridge = new MCPWebBridgeNode({
68
+ * name: 'My App',
69
+ * port: 3001,
70
+ * ssl: {
71
+ * key: readFileSync('./localhost-key.pem'),
72
+ * cert: readFileSync('./localhost.pem'),
73
+ * },
74
+ * });
75
+ * ```
76
+ */
77
+ ssl?: MCPWebBridgeNodeSSLConfig;
78
+ }
79
+
80
+ /**
81
+ * Wraps a Node.js ws WebSocket in our runtime-agnostic interface.
82
+ */
83
+ function wrapWebSocket(ws: WebSocket): WebSocketConnection {
84
+ const messageHandlers = new Set<(data: string) => void>();
85
+
86
+ ws.on('message', (data) => {
87
+ const str = data.toString();
88
+ for (const handler of messageHandlers) {
89
+ handler(str);
90
+ }
91
+ });
92
+
93
+ return {
94
+ send(data: string): void {
95
+ if (ws.readyState === WebSocket.OPEN) {
96
+ ws.send(data);
97
+ }
98
+ },
99
+
100
+ close(code?: number, reason?: string): void {
101
+ ws.close(code, reason);
102
+ },
103
+
104
+ get readyState(): 'CONNECTING' | 'OPEN' | 'CLOSING' | 'CLOSED' {
105
+ return readyStateToString(ws.readyState);
106
+ },
107
+
108
+ onMessage(handler: (data: string) => void): void {
109
+ messageHandlers.add(handler);
110
+ },
111
+
112
+ offMessage(handler: (data: string) => void): void {
113
+ messageHandlers.delete(handler);
114
+ },
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Wraps a Node.js IncomingMessage in our runtime-agnostic HttpRequest interface.
120
+ */
121
+ function wrapRequest(req: IncomingMessage, body: string): HttpRequest {
122
+ return {
123
+ method: req.method || 'GET',
124
+ url: `http://${req.headers.host || 'localhost'}${req.url || '/'}`,
125
+ headers: {
126
+ get(name: string): string | null {
127
+ const value = req.headers[name.toLowerCase()];
128
+ if (Array.isArray(value)) {
129
+ return value[0] || null;
130
+ }
131
+ return value || null;
132
+ },
133
+ },
134
+ text(): Promise<string> {
135
+ return Promise.resolve(body);
136
+ },
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Node.js adapter for MCPWebBridge.
142
+ * Provides a single-port server for both WebSocket and HTTP traffic.
143
+ */
144
+ export class MCPWebBridgeNode {
145
+ #core: MCPWebBridge;
146
+ #server: Server;
147
+ #wss: WebSocketServer;
148
+ #port: number;
149
+ #host: string;
150
+ #isSecure: boolean;
151
+ #readyPromise: Promise<void>;
152
+ #isReady = false;
153
+
154
+ constructor(config: MCPWebBridgeNodeConfig) {
155
+ this.#port = config.port ?? 3001;
156
+ this.#host = config.host ?? '0.0.0.0';
157
+ this.#isSecure = !!config.ssl;
158
+
159
+ // Create the core with a timer-based scheduler
160
+ const scheduler = new TimerScheduler();
161
+ this.#core = new MCPWebBridge(config, scheduler);
162
+ const handlers = this.#core.getHandlers();
163
+
164
+ // Request handler (shared between HTTP and HTTPS)
165
+ const requestHandler = (req: IncomingMessage, res: ServerResponse) => {
166
+ // Collect body
167
+ let body = '';
168
+ req.on('data', (chunk) => {
169
+ body += chunk;
170
+ });
171
+ req.on('end', () => {
172
+ const wrappedReq = wrapRequest(req, body);
173
+ handlers.onHttpRequest(wrappedReq).then((response) => {
174
+ // Check if this is an SSE response
175
+ if (isSSEResponse(response)) {
176
+ // Set headers for SSE
177
+ res.writeHead(response.status, response.headers);
178
+
179
+ // Create writer function that sends SSE-formatted data
180
+ const writer = (data: string): void => {
181
+ res.write(`data: ${data}\n\n`);
182
+ };
183
+
184
+ // Track if connection is still open
185
+ let isOpen = true;
186
+
187
+ // Handle client disconnect
188
+ req.on('close', () => {
189
+ isOpen = false;
190
+ });
191
+
192
+ // Set up the SSE stream
193
+ response.setup(writer, () => {
194
+ if (isOpen) {
195
+ res.end();
196
+ }
197
+ });
198
+
199
+ // Keep connection alive with periodic comments
200
+ const keepAlive = setInterval(() => {
201
+ if (isOpen) {
202
+ res.write(': keepalive\n\n');
203
+ } else {
204
+ clearInterval(keepAlive);
205
+ }
206
+ }, 30000);
207
+
208
+ // Clean up on close
209
+ res.on('close', () => {
210
+ clearInterval(keepAlive);
211
+ });
212
+ } else {
213
+ // Regular HTTP response
214
+ res.writeHead(response.status, response.headers);
215
+ res.end(response.body);
216
+ }
217
+ });
218
+ });
219
+ };
220
+
221
+ // Create HTTP or HTTPS server based on SSL config
222
+ if (config.ssl) {
223
+ this.#server = createHttpsServer(
224
+ { key: config.ssl.key, cert: config.ssl.cert },
225
+ requestHandler,
226
+ );
227
+ } else {
228
+ this.#server = createHttpServer(requestHandler);
229
+ }
230
+
231
+ // Create WebSocket server without its own port (noServer mode)
232
+ this.#wss = new WebSocketServer({ noServer: true });
233
+
234
+ // Handle WebSocket upgrade requests
235
+ this.#server.on('upgrade', (req: IncomingMessage, socket, head) => {
236
+ const urlStr = `http://${req.headers.host || 'localhost'}${req.url || '/'}`;
237
+ const url = new URL(urlStr);
238
+ const sessionId = url.searchParams.get('session');
239
+
240
+ if (!sessionId) {
241
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
242
+ socket.destroy();
243
+ return;
244
+ }
245
+
246
+ this.#wss.handleUpgrade(req, socket, head, (ws) => {
247
+ const wrapped = wrapWebSocket(ws);
248
+
249
+ if (handlers.onWebSocketConnect(sessionId, wrapped, url)) {
250
+ ws.on('message', (data) => {
251
+ handlers.onWebSocketMessage(sessionId, wrapped, data.toString());
252
+ });
253
+
254
+ ws.on('close', () => {
255
+ handlers.onWebSocketClose(sessionId);
256
+ });
257
+
258
+ ws.on('error', (error) => {
259
+ console.error(`WebSocket error for session ${sessionId}:`, error);
260
+ });
261
+ } else {
262
+ ws.close(1008, 'Connection rejected');
263
+ }
264
+ });
265
+ });
266
+
267
+ // Create a promise that resolves when the server is ready or rejects on error
268
+ this.#readyPromise = new Promise<void>((resolve, reject) => {
269
+ // Handle server errors (including EADDRINUSE)
270
+ this.#server.on('error', (error: NodeJS.ErrnoException) => {
271
+ if (error.code === 'EADDRINUSE') {
272
+ const displayHost = this.#host === '0.0.0.0' ? 'localhost' : this.#host;
273
+ console.error(`\nāŒ Port ${this.#port} is already in use.`);
274
+ console.error(` Another process is listening on ${displayHost}:${this.#port}`);
275
+ console.error(`\n To fix this, either:`);
276
+ console.error(` 1. Stop the other process using port ${this.#port}:`);
277
+ console.error(` lsof -i :${this.#port} # Find the process`);
278
+ console.error(` kill <PID> # Kill it`);
279
+ console.error(` 2. Or use a different port in your bridge config\n`);
280
+ } else {
281
+ console.error(`\nāŒ Failed to start bridge server:`, error.message);
282
+ }
283
+ reject(error);
284
+ });
285
+
286
+ // Start listening
287
+ this.#server.listen(this.#port, this.#host, () => {
288
+ this.#isReady = true;
289
+ const displayHost = this.#host === '0.0.0.0' ? 'localhost' : this.#host;
290
+ const wsProtocol = this.#isSecure ? 'wss' : 'ws';
291
+ const httpProtocol = this.#isSecure ? 'https' : 'http';
292
+ console.log(`šŸŒ‰ MCP Web Bridge listening on ${displayHost}:${this.#port}`);
293
+ console.log(` WebSocket: ${wsProtocol}://${displayHost}:${this.#port}`);
294
+ console.log(` HTTP/MCP: ${httpProtocol}://${displayHost}:${this.#port}`);
295
+ resolve();
296
+ });
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Returns a promise that resolves when the server is ready to accept connections.
302
+ * Rejects if the server fails to start (e.g., port already in use).
303
+ *
304
+ * @example
305
+ * ```typescript
306
+ * const bridge = new MCPWebBridgeNode({ name: 'My App', port: 3001 });
307
+ * await bridge.ready();
308
+ * console.log('Bridge is ready!');
309
+ * ```
310
+ */
311
+ ready(): Promise<void> {
312
+ return this.#readyPromise;
313
+ }
314
+
315
+ /**
316
+ * Whether the server is ready to accept connections.
317
+ */
318
+ get isReady(): boolean {
319
+ return this.#isReady;
320
+ }
321
+
322
+ /**
323
+ * Get the underlying MCPWebBridge core instance.
324
+ * Useful for advanced usage or custom integrations.
325
+ */
326
+ get core(): MCPWebBridge {
327
+ return this.#core;
328
+ }
329
+
330
+ /**
331
+ * Get the bridge handlers for custom integrations.
332
+ */
333
+ getHandlers() {
334
+ return this.#core.getHandlers();
335
+ }
336
+
337
+ /**
338
+ * Get the port the server is listening on.
339
+ */
340
+ get port(): number {
341
+ return this.#port;
342
+ }
343
+
344
+ /**
345
+ * Whether the server is using SSL/TLS (HTTPS/WSS).
346
+ */
347
+ get isSecure(): boolean {
348
+ return this.#isSecure;
349
+ }
350
+
351
+ /**
352
+ * Gracefully shut down the bridge.
353
+ */
354
+ async close(): Promise<void> {
355
+ // Close the core (cleans up sessions, timers)
356
+ await this.#core.close();
357
+
358
+ // Force close all WebSocket connections
359
+ for (const client of this.#wss.clients) {
360
+ client.terminate();
361
+ }
362
+
363
+ // Close WebSocket server with timeout
364
+ await Promise.race([
365
+ new Promise<void>((resolve) => {
366
+ this.#wss.close(() => resolve());
367
+ }),
368
+ new Promise<void>((resolve) => setTimeout(resolve, 1000)),
369
+ ]);
370
+
371
+ // Close HTTP server with timeout
372
+ await Promise.race([
373
+ new Promise<void>((resolve) => {
374
+ this.#server.close(() => resolve());
375
+ }),
376
+ new Promise<void>((resolve) => setTimeout(resolve, 1000)),
377
+ ]);
378
+ }
379
+ }
380
+
381
+ /**
382
+ * For backwards compatibility, also export as Bridge
383
+ * @deprecated Use MCPWebBridgeNode instead
384
+ */
385
+ export const Bridge = MCPWebBridgeNode;