@mcp-fe/mcp-worker 0.0.17 → 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/README.md +184 -349
- package/docs/api.md +418 -0
- package/docs/architecture.md +195 -0
- package/docs/guide.md +454 -0
- package/docs/index.md +109 -0
- package/docs/initialization.md +188 -0
- package/docs/worker-details.md +435 -0
- package/index.js +365 -5
- package/mcp-service-worker.js +505 -204
- package/mcp-shared-worker.js +487 -173
- package/package.json +1 -1
- package/src/index.d.ts +2 -1
- package/src/index.d.ts.map +1 -1
- package/src/lib/built-in-tools.d.ts +9 -0
- package/src/lib/built-in-tools.d.ts.map +1 -0
- package/src/lib/mcp-controller.d.ts +18 -0
- package/src/lib/mcp-controller.d.ts.map +1 -1
- package/src/lib/mcp-server.d.ts +11 -0
- package/src/lib/mcp-server.d.ts.map +1 -1
- package/src/lib/tool-registry.d.ts +28 -0
- package/src/lib/tool-registry.d.ts.map +1 -0
- package/src/lib/worker-client.d.ts +123 -0
- package/src/lib/worker-client.d.ts.map +1 -1
package/docs/api.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Complete API documentation for `@mcp-fe/mcp-worker`.
|
|
4
|
+
|
|
5
|
+
## WorkerClient
|
|
6
|
+
|
|
7
|
+
The main singleton instance for communicating with the MCP worker.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { workerClient } from '@mcp-fe/mcp-worker';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Initialization
|
|
14
|
+
|
|
15
|
+
#### `workerClient.init(options?)`
|
|
16
|
+
|
|
17
|
+
Initializes the worker client with optional configuration.
|
|
18
|
+
|
|
19
|
+
**Parameters:**
|
|
20
|
+
- `options?: WorkerClientInitOptions | ServiceWorkerRegistration`
|
|
21
|
+
|
|
22
|
+
**WorkerClientInitOptions:**
|
|
23
|
+
```typescript
|
|
24
|
+
interface WorkerClientInitOptions {
|
|
25
|
+
sharedWorkerUrl?: string; // Default: '/mcp-shared-worker.js'
|
|
26
|
+
serviceWorkerUrl?: string; // Default: '/mcp-service-worker.js'
|
|
27
|
+
backendWsUrl?: string; // Default: 'ws://localhost:3001'
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Returns:** `Promise<void>`
|
|
32
|
+
|
|
33
|
+
**Examples:**
|
|
34
|
+
```typescript
|
|
35
|
+
// Basic initialization with defaults
|
|
36
|
+
await workerClient.init();
|
|
37
|
+
|
|
38
|
+
// Custom configuration
|
|
39
|
+
await workerClient.init({
|
|
40
|
+
backendWsUrl: 'wss://my-mcp-proxy.com/ws',
|
|
41
|
+
sharedWorkerUrl: '/workers/mcp-shared-worker.js'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Use existing ServiceWorker registration
|
|
45
|
+
const registration = await navigator.serviceWorker.register('/mcp-service-worker.js');
|
|
46
|
+
await workerClient.init(registration);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Event Storage
|
|
50
|
+
|
|
51
|
+
#### `workerClient.post(type, payload?)`
|
|
52
|
+
|
|
53
|
+
Send a fire-and-forget message to the worker.
|
|
54
|
+
|
|
55
|
+
**Parameters:**
|
|
56
|
+
- `type: string` - Message type
|
|
57
|
+
- `payload?: Record<string, unknown>` - Message payload
|
|
58
|
+
|
|
59
|
+
**Returns:** `Promise<void>`
|
|
60
|
+
|
|
61
|
+
**Example:**
|
|
62
|
+
```typescript
|
|
63
|
+
// Store a user event
|
|
64
|
+
await workerClient.post('STORE_EVENT', {
|
|
65
|
+
event: {
|
|
66
|
+
type: 'click',
|
|
67
|
+
element: 'button',
|
|
68
|
+
elementText: 'Submit Form',
|
|
69
|
+
path: '/checkout',
|
|
70
|
+
timestamp: Date.now()
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### `workerClient.request(type, payload?, timeoutMs?)`
|
|
76
|
+
|
|
77
|
+
Send a request expecting a response via MessageChannel.
|
|
78
|
+
|
|
79
|
+
**Parameters:**
|
|
80
|
+
- `type: string` - Request type
|
|
81
|
+
- `payload?: Record<string, unknown>` - Request payload
|
|
82
|
+
- `timeoutMs?: number` - Timeout in milliseconds (default: 5000)
|
|
83
|
+
|
|
84
|
+
**Returns:** `Promise<T>` - Response data
|
|
85
|
+
|
|
86
|
+
**Example:**
|
|
87
|
+
```typescript
|
|
88
|
+
// Get stored events
|
|
89
|
+
const response = await workerClient.request('GET_EVENTS', {
|
|
90
|
+
type: 'click',
|
|
91
|
+
limit: 10
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
console.log('Recent clicks:', response.events);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Authentication
|
|
98
|
+
|
|
99
|
+
#### `workerClient.setAuthToken(token)`
|
|
100
|
+
|
|
101
|
+
Set authentication token for the current session.
|
|
102
|
+
|
|
103
|
+
**Parameters:**
|
|
104
|
+
- `token: string` - Authentication token (e.g., JWT)
|
|
105
|
+
|
|
106
|
+
**Example:**
|
|
107
|
+
```typescript
|
|
108
|
+
// Set token after user login
|
|
109
|
+
workerClient.setAuthToken('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...');
|
|
110
|
+
|
|
111
|
+
// Clear token on logout
|
|
112
|
+
workerClient.setAuthToken('');
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Connection Status
|
|
116
|
+
|
|
117
|
+
#### `workerClient.getConnectionStatus()`
|
|
118
|
+
|
|
119
|
+
Get current connection status to the MCP proxy server.
|
|
120
|
+
|
|
121
|
+
**Returns:** `Promise<boolean>` - Connection status
|
|
122
|
+
|
|
123
|
+
**Example:**
|
|
124
|
+
```typescript
|
|
125
|
+
const isConnected = await workerClient.getConnectionStatus();
|
|
126
|
+
console.log('MCP connection:', isConnected ? 'Connected' : 'Disconnected');
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### `workerClient.onConnectionStatus(callback)`
|
|
130
|
+
|
|
131
|
+
Subscribe to connection status changes.
|
|
132
|
+
|
|
133
|
+
**Parameters:**
|
|
134
|
+
- `callback: (connected: boolean) => void` - Status change callback
|
|
135
|
+
|
|
136
|
+
**Example:**
|
|
137
|
+
```typescript
|
|
138
|
+
const handleConnectionChange = (connected: boolean) => {
|
|
139
|
+
console.log('Connection status:', connected ? 'Connected' : 'Disconnected');
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
workerClient.onConnectionStatus(handleConnectionChange);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `workerClient.offConnectionStatus(callback)`
|
|
146
|
+
|
|
147
|
+
Unsubscribe from connection status changes.
|
|
148
|
+
|
|
149
|
+
**Parameters:**
|
|
150
|
+
- `callback: (connected: boolean) => void` - Previously registered callback
|
|
151
|
+
|
|
152
|
+
**Example:**
|
|
153
|
+
```typescript
|
|
154
|
+
workerClient.offConnectionStatus(handleConnectionChange);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Dynamic Tool Registration
|
|
158
|
+
|
|
159
|
+
#### `workerClient.registerTool(name, description, inputSchema, handler)`
|
|
160
|
+
|
|
161
|
+
Register a custom MCP tool dynamically.
|
|
162
|
+
|
|
163
|
+
**Parameters:**
|
|
164
|
+
- `name: string` - Tool name (use snake_case)
|
|
165
|
+
- `description: string` - Tool description (AI uses this)
|
|
166
|
+
- `inputSchema: Record<string, unknown>` - JSON Schema for input validation
|
|
167
|
+
- `handler: (args: unknown) => Promise<ToolResult>` - Async handler function
|
|
168
|
+
|
|
169
|
+
**Returns:** `Promise<void>`
|
|
170
|
+
|
|
171
|
+
**Example:**
|
|
172
|
+
```typescript
|
|
173
|
+
await workerClient.registerTool(
|
|
174
|
+
'get_user_data',
|
|
175
|
+
'Get current user information',
|
|
176
|
+
{ type: 'object', properties: {} },
|
|
177
|
+
async () => {
|
|
178
|
+
const user = getCurrentUser();
|
|
179
|
+
return {
|
|
180
|
+
content: [{
|
|
181
|
+
type: 'text',
|
|
182
|
+
text: JSON.stringify(user)
|
|
183
|
+
}]
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
See [Dynamic Tool Registration Guide](./guide.md) for complete documentation.
|
|
190
|
+
|
|
191
|
+
#### `workerClient.unregisterTool(name)`
|
|
192
|
+
|
|
193
|
+
Unregister a previously registered tool.
|
|
194
|
+
|
|
195
|
+
**Parameters:**
|
|
196
|
+
- `name: string` - Tool name to unregister
|
|
197
|
+
|
|
198
|
+
**Returns:** `Promise<boolean>` - True if tool was found and removed
|
|
199
|
+
|
|
200
|
+
**Example:**
|
|
201
|
+
```typescript
|
|
202
|
+
const success = await workerClient.unregisterTool('get_user_data');
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### `workerClient.onToolChange(name, callback)`
|
|
206
|
+
|
|
207
|
+
Subscribe to tool registration changes (for reactive updates).
|
|
208
|
+
|
|
209
|
+
**Parameters:**
|
|
210
|
+
- `name: string` - Tool name to watch
|
|
211
|
+
- `callback: (info: ToolInfo | null) => void` - Change callback
|
|
212
|
+
|
|
213
|
+
**Returns:** `() => void` - Unsubscribe function
|
|
214
|
+
|
|
215
|
+
**Example:**
|
|
216
|
+
```typescript
|
|
217
|
+
const unsubscribe = workerClient.onToolChange('my_tool', (info) => {
|
|
218
|
+
console.log('Tool info:', info);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Later: unsubscribe
|
|
222
|
+
unsubscribe();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### `workerClient.getToolInfo(name)`
|
|
226
|
+
|
|
227
|
+
Get information about a registered tool.
|
|
228
|
+
|
|
229
|
+
**Parameters:**
|
|
230
|
+
- `name: string` - Tool name
|
|
231
|
+
|
|
232
|
+
**Returns:** `ToolInfo | null`
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
interface ToolInfo {
|
|
236
|
+
refCount: number;
|
|
237
|
+
isRegistered: boolean;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Example:**
|
|
242
|
+
```typescript
|
|
243
|
+
const info = workerClient.getToolInfo('my_tool');
|
|
244
|
+
if (info) {
|
|
245
|
+
console.log('RefCount:', info.refCount);
|
|
246
|
+
console.log('Registered:', info.isRegistered);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### `workerClient.isToolRegistered(name)`
|
|
251
|
+
|
|
252
|
+
Check if a tool is registered.
|
|
253
|
+
|
|
254
|
+
**Parameters:**
|
|
255
|
+
- `name: string` - Tool name
|
|
256
|
+
|
|
257
|
+
**Returns:** `boolean`
|
|
258
|
+
|
|
259
|
+
**Example:**
|
|
260
|
+
```typescript
|
|
261
|
+
if (workerClient.isToolRegistered('my_tool')) {
|
|
262
|
+
console.log('Tool is registered');
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### `workerClient.getRegisteredTools()`
|
|
267
|
+
|
|
268
|
+
Get all registered tool names.
|
|
269
|
+
|
|
270
|
+
**Returns:** `string[]`
|
|
271
|
+
|
|
272
|
+
**Example:**
|
|
273
|
+
```typescript
|
|
274
|
+
const tools = workerClient.getRegisteredTools();
|
|
275
|
+
console.log('Registered tools:', tools);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Worker State
|
|
279
|
+
|
|
280
|
+
#### `workerClient.initialized`
|
|
281
|
+
|
|
282
|
+
Check if the worker is initialized.
|
|
283
|
+
|
|
284
|
+
**Type:** `boolean` (getter)
|
|
285
|
+
|
|
286
|
+
**Example:**
|
|
287
|
+
```typescript
|
|
288
|
+
if (workerClient.initialized) {
|
|
289
|
+
console.log('Worker is ready');
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
#### `workerClient.waitForInit()`
|
|
294
|
+
|
|
295
|
+
Wait for worker initialization to complete.
|
|
296
|
+
|
|
297
|
+
**Returns:** `Promise<void>`
|
|
298
|
+
|
|
299
|
+
**Example:**
|
|
300
|
+
```typescript
|
|
301
|
+
await workerClient.waitForInit();
|
|
302
|
+
console.log('Worker is now initialized');
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Type Definitions
|
|
306
|
+
|
|
307
|
+
### ToolResult
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
interface ToolResult {
|
|
311
|
+
content: Array<{
|
|
312
|
+
type: 'text' | 'image' | 'resource';
|
|
313
|
+
text?: string;
|
|
314
|
+
data?: string;
|
|
315
|
+
mimeType?: string;
|
|
316
|
+
}>;
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### ToolHandler
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
type ToolHandler = (args: unknown) => Promise<ToolResult>;
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### UserEvent
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
interface UserEvent {
|
|
330
|
+
id: string;
|
|
331
|
+
type: 'navigation' | 'click' | 'input' | 'custom';
|
|
332
|
+
timestamp: number;
|
|
333
|
+
path?: string;
|
|
334
|
+
from?: string; // navigation: previous route
|
|
335
|
+
to?: string; // navigation: current route
|
|
336
|
+
element?: string; // interaction: element tag
|
|
337
|
+
elementId?: string; // interaction: element ID
|
|
338
|
+
elementClass?: string; // interaction: element classes
|
|
339
|
+
elementText?: string; // interaction: element text content
|
|
340
|
+
metadata?: Record<string, unknown>;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Advanced Usage
|
|
345
|
+
|
|
346
|
+
### Custom Event Storage
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// Store custom business events
|
|
350
|
+
await workerClient.post('STORE_EVENT', {
|
|
351
|
+
event: {
|
|
352
|
+
type: 'custom',
|
|
353
|
+
timestamp: Date.now(),
|
|
354
|
+
metadata: {
|
|
355
|
+
eventName: 'purchase_completed',
|
|
356
|
+
orderId: '12345',
|
|
357
|
+
amount: 99.99,
|
|
358
|
+
currency: 'USD'
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Querying Specific Events
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// Get navigation events from the last hour
|
|
368
|
+
const response = await workerClient.request('GET_EVENTS', {
|
|
369
|
+
type: 'navigation',
|
|
370
|
+
startTime: Date.now() - (60 * 60 * 1000),
|
|
371
|
+
limit: 50
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Get clicks on specific elements
|
|
375
|
+
const clicks = await workerClient.request('GET_EVENTS', {
|
|
376
|
+
type: 'click',
|
|
377
|
+
path: '/checkout',
|
|
378
|
+
limit: 20
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Error Handling
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
try {
|
|
386
|
+
await workerClient.init();
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (error.message.includes('SharedWorker')) {
|
|
389
|
+
console.log('SharedWorker not supported, using ServiceWorker fallback');
|
|
390
|
+
} else {
|
|
391
|
+
console.error('Worker initialization failed:', error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Handle request timeouts
|
|
396
|
+
try {
|
|
397
|
+
const data = await workerClient.request('GET_EVENTS', {}, 2000); // 2s timeout
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error.message.includes('timeout')) {
|
|
400
|
+
console.log('Request timed out');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## MCP Tools Exposed
|
|
406
|
+
|
|
407
|
+
The worker exposes these MCP tools to AI agents:
|
|
408
|
+
|
|
409
|
+
- `get_user_events` - Query stored user interaction events
|
|
410
|
+
- `get_connection_status` - Check WebSocket connection status
|
|
411
|
+
- `get_session_info` - Get current session information
|
|
412
|
+
- Custom tools registered via `registerTool()`
|
|
413
|
+
|
|
414
|
+
## See Also
|
|
415
|
+
|
|
416
|
+
- [Guide](./guide.md) - Dynamic tool registration guide
|
|
417
|
+
- [Worker Implementation Details](./worker-details.md) - Technical details
|
|
418
|
+
- [Architecture](./architecture.md) - How it works
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Proxy Architecture
|
|
2
|
+
|
|
3
|
+
How dynamic tool registration works with handlers running in the main thread.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The library uses a **proxy pattern** where:
|
|
8
|
+
- ✅ Handlers run in **main thread** (browser context)
|
|
9
|
+
- ✅ Worker acts as **proxy** between MCP and handlers
|
|
10
|
+
- ✅ **No serialization** of function code
|
|
11
|
+
- ✅ **Full access** to browser APIs, React, imports, etc.
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌─────────────────────┐
|
|
17
|
+
│ MCP Client │
|
|
18
|
+
│ (Claude, etc.) │
|
|
19
|
+
└──────────┬──────────┘
|
|
20
|
+
│ MCP Protocol
|
|
21
|
+
▼
|
|
22
|
+
┌─────────────────────┐
|
|
23
|
+
│ Shared/Service │
|
|
24
|
+
│ Worker (MCP Server)│
|
|
25
|
+
│ │
|
|
26
|
+
│ Tool Registry │
|
|
27
|
+
│ ├─ Proxy Handler │ ← metadata + proxy
|
|
28
|
+
│ └─ postMessage │
|
|
29
|
+
└──────────┬──────────┘
|
|
30
|
+
│ postMessage({ type: 'CALL_TOOL', args, callId })
|
|
31
|
+
▼
|
|
32
|
+
┌─────────────────────┐
|
|
33
|
+
│ Main Thread │
|
|
34
|
+
│ (Browser Context) │
|
|
35
|
+
│ │
|
|
36
|
+
│ WorkerClient │
|
|
37
|
+
│ ├─ toolHandlers │ ← actual handler functions
|
|
38
|
+
│ └─ execute │ ← with full API access
|
|
39
|
+
└──────────┬──────────┘
|
|
40
|
+
│ postMessage({ type: 'TOOL_CALL_RESULT', result })
|
|
41
|
+
▼
|
|
42
|
+
┌─────────────────────┐
|
|
43
|
+
│ Worker │
|
|
44
|
+
│ ├─ resolve Promise │
|
|
45
|
+
│ └─ return to MCP │
|
|
46
|
+
└─────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Benefits
|
|
50
|
+
|
|
51
|
+
### No Serialization Issues
|
|
52
|
+
- Handlers are normal functions in main thread
|
|
53
|
+
- No `.toString()` → `new Function()` conversion
|
|
54
|
+
- All closures and imports preserved
|
|
55
|
+
|
|
56
|
+
### Full Browser API Access
|
|
57
|
+
```typescript
|
|
58
|
+
await client.registerTool('get_page_info', '...', {}, async () => {
|
|
59
|
+
// ✅ DOM access
|
|
60
|
+
const title = document.title;
|
|
61
|
+
|
|
62
|
+
// ✅ localStorage
|
|
63
|
+
const theme = localStorage.getItem('theme');
|
|
64
|
+
|
|
65
|
+
// ✅ React hooks/context (if handler in component)
|
|
66
|
+
const user = useUser();
|
|
67
|
+
|
|
68
|
+
return { content: [{ type: 'text', text: JSON.stringify({ title, theme, user }) }] };
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Use Any Imports
|
|
73
|
+
```typescript
|
|
74
|
+
import { z } from 'zod';
|
|
75
|
+
import { myApi } from './api';
|
|
76
|
+
|
|
77
|
+
await client.registerTool('validate', '...', schema, async (args: any) => {
|
|
78
|
+
// ✅ Use any imports!
|
|
79
|
+
const validated = z.object({ ... }).parse(args);
|
|
80
|
+
const result = await myApi.callSomething(validated);
|
|
81
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Easy Testing
|
|
86
|
+
```typescript
|
|
87
|
+
// Handler is a normal async function
|
|
88
|
+
const myHandler = async (args: any) => {
|
|
89
|
+
// ... logic ...
|
|
90
|
+
return { content: [{ type: 'text', text: '...' }] };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Test directly
|
|
94
|
+
test('myHandler works', async () => {
|
|
95
|
+
const result = await myHandler({ test: 'data' });
|
|
96
|
+
expect(result.content[0].text).toContain('data');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Then register
|
|
100
|
+
await client.registerTool('my_tool', '...', schema, myHandler);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Implementation
|
|
104
|
+
|
|
105
|
+
### WorkerClient (Main Thread)
|
|
106
|
+
|
|
107
|
+
Stores handlers locally and executes them when called:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
private toolHandlers = new Map<string, HandlerFunction>();
|
|
111
|
+
|
|
112
|
+
public async registerTool(name, description, schema, handler) {
|
|
113
|
+
// Store handler in main thread
|
|
114
|
+
this.toolHandlers.set(name, handler);
|
|
115
|
+
|
|
116
|
+
// Tell worker to create proxy
|
|
117
|
+
await this.request('REGISTER_TOOL', {
|
|
118
|
+
name, description, inputSchema: schema,
|
|
119
|
+
handlerType: 'proxy' // ← important!
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async handleToolCall(toolName: string, args: unknown, callId: string) {
|
|
124
|
+
try {
|
|
125
|
+
const handler = this.toolHandlers.get(toolName);
|
|
126
|
+
const result = await handler(args); // ← runs in main thread!
|
|
127
|
+
|
|
128
|
+
this.sendToolCallResult(callId, { success: true, result });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.sendToolCallResult(callId, { success: false, error: error.message });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### MCPController (Worker)
|
|
136
|
+
|
|
137
|
+
Creates proxy handler and forwards calls:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
public async handleRegisterTool(toolData: Record<string, unknown>) {
|
|
141
|
+
const { name, description, inputSchema, handlerType } = toolData;
|
|
142
|
+
|
|
143
|
+
if (handlerType === 'proxy') {
|
|
144
|
+
// Create proxy handler that forwards to main thread
|
|
145
|
+
mcpServer.registerTool(name, description, inputSchema,
|
|
146
|
+
async (args: unknown) => {
|
|
147
|
+
// Forward to main thread
|
|
148
|
+
const callId = generateId();
|
|
149
|
+
this.broadcast({ type: 'CALL_TOOL', toolName: name, args, callId });
|
|
150
|
+
|
|
151
|
+
// Wait for result
|
|
152
|
+
return await this.waitForToolCallResult(callId);
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Message Flow
|
|
160
|
+
|
|
161
|
+
1. **MCP Client** calls tool via MCP protocol
|
|
162
|
+
2. **Worker** receives MCP tool call
|
|
163
|
+
3. **Worker** sends `CALL_TOOL` message to main thread
|
|
164
|
+
4. **Main Thread** executes handler function
|
|
165
|
+
5. **Main Thread** sends `TOOL_CALL_RESULT` back
|
|
166
|
+
6. **Worker** resolves promise and returns to MCP
|
|
167
|
+
7. **MCP Client** receives result
|
|
168
|
+
|
|
169
|
+
## Why This Works
|
|
170
|
+
|
|
171
|
+
- **Worker**: Runs 24/7, maintains MCP connection
|
|
172
|
+
- **Main Thread**: Has full browser API access
|
|
173
|
+
- **Messages**: Bridge between the two contexts
|
|
174
|
+
- **Handlers**: Execute where they have access to everything
|
|
175
|
+
|
|
176
|
+
## Trade-offs
|
|
177
|
+
|
|
178
|
+
### Advantages
|
|
179
|
+
- ✅ Full browser API access
|
|
180
|
+
- ✅ No serialization issues
|
|
181
|
+
- ✅ Easy to implement
|
|
182
|
+
- ✅ Easy to test
|
|
183
|
+
- ✅ Works with any library
|
|
184
|
+
|
|
185
|
+
### Considerations
|
|
186
|
+
- Message passing overhead (minimal, ~1-2ms)
|
|
187
|
+
- Handler must be async (already required by MCP)
|
|
188
|
+
|
|
189
|
+
## Usage
|
|
190
|
+
|
|
191
|
+
See [Guide](./guide.md) for complete usage guide and examples.
|
|
192
|
+
|
|
193
|
+
See [examples/](../examples/) for runnable code examples.
|
|
194
|
+
|
|
195
|
+
```
|