@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.
- package/LICENSE +201 -0
- package/README.md +311 -0
- package/dist/adapters/bun.d.ts +95 -0
- package/dist/adapters/bun.d.ts.map +1 -0
- package/dist/adapters/bun.js +286 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/deno.d.ts +89 -0
- package/dist/adapters/deno.d.ts.map +1 -0
- package/dist/adapters/deno.js +249 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/index.d.ts +21 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/node.d.ts +112 -0
- package/dist/adapters/node.d.ts.map +1 -0
- package/dist/adapters/node.js +309 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/partykit.d.ts +153 -0
- package/dist/adapters/partykit.d.ts.map +1 -0
- package/dist/adapters/partykit.js +372 -0
- package/dist/adapters/partykit.js.map +1 -0
- package/dist/bridge.d.ts +38 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +1004 -0
- package/dist/bridge.js.map +1 -0
- package/dist/core.d.ts +75 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1508 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +69 -0
- package/dist/runtime/scheduler.d.ts.map +1 -0
- package/dist/runtime/scheduler.js +88 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/types.d.ts +144 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +82 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/schemas.d.ts +6 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +6 -0
- package/dist/schemas.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +28 -0
- package/src/adapters/bun.ts +354 -0
- package/src/adapters/deno.ts +282 -0
- package/src/adapters/index.ts +28 -0
- package/src/adapters/node.ts +385 -0
- package/src/adapters/partykit.ts +482 -0
- package/src/bridge.test.ts +64 -0
- package/src/core.ts +2176 -0
- package/src/index.ts +90 -0
- package/src/limits.test.ts +436 -0
- package/src/remote-mcp.test.ts +770 -0
- package/src/runtime/index.ts +24 -0
- package/src/runtime/scheduler.ts +130 -0
- package/src/runtime/types.ts +229 -0
- package/src/schemas.ts +6 -0
- package/src/session-naming.test.ts +443 -0
- package/src/types.ts +180 -0
- 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;
|