@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,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCPWebBridgeParty - PartyKit adapter for the MCP Web Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Enables deployment to Cloudflare's edge network via PartyKit.
|
|
5
|
+
* Uses PartyKit's Party.Server interface with Durable Objects for state management.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // server.ts - PartyKit server entry point
|
|
10
|
+
* import { createPartyKitBridge } from '@mcp-web/bridge';
|
|
11
|
+
*
|
|
12
|
+
* export default createPartyKitBridge({
|
|
13
|
+
* name: 'My App',
|
|
14
|
+
* description: 'My awesome app on the edge',
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example With partykit.json configuration
|
|
19
|
+
* ```json
|
|
20
|
+
* {
|
|
21
|
+
* "name": "my-mcp-bridge",
|
|
22
|
+
* "main": "server.ts",
|
|
23
|
+
* "compatibility_date": "2024-01-01"
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example Deploy to PartyKit
|
|
28
|
+
* ```bash
|
|
29
|
+
* npx partykit deploy
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* PartyKit provides:
|
|
34
|
+
* - Global edge deployment via Cloudflare
|
|
35
|
+
* - Durable Objects for stateful WebSocket handling
|
|
36
|
+
* - Hibernation support for cost efficiency
|
|
37
|
+
* - Alarms for scheduled tasks (used instead of setInterval)
|
|
38
|
+
*
|
|
39
|
+
* Key differences from other adapters:
|
|
40
|
+
* - No explicit port configuration (managed by PartyKit/Cloudflare)
|
|
41
|
+
* - Uses `Party.storage.setAlarm()` for session timeout checks
|
|
42
|
+
* - State can persist across hibernation cycles
|
|
43
|
+
*
|
|
44
|
+
* @see https://docs.partykit.io/
|
|
45
|
+
* @see https://docs.partykit.io/guides/scheduling-tasks-with-alarms/
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import type { MCPWebConfig } from '@mcp-web/types';
|
|
49
|
+
import { MCPWebBridge } from '../core.js';
|
|
50
|
+
import type { Scheduler } from '../runtime/scheduler.js';
|
|
51
|
+
import type { HttpRequest, HttpResponse, WebSocketConnection } from '../runtime/types.js';
|
|
52
|
+
import { isSSEResponse } from '../runtime/types.js';
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// PartyKit Type Definitions (simplified for stub)
|
|
56
|
+
// In production, import from 'partykit/server'
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* PartyKit connection interface (simplified).
|
|
61
|
+
* @see https://docs.partykit.io/reference/partyserver-api/
|
|
62
|
+
*/
|
|
63
|
+
interface PartyConnection {
|
|
64
|
+
id: string;
|
|
65
|
+
send(message: string | ArrayBuffer): void;
|
|
66
|
+
close(code?: number, reason?: string): void;
|
|
67
|
+
readyState: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* PartyKit room interface (simplified).
|
|
72
|
+
*/
|
|
73
|
+
interface PartyRoom {
|
|
74
|
+
id: string;
|
|
75
|
+
storage: {
|
|
76
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
77
|
+
put<T>(key: string, value: T): Promise<void>;
|
|
78
|
+
delete(key: string): Promise<boolean>;
|
|
79
|
+
setAlarm(time: number | Date): Promise<void>;
|
|
80
|
+
getAlarm(): Promise<number | null>;
|
|
81
|
+
deleteAlarm(): Promise<void>;
|
|
82
|
+
};
|
|
83
|
+
broadcast(message: string, exclude?: string[]): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* PartyKit connection context (simplified).
|
|
88
|
+
*/
|
|
89
|
+
interface PartyConnectionContext {
|
|
90
|
+
request: Request;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* PartyKit server interface that bridge implements.
|
|
95
|
+
*/
|
|
96
|
+
interface PartyServer {
|
|
97
|
+
onConnect?(connection: PartyConnection, ctx: PartyConnectionContext): void | Promise<void>;
|
|
98
|
+
onMessage?(message: string, sender: PartyConnection): void | Promise<void>;
|
|
99
|
+
onClose?(connection: PartyConnection): void | Promise<void>;
|
|
100
|
+
onError?(connection: PartyConnection, error: Error): void | Promise<void>;
|
|
101
|
+
onRequest?(request: Request): Response | Promise<Response>;
|
|
102
|
+
onAlarm?(): void | Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Configuration
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Configuration for the PartyKit bridge adapter.
|
|
111
|
+
* Note: Port is not configurable - PartyKit manages this.
|
|
112
|
+
*/
|
|
113
|
+
export interface MCPWebBridgePartyConfig extends Omit<MCPWebConfig, 'bridgeUrl'> {
|
|
114
|
+
/**
|
|
115
|
+
* Session timeout check interval in milliseconds.
|
|
116
|
+
* PartyKit uses alarms instead of setInterval, so this determines
|
|
117
|
+
* how often the alarm fires to check for expired sessions.
|
|
118
|
+
* Default: 60000 (1 minute)
|
|
119
|
+
*/
|
|
120
|
+
sessionCheckIntervalMs?: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Alarm-based Scheduler
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Scheduler implementation using PartyKit alarms.
|
|
129
|
+
*
|
|
130
|
+
* PartyKit only supports one alarm at a time per room, so this scheduler
|
|
131
|
+
* tracks multiple scheduled callbacks and uses the alarm for the soonest one.
|
|
132
|
+
*
|
|
133
|
+
* @see https://docs.partykit.io/guides/scheduling-tasks-with-alarms/
|
|
134
|
+
*/
|
|
135
|
+
export class AlarmScheduler implements Scheduler {
|
|
136
|
+
#room: PartyRoom;
|
|
137
|
+
#callbacks = new Map<string, { callback: () => void; fireAt: number }>();
|
|
138
|
+
#intervals = new Map<string, { callback: () => void; intervalMs: number; nextFireAt: number }>();
|
|
139
|
+
#nextAlarmId: string | null = null;
|
|
140
|
+
|
|
141
|
+
constructor(room: PartyRoom) {
|
|
142
|
+
this.#room = room;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
schedule(callback: () => void, delayMs: number): string {
|
|
146
|
+
const id = crypto.randomUUID();
|
|
147
|
+
const fireAt = Date.now() + delayMs;
|
|
148
|
+
this.#callbacks.set(id, { callback, fireAt });
|
|
149
|
+
this.#updateAlarm();
|
|
150
|
+
return id;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
cancel(id: string): void {
|
|
154
|
+
this.#callbacks.delete(id);
|
|
155
|
+
this.#updateAlarm();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
scheduleInterval(callback: () => void, intervalMs: number): string {
|
|
159
|
+
const id = crypto.randomUUID();
|
|
160
|
+
const nextFireAt = Date.now() + intervalMs;
|
|
161
|
+
this.#intervals.set(id, { callback, intervalMs, nextFireAt });
|
|
162
|
+
this.#updateAlarm();
|
|
163
|
+
return id;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
cancelInterval(id: string): void {
|
|
167
|
+
this.#intervals.delete(id);
|
|
168
|
+
this.#updateAlarm();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
dispose(): void {
|
|
172
|
+
this.#callbacks.clear();
|
|
173
|
+
this.#intervals.clear();
|
|
174
|
+
this.#room.storage.deleteAlarm().catch(() => {});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Called by the PartyKit server's onAlarm() handler.
|
|
179
|
+
* Executes due callbacks and reschedules the next alarm.
|
|
180
|
+
*/
|
|
181
|
+
async handleAlarm(): Promise<void> {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
|
|
184
|
+
// Execute due one-time callbacks
|
|
185
|
+
for (const [id, { callback, fireAt }] of this.#callbacks) {
|
|
186
|
+
if (fireAt <= now) {
|
|
187
|
+
this.#callbacks.delete(id);
|
|
188
|
+
try {
|
|
189
|
+
callback();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Alarm callback error:', error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Execute due interval callbacks and reschedule them
|
|
197
|
+
for (const [id, interval] of this.#intervals) {
|
|
198
|
+
if (interval.nextFireAt <= now) {
|
|
199
|
+
interval.nextFireAt = now + interval.intervalMs;
|
|
200
|
+
try {
|
|
201
|
+
interval.callback();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Interval callback error:', error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Schedule next alarm
|
|
209
|
+
await this.#updateAlarm();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async #updateAlarm(): Promise<void> {
|
|
213
|
+
// Find the soonest scheduled callback
|
|
214
|
+
let soonest: number | null = null;
|
|
215
|
+
|
|
216
|
+
for (const { fireAt } of this.#callbacks.values()) {
|
|
217
|
+
if (soonest === null || fireAt < soonest) {
|
|
218
|
+
soonest = fireAt;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const { nextFireAt } of this.#intervals.values()) {
|
|
223
|
+
if (soonest === null || nextFireAt < soonest) {
|
|
224
|
+
soonest = nextFireAt;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (soonest !== null) {
|
|
229
|
+
await this.#room.storage.setAlarm(soonest);
|
|
230
|
+
} else {
|
|
231
|
+
await this.#room.storage.deleteAlarm();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// WebSocket Wrapper
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Extended WebSocket connection with internal dispatch method.
|
|
242
|
+
*/
|
|
243
|
+
interface PartyWebSocketConnection extends WebSocketConnection {
|
|
244
|
+
_dispatchMessage(data: string): void;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Wraps a PartyKit Connection in our runtime-agnostic interface.
|
|
249
|
+
*/
|
|
250
|
+
function wrapPartyConnection(connection: PartyConnection): PartyWebSocketConnection {
|
|
251
|
+
const messageHandlers = new Set<(data: string) => void>();
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
send(data: string): void {
|
|
255
|
+
if (connection.readyState === 1) {
|
|
256
|
+
// OPEN
|
|
257
|
+
connection.send(data);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
close(code?: number, reason?: string): void {
|
|
262
|
+
connection.close(code, reason);
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
get readyState(): 'CONNECTING' | 'OPEN' | 'CLOSING' | 'CLOSED' {
|
|
266
|
+
switch (connection.readyState) {
|
|
267
|
+
case 0:
|
|
268
|
+
return 'CONNECTING';
|
|
269
|
+
case 1:
|
|
270
|
+
return 'OPEN';
|
|
271
|
+
case 2:
|
|
272
|
+
return 'CLOSING';
|
|
273
|
+
case 3:
|
|
274
|
+
default:
|
|
275
|
+
return 'CLOSED';
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
onMessage(handler: (data: string) => void): void {
|
|
280
|
+
messageHandlers.add(handler);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
offMessage(handler: (data: string) => void): void {
|
|
284
|
+
messageHandlers.delete(handler);
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
// Internal: called by the PartyKit server to dispatch messages
|
|
288
|
+
_dispatchMessage(data: string): void {
|
|
289
|
+
for (const handler of messageHandlers) {
|
|
290
|
+
handler(data);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Wraps a PartyKit/Cloudflare Request in our runtime-agnostic HttpRequest interface.
|
|
298
|
+
*/
|
|
299
|
+
function wrapPartyRequest(req: Request): HttpRequest {
|
|
300
|
+
return {
|
|
301
|
+
method: req.method,
|
|
302
|
+
url: req.url,
|
|
303
|
+
headers: {
|
|
304
|
+
get(name: string): string | null {
|
|
305
|
+
return req.headers.get(name);
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
text(): Promise<string> {
|
|
309
|
+
return req.text();
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// PartyKit Bridge Server
|
|
316
|
+
// ============================================================================
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Creates a PartyKit-compatible bridge server class.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* // server.ts
|
|
324
|
+
* import { createPartyKitBridge } from '@mcp-web/bridge';
|
|
325
|
+
*
|
|
326
|
+
* export default createPartyKitBridge({
|
|
327
|
+
* name: 'My Bridge',
|
|
328
|
+
* description: 'MCP Web bridge on the edge',
|
|
329
|
+
* });
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
export function createPartyKitBridge(config: MCPWebBridgePartyConfig): new (room: PartyRoom) => PartyServer {
|
|
333
|
+
return class MCPWebBridgeParty implements PartyServer {
|
|
334
|
+
#room: PartyRoom;
|
|
335
|
+
#core: MCPWebBridge;
|
|
336
|
+
#scheduler: AlarmScheduler;
|
|
337
|
+
#connections = new Map<string, PartyWebSocketConnection>();
|
|
338
|
+
#connectionSessionIds = new Map<string, string>(); // connectionId -> sessionId
|
|
339
|
+
|
|
340
|
+
constructor(room: PartyRoom) {
|
|
341
|
+
this.#room = room;
|
|
342
|
+
this.#scheduler = new AlarmScheduler(room);
|
|
343
|
+
this.#core = new MCPWebBridge(
|
|
344
|
+
config,
|
|
345
|
+
this.#scheduler
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
console.log(`🌉 MCP Web Bridge (PartyKit) initialized for room: ${room.id}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Handle new WebSocket connections.
|
|
353
|
+
*/
|
|
354
|
+
async onConnect(connection: PartyConnection, ctx: PartyConnectionContext): Promise<void> {
|
|
355
|
+
const url = new URL(ctx.request.url);
|
|
356
|
+
const sessionId = url.searchParams.get('session');
|
|
357
|
+
|
|
358
|
+
if (!sessionId) {
|
|
359
|
+
connection.close(1008, 'Missing session parameter');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const wrapped = wrapPartyConnection(connection);
|
|
364
|
+
this.#connections.set(connection.id, wrapped);
|
|
365
|
+
this.#connectionSessionIds.set(connection.id, sessionId);
|
|
366
|
+
|
|
367
|
+
const handlers = this.#core.getHandlers();
|
|
368
|
+
handlers.onWebSocketConnect(sessionId, wrapped, url);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Handle incoming WebSocket messages.
|
|
373
|
+
*/
|
|
374
|
+
async onMessage(message: string, sender: PartyConnection): Promise<void> {
|
|
375
|
+
const sessionId = this.#connectionSessionIds.get(sender.id);
|
|
376
|
+
const wrapped = this.#connections.get(sender.id);
|
|
377
|
+
|
|
378
|
+
if (!sessionId || !wrapped) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Dispatch to message handlers on the wrapper
|
|
383
|
+
wrapped._dispatchMessage(message);
|
|
384
|
+
|
|
385
|
+
// Notify the bridge core
|
|
386
|
+
const handlers = this.#core.getHandlers();
|
|
387
|
+
handlers.onWebSocketMessage(sessionId, wrapped, message);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Handle WebSocket connection close.
|
|
392
|
+
*/
|
|
393
|
+
async onClose(connection: PartyConnection): Promise<void> {
|
|
394
|
+
const sessionId = this.#connectionSessionIds.get(connection.id);
|
|
395
|
+
|
|
396
|
+
if (sessionId) {
|
|
397
|
+
const handlers = this.#core.getHandlers();
|
|
398
|
+
handlers.onWebSocketClose(sessionId);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this.#connections.delete(connection.id);
|
|
402
|
+
this.#connectionSessionIds.delete(connection.id);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Handle WebSocket errors.
|
|
407
|
+
*/
|
|
408
|
+
async onError(connection: PartyConnection, error: Error): Promise<void> {
|
|
409
|
+
const sessionId = this.#connectionSessionIds.get(connection.id);
|
|
410
|
+
console.error(`WebSocket error for session ${sessionId}:`, error);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Handle HTTP requests (MCP JSON-RPC, query endpoints).
|
|
415
|
+
*/
|
|
416
|
+
async onRequest(request: Request): Promise<Response> {
|
|
417
|
+
const wrappedReq = wrapPartyRequest(request);
|
|
418
|
+
const handlers = this.#core.getHandlers();
|
|
419
|
+
const httpResponse = await handlers.onHttpRequest(wrappedReq);
|
|
420
|
+
|
|
421
|
+
// Check if this is an SSE response
|
|
422
|
+
if (isSSEResponse(httpResponse)) {
|
|
423
|
+
// Create a ReadableStream for SSE
|
|
424
|
+
const stream = new ReadableStream({
|
|
425
|
+
start(controller) {
|
|
426
|
+
// Create writer function that sends SSE-formatted data
|
|
427
|
+
const writer = (data: string): void => {
|
|
428
|
+
controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`));
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Set up the SSE stream
|
|
432
|
+
httpResponse.setup(writer, () => {
|
|
433
|
+
controller.close();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Keep connection alive with periodic comments
|
|
437
|
+
const keepAlive = setInterval(() => {
|
|
438
|
+
try {
|
|
439
|
+
controller.enqueue(new TextEncoder().encode(': keepalive\n\n'));
|
|
440
|
+
} catch {
|
|
441
|
+
clearInterval(keepAlive);
|
|
442
|
+
}
|
|
443
|
+
}, 30000);
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return new Response(stream, {
|
|
448
|
+
status: httpResponse.status,
|
|
449
|
+
headers: httpResponse.headers,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return new Response(httpResponse.body, {
|
|
454
|
+
status: httpResponse.status,
|
|
455
|
+
headers: httpResponse.headers,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Handle PartyKit alarms for scheduled tasks.
|
|
461
|
+
* @see https://docs.partykit.io/guides/scheduling-tasks-with-alarms/
|
|
462
|
+
*/
|
|
463
|
+
async onAlarm(): Promise<void> {
|
|
464
|
+
await this.#scheduler.handleAlarm();
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Pre-configured bridge class for direct export.
|
|
471
|
+
* Use `createPartyKitBridge()` if you need custom configuration.
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```typescript
|
|
475
|
+
* // For simple cases where you configure via environment
|
|
476
|
+
* export { MCPWebBridgeParty } from '@mcp-web/bridge';
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
export const MCPWebBridgeParty = createPartyKitBridge({
|
|
480
|
+
name: 'MCP Web Bridge',
|
|
481
|
+
description: 'MCP Web bridge server on PartyKit',
|
|
482
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { McpWebConfigSchema } from '@mcp-web/types';
|
|
3
|
+
|
|
4
|
+
test('Bridge config schema validates valid configuration', () => {
|
|
5
|
+
const result = McpWebConfigSchema.safeParse({
|
|
6
|
+
bridgeUrl: 'localhost:4001',
|
|
7
|
+
name: 'Test Bridge',
|
|
8
|
+
description: 'Test description',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(result.success).toBe(true);
|
|
12
|
+
if (result.success) {
|
|
13
|
+
expect(result.data.bridgeUrl === 'localhost:4001').toBe(true);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('Bridge config schema strips protocol from bridgeUrl', () => {
|
|
18
|
+
const result = McpWebConfigSchema.safeParse({
|
|
19
|
+
bridgeUrl: 'ws://localhost:4001',
|
|
20
|
+
name: 'Test Bridge',
|
|
21
|
+
description: 'Test description',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(result.success).toBe(true);
|
|
25
|
+
if (result.success) {
|
|
26
|
+
// Protocol should be stripped by transform
|
|
27
|
+
expect(result.data.bridgeUrl === 'localhost:4001').toBe(true);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('Bridge config schema uses default bridgeUrl', () => {
|
|
32
|
+
const result = McpWebConfigSchema.safeParse({
|
|
33
|
+
name: 'Test Bridge',
|
|
34
|
+
description: 'Test description',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(result.success).toBe(true);
|
|
38
|
+
if (result.success) {
|
|
39
|
+
expect(result.data.bridgeUrl === 'localhost:3001').toBe(true);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('Bridge config schema accepts agentUrl and strips protocol', () => {
|
|
44
|
+
const result = McpWebConfigSchema.safeParse({
|
|
45
|
+
name: 'Test Bridge',
|
|
46
|
+
description: 'Test description',
|
|
47
|
+
bridgeUrl: 'localhost:4001',
|
|
48
|
+
agentUrl: 'http://localhost:3003',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.success).toBe(true);
|
|
52
|
+
if (result.success) {
|
|
53
|
+
// Protocol should be stripped by transform
|
|
54
|
+
expect(result.data.agentUrl === 'localhost:3003').toBe(true);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('Bridge config schema requires name and description', () => {
|
|
59
|
+
const result = McpWebConfigSchema.safeParse({
|
|
60
|
+
bridgeUrl: 'localhost:4001',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.success).toBe(false);
|
|
64
|
+
});
|