@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,443 @@
|
|
|
1
|
+
import { test, expect, afterEach, describe } from 'bun:test';
|
|
2
|
+
import { MCPWebBridgeNode } from './adapters/node.js';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
|
|
5
|
+
// Helper to create a mock WebSocket client
|
|
6
|
+
function createMockClient(port: number, sessionId: string): Promise<WebSocket> {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const ws = new WebSocket(`ws://localhost:${port}?session=${sessionId}`);
|
|
9
|
+
ws.on('open', () => resolve(ws));
|
|
10
|
+
ws.on('error', reject);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Helper to wait for a specific message type
|
|
15
|
+
function waitForMessage<T>(ws: WebSocket, type: string, timeout = 5000): Promise<T> {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const timer = setTimeout(() => reject(new Error(`Timeout waiting for ${type}`)), timeout);
|
|
18
|
+
|
|
19
|
+
const handler = (data: Buffer) => {
|
|
20
|
+
try {
|
|
21
|
+
const message = JSON.parse(data.toString());
|
|
22
|
+
if (message.type === type) {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
ws.removeListener('message', handler);
|
|
25
|
+
resolve(message);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Ignore parse errors
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
ws.on('message', handler);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper to authenticate a client with optional session name
|
|
37
|
+
async function authenticateClient(
|
|
38
|
+
ws: WebSocket,
|
|
39
|
+
authToken: string,
|
|
40
|
+
sessionName?: string
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
ws.send(JSON.stringify({
|
|
43
|
+
type: 'authenticate',
|
|
44
|
+
authToken,
|
|
45
|
+
origin: 'http://localhost:3000',
|
|
46
|
+
pageTitle: 'Test Page',
|
|
47
|
+
sessionName,
|
|
48
|
+
userAgent: 'Test Agent',
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
await waitForMessage(ws, 'authenticated');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Helper to register a tool
|
|
56
|
+
function registerTool(ws: WebSocket, name: string, description: string): void {
|
|
57
|
+
ws.send(JSON.stringify({
|
|
58
|
+
type: 'register-tool',
|
|
59
|
+
tool: { name, description },
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Helper to make MCP HTTP requests
|
|
64
|
+
async function mcpRequest(
|
|
65
|
+
port: number,
|
|
66
|
+
method: string,
|
|
67
|
+
params?: Record<string, unknown>,
|
|
68
|
+
authToken?: string
|
|
69
|
+
): Promise<unknown> {
|
|
70
|
+
const response = await fetch(`http://localhost:${port}`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
jsonrpc: '2.0',
|
|
78
|
+
id: 1,
|
|
79
|
+
method,
|
|
80
|
+
params,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
return response.json();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Session Naming Tests
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
describe('Session Naming', () => {
|
|
91
|
+
let bridge: MCPWebBridgeNode;
|
|
92
|
+
let clients: WebSocket[] = [];
|
|
93
|
+
const port = 4701;
|
|
94
|
+
|
|
95
|
+
afterEach(async () => {
|
|
96
|
+
for (const client of clients) {
|
|
97
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
98
|
+
client.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
clients = [];
|
|
102
|
+
|
|
103
|
+
if (bridge) {
|
|
104
|
+
await bridge.close();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('session with a name authenticates successfully', async () => {
|
|
109
|
+
bridge = new MCPWebBridgeNode({
|
|
110
|
+
name: 'Test Bridge',
|
|
111
|
+
description: 'Test',
|
|
112
|
+
port,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const client = await createMockClient(port, 'session-named-1');
|
|
116
|
+
clients.push(client);
|
|
117
|
+
await authenticateClient(client, 'token-1', 'Game 1');
|
|
118
|
+
|
|
119
|
+
expect(client.readyState).toBe(WebSocket.OPEN);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('sessions without names work as before', async () => {
|
|
123
|
+
bridge = new MCPWebBridgeNode({
|
|
124
|
+
name: 'Test Bridge',
|
|
125
|
+
description: 'Test',
|
|
126
|
+
port,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const client1 = await createMockClient(port, 'session-unnamed-1');
|
|
130
|
+
clients.push(client1);
|
|
131
|
+
await authenticateClient(client1, 'token-1');
|
|
132
|
+
|
|
133
|
+
const client2 = await createMockClient(port, 'session-unnamed-2');
|
|
134
|
+
clients.push(client2);
|
|
135
|
+
await authenticateClient(client2, 'token-1');
|
|
136
|
+
|
|
137
|
+
expect(client1.readyState).toBe(WebSocket.OPEN);
|
|
138
|
+
expect(client2.readyState).toBe(WebSocket.OPEN);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('rejects duplicate session name under same auth token', async () => {
|
|
142
|
+
bridge = new MCPWebBridgeNode({
|
|
143
|
+
name: 'Test Bridge',
|
|
144
|
+
description: 'Test',
|
|
145
|
+
port,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const authToken = 'token-dup-name';
|
|
149
|
+
|
|
150
|
+
// First session with name "Game 1"
|
|
151
|
+
const client1 = await createMockClient(port, 'session-dup-1');
|
|
152
|
+
clients.push(client1);
|
|
153
|
+
await authenticateClient(client1, authToken, 'Game 1');
|
|
154
|
+
|
|
155
|
+
// Second session with same name "Game 1" — should be rejected
|
|
156
|
+
const client2 = await createMockClient(port, 'session-dup-2');
|
|
157
|
+
clients.push(client2);
|
|
158
|
+
|
|
159
|
+
const closePromise = new Promise<{ code: number; reason: string }>((resolve) => {
|
|
160
|
+
client2.on('close', (code, reason) => {
|
|
161
|
+
resolve({ code, reason: reason.toString() });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
client2.send(JSON.stringify({
|
|
166
|
+
type: 'authenticate',
|
|
167
|
+
authToken,
|
|
168
|
+
origin: 'http://localhost:3000',
|
|
169
|
+
sessionName: 'Game 1',
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
// Should receive authentication-failed message
|
|
174
|
+
const failedMessage = await waitForMessage<{ type: string; error: string; code: string }>(
|
|
175
|
+
client2,
|
|
176
|
+
'authentication-failed'
|
|
177
|
+
);
|
|
178
|
+
expect(failedMessage.code).toBe('SessionNameAlreadyInUse');
|
|
179
|
+
expect(failedMessage.error).toContain('Game 1');
|
|
180
|
+
|
|
181
|
+
// Connection should be closed
|
|
182
|
+
const closeResult = await closePromise;
|
|
183
|
+
expect(closeResult.code).toBe(1008);
|
|
184
|
+
expect(closeResult.reason).toBe('Session name already in use');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('allows same session name under different auth tokens', async () => {
|
|
188
|
+
bridge = new MCPWebBridgeNode({
|
|
189
|
+
name: 'Test Bridge',
|
|
190
|
+
description: 'Test',
|
|
191
|
+
port,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// "Game 1" under token-a
|
|
195
|
+
const client1 = await createMockClient(port, 'session-cross-1');
|
|
196
|
+
clients.push(client1);
|
|
197
|
+
await authenticateClient(client1, 'token-a', 'Game 1');
|
|
198
|
+
|
|
199
|
+
// "Game 1" under token-b — should succeed (different token)
|
|
200
|
+
const client2 = await createMockClient(port, 'session-cross-2');
|
|
201
|
+
clients.push(client2);
|
|
202
|
+
await authenticateClient(client2, 'token-b', 'Game 1');
|
|
203
|
+
|
|
204
|
+
expect(client1.readyState).toBe(WebSocket.OPEN);
|
|
205
|
+
expect(client2.readyState).toBe(WebSocket.OPEN);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('allows different session names under same auth token', async () => {
|
|
209
|
+
bridge = new MCPWebBridgeNode({
|
|
210
|
+
name: 'Test Bridge',
|
|
211
|
+
description: 'Test',
|
|
212
|
+
port,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const authToken = 'token-diff-names';
|
|
216
|
+
|
|
217
|
+
const client1 = await createMockClient(port, 'session-diff-1');
|
|
218
|
+
clients.push(client1);
|
|
219
|
+
await authenticateClient(client1, authToken, 'Game 1');
|
|
220
|
+
|
|
221
|
+
const client2 = await createMockClient(port, 'session-diff-2');
|
|
222
|
+
clients.push(client2);
|
|
223
|
+
await authenticateClient(client2, authToken, 'Game 2');
|
|
224
|
+
|
|
225
|
+
expect(client1.readyState).toBe(WebSocket.OPEN);
|
|
226
|
+
expect(client2.readyState).toBe(WebSocket.OPEN);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('allows named and unnamed sessions under same token', async () => {
|
|
230
|
+
bridge = new MCPWebBridgeNode({
|
|
231
|
+
name: 'Test Bridge',
|
|
232
|
+
description: 'Test',
|
|
233
|
+
port,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const authToken = 'token-mixed';
|
|
237
|
+
|
|
238
|
+
const client1 = await createMockClient(port, 'session-mixed-1');
|
|
239
|
+
clients.push(client1);
|
|
240
|
+
await authenticateClient(client1, authToken, 'Game 1');
|
|
241
|
+
|
|
242
|
+
// Unnamed session — should work fine
|
|
243
|
+
const client2 = await createMockClient(port, 'session-mixed-2');
|
|
244
|
+
clients.push(client2);
|
|
245
|
+
await authenticateClient(client2, authToken);
|
|
246
|
+
|
|
247
|
+
expect(client1.readyState).toBe(WebSocket.OPEN);
|
|
248
|
+
expect(client2.readyState).toBe(WebSocket.OPEN);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('reclaiming a name after the original session disconnects', async () => {
|
|
252
|
+
bridge = new MCPWebBridgeNode({
|
|
253
|
+
name: 'Test Bridge',
|
|
254
|
+
description: 'Test',
|
|
255
|
+
port,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const authToken = 'token-reclaim';
|
|
259
|
+
|
|
260
|
+
// Create and authenticate with "Game 1"
|
|
261
|
+
const client1 = await createMockClient(port, 'session-reclaim-1');
|
|
262
|
+
clients.push(client1);
|
|
263
|
+
await authenticateClient(client1, authToken, 'Game 1');
|
|
264
|
+
|
|
265
|
+
// Disconnect client1
|
|
266
|
+
client1.close();
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
268
|
+
|
|
269
|
+
// New client should be able to use "Game 1"
|
|
270
|
+
const client2 = await createMockClient(port, 'session-reclaim-2');
|
|
271
|
+
clients.push(client2);
|
|
272
|
+
await authenticateClient(client2, authToken, 'Game 1');
|
|
273
|
+
|
|
274
|
+
expect(client2.readyState).toBe(WebSocket.OPEN);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// Session Name in list_sessions
|
|
280
|
+
// ============================================================================
|
|
281
|
+
|
|
282
|
+
describe('Session Name in list_sessions', () => {
|
|
283
|
+
let bridge: MCPWebBridgeNode;
|
|
284
|
+
let clients: WebSocket[] = [];
|
|
285
|
+
const port = 4702;
|
|
286
|
+
|
|
287
|
+
afterEach(async () => {
|
|
288
|
+
for (const client of clients) {
|
|
289
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
290
|
+
client.close();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
clients = [];
|
|
294
|
+
|
|
295
|
+
if (bridge) {
|
|
296
|
+
await bridge.close();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('list_sessions includes session_name for named sessions', async () => {
|
|
301
|
+
bridge = new MCPWebBridgeNode({
|
|
302
|
+
name: 'Test Bridge',
|
|
303
|
+
description: 'Test',
|
|
304
|
+
port,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const authToken = 'token-list';
|
|
308
|
+
|
|
309
|
+
const client1 = await createMockClient(port, 'session-list-1');
|
|
310
|
+
clients.push(client1);
|
|
311
|
+
await authenticateClient(client1, authToken, 'Game 1');
|
|
312
|
+
registerTool(client1, 'move_piece', 'Move a piece');
|
|
313
|
+
|
|
314
|
+
// Small delay for tool registration
|
|
315
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
316
|
+
|
|
317
|
+
const client2 = await createMockClient(port, 'session-list-2');
|
|
318
|
+
clients.push(client2);
|
|
319
|
+
await authenticateClient(client2, authToken, 'Game 2');
|
|
320
|
+
registerTool(client2, 'move_piece', 'Move a piece');
|
|
321
|
+
|
|
322
|
+
// Small delay for tool registration
|
|
323
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
324
|
+
|
|
325
|
+
// Initialize MCP session first
|
|
326
|
+
const initResult = await fetch(`http://localhost:${port}`, {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
headers: {
|
|
329
|
+
'Content-Type': 'application/json',
|
|
330
|
+
Authorization: `Bearer ${authToken}`,
|
|
331
|
+
},
|
|
332
|
+
body: JSON.stringify({
|
|
333
|
+
jsonrpc: '2.0',
|
|
334
|
+
id: 0,
|
|
335
|
+
method: 'initialize',
|
|
336
|
+
params: {},
|
|
337
|
+
}),
|
|
338
|
+
});
|
|
339
|
+
const initJson = await initResult.json() as { result: unknown };
|
|
340
|
+
const mcpSessionId = initResult.headers.get('mcp-session-id');
|
|
341
|
+
|
|
342
|
+
// Call list_sessions tool
|
|
343
|
+
const response = await fetch(`http://localhost:${port}`, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: {
|
|
346
|
+
'Content-Type': 'application/json',
|
|
347
|
+
Authorization: `Bearer ${authToken}`,
|
|
348
|
+
...(mcpSessionId ? { 'Mcp-Session-Id': mcpSessionId } : {}),
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify({
|
|
351
|
+
jsonrpc: '2.0',
|
|
352
|
+
id: 1,
|
|
353
|
+
method: 'tools/call',
|
|
354
|
+
params: {
|
|
355
|
+
name: 'list_sessions',
|
|
356
|
+
arguments: {},
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const result = await response.json() as {
|
|
362
|
+
result: {
|
|
363
|
+
content: Array<{ type: string; text: string }>;
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const sessions = JSON.parse(result.result.content[0].text).sessions;
|
|
368
|
+
expect(sessions).toHaveLength(2);
|
|
369
|
+
|
|
370
|
+
const session1 = sessions.find((s: { session_name: string }) => s.session_name === 'Game 1');
|
|
371
|
+
const session2 = sessions.find((s: { session_name: string }) => s.session_name === 'Game 2');
|
|
372
|
+
|
|
373
|
+
expect(session1).toBeDefined();
|
|
374
|
+
expect(session1.session_id).toBe('session-list-1');
|
|
375
|
+
expect(session1.session_name).toBe('Game 1');
|
|
376
|
+
|
|
377
|
+
expect(session2).toBeDefined();
|
|
378
|
+
expect(session2.session_id).toBe('session-list-2');
|
|
379
|
+
expect(session2.session_name).toBe('Game 2');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('list_sessions returns undefined session_name for unnamed sessions', async () => {
|
|
383
|
+
bridge = new MCPWebBridgeNode({
|
|
384
|
+
name: 'Test Bridge',
|
|
385
|
+
description: 'Test',
|
|
386
|
+
port,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const authToken = 'token-list-unnamed';
|
|
390
|
+
|
|
391
|
+
const client1 = await createMockClient(port, 'session-unnamed-list');
|
|
392
|
+
clients.push(client1);
|
|
393
|
+
await authenticateClient(client1, authToken);
|
|
394
|
+
registerTool(client1, 'some_tool', 'Some tool');
|
|
395
|
+
|
|
396
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
397
|
+
|
|
398
|
+
// Initialize MCP session
|
|
399
|
+
const initResult = await fetch(`http://localhost:${port}`, {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
headers: {
|
|
402
|
+
'Content-Type': 'application/json',
|
|
403
|
+
Authorization: `Bearer ${authToken}`,
|
|
404
|
+
},
|
|
405
|
+
body: JSON.stringify({
|
|
406
|
+
jsonrpc: '2.0',
|
|
407
|
+
id: 0,
|
|
408
|
+
method: 'initialize',
|
|
409
|
+
params: {},
|
|
410
|
+
}),
|
|
411
|
+
});
|
|
412
|
+
const mcpSessionId = initResult.headers.get('mcp-session-id');
|
|
413
|
+
|
|
414
|
+
// Call list_sessions
|
|
415
|
+
const response = await fetch(`http://localhost:${port}`, {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
headers: {
|
|
418
|
+
'Content-Type': 'application/json',
|
|
419
|
+
Authorization: `Bearer ${authToken}`,
|
|
420
|
+
...(mcpSessionId ? { 'Mcp-Session-Id': mcpSessionId } : {}),
|
|
421
|
+
},
|
|
422
|
+
body: JSON.stringify({
|
|
423
|
+
jsonrpc: '2.0',
|
|
424
|
+
id: 1,
|
|
425
|
+
method: 'tools/call',
|
|
426
|
+
params: {
|
|
427
|
+
name: 'list_sessions',
|
|
428
|
+
arguments: {},
|
|
429
|
+
},
|
|
430
|
+
}),
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const result = await response.json() as {
|
|
434
|
+
result: {
|
|
435
|
+
content: Array<{ type: string; text: string }>;
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const sessions = JSON.parse(result.result.content[0].text).sessions;
|
|
440
|
+
expect(sessions).toHaveLength(1);
|
|
441
|
+
expect(sessions[0].session_name).toBeUndefined();
|
|
442
|
+
});
|
|
443
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
McpRequestMetaParams,
|
|
3
|
+
QueryAcceptedMessage,
|
|
4
|
+
QueryCancelMessage,
|
|
5
|
+
QueryCompleteBridgeMessage,
|
|
6
|
+
QueryCompleteClientMessage,
|
|
7
|
+
QueryFailureMessage,
|
|
8
|
+
QueryMessage,
|
|
9
|
+
QueryProgressMessage,
|
|
10
|
+
ResourceMetadata,
|
|
11
|
+
ToolMetadata
|
|
12
|
+
} from '@mcp-web/types';
|
|
13
|
+
import type * as WS from 'ws';
|
|
14
|
+
import type { z } from 'zod';
|
|
15
|
+
|
|
16
|
+
export interface AuthenticateMessage {
|
|
17
|
+
type: 'authenticate';
|
|
18
|
+
sessionId: string;
|
|
19
|
+
authToken: string;
|
|
20
|
+
origin: string;
|
|
21
|
+
pageTitle?: string;
|
|
22
|
+
sessionName?: string;
|
|
23
|
+
userAgent?: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AuthenticatedMessage {
|
|
28
|
+
type: 'authenticated';
|
|
29
|
+
mcpPort?: number;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
success: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AuthenticationFailedMessage {
|
|
35
|
+
type: 'authentication-failed';
|
|
36
|
+
error: string;
|
|
37
|
+
code: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RegisterToolMessage {
|
|
41
|
+
type: 'register-tool';
|
|
42
|
+
tool: {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
inputSchema?: z.core.JSONSchema.JSONSchema;
|
|
46
|
+
outputSchema?: z.core.JSONSchema.JSONSchema;
|
|
47
|
+
_meta?: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RegisterResourceMessage {
|
|
52
|
+
type: 'register-resource';
|
|
53
|
+
resource: ResourceMetadata;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResourceReadMessage {
|
|
57
|
+
type: 'resource-read';
|
|
58
|
+
requestId: string;
|
|
59
|
+
uri: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ResourceResponseMessage {
|
|
63
|
+
type: 'resource-response';
|
|
64
|
+
requestId: string;
|
|
65
|
+
content?: string;
|
|
66
|
+
blob?: string;
|
|
67
|
+
mimeType: string;
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ActivityMessage {
|
|
72
|
+
type: 'activity';
|
|
73
|
+
timestamp: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ToolCallMessage {
|
|
77
|
+
type: 'tool-call';
|
|
78
|
+
requestId: string;
|
|
79
|
+
toolName: string;
|
|
80
|
+
toolInput?: Record<string, unknown>;
|
|
81
|
+
queryId?: string; // Added for query context
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ToolRegistrationErrorMessage {
|
|
85
|
+
type: 'tool-registration-error';
|
|
86
|
+
toolName: string;
|
|
87
|
+
error: string;
|
|
88
|
+
message: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ToolResponseMessage {
|
|
92
|
+
type: 'tool-response';
|
|
93
|
+
requestId: string;
|
|
94
|
+
result: unknown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type FrontendMessage =
|
|
98
|
+
| AuthenticateMessage
|
|
99
|
+
| RegisterToolMessage
|
|
100
|
+
| RegisterResourceMessage
|
|
101
|
+
| ActivityMessage
|
|
102
|
+
| ToolResponseMessage
|
|
103
|
+
| ResourceResponseMessage
|
|
104
|
+
| QueryMessage
|
|
105
|
+
| QueryCompleteClientMessage
|
|
106
|
+
| QueryProgressMessage
|
|
107
|
+
| QueryCancelMessage;
|
|
108
|
+
|
|
109
|
+
export type BridgeMessage =
|
|
110
|
+
| AuthenticatedMessage
|
|
111
|
+
| AuthenticationFailedMessage
|
|
112
|
+
| ToolCallMessage
|
|
113
|
+
| ToolRegistrationErrorMessage
|
|
114
|
+
| ResourceReadMessage
|
|
115
|
+
| QueryAcceptedMessage
|
|
116
|
+
| QueryProgressMessage
|
|
117
|
+
| QueryCompleteBridgeMessage
|
|
118
|
+
| QueryFailureMessage
|
|
119
|
+
| QueryCancelMessage;
|
|
120
|
+
|
|
121
|
+
export interface TrackedToolCall {
|
|
122
|
+
tool: string;
|
|
123
|
+
arguments: unknown;
|
|
124
|
+
result: unknown;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type QueryState = 'active' | 'completed' | 'failed' | 'cancelled';
|
|
128
|
+
|
|
129
|
+
export interface QueryTracking {
|
|
130
|
+
sessionId: string;
|
|
131
|
+
responseTool?: string;
|
|
132
|
+
toolCalls: TrackedToolCall[];
|
|
133
|
+
ws: WS.WebSocket;
|
|
134
|
+
state: QueryState;
|
|
135
|
+
tools?: ToolMetadata[];
|
|
136
|
+
restrictTools?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface McpRequest {
|
|
140
|
+
jsonrpc: string;
|
|
141
|
+
id: string | number;
|
|
142
|
+
method: string;
|
|
143
|
+
params?: {
|
|
144
|
+
name?: string;
|
|
145
|
+
arguments?: Record<string, unknown>;
|
|
146
|
+
_meta?: McpRequestMetaParams;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface McpResponse {
|
|
151
|
+
jsonrpc: string;
|
|
152
|
+
id: string | number;
|
|
153
|
+
result?: unknown;
|
|
154
|
+
error?: {
|
|
155
|
+
code: number;
|
|
156
|
+
message: string;
|
|
157
|
+
data?: unknown;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ToolDefinition {
|
|
162
|
+
name: string;
|
|
163
|
+
description: string;
|
|
164
|
+
inputSchema?: z.core.JSONSchema.JSONSchema;
|
|
165
|
+
outputSchema?: z.core.JSONSchema.JSONSchema;
|
|
166
|
+
handler?: string; // Reference to frontend handler
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface SessionData {
|
|
170
|
+
ws: WS.WebSocket;
|
|
171
|
+
authToken: string;
|
|
172
|
+
origin: string;
|
|
173
|
+
pageTitle?: string;
|
|
174
|
+
sessionName?: string;
|
|
175
|
+
userAgent?: string;
|
|
176
|
+
connectedAt: number;
|
|
177
|
+
lastActivity: number;
|
|
178
|
+
tools: Map<string, ToolDefinition>;
|
|
179
|
+
resources: Map<string, ResourceMetadata>;
|
|
180
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "node16",
|
|
5
|
+
"moduleResolution": "node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src"
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*"],
|
|
11
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
12
|
+
}
|