@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
package/dist/bridge.js ADDED
@@ -0,0 +1,1004 @@
1
+ #!/usr/bin/env node
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _MCPWebBridge_instances, _MCPWebBridge_sessions, _MCPWebBridge_queries, _MCPWebBridge_config, _MCPWebBridge_wsServer, _MCPWebBridge_mcpServer, _MCPWebBridge_tokenSessionIds, _MCPWebBridge_tokenQueryCounts, _MCPWebBridge_sessionTimeoutInterval, _MCPWebBridge_decrementQueryCount, _MCPWebBridge_decrementQueryCountForQuery, _MCPWebBridge_closeOldestSessionForToken, _MCPWebBridge_cleanupSession, _MCPWebBridge_startSessionTimeoutChecker;
14
+ import crypto from 'node:crypto';
15
+ import { readFileSync } from 'node:fs';
16
+ import { createServer } from 'node:http';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath, URL } from 'node:url';
19
+ import { InternalErrorCode, InvalidSessionErrorCode, McpWebConfigSchema, MissingAuthenticationErrorCode, NoSessionsFoundErrorCode, QueryAcceptedMessageSchema, QueryCancelMessageSchema, QueryCompleteBridgeMessageSchema, QueryCompleteClientMessageSchema, QueryFailureMessageSchema, QueryLimitExceededErrorCode, QueryMessageSchema, QueryNotActiveErrorCode, QueryNotFoundErrorCode, QueryProgressMessageSchema, SessionExpiredErrorCode, SessionLimitExceededErrorCode, SessionNotFoundErrorCode, SessionNotSpecifiedErrorCode, ToolNameRequiredErrorCode, ToolNotAllowedErrorCode, ToolNotFoundErrorCode, UnknownMethodErrorCode, } from '@mcp-web/types';
20
+ import { WebSocket, WebSocketServer } from 'ws';
21
+ const SessionNotSpecifiedErrorDetails = 'Multiple sessions available. See `available_sessions` or call the `list_sessions` tool to discover available sessions and specify the session using `_meta.sessionId`.';
22
+ /**
23
+ * Builds the query URL by appending the UUID to the agent URL.
24
+ * If the agent URL has no path (e.g., 'http://localhost:3000'), defaults to '/query'.
25
+ * Otherwise uses the existing path (e.g., 'http://localhost:3000/api/v1/query').
26
+ */
27
+ const buildQueryUrl = (agentUrl, uuid) => {
28
+ const url = new URL(agentUrl);
29
+ // If URL has no path or just '/', default to '/query'
30
+ if (url.pathname === '/' || url.pathname === '') {
31
+ url.pathname = '/query';
32
+ }
33
+ // Ensure path doesn't end with /
34
+ if (url.pathname.endsWith('/')) {
35
+ url.pathname = url.pathname.slice(0, -1);
36
+ }
37
+ // Append UUID
38
+ url.pathname = `${url.pathname}/${uuid}`;
39
+ return url.toString();
40
+ };
41
+ export class MCPWebBridge {
42
+ constructor(config = {
43
+ wsPort: 3001,
44
+ mcpPort: 3002,
45
+ name: "Web App Controller",
46
+ description: "Control web applications and dashboards through your browser"
47
+ }) {
48
+ _MCPWebBridge_instances.add(this);
49
+ _MCPWebBridge_sessions.set(this, new Map());
50
+ _MCPWebBridge_queries.set(this, new Map());
51
+ _MCPWebBridge_config.set(this, void 0);
52
+ _MCPWebBridge_wsServer.set(this, void 0);
53
+ _MCPWebBridge_mcpServer.set(this, void 0);
54
+ // Session & Query limit tracking
55
+ _MCPWebBridge_tokenSessionIds.set(this, new Map()); // token -> sessionIds
56
+ _MCPWebBridge_tokenQueryCounts.set(this, new Map()); // token -> active query count
57
+ _MCPWebBridge_sessionTimeoutInterval.set(this, void 0);
58
+ // Validate the configuration
59
+ const parsedConfig = McpWebConfigSchema.safeParse(config);
60
+ if (!parsedConfig.success) {
61
+ throw new Error(`Invalid bridge server configuration: ${parsedConfig.error.message}`);
62
+ }
63
+ __classPrivateFieldSet(this, _MCPWebBridge_config, parsedConfig.data, "f");
64
+ __classPrivateFieldSet(this, _MCPWebBridge_wsServer, this.setupWebSocketServer(__classPrivateFieldGet(this, _MCPWebBridge_config, "f").wsPort), "f");
65
+ __classPrivateFieldSet(this, _MCPWebBridge_mcpServer, this.setupMCPServer(__classPrivateFieldGet(this, _MCPWebBridge_config, "f").mcpPort), "f");
66
+ // Start session timeout checker if configured
67
+ if (__classPrivateFieldGet(this, _MCPWebBridge_config, "f").sessionMaxDurationMs) {
68
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_startSessionTimeoutChecker).call(this);
69
+ }
70
+ }
71
+ setupWebSocketServer(wsPort) {
72
+ const wsServer = new WebSocketServer({
73
+ port: wsPort,
74
+ verifyClient: () => {
75
+ // Add origin verification if needed
76
+ return true;
77
+ }
78
+ });
79
+ wsServer.on('connection', (ws, req) => {
80
+ if (!req.url) {
81
+ ws.close(1008, 'Missing URL');
82
+ return;
83
+ }
84
+ const url = new URL(req.url, 'ws://localhost');
85
+ const sessionId = url.searchParams.get('session');
86
+ if (!sessionId) {
87
+ ws.close(1008, 'Missing session key');
88
+ return;
89
+ }
90
+ ws.on('message', (data) => {
91
+ try {
92
+ const message = JSON.parse(data.toString());
93
+ this.handleFrontendMessage(sessionId, message, ws);
94
+ }
95
+ catch (error) {
96
+ console.error('Invalid JSON message:', error);
97
+ ws.close(1003, 'Invalid JSON');
98
+ }
99
+ });
100
+ ws.on('close', () => {
101
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_cleanupSession).call(this, sessionId);
102
+ });
103
+ ws.on('error', (error) => {
104
+ console.error(`WebSocket error for session ${sessionId}:`, error);
105
+ });
106
+ });
107
+ return wsServer;
108
+ }
109
+ setupMCPServer(mcpPort) {
110
+ const mcpServer = createServer((req, res) => {
111
+ // Enable CORS
112
+ res.setHeader('Access-Control-Allow-Origin', '*');
113
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
114
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
115
+ if (req.method === 'OPTIONS') {
116
+ res.writeHead(200);
117
+ res.end();
118
+ return;
119
+ }
120
+ // Route query progress/complete/fail/cancel endpoints
121
+ const url = req.url || '';
122
+ const queryProgressMatch = url.match(/^\/query\/([^/]+)\/progress$/);
123
+ const queryCompleteMatch = url.match(/^\/query\/([^/]+)\/complete$/);
124
+ const queryFailMatch = url.match(/^\/query\/([^/]+)\/fail$/);
125
+ const queryCancelMatch = url.match(/^\/query\/([^/]+)\/cancel$/);
126
+ if (req.method === 'POST' && queryProgressMatch) {
127
+ const uuid = queryProgressMatch[1];
128
+ let body = '';
129
+ req.on('data', (chunk) => { body += chunk; });
130
+ req.on('end', () => {
131
+ this.handleQueryProgressEndpoint(req, res, uuid, body);
132
+ });
133
+ return;
134
+ }
135
+ if (req.method === 'PUT' && queryCompleteMatch) {
136
+ const uuid = queryCompleteMatch[1];
137
+ let body = '';
138
+ req.on('data', (chunk) => { body += chunk; });
139
+ req.on('end', () => {
140
+ this.handleQueryCompleteEndpoint(req, res, uuid, body);
141
+ });
142
+ return;
143
+ }
144
+ if (req.method === 'PUT' && queryFailMatch) {
145
+ const uuid = queryFailMatch[1];
146
+ let body = '';
147
+ req.on('data', (chunk) => { body += chunk; });
148
+ req.on('end', () => {
149
+ this.handleQueryFailEndpoint(req, res, uuid, body);
150
+ });
151
+ return;
152
+ }
153
+ if (req.method === 'PUT' && queryCancelMatch) {
154
+ const uuid = queryCancelMatch[1];
155
+ let body = '';
156
+ req.on('data', (chunk) => { body += chunk; });
157
+ req.on('end', () => {
158
+ this.handleQueryCancelEndpoint(req, res, uuid, body);
159
+ });
160
+ return;
161
+ }
162
+ if (req.method === 'POST') {
163
+ let body = '';
164
+ req.on('data', (chunk) => { body += chunk; });
165
+ req.on('end', () => {
166
+ this.handleMCPRequest(req, res, body);
167
+ });
168
+ }
169
+ else {
170
+ res.writeHead(404);
171
+ res.end('Not Found');
172
+ }
173
+ });
174
+ mcpServer.listen(mcpPort);
175
+ return mcpServer;
176
+ }
177
+ handleFrontendMessage(sessionId, message, ws) {
178
+ switch (message.type) {
179
+ case 'authenticate':
180
+ this.handleAuthentication(sessionId, message, ws);
181
+ break;
182
+ case 'register-tool':
183
+ this.handleToolRegistration(sessionId, message);
184
+ break;
185
+ case 'activity':
186
+ this.handleActivity(sessionId, message);
187
+ break;
188
+ case 'tool-response':
189
+ // tool-response messages are handled by per-request listeners
190
+ // in handleToolCall() at line ~770, not here in the main router
191
+ break;
192
+ case 'query':
193
+ this.handleQuery(sessionId, message, ws);
194
+ break;
195
+ case 'query_cancel':
196
+ this.handleQueryCancel(message);
197
+ break;
198
+ default:
199
+ // biome-ignore lint/suspicious/noExplicitAny: Edge case handling
200
+ console.warn(`Unknown message type: ${message.type}`);
201
+ }
202
+ }
203
+ handleAuthentication(sessionId, message, ws) {
204
+ const { authToken } = message;
205
+ // Check session limit
206
+ if (__classPrivateFieldGet(this, _MCPWebBridge_config, "f").maxSessionsPerToken) {
207
+ const existingSessions = __classPrivateFieldGet(this, _MCPWebBridge_tokenSessionIds, "f").get(authToken);
208
+ const currentCount = existingSessions?.size ?? 0;
209
+ if (currentCount >= __classPrivateFieldGet(this, _MCPWebBridge_config, "f").maxSessionsPerToken) {
210
+ if (__classPrivateFieldGet(this, _MCPWebBridge_config, "f").onSessionLimitExceeded === 'close_oldest') {
211
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_closeOldestSessionForToken).call(this, authToken);
212
+ }
213
+ else {
214
+ ws.send(JSON.stringify({
215
+ type: 'authentication-failed',
216
+ error: 'Session limit exceeded',
217
+ code: SessionLimitExceededErrorCode
218
+ }));
219
+ ws.close(1008, 'Session limit exceeded');
220
+ return;
221
+ }
222
+ }
223
+ }
224
+ const sessionData = {
225
+ ws,
226
+ authToken: message.authToken,
227
+ origin: message.origin,
228
+ pageTitle: message.pageTitle,
229
+ userAgent: message.userAgent,
230
+ connectedAt: Date.now(),
231
+ lastActivity: Date.now(),
232
+ tools: new Map()
233
+ };
234
+ __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").set(sessionId, sessionData);
235
+ // Track session for this token
236
+ const sessionIds = __classPrivateFieldGet(this, _MCPWebBridge_tokenSessionIds, "f").get(authToken) ?? new Set();
237
+ sessionIds.add(sessionId);
238
+ __classPrivateFieldGet(this, _MCPWebBridge_tokenSessionIds, "f").set(authToken, sessionIds);
239
+ ws.send(JSON.stringify({
240
+ type: 'authenticated',
241
+ // @ts-expect-error We know the port exists.
242
+ mcpPort: __classPrivateFieldGet(this, _MCPWebBridge_mcpServer, "f").address()?.port,
243
+ sessionId,
244
+ success: true
245
+ }));
246
+ }
247
+ handleToolRegistration(sessionId, message) {
248
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(sessionId);
249
+ if (!session) {
250
+ console.warn(`Tool registration for unknown session: ${sessionId}`);
251
+ return;
252
+ }
253
+ console.log('registering tool for session', sessionId, message);
254
+ session.tools.set(message.tool.name, message.tool);
255
+ }
256
+ handleActivity(sessionId, message) {
257
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(sessionId);
258
+ if (session) {
259
+ session.lastActivity = message.timestamp;
260
+ }
261
+ }
262
+ async handleQueryCancel(message) {
263
+ const cancelMessage = QueryCancelMessageSchema.parse(message);
264
+ const { uuid } = cancelMessage;
265
+ const query = __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").get(uuid);
266
+ if (!query) {
267
+ console.warn(`Cancel requested for unknown query: ${uuid}`);
268
+ return;
269
+ }
270
+ // Mark query as cancelled
271
+ query.state = 'cancelled';
272
+ // Notify agent that query no longer exists (optional - agent may not implement DELETE)
273
+ if (__classPrivateFieldGet(this, _MCPWebBridge_config, "f").agentUrl) {
274
+ try {
275
+ await fetch(buildQueryUrl(__classPrivateFieldGet(this, _MCPWebBridge_config, "f").agentUrl, uuid), {
276
+ method: 'DELETE',
277
+ headers: { 'Content-Type': 'application/json' }
278
+ });
279
+ }
280
+ catch (error) {
281
+ // Agent may not implement DELETE endpoint, which is fine
282
+ console.debug(`Failed to notify agent of query deletion (optional): ${error instanceof Error ? error.message : String(error)}`);
283
+ }
284
+ }
285
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_decrementQueryCountForQuery).call(this, query);
286
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").delete(uuid);
287
+ }
288
+ async handleQuery(sessionId, message, ws) {
289
+ const { uuid, responseTool, tools, restrictTools } = message;
290
+ if (!__classPrivateFieldGet(this, _MCPWebBridge_config, "f").agentUrl) {
291
+ ws.send(JSON.stringify(QueryFailureMessageSchema.parse({
292
+ uuid,
293
+ error: 'Missing Agent URL'
294
+ })));
295
+ return;
296
+ }
297
+ // Get session for query limit checking
298
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(sessionId);
299
+ if (!session) {
300
+ ws.send(JSON.stringify(QueryFailureMessageSchema.parse({
301
+ uuid,
302
+ error: 'Session not found'
303
+ })));
304
+ return;
305
+ }
306
+ // Check query limit
307
+ if (__classPrivateFieldGet(this, _MCPWebBridge_config, "f").maxInFlightQueriesPerToken) {
308
+ const currentQueries = __classPrivateFieldGet(this, _MCPWebBridge_tokenQueryCounts, "f").get(session.authToken) ?? 0;
309
+ if (currentQueries >= __classPrivateFieldGet(this, _MCPWebBridge_config, "f").maxInFlightQueriesPerToken) {
310
+ ws.send(JSON.stringify(QueryFailureMessageSchema.parse({
311
+ uuid,
312
+ error: 'Query limit exceeded. Wait for existing queries to complete.',
313
+ code: QueryLimitExceededErrorCode
314
+ })));
315
+ return;
316
+ }
317
+ }
318
+ // Increment query count for this token
319
+ __classPrivateFieldGet(this, _MCPWebBridge_tokenQueryCounts, "f").set(session.authToken, (__classPrivateFieldGet(this, _MCPWebBridge_tokenQueryCounts, "f").get(session.authToken) ?? 0) + 1);
320
+ try {
321
+ // Track this query
322
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").set(uuid, {
323
+ sessionId,
324
+ responseTool: responseTool?.name,
325
+ toolCalls: [],
326
+ ws,
327
+ state: 'active',
328
+ tools,
329
+ restrictTools
330
+ });
331
+ // Forward query to agent
332
+ const response = await fetch(buildQueryUrl(__classPrivateFieldGet(this, _MCPWebBridge_config, "f").agentUrl, uuid), {
333
+ method: 'PUT',
334
+ headers: {
335
+ 'Content-Type': 'application/json',
336
+ ...(__classPrivateFieldGet(this, _MCPWebBridge_config, "f").authToken && { 'Authorization': `Bearer ${__classPrivateFieldGet(this, _MCPWebBridge_config, "f").authToken}` })
337
+ },
338
+ body: JSON.stringify(QueryMessageSchema.parse(message))
339
+ });
340
+ if (!response.ok) {
341
+ throw new Error(`Agent responded with ${response.status}: ${response.statusText}`);
342
+ }
343
+ // Send immediate acceptance back to frontend
344
+ ws.send(JSON.stringify(QueryAcceptedMessageSchema.parse({ uuid })));
345
+ }
346
+ catch (error) {
347
+ console.error(`Error forwarding query ${uuid}:`, error);
348
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").delete(uuid);
349
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_decrementQueryCount).call(this, session.authToken);
350
+ ws.send(JSON.stringify(QueryFailureMessageSchema.parse({
351
+ uuid,
352
+ error: `${error instanceof Error ? error.message : String(error)}`
353
+ })));
354
+ }
355
+ }
356
+ handleQueryProgressEndpoint(_req, res, uuid, body) {
357
+ try {
358
+ const message = JSON.parse(body);
359
+ const progressMessage = QueryProgressMessageSchema.parse({ uuid, ...message });
360
+ const query = __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").get(uuid);
361
+ if (!query) {
362
+ res.writeHead(404, { 'Content-Type': 'application/json' });
363
+ res.end(JSON.stringify({ error: QueryNotFoundErrorCode }));
364
+ return;
365
+ }
366
+ // Forward progress to frontend
367
+ if (query.ws.readyState === WebSocket.OPEN) {
368
+ query.ws.send(JSON.stringify(progressMessage));
369
+ }
370
+ res.writeHead(200, { 'Content-Type': 'application/json' });
371
+ res.end(JSON.stringify({ success: true }));
372
+ }
373
+ catch (error) {
374
+ console.error('Error handling query progress:', error);
375
+ res.writeHead(400, { 'Content-Type': 'application/json' });
376
+ res.end(JSON.stringify({ error: 'Invalid request body' }));
377
+ }
378
+ }
379
+ handleQueryCompleteEndpoint(_req, res, uuid, body) {
380
+ try {
381
+ const message = JSON.parse(body);
382
+ const completeMessage = QueryCompleteClientMessageSchema.parse({ uuid, ...message });
383
+ const query = __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").get(uuid);
384
+ if (!query) {
385
+ res.writeHead(404, { 'Content-Type': 'application/json' });
386
+ res.end(JSON.stringify({ error: QueryNotFoundErrorCode }));
387
+ return;
388
+ }
389
+ // Edge case: responseTool specified but agent called queryComplete()
390
+ if (query.responseTool) {
391
+ const errorMessage = QueryFailureMessageSchema.parse({
392
+ uuid,
393
+ error: `Query specified responseTool '${query.responseTool}' but agent called queryComplete() instead`
394
+ });
395
+ if (query.ws.readyState === WebSocket.OPEN) {
396
+ query.ws.send(JSON.stringify(errorMessage));
397
+ }
398
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_decrementQueryCountForQuery).call(this, query);
399
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").delete(uuid);
400
+ res.writeHead(400, { 'Content-Type': 'application/json' });
401
+ res.end(JSON.stringify({ error: errorMessage.error }));
402
+ return;
403
+ }
404
+ // Mark query as completed
405
+ query.state = 'completed';
406
+ // Send completion with tracked tool calls
407
+ const bridgeMessage = QueryCompleteBridgeMessageSchema.parse({
408
+ uuid,
409
+ message: completeMessage.message,
410
+ toolCalls: query.toolCalls
411
+ });
412
+ if (query.ws.readyState === WebSocket.OPEN) {
413
+ query.ws.send(JSON.stringify(bridgeMessage));
414
+ }
415
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_decrementQueryCountForQuery).call(this, query);
416
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").delete(uuid);
417
+ res.writeHead(200, { 'Content-Type': 'application/json' });
418
+ res.end(JSON.stringify({ success: true }));
419
+ }
420
+ catch (error) {
421
+ console.error('Error handling query complete:', error);
422
+ res.writeHead(400, { 'Content-Type': 'application/json' });
423
+ res.end(JSON.stringify({ error: 'Invalid request body' }));
424
+ }
425
+ }
426
+ handleQueryFailEndpoint(_req, res, uuid, body) {
427
+ try {
428
+ const message = JSON.parse(body);
429
+ const failureMessage = QueryFailureMessageSchema.parse({ uuid, ...message });
430
+ const query = __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").get(uuid);
431
+ if (!query) {
432
+ res.writeHead(404, { 'Content-Type': 'application/json' });
433
+ res.end(JSON.stringify({ error: QueryNotFoundErrorCode }));
434
+ return;
435
+ }
436
+ // Mark query as failed
437
+ query.state = 'failed';
438
+ // Send failure message to frontend
439
+ if (query.ws.readyState === WebSocket.OPEN) {
440
+ query.ws.send(JSON.stringify(failureMessage));
441
+ }
442
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_decrementQueryCountForQuery).call(this, query);
443
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").delete(uuid);
444
+ res.writeHead(200, { 'Content-Type': 'application/json' });
445
+ res.end(JSON.stringify({ success: true }));
446
+ }
447
+ catch (error) {
448
+ console.error('Error handling query fail:', error);
449
+ res.writeHead(400, { 'Content-Type': 'application/json' });
450
+ res.end(JSON.stringify({ error: 'Invalid request body' }));
451
+ }
452
+ }
453
+ handleQueryCancelEndpoint(_req, res, uuid, body) {
454
+ try {
455
+ const query = __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").get(uuid);
456
+ if (!query) {
457
+ res.writeHead(404, { 'Content-Type': 'application/json' });
458
+ res.end(JSON.stringify({ error: QueryNotFoundErrorCode }));
459
+ return;
460
+ }
461
+ // Mark query as cancelled
462
+ query.state = 'cancelled';
463
+ // Send cancellation message to frontend (as a failure with specific message)
464
+ const cancellationMessage = QueryCancelMessageSchema.parse({
465
+ uuid,
466
+ reason: body ? JSON.parse(body).reason : undefined
467
+ });
468
+ if (query.ws.readyState === WebSocket.OPEN) {
469
+ query.ws.send(JSON.stringify(cancellationMessage));
470
+ }
471
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_decrementQueryCountForQuery).call(this, query);
472
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").delete(uuid);
473
+ res.writeHead(200, { 'Content-Type': 'application/json' });
474
+ res.end(JSON.stringify({ success: true }));
475
+ }
476
+ catch (error) {
477
+ console.error('Error handling query cancel:', error);
478
+ res.writeHead(400, { 'Content-Type': 'application/json' });
479
+ res.end(JSON.stringify({ error: 'Invalid request body' }));
480
+ }
481
+ }
482
+ async handleMCPRequest(req, res, body) {
483
+ try {
484
+ const mcpRequest = JSON.parse(body);
485
+ // Extract auth from header OR query context
486
+ const authHeader = req.headers.authorization;
487
+ const authToken = authHeader?.replace('Bearer ', '');
488
+ const queryId = mcpRequest.params?._meta?.queryId;
489
+ const sessions = new Map();
490
+ if (queryId) {
491
+ // Authenticate via query context
492
+ const query = __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").get(queryId);
493
+ if (!query) {
494
+ this.sendMCPError(res, mcpRequest.id, -32600, QueryNotFoundErrorCode);
495
+ return;
496
+ }
497
+ if (query.state !== 'active') {
498
+ this.sendMCPError(res, mcpRequest.id, -32600, QueryNotActiveErrorCode);
499
+ return;
500
+ }
501
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(query.sessionId);
502
+ if (!session) {
503
+ this.sendMCPError(res, mcpRequest.id, -32600, InvalidSessionErrorCode);
504
+ return;
505
+ }
506
+ sessions.set(query.sessionId, session);
507
+ }
508
+ else if (authToken) {
509
+ // Traditional authentication
510
+ Array.from(__classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").entries())
511
+ .filter(([_, session]) => session.authToken === authToken)
512
+ .forEach(([sessionId, session]) => {
513
+ sessions.set(sessionId, session);
514
+ });
515
+ }
516
+ else {
517
+ this.sendMCPError(res, mcpRequest.id, -32600, MissingAuthenticationErrorCode);
518
+ return;
519
+ }
520
+ if (sessions.size === 0) {
521
+ this.sendMCPError(res, mcpRequest.id, -32600, NoSessionsFoundErrorCode);
522
+ return;
523
+ }
524
+ let result;
525
+ switch (mcpRequest.method) {
526
+ case 'initialize':
527
+ result = await this.handleInitialize();
528
+ break;
529
+ case 'tools/list':
530
+ result = await this.handleToolsList(sessions, mcpRequest.params);
531
+ break;
532
+ case 'tools/call':
533
+ result = await this.handleToolCall(sessions, mcpRequest.params);
534
+ break;
535
+ case 'resources/list':
536
+ result = await this.handleResourcesList(sessions, mcpRequest.params);
537
+ break;
538
+ case 'resources/read':
539
+ result = await this.handleResourceRead(sessions, mcpRequest.params);
540
+ break;
541
+ case 'prompts/list':
542
+ result = await this.handlePromptsList(sessions, mcpRequest.params);
543
+ break;
544
+ default: {
545
+ this.sendMCPError(res, mcpRequest.id, -32601, UnknownMethodErrorCode);
546
+ return;
547
+ }
548
+ }
549
+ // Check if result contains a fatal error (has error_is_fatal: true)
550
+ // Recoverable errors have isError: true with partial data and go in result
551
+ if (result && typeof result === 'object' && 'error_is_fatal' in result && result.error_is_fatal === true) {
552
+ const fatalError = result;
553
+ // Use -32602 (Invalid params) for client errors like missing sessionId
554
+ this.sendMCPError(res, mcpRequest.id, -32602, fatalError.error_message, fatalError);
555
+ return;
556
+ }
557
+ // Everything else (including soft errors with isError: true) goes in result
558
+ this.sendMCPResponse(res, mcpRequest.id, result);
559
+ }
560
+ catch (error) {
561
+ console.error('MCP request error:', error);
562
+ this.sendMCPError(res, 0, -32603, InternalErrorCode);
563
+ }
564
+ }
565
+ getVersion() {
566
+ try {
567
+ const __filename = fileURLToPath(import.meta.url);
568
+ const __dirname = dirname(__filename);
569
+ const packageJsonPath = join(__dirname, '..', 'package.json');
570
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
571
+ return packageJson.version || "1.0.0";
572
+ }
573
+ catch (error) {
574
+ console.warn('Failed to read version from package.json:', error);
575
+ return "1.0.0";
576
+ }
577
+ }
578
+ async handleInitialize() {
579
+ return {
580
+ protocolVersion: "2024-11-05",
581
+ capabilities: {
582
+ tools: {},
583
+ resources: {},
584
+ prompts: {}
585
+ },
586
+ serverInfo: {
587
+ name: __classPrivateFieldGet(this, _MCPWebBridge_config, "f").name,
588
+ description: __classPrivateFieldGet(this, _MCPWebBridge_config, "f").description,
589
+ version: this.getVersion(),
590
+ ...(__classPrivateFieldGet(this, _MCPWebBridge_config, "f").icon && { icon: __classPrivateFieldGet(this, _MCPWebBridge_config, "f").icon })
591
+ }
592
+ };
593
+ }
594
+ getSessionAndSessionId(sessions, sessionId) {
595
+ if (!sessionId) {
596
+ if (sessions.size === 1) {
597
+ sessionId = sessions.keys().next().value;
598
+ if (!sessionId) {
599
+ return undefined;
600
+ }
601
+ }
602
+ else {
603
+ return undefined;
604
+ }
605
+ }
606
+ const session = sessions.get(sessionId);
607
+ if (!session) {
608
+ return undefined;
609
+ }
610
+ return [sessionId, session];
611
+ }
612
+ getSessionFromMetaParams(sessions, params) {
613
+ const sessionId = params?._meta?.sessionId;
614
+ return this.getSessionAndSessionId(sessions, sessionId)?.[1];
615
+ }
616
+ createSessionNotFoundError(sessions) {
617
+ if (sessions.size > 1) {
618
+ return {
619
+ error: SessionNotSpecifiedErrorCode,
620
+ error_message: SessionNotSpecifiedErrorDetails,
621
+ available_sessions: this.listSessions(sessions)
622
+ };
623
+ }
624
+ return { error: SessionNotFoundErrorCode };
625
+ }
626
+ async handleToolsList(sessions, params) {
627
+ const session = this.getSessionFromMetaParams(sessions, params);
628
+ const listSessionsTool = {
629
+ name: "list_sessions",
630
+ description: "List all browser sessions with their available tools",
631
+ inputSchema: {
632
+ type: "object",
633
+ properties: {},
634
+ required: []
635
+ }
636
+ };
637
+ // If no session found and multiple sessions exist, return list_sessions tool only with isError
638
+ if (!session && sessions.size > 1) {
639
+ return {
640
+ tools: [listSessionsTool],
641
+ isError: true,
642
+ error: SessionNotSpecifiedErrorCode,
643
+ error_message: SessionNotSpecifiedErrorDetails,
644
+ error_is_fatal: false,
645
+ available_sessions: this.listSessions(sessions)
646
+ };
647
+ }
648
+ // If no session at all (shouldn't happen with proper auth), return fatal error
649
+ if (!session) {
650
+ return {
651
+ error: SessionNotFoundErrorCode,
652
+ error_message: 'No session found for the provided authentication',
653
+ error_is_fatal: true
654
+ };
655
+ }
656
+ const tools = [listSessionsTool];
657
+ for (const tool of session.tools.values()) {
658
+ const sessionAwareTool = {
659
+ name: tool.name,
660
+ description: tool.description,
661
+ inputSchema: {
662
+ type: "object",
663
+ properties: {
664
+ session_id: {
665
+ type: "string",
666
+ description: "Session ID (optional - will auto-select if only one session active)",
667
+ },
668
+ ...(tool.inputSchema?.properties || {})
669
+ },
670
+ required: tool.inputSchema?.required || []
671
+ }
672
+ };
673
+ tools.push(sessionAwareTool);
674
+ }
675
+ return { tools };
676
+ }
677
+ async handleToolCall(sessions, params) {
678
+ const { name: toolName, arguments: toolInput, _meta } = params || {};
679
+ if (!toolName) {
680
+ return { error: ToolNameRequiredErrorCode };
681
+ }
682
+ // Extract query ID from context if available
683
+ const queryId = _meta?.queryId;
684
+ // If this is part of a query, validate the query state and restrictions
685
+ if (queryId) {
686
+ const query = __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").get(queryId);
687
+ if (!query) {
688
+ return { error: QueryNotFoundErrorCode };
689
+ }
690
+ if (query.state !== 'active') {
691
+ return { error: QueryNotActiveErrorCode };
692
+ }
693
+ // Check tool restrictions if query has them
694
+ if (query.restrictTools && query.tools) {
695
+ const allowed = query.tools.some(t => t.name === toolName);
696
+ if (!allowed) {
697
+ return {
698
+ error: ToolNotAllowedErrorCode,
699
+ details: 'The query restricts the allowed tool calls. Use one of `allowed_tools`.',
700
+ allowed_tools: query.tools.map(t => t.name)
701
+ };
702
+ }
703
+ }
704
+ }
705
+ if (toolName === 'list_sessions') {
706
+ return { sessions: this.listSessions(sessions) };
707
+ }
708
+ // Handle session-specific tool
709
+ const [sessionId, session] = this.getSessionAndSessionId(sessions, toolInput?.session_id || _meta?.sessionId) || [];
710
+ if (!sessionId || !session) {
711
+ return this.createSessionNotFoundError(sessions);
712
+ }
713
+ // Check if tool exists in this session
714
+ if (!session.tools.has(toolName)) {
715
+ return {
716
+ error: ToolNotFoundErrorCode,
717
+ available_tools: Array.from(session.tools.keys())
718
+ };
719
+ }
720
+ // Forward tool call to frontend
721
+ return this.forwardToolCallToSession(sessionId, toolName, toolInput, queryId);
722
+ }
723
+ async handleResourcesList(sessions, params) {
724
+ const session = this.getSessionFromMetaParams(sessions, params);
725
+ // Built-in resource that provides session discovery (like list_sessions tool)
726
+ const sessionListResource = {
727
+ uri: "sessions://list",
728
+ name: "sessions",
729
+ title: "Active Browser Sessions",
730
+ description: "List of all active browser sessions for this authentication context",
731
+ mimeType: "application/json"
732
+ };
733
+ // If no session found and multiple sessions exist, return discovery resource only with isError
734
+ if (!session && sessions.size > 1) {
735
+ return {
736
+ resources: [sessionListResource],
737
+ isError: true,
738
+ error: SessionNotSpecifiedErrorCode,
739
+ error_message: SessionNotSpecifiedErrorDetails,
740
+ error_is_fatal: false,
741
+ available_sessions: this.listSessions(sessions)
742
+ };
743
+ }
744
+ // If no session at all (shouldn't happen with proper auth), return fatal error
745
+ if (!session) {
746
+ return {
747
+ error: SessionNotFoundErrorCode,
748
+ error_message: 'No session found for the provided authentication',
749
+ error_is_fatal: true
750
+ };
751
+ }
752
+ // TODO: Add session-specific resources
753
+ const resources = [
754
+ sessionListResource,
755
+ // ...session.resources (future)
756
+ ];
757
+ return { resources };
758
+ }
759
+ async handleResourceRead(sessions, params) {
760
+ const { uri } = params || {};
761
+ if (!uri) {
762
+ return { error: "Resource URI is required" };
763
+ }
764
+ // Handle built-in sessions resource
765
+ if (uri === "sessions://list") {
766
+ const sessionData = this.listSessions(sessions);
767
+ return {
768
+ contents: [{
769
+ uri: "sessions://list",
770
+ mimeType: "application/json",
771
+ text: JSON.stringify(sessionData, null, 2)
772
+ }]
773
+ };
774
+ }
775
+ // TODO: Handle session-specific resources
776
+ return { error: "Resource not found" };
777
+ }
778
+ listSessions(sessions) {
779
+ const sessionList = Array.from(sessions.entries()).map(([key, session]) => ({
780
+ session_id: key,
781
+ origin: session.origin,
782
+ page_title: session.pageTitle,
783
+ connected_at: new Date(session.connectedAt).toISOString(),
784
+ last_activity: new Date(session.lastActivity).toISOString(),
785
+ available_tools: Array.from(session.tools.keys())
786
+ }));
787
+ return sessionList;
788
+ }
789
+ async handlePromptsList(sessions, params) {
790
+ const session = this.getSessionFromMetaParams(sessions, params);
791
+ // If no session found and multiple sessions exist, return empty prompts with isError
792
+ if (!session && sessions.size > 1) {
793
+ return {
794
+ prompts: [],
795
+ isError: true,
796
+ error: SessionNotSpecifiedErrorCode,
797
+ error_message: SessionNotSpecifiedErrorDetails,
798
+ error_is_fatal: false,
799
+ available_sessions: this.listSessions(sessions),
800
+ };
801
+ }
802
+ // If no session at all (shouldn't happen with proper auth), return fatal error
803
+ if (!session) {
804
+ return {
805
+ error: SessionNotFoundErrorCode,
806
+ error_message: 'No session found for the provided authentication',
807
+ error_is_fatal: true
808
+ };
809
+ }
810
+ // TO DO: implement prompt exposure
811
+ return { prompts: [] };
812
+ }
813
+ async forwardToolCallToSession(sessionId, toolName, toolInput, queryId) {
814
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(sessionId);
815
+ if (!session || session.ws.readyState !== WebSocket.OPEN) {
816
+ return { error: "Session not available" };
817
+ }
818
+ // Generate request ID for tracking
819
+ const requestId = crypto.randomUUID();
820
+ // Send tool call to frontend
821
+ const toolCall = {
822
+ type: 'tool-call',
823
+ requestId,
824
+ toolName,
825
+ toolInput,
826
+ ...(queryId && { queryId })
827
+ };
828
+ return new Promise((resolve) => {
829
+ // Set up response handler
830
+ const timeout = setTimeout(() => {
831
+ resolve({ error: "Tool call timeout" });
832
+ }, 30000); // 30 second timeout
833
+ const handleResponse = (data) => {
834
+ try {
835
+ const message = JSON.parse(data.toString());
836
+ if (message.type === 'tool-response' && message.requestId === requestId) {
837
+ clearTimeout(timeout);
838
+ session.ws.removeListener('message', handleResponse);
839
+ const toolResult = message.result;
840
+ // Track tool call if part of a query
841
+ if (queryId) {
842
+ const query = __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").get(queryId);
843
+ if (query) {
844
+ query.toolCalls.push({
845
+ tool: toolName,
846
+ arguments: toolInput,
847
+ result: toolResult
848
+ });
849
+ // Check if this was the responseTool - auto-complete query
850
+ if (query.responseTool === toolName) {
851
+ // Check if tool call succeeded
852
+ if (!(toolResult && typeof toolResult === 'object' && 'error' in toolResult)) {
853
+ // Tool succeeded - auto-complete query
854
+ const bridgeMessage = QueryCompleteBridgeMessageSchema.parse({
855
+ uuid: queryId,
856
+ message: undefined,
857
+ toolCalls: query.toolCalls
858
+ });
859
+ if (query.ws.readyState === WebSocket.OPEN) {
860
+ query.ws.send(JSON.stringify(bridgeMessage));
861
+ }
862
+ // Only delete query on success
863
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").delete(queryId);
864
+ }
865
+ }
866
+ }
867
+ }
868
+ resolve(toolResult);
869
+ }
870
+ }
871
+ catch (_error) {
872
+ // Ignore invalid JSON
873
+ }
874
+ };
875
+ session.ws.addListener('message', handleResponse);
876
+ session.ws.send(JSON.stringify(toolCall));
877
+ });
878
+ }
879
+ sendMCPResponse(res, id, result) {
880
+ const response = {
881
+ jsonrpc: "2.0",
882
+ id,
883
+ result
884
+ };
885
+ res.writeHead(200, { 'Content-Type': 'application/json' });
886
+ res.end(JSON.stringify(response));
887
+ }
888
+ sendMCPError(res, id, code, message, data) {
889
+ const response = {
890
+ jsonrpc: "2.0",
891
+ id,
892
+ error: { code, message, data }
893
+ };
894
+ res.writeHead(200, { 'Content-Type': 'application/json' });
895
+ res.end(JSON.stringify(response));
896
+ }
897
+ /**
898
+ * Close the bridge servers and cleanup all connections
899
+ */
900
+ async close() {
901
+ // Stop session timeout checker
902
+ if (__classPrivateFieldGet(this, _MCPWebBridge_sessionTimeoutInterval, "f")) {
903
+ clearInterval(__classPrivateFieldGet(this, _MCPWebBridge_sessionTimeoutInterval, "f"));
904
+ __classPrivateFieldSet(this, _MCPWebBridge_sessionTimeoutInterval, undefined, "f");
905
+ }
906
+ // Close all WebSocket connections first
907
+ for (const session of __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").values()) {
908
+ if (session.ws.readyState === WebSocket.OPEN) {
909
+ session.ws.close(1000, 'Server shutting down');
910
+ }
911
+ }
912
+ __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").clear();
913
+ // Clear queries and tracking maps
914
+ __classPrivateFieldGet(this, _MCPWebBridge_queries, "f").clear();
915
+ __classPrivateFieldGet(this, _MCPWebBridge_tokenSessionIds, "f").clear();
916
+ __classPrivateFieldGet(this, _MCPWebBridge_tokenQueryCounts, "f").clear();
917
+ // Close servers with timeout to prevent hanging
918
+ const closeWithTimeout = (closePromise, timeoutMs = 1000) => {
919
+ return Promise.race([
920
+ closePromise,
921
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
922
+ ]);
923
+ };
924
+ // Close WebSocket server
925
+ if (__classPrivateFieldGet(this, _MCPWebBridge_wsServer, "f")) {
926
+ await closeWithTimeout(new Promise((resolve) => {
927
+ __classPrivateFieldGet(this, _MCPWebBridge_wsServer, "f")?.close(() => resolve());
928
+ }));
929
+ }
930
+ // Close MCP server
931
+ if (__classPrivateFieldGet(this, _MCPWebBridge_mcpServer, "f")) {
932
+ await closeWithTimeout(new Promise((resolve) => {
933
+ __classPrivateFieldGet(this, _MCPWebBridge_mcpServer, "f")?.close(() => resolve());
934
+ }));
935
+ }
936
+ }
937
+ }
938
+ _MCPWebBridge_sessions = new WeakMap(), _MCPWebBridge_queries = new WeakMap(), _MCPWebBridge_config = new WeakMap(), _MCPWebBridge_wsServer = new WeakMap(), _MCPWebBridge_mcpServer = new WeakMap(), _MCPWebBridge_tokenSessionIds = new WeakMap(), _MCPWebBridge_tokenQueryCounts = new WeakMap(), _MCPWebBridge_sessionTimeoutInterval = new WeakMap(), _MCPWebBridge_instances = new WeakSet(), _MCPWebBridge_decrementQueryCount = function _MCPWebBridge_decrementQueryCount(authToken) {
939
+ const count = __classPrivateFieldGet(this, _MCPWebBridge_tokenQueryCounts, "f").get(authToken) ?? 0;
940
+ if (count <= 1) {
941
+ __classPrivateFieldGet(this, _MCPWebBridge_tokenQueryCounts, "f").delete(authToken);
942
+ }
943
+ else {
944
+ __classPrivateFieldGet(this, _MCPWebBridge_tokenQueryCounts, "f").set(authToken, count - 1);
945
+ }
946
+ }, _MCPWebBridge_decrementQueryCountForQuery = function _MCPWebBridge_decrementQueryCountForQuery(query) {
947
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(query.sessionId);
948
+ if (session) {
949
+ __classPrivateFieldGet(this, _MCPWebBridge_instances, "m", _MCPWebBridge_decrementQueryCount).call(this, session.authToken);
950
+ }
951
+ }, _MCPWebBridge_closeOldestSessionForToken = function _MCPWebBridge_closeOldestSessionForToken(authToken) {
952
+ const sessionIds = __classPrivateFieldGet(this, _MCPWebBridge_tokenSessionIds, "f").get(authToken);
953
+ if (!sessionIds || sessionIds.size === 0)
954
+ return;
955
+ let oldest = null;
956
+ for (const sessionId of sessionIds) {
957
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(sessionId);
958
+ if (session && (!oldest || session.connectedAt < oldest.connectedAt)) {
959
+ oldest = { sessionId, connectedAt: session.connectedAt };
960
+ }
961
+ }
962
+ if (oldest) {
963
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(oldest.sessionId);
964
+ if (session) {
965
+ session.ws.send(JSON.stringify({
966
+ type: 'session-closed',
967
+ reason: 'Session limit exceeded, closing oldest session',
968
+ code: SessionLimitExceededErrorCode
969
+ }));
970
+ session.ws.close(1008, 'Session limit exceeded');
971
+ }
972
+ }
973
+ }, _MCPWebBridge_cleanupSession = function _MCPWebBridge_cleanupSession(sessionId) {
974
+ const session = __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").get(sessionId);
975
+ if (session) {
976
+ // Remove from token tracking
977
+ const sessionIds = __classPrivateFieldGet(this, _MCPWebBridge_tokenSessionIds, "f").get(session.authToken);
978
+ if (sessionIds) {
979
+ sessionIds.delete(sessionId);
980
+ if (sessionIds.size === 0) {
981
+ __classPrivateFieldGet(this, _MCPWebBridge_tokenSessionIds, "f").delete(session.authToken);
982
+ }
983
+ }
984
+ }
985
+ __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f").delete(sessionId);
986
+ }, _MCPWebBridge_startSessionTimeoutChecker = function _MCPWebBridge_startSessionTimeoutChecker() {
987
+ const maxDuration = __classPrivateFieldGet(this, _MCPWebBridge_config, "f").sessionMaxDurationMs;
988
+ if (!maxDuration)
989
+ return;
990
+ __classPrivateFieldSet(this, _MCPWebBridge_sessionTimeoutInterval, setInterval(() => {
991
+ const now = Date.now();
992
+ for (const [_sessionId, session] of __classPrivateFieldGet(this, _MCPWebBridge_sessions, "f")) {
993
+ if (now - session.connectedAt > maxDuration) {
994
+ session.ws.send(JSON.stringify({
995
+ type: 'session-expired',
996
+ code: SessionExpiredErrorCode
997
+ }));
998
+ session.ws.close(1008, 'Session expired');
999
+ // Note: cleanup happens in the 'close' event handler
1000
+ }
1001
+ }
1002
+ }, 60000), "f"); // Check every minute
1003
+ };
1004
+ //# sourceMappingURL=bridge.js.map