@mcp-fe/mcp-worker 0.1.11 → 0.2.2
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 +27 -29
- package/docs/index.md +1 -0
- package/docs/multi-tab.md +637 -637
- package/docs/native-webmcp.md +232 -0
- package/docs/project-structure.md +172 -172
- package/docs/tab-manager.md +150 -150
- package/index.js +356 -59
- package/mcp-service-worker.js +43 -2
- package/mcp-shared-worker.js +43 -2
- package/package.json +1 -1
- package/src/client/index.d.ts +2 -0
- package/src/client/index.d.ts.map +1 -1
- package/src/client/web-mcp-adapter.d.ts +97 -0
- package/src/client/web-mcp-adapter.d.ts.map +1 -0
- package/src/client/web-mcp-types.d.ts +122 -0
- package/src/client/web-mcp-types.d.ts.map +1 -0
- package/src/client/worker-client.d.ts +31 -0
- package/src/client/worker-client.d.ts.map +1 -1
- package/src/worker/mcp-controller.d.ts +5 -0
- package/src/worker/mcp-controller.d.ts.map +1 -1
package/docs/multi-tab.md
CHANGED
|
@@ -1,637 +1,637 @@
|
|
|
1
|
-
# Multi-Tab Support
|
|
2
|
-
|
|
3
|
-
Comprehensive guide to multi-tab architecture and usage patterns.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
The MCP Worker library provides seamless multi-tab support with intelligent routing. Each browser tab can register tools independently, and the worker automatically routes tool calls to the appropriate tab.
|
|
8
|
-
|
|
9
|
-
## Architecture
|
|
10
|
-
|
|
11
|
-
### Tab Lifecycle
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
1. Tab Opens
|
|
15
|
-
└─> Generate UUID (crypto.randomUUID())
|
|
16
|
-
└─> Store in sessionStorage
|
|
17
|
-
└─> Register with worker (REGISTER_TAB)
|
|
18
|
-
└─> Mark as active (SET_ACTIVE_TAB)
|
|
19
|
-
|
|
20
|
-
2. Tab Focus
|
|
21
|
-
└─> Update active tab (SET_ACTIVE_TAB)
|
|
22
|
-
|
|
23
|
-
3. Tab Closes
|
|
24
|
-
└─> Tools remain available if other tabs have them
|
|
25
|
-
└─> Reference counting prevents premature unregistration
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### Tab Registry
|
|
29
|
-
|
|
30
|
-
The worker maintains a registry of all active tabs:
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
{
|
|
34
|
-
tabId: string; // UUID from crypto.randomUUID()
|
|
35
|
-
url: string; // window.location.href
|
|
36
|
-
title: string; // document.title
|
|
37
|
-
lastSeen: number; // timestamp of last activity
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Tool Registry Per Tab
|
|
42
|
-
|
|
43
|
-
Tools are tracked per-tab using Sets:
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
Map<ToolName, Set<TabId>>
|
|
47
|
-
|
|
48
|
-
Example:
|
|
49
|
-
{
|
|
50
|
-
"get_page_info": Set(["tab-abc", "tab-def"]),
|
|
51
|
-
"get_user_data": Set(["tab-abc"]),
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Tool Lifecycle & Navigation
|
|
56
|
-
|
|
57
|
-
### Automatic Cleanup
|
|
58
|
-
|
|
59
|
-
Tools are **automatically unregistered** when:
|
|
60
|
-
|
|
61
|
-
1. **Page navigation** (beforeunload event)
|
|
62
|
-
2. **Page close** (pagehide event)
|
|
63
|
-
3. **Page refresh** (F5)
|
|
64
|
-
4. **Component unmount** (via React hooks like useMCPTool)
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
// WorkerClient automatically handles cleanup
|
|
68
|
-
window.addEventListener('beforeunload', () => {
|
|
69
|
-
// Unregister all tools from this tab
|
|
70
|
-
toolRegistry.forEach(tool => {
|
|
71
|
-
unregisterTool(tool.name, this.tabId);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### How It Works
|
|
77
|
-
|
|
78
|
-
**Registration:**
|
|
79
|
-
```typescript
|
|
80
|
-
// Tab A: Dashboard page
|
|
81
|
-
await workerClient.registerTool('get_dashboard_data', ...);
|
|
82
|
-
// → Worker: toolHandlersByTab.set('get_dashboard_data', Set([tabA]))
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**Navigation (automatic cleanup):**
|
|
86
|
-
```typescript
|
|
87
|
-
// User navigates Tab A away from Dashboard
|
|
88
|
-
// → beforeunload event fires
|
|
89
|
-
// → WorkerClient.cleanupAllTools() called
|
|
90
|
-
// → Sends: UNREGISTER_TOOL({ name: 'get_dashboard_data', tabId: tabA })
|
|
91
|
-
// → Worker: toolHandlersByTab.get('get_dashboard_data').delete(tabA)
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**Routing after navigation:**
|
|
95
|
-
```typescript
|
|
96
|
-
// Tab B still has 'get_dashboard_data' registered
|
|
97
|
-
// AI calls: get_dashboard_data()
|
|
98
|
-
// → Worker checks: tabA (active) has tool? NO
|
|
99
|
-
// → Worker checks: other tabs have tool? YES (tabB)
|
|
100
|
-
// → Routes to Tab B automatically ✓
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### SPA Navigation Pattern
|
|
104
|
-
|
|
105
|
-
In Single Page Applications, components mount/unmount frequently:
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
// Dashboard component
|
|
109
|
-
function DashboardPage() {
|
|
110
|
-
// Tool registers on mount
|
|
111
|
-
useMCPTool({
|
|
112
|
-
name: 'get_dashboard_data',
|
|
113
|
-
// ...
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Tool unregisters on unmount (automatic!)
|
|
117
|
-
// - When user navigates to /settings
|
|
118
|
-
// - When component is destroyed
|
|
119
|
-
// - When route changes
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
**Multi-tab behavior:**
|
|
124
|
-
- Tab A: /dashboard → registers tool
|
|
125
|
-
- Tab B: /dashboard → registers tool (refCount = 2)
|
|
126
|
-
- Tab A navigates to /settings → unregisters tool (refCount = 1)
|
|
127
|
-
- Tool still available via Tab B ✓
|
|
128
|
-
|
|
129
|
-
### Edge Cases
|
|
130
|
-
|
|
131
|
-
**All tabs lose tool:**
|
|
132
|
-
```typescript
|
|
133
|
-
// Both tabs on /dashboard
|
|
134
|
-
Tab A: registers tool
|
|
135
|
-
Tab B: registers tool
|
|
136
|
-
|
|
137
|
-
// Both tabs navigate away
|
|
138
|
-
Tab A → /settings (unregisters)
|
|
139
|
-
Tab B → /profile (unregisters)
|
|
140
|
-
|
|
141
|
-
// Tool completely unregistered from MCP
|
|
142
|
-
get_dashboard_data()
|
|
143
|
-
// → Error: "Tool not available"
|
|
144
|
-
|
|
145
|
-
// User returns to /dashboard in Tab A
|
|
146
|
-
// → Component mounts → registers tool again
|
|
147
|
-
// → Tool available again ✓
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
**Refresh handling:**
|
|
151
|
-
```typescript
|
|
152
|
-
// Tab has tools registered
|
|
153
|
-
// User presses F5
|
|
154
|
-
|
|
155
|
-
// → beforeunload fires
|
|
156
|
-
// → All tools unregistered
|
|
157
|
-
// → Page reloads
|
|
158
|
-
// → Components mount
|
|
159
|
-
// → Tools registered again
|
|
160
|
-
// → Tab ID preserved (sessionStorage)
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### 1. Smart Routing (Default)
|
|
164
|
-
|
|
165
|
-
Intelligent routing that prioritizes user intent and availability:
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// Priority order:
|
|
169
|
-
1. Explicit tabId parameter (always respected)
|
|
170
|
-
2. Only one tab has tool → route to it (regardless of focus)
|
|
171
|
-
3. Active/focused tab has tool → use it
|
|
172
|
-
4. Active tab doesn't have tool → use first available
|
|
173
|
-
5. No active tab → use first available
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**Example Scenarios:**
|
|
177
|
-
|
|
178
|
-
**Scenario 1: Single tab with tool (most intuitive)**
|
|
179
|
-
```typescript
|
|
180
|
-
// Tab A (active): Has toolX
|
|
181
|
-
// Tab B (inactive): Doesn't have toolX
|
|
182
|
-
|
|
183
|
-
toolX()
|
|
184
|
-
→ Routes to Tab A ✓ (active tab has it)
|
|
185
|
-
|
|
186
|
-
// Now focus Tab B
|
|
187
|
-
// Tab A (inactive): Has toolX
|
|
188
|
-
// Tab B (active): Doesn't have toolX
|
|
189
|
-
|
|
190
|
-
toolX()
|
|
191
|
-
→ Routes to Tab A ✓ (only tab with tool, even though not active!)
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
**Scenario 2: Multiple tabs with tool**
|
|
195
|
-
```typescript
|
|
196
|
-
// Tab A (active): Has toolX
|
|
197
|
-
// Tab B (inactive): Has toolX
|
|
198
|
-
|
|
199
|
-
toolX()
|
|
200
|
-
→ Routes to Tab A ✓ (active tab preferred when multiple available)
|
|
201
|
-
|
|
202
|
-
// Now focus Tab B
|
|
203
|
-
// Tab A (inactive): Has toolX
|
|
204
|
-
// Tab B (active): Has toolX
|
|
205
|
-
|
|
206
|
-
toolX()
|
|
207
|
-
→ Routes to Tab B ✓ (new active tab)
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**Scenario 3: Explicit targeting**
|
|
211
|
-
```typescript
|
|
212
|
-
// Tab A (active): Has toolX
|
|
213
|
-
// Tab B (inactive): Has toolX
|
|
214
|
-
|
|
215
|
-
toolX({ tabId: "tab-b-id" })
|
|
216
|
-
→ Routes to Tab B ✓ (explicit parameter always wins)
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
**Scenario 4: Active tab loses tool (navigation)**
|
|
220
|
-
```typescript
|
|
221
|
-
// Initial state
|
|
222
|
-
// Tab A (active): Has toolX
|
|
223
|
-
// Tab B (inactive): Has toolX
|
|
224
|
-
|
|
225
|
-
toolX()
|
|
226
|
-
→ Routes to Tab A ✓ (active tab)
|
|
227
|
-
|
|
228
|
-
// User navigates Tab A to different page
|
|
229
|
-
// Tab A (active): No longer has toolX (page changed)
|
|
230
|
-
// Tab B (inactive): Has toolX
|
|
231
|
-
|
|
232
|
-
toolX()
|
|
233
|
-
→ Routes to Tab B ✓ (active tab doesn't have tool, auto-fallback!)
|
|
234
|
-
// No error, seamless transition
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Visual Flow:**
|
|
238
|
-
```
|
|
239
|
-
Before Navigation:
|
|
240
|
-
┌─────────────┐ ┌─────────────┐
|
|
241
|
-
│ Tab A │ Active │ Tab B │
|
|
242
|
-
│ /dashboard │ ✓ │ /dashboard │
|
|
243
|
-
│ has toolX │ │ has toolX │
|
|
244
|
-
└─────────────┘ └─────────────┘
|
|
245
|
-
↑
|
|
246
|
-
toolX() routes here
|
|
247
|
-
|
|
248
|
-
After Navigation (Tab A → /settings):
|
|
249
|
-
┌─────────────┐ ┌─────────────┐
|
|
250
|
-
│ Tab A │ Active │ Tab B │
|
|
251
|
-
│ /settings │ ✓ │ /dashboard │
|
|
252
|
-
│ NO toolX │ │ has toolX │
|
|
253
|
-
└─────────────┘ └─────────────┘
|
|
254
|
-
↑
|
|
255
|
-
toolX() auto-routes here!
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
This ensures tools continue to work even when the user navigates away from pages that provided specific tools.
|
|
259
|
-
|
|
260
|
-
### 2. Built-in Discovery Tool
|
|
261
|
-
|
|
262
|
-
`list_browser_tabs` provides tab discovery:
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// Always returns current tab state
|
|
266
|
-
const tabs = await list_browser_tabs();
|
|
267
|
-
|
|
268
|
-
[
|
|
269
|
-
{
|
|
270
|
-
tabId: "550e8400-e29b-41d4-a716-446655440000",
|
|
271
|
-
url: "https://app.example.com/dashboard",
|
|
272
|
-
title: "Dashboard - My App",
|
|
273
|
-
isActive: true,
|
|
274
|
-
lastSeen: "2026-02-04T10:30:00.000Z"
|
|
275
|
-
},
|
|
276
|
-
{
|
|
277
|
-
tabId: "6fa459ea-ee8a-3ca4-894e-db77e160355e",
|
|
278
|
-
url: "https://app.example.com/settings",
|
|
279
|
-
title: "Settings - My App",
|
|
280
|
-
isActive: false,
|
|
281
|
-
lastSeen: "2026-02-04T10:29:45.000Z"
|
|
282
|
-
}
|
|
283
|
-
]
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## Tool Schema Enhancement
|
|
287
|
-
|
|
288
|
-
All registered tools automatically receive an optional `tabId` parameter:
|
|
289
|
-
|
|
290
|
-
**Original Schema:**
|
|
291
|
-
```json
|
|
292
|
-
{
|
|
293
|
-
"type": "object",
|
|
294
|
-
"properties": {
|
|
295
|
-
"username": { "type": "string" }
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
**Enhanced Schema:**
|
|
301
|
-
```json
|
|
302
|
-
{
|
|
303
|
-
"type": "object",
|
|
304
|
-
"properties": {
|
|
305
|
-
"username": { "type": "string" },
|
|
306
|
-
"tabId": {
|
|
307
|
-
"type": "string",
|
|
308
|
-
"description": "Optional: Target specific tab by ID. If not provided, uses the currently focused tab. Use list_browser_tabs to discover available tabs."
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
The `tabId` parameter is automatically added by the library - you don't need to include it in your schema.
|
|
315
|
-
|
|
316
|
-
## Use Cases
|
|
317
|
-
|
|
318
|
-
### 1. Debugging Specific Tab
|
|
319
|
-
|
|
320
|
-
AI can debug a specific tab while user works in another:
|
|
321
|
-
|
|
322
|
-
```
|
|
323
|
-
User: "Check the state of the Settings tab"
|
|
324
|
-
|
|
325
|
-
AI:
|
|
326
|
-
1. list_browser_tabs()
|
|
327
|
-
→ Finds Settings tab ID: "6fa459ea..."
|
|
328
|
-
|
|
329
|
-
2. get_react_state({ tabId: "6fa459ea..." })
|
|
330
|
-
→ Gets state from Settings tab
|
|
331
|
-
|
|
332
|
-
3. analyze_component({ tabId: "6fa459ea...", component: "UserForm" })
|
|
333
|
-
→ Analyzes specific component
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
### 2. Cross-Tab Comparison
|
|
337
|
-
|
|
338
|
-
Compare state/data across multiple tabs:
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
const tabs = await list_browser_tabs();
|
|
342
|
-
|
|
343
|
-
const states = await Promise.all(
|
|
344
|
-
tabs.map(tab =>
|
|
345
|
-
get_react_state({ tabId: tab.tabId })
|
|
346
|
-
)
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
// Compare states across tabs
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### 3. Focus-Driven Interaction
|
|
353
|
-
|
|
354
|
-
Natural interaction with focused tab:
|
|
355
|
-
|
|
356
|
-
```
|
|
357
|
-
User: "What's on this page?"
|
|
358
|
-
AI: get_page_info() // No tabId needed
|
|
359
|
-
→ Automatically uses focused tab
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### 4. Multi-Screen Workflows
|
|
363
|
-
|
|
364
|
-
User has multiple monitors with different tabs:
|
|
365
|
-
|
|
366
|
-
```
|
|
367
|
-
Monitor 1: Dashboard (focused)
|
|
368
|
-
Monitor 2: Analytics
|
|
369
|
-
Monitor 3: Settings
|
|
370
|
-
|
|
371
|
-
// AI can work with any tab
|
|
372
|
-
get_metrics({ tabId: "analytics-tab" }) // Monitor 2
|
|
373
|
-
update_settings({ tabId: "settings-tab" }) // Monitor 3
|
|
374
|
-
show_alert() // Monitor 1 (focused)
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
## Reference Counting
|
|
378
|
-
|
|
379
|
-
Tools use reference counting to handle multiple registrations:
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
// Tab 1 registers tool
|
|
383
|
-
workerClient.registerTool('get_data', ...)
|
|
384
|
-
// → Registered with MCP, refCount = 1
|
|
385
|
-
|
|
386
|
-
// Tab 2 registers same tool
|
|
387
|
-
workerClient.registerTool('get_data', ...)
|
|
388
|
-
// → NOT re-registered with MCP, refCount = 2
|
|
389
|
-
|
|
390
|
-
// Tab 1 closes/unregisters
|
|
391
|
-
workerClient.unregisterTool('get_data')
|
|
392
|
-
// → NOT unregistered from MCP, refCount = 1
|
|
393
|
-
|
|
394
|
-
// Tab 2 closes/unregisters
|
|
395
|
-
workerClient.unregisterTool('get_data')
|
|
396
|
-
// → Unregistered from MCP, refCount = 0
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
## Tab Persistence
|
|
400
|
-
|
|
401
|
-
### SessionStorage Persistence
|
|
402
|
-
|
|
403
|
-
Tab IDs are stored in `sessionStorage`, which means:
|
|
404
|
-
|
|
405
|
-
- ✅ **Page refresh (F5)**: Tab keeps same ID
|
|
406
|
-
- ✅ **Navigation**: Tab keeps same ID
|
|
407
|
-
- ❌ **Duplicate tab**: New tab gets new ID
|
|
408
|
-
- ❌ **New window**: New window gets new ID
|
|
409
|
-
- ❌ **Private/Incognito**: Each session independent
|
|
410
|
-
|
|
411
|
-
### Tab ID Format
|
|
412
|
-
|
|
413
|
-
```typescript
|
|
414
|
-
// UUID v4 from crypto.randomUUID()
|
|
415
|
-
"550e8400-e29b-41d4-a716-446655440000"
|
|
416
|
-
|
|
417
|
-
// Fallback if crypto unavailable
|
|
418
|
-
"fallback_1738668000000_xyz123"
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
## Error Handling
|
|
422
|
-
|
|
423
|
-
### Tab Not Found
|
|
424
|
-
|
|
425
|
-
```typescript
|
|
426
|
-
get_page_info({ tabId: "invalid-id" })
|
|
427
|
-
|
|
428
|
-
// Error response:
|
|
429
|
-
{
|
|
430
|
-
error: "Tool 'get_page_info' not available in tab 'invalid-id'. Available tabs: tab-1, tab-2"
|
|
431
|
-
}
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
### No Active Tab
|
|
435
|
-
|
|
436
|
-
```typescript
|
|
437
|
-
// All tabs minimized/backgrounded
|
|
438
|
-
get_page_info() // No tabId, no active tab
|
|
439
|
-
|
|
440
|
-
// Behavior: Uses first available tab + warning log
|
|
441
|
-
// Better than error - tool still works
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### Tool Not Registered
|
|
445
|
-
|
|
446
|
-
```typescript
|
|
447
|
-
// Tab 1: Registers tool A
|
|
448
|
-
// Tab 2: Registers tool B
|
|
449
|
-
|
|
450
|
-
get_tool_b({ tabId: "tab-1" })
|
|
451
|
-
|
|
452
|
-
// Error:
|
|
453
|
-
{
|
|
454
|
-
error: "Tool 'get_tool_b' not available in tab 'tab-1'. Available tabs: tab-2"
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
## API Reference
|
|
459
|
-
|
|
460
|
-
### WorkerClient Methods
|
|
461
|
-
|
|
462
|
-
#### `getTabId(): string`
|
|
463
|
-
|
|
464
|
-
Get the unique ID of the current tab.
|
|
465
|
-
|
|
466
|
-
```typescript
|
|
467
|
-
const tabId = workerClient.getTabId();
|
|
468
|
-
console.log(tabId); // "550e8400-e29b-41d4-a716-446655440000"
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
#### `getTabInfo(): TabInfo`
|
|
472
|
-
|
|
473
|
-
Get info about current tab (for debugging).
|
|
474
|
-
|
|
475
|
-
```typescript
|
|
476
|
-
const info = workerClient.getTabInfo();
|
|
477
|
-
// {
|
|
478
|
-
// tabId: "550e8400-...",
|
|
479
|
-
// url: "https://app.example.com/dashboard",
|
|
480
|
-
// title: "Dashboard - My App"
|
|
481
|
-
// }
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
> **Note:** To check which tab is currently active, use the `list_browser_tabs` tool which queries the worker's `TabManager` (the authoritative source).
|
|
485
|
-
|
|
486
|
-
#### `static clearTabId(): void`
|
|
487
|
-
|
|
488
|
-
Clear tab ID from sessionStorage (for testing).
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
WorkerClient.clearTabId();
|
|
492
|
-
// Refresh page to generate new ID
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### MCPController Methods
|
|
496
|
-
|
|
497
|
-
#### `handleRegisterTab(data): void`
|
|
498
|
-
|
|
499
|
-
Register a tab with the worker (called automatically).
|
|
500
|
-
|
|
501
|
-
#### `handleSetActiveTab(data): void`
|
|
502
|
-
|
|
503
|
-
Update active tab tracking (called automatically).
|
|
504
|
-
|
|
505
|
-
## Best Practices
|
|
506
|
-
|
|
507
|
-
### 1. Let Auto-Routing Work
|
|
508
|
-
|
|
509
|
-
Don't pass `tabId` unless you need to target a specific tab:
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
// ✅ Good - natural interaction
|
|
513
|
-
get_page_info()
|
|
514
|
-
|
|
515
|
-
// ❌ Unnecessary - harder to use
|
|
516
|
-
get_page_info({ tabId: workerClient.getTabId() })
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### 2. Use Discovery When Needed
|
|
520
|
-
|
|
521
|
-
Use `list_browser_tabs` when you need tab-specific operations:
|
|
522
|
-
|
|
523
|
-
```typescript
|
|
524
|
-
// ✅ Good - explicit discovery
|
|
525
|
-
const tabs = await list_browser_tabs();
|
|
526
|
-
const settingsTab = tabs.find(t => t.url.includes('/settings'));
|
|
527
|
-
await get_state({ tabId: settingsTab.tabId });
|
|
528
|
-
|
|
529
|
-
// ❌ Bad - guessing tab IDs
|
|
530
|
-
await get_state({ tabId: "some-random-id" });
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### 3. Design Stateless Tools
|
|
534
|
-
|
|
535
|
-
Prefer tools that don't depend heavily on local component state:
|
|
536
|
-
|
|
537
|
-
```typescript
|
|
538
|
-
// ✅ Good - API calls work from any tab
|
|
539
|
-
registerTool('fetch_user', async (args) => {
|
|
540
|
-
const response = await fetch(`/api/users/${args.id}`);
|
|
541
|
-
return { content: [{ type: 'text', text: await response.text() }] };
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
// ⚠️ Be aware - local state might differ per tab
|
|
545
|
-
registerTool('get_form_state', async () => {
|
|
546
|
-
const formState = useFormStore.getState(); // Different per tab
|
|
547
|
-
return { content: [{ type: 'text', text: JSON.stringify(formState) }] };
|
|
548
|
-
});
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### 4. Document Tab-Specific Behavior
|
|
552
|
-
|
|
553
|
-
If your tool behaves differently per tab, document it:
|
|
554
|
-
|
|
555
|
-
```typescript
|
|
556
|
-
registerTool(
|
|
557
|
-
'get_user_preferences',
|
|
558
|
-
'Get user preferences from current tab context. NOTE: Preferences may differ per tab if user is editing in multiple tabs.',
|
|
559
|
-
schema,
|
|
560
|
-
handler
|
|
561
|
-
);
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
## Migration from Single-Tab
|
|
565
|
-
|
|
566
|
-
Existing single-tab code works without changes:
|
|
567
|
-
|
|
568
|
-
```typescript
|
|
569
|
-
// Before (single-tab)
|
|
570
|
-
await workerClient.registerTool('my_tool', ...);
|
|
571
|
-
|
|
572
|
-
// After (multi-tab) - same code works!
|
|
573
|
-
await workerClient.registerTool('my_tool', ...);
|
|
574
|
-
// Now works across multiple tabs automatically
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
The `tabId` parameter is optional, so existing tools continue to work with the focused tab.
|
|
578
|
-
|
|
579
|
-
## Troubleshooting
|
|
580
|
-
|
|
581
|
-
### Issue: Tool calls go to wrong tab
|
|
582
|
-
|
|
583
|
-
**Solution**: Use `list_browser_tabs` to verify which tab you're targeting:
|
|
584
|
-
|
|
585
|
-
```typescript
|
|
586
|
-
const tabs = await list_browser_tabs();
|
|
587
|
-
console.log(tabs);
|
|
588
|
-
// Find the correct tabId
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
### Issue: Tab ID changes on refresh
|
|
592
|
-
|
|
593
|
-
**Check**: SessionStorage might be disabled (private mode)
|
|
594
|
-
|
|
595
|
-
```typescript
|
|
596
|
-
// Test sessionStorage
|
|
597
|
-
try {
|
|
598
|
-
sessionStorage.setItem('test', '1');
|
|
599
|
-
sessionStorage.removeItem('test');
|
|
600
|
-
console.log('SessionStorage works ✓');
|
|
601
|
-
} catch {
|
|
602
|
-
console.log('SessionStorage blocked ✗');
|
|
603
|
-
// Fallback ID will be used (changes on refresh)
|
|
604
|
-
}
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
### Issue: Multiple tabs show same tool registered multiple times
|
|
608
|
-
|
|
609
|
-
**This is expected**: Reference counting means:
|
|
610
|
-
- Tool registered once with MCP
|
|
611
|
-
- Multiple tabs can have handlers
|
|
612
|
-
- Worker routes to correct tab automatically
|
|
613
|
-
|
|
614
|
-
## Performance
|
|
615
|
-
|
|
616
|
-
Multi-tab adds minimal overhead:
|
|
617
|
-
|
|
618
|
-
- Tab registration: ~1ms
|
|
619
|
-
- Tab routing: ~0.1ms (Map lookup)
|
|
620
|
-
- Memory per tab: ~100 bytes (tab info)
|
|
621
|
-
|
|
622
|
-
Total overhead for 10 tabs: < 1ms + 1KB memory
|
|
623
|
-
|
|
624
|
-
## Security
|
|
625
|
-
|
|
626
|
-
Tab IDs are not secret:
|
|
627
|
-
- Used for routing only
|
|
628
|
-
- No authentication/authorization
|
|
629
|
-
- All tabs in same browser share worker
|
|
630
|
-
|
|
631
|
-
Do not use tab IDs as security tokens.
|
|
632
|
-
|
|
633
|
-
## See Also
|
|
634
|
-
|
|
635
|
-
- [Architecture](./architecture.md) - Detailed architecture diagrams
|
|
636
|
-
- [Guide](./guide.md) - General usage guide
|
|
637
|
-
- [API Reference](./api.md) - Complete API documentation
|
|
1
|
+
# Multi-Tab Support
|
|
2
|
+
|
|
3
|
+
Comprehensive guide to multi-tab architecture and usage patterns.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The MCP Worker library provides seamless multi-tab support with intelligent routing. Each browser tab can register tools independently, and the worker automatically routes tool calls to the appropriate tab.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
### Tab Lifecycle
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
1. Tab Opens
|
|
15
|
+
└─> Generate UUID (crypto.randomUUID())
|
|
16
|
+
└─> Store in sessionStorage
|
|
17
|
+
└─> Register with worker (REGISTER_TAB)
|
|
18
|
+
└─> Mark as active (SET_ACTIVE_TAB)
|
|
19
|
+
|
|
20
|
+
2. Tab Focus
|
|
21
|
+
└─> Update active tab (SET_ACTIVE_TAB)
|
|
22
|
+
|
|
23
|
+
3. Tab Closes
|
|
24
|
+
└─> Tools remain available if other tabs have them
|
|
25
|
+
└─> Reference counting prevents premature unregistration
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Tab Registry
|
|
29
|
+
|
|
30
|
+
The worker maintains a registry of all active tabs:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
{
|
|
34
|
+
tabId: string; // UUID from crypto.randomUUID()
|
|
35
|
+
url: string; // window.location.href
|
|
36
|
+
title: string; // document.title
|
|
37
|
+
lastSeen: number; // timestamp of last activity
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Tool Registry Per Tab
|
|
42
|
+
|
|
43
|
+
Tools are tracked per-tab using Sets:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
Map<ToolName, Set<TabId>>
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
{
|
|
50
|
+
"get_page_info": Set(["tab-abc", "tab-def"]),
|
|
51
|
+
"get_user_data": Set(["tab-abc"]),
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Tool Lifecycle & Navigation
|
|
56
|
+
|
|
57
|
+
### Automatic Cleanup
|
|
58
|
+
|
|
59
|
+
Tools are **automatically unregistered** when:
|
|
60
|
+
|
|
61
|
+
1. **Page navigation** (beforeunload event)
|
|
62
|
+
2. **Page close** (pagehide event)
|
|
63
|
+
3. **Page refresh** (F5)
|
|
64
|
+
4. **Component unmount** (via React hooks like useMCPTool)
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// WorkerClient automatically handles cleanup
|
|
68
|
+
window.addEventListener('beforeunload', () => {
|
|
69
|
+
// Unregister all tools from this tab
|
|
70
|
+
toolRegistry.forEach(tool => {
|
|
71
|
+
unregisterTool(tool.name, this.tabId);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### How It Works
|
|
77
|
+
|
|
78
|
+
**Registration:**
|
|
79
|
+
```typescript
|
|
80
|
+
// Tab A: Dashboard page
|
|
81
|
+
await workerClient.registerTool('get_dashboard_data', ...);
|
|
82
|
+
// → Worker: toolHandlersByTab.set('get_dashboard_data', Set([tabA]))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Navigation (automatic cleanup):**
|
|
86
|
+
```typescript
|
|
87
|
+
// User navigates Tab A away from Dashboard
|
|
88
|
+
// → beforeunload event fires
|
|
89
|
+
// → WorkerClient.cleanupAllTools() called
|
|
90
|
+
// → Sends: UNREGISTER_TOOL({ name: 'get_dashboard_data', tabId: tabA })
|
|
91
|
+
// → Worker: toolHandlersByTab.get('get_dashboard_data').delete(tabA)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Routing after navigation:**
|
|
95
|
+
```typescript
|
|
96
|
+
// Tab B still has 'get_dashboard_data' registered
|
|
97
|
+
// AI calls: get_dashboard_data()
|
|
98
|
+
// → Worker checks: tabA (active) has tool? NO
|
|
99
|
+
// → Worker checks: other tabs have tool? YES (tabB)
|
|
100
|
+
// → Routes to Tab B automatically ✓
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### SPA Navigation Pattern
|
|
104
|
+
|
|
105
|
+
In Single Page Applications, components mount/unmount frequently:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Dashboard component
|
|
109
|
+
function DashboardPage() {
|
|
110
|
+
// Tool registers on mount
|
|
111
|
+
useMCPTool({
|
|
112
|
+
name: 'get_dashboard_data',
|
|
113
|
+
// ...
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Tool unregisters on unmount (automatic!)
|
|
117
|
+
// - When user navigates to /settings
|
|
118
|
+
// - When component is destroyed
|
|
119
|
+
// - When route changes
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Multi-tab behavior:**
|
|
124
|
+
- Tab A: /dashboard → registers tool
|
|
125
|
+
- Tab B: /dashboard → registers tool (refCount = 2)
|
|
126
|
+
- Tab A navigates to /settings → unregisters tool (refCount = 1)
|
|
127
|
+
- Tool still available via Tab B ✓
|
|
128
|
+
|
|
129
|
+
### Edge Cases
|
|
130
|
+
|
|
131
|
+
**All tabs lose tool:**
|
|
132
|
+
```typescript
|
|
133
|
+
// Both tabs on /dashboard
|
|
134
|
+
Tab A: registers tool
|
|
135
|
+
Tab B: registers tool
|
|
136
|
+
|
|
137
|
+
// Both tabs navigate away
|
|
138
|
+
Tab A → /settings (unregisters)
|
|
139
|
+
Tab B → /profile (unregisters)
|
|
140
|
+
|
|
141
|
+
// Tool completely unregistered from MCP
|
|
142
|
+
get_dashboard_data()
|
|
143
|
+
// → Error: "Tool not available"
|
|
144
|
+
|
|
145
|
+
// User returns to /dashboard in Tab A
|
|
146
|
+
// → Component mounts → registers tool again
|
|
147
|
+
// → Tool available again ✓
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Refresh handling:**
|
|
151
|
+
```typescript
|
|
152
|
+
// Tab has tools registered
|
|
153
|
+
// User presses F5
|
|
154
|
+
|
|
155
|
+
// → beforeunload fires
|
|
156
|
+
// → All tools unregistered
|
|
157
|
+
// → Page reloads
|
|
158
|
+
// → Components mount
|
|
159
|
+
// → Tools registered again
|
|
160
|
+
// → Tab ID preserved (sessionStorage)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 1. Smart Routing (Default)
|
|
164
|
+
|
|
165
|
+
Intelligent routing that prioritizes user intent and availability:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// Priority order:
|
|
169
|
+
1. Explicit tabId parameter (always respected)
|
|
170
|
+
2. Only one tab has tool → route to it (regardless of focus)
|
|
171
|
+
3. Active/focused tab has tool → use it
|
|
172
|
+
4. Active tab doesn't have tool → use first available
|
|
173
|
+
5. No active tab → use first available
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Example Scenarios:**
|
|
177
|
+
|
|
178
|
+
**Scenario 1: Single tab with tool (most intuitive)**
|
|
179
|
+
```typescript
|
|
180
|
+
// Tab A (active): Has toolX
|
|
181
|
+
// Tab B (inactive): Doesn't have toolX
|
|
182
|
+
|
|
183
|
+
toolX()
|
|
184
|
+
→ Routes to Tab A ✓ (active tab has it)
|
|
185
|
+
|
|
186
|
+
// Now focus Tab B
|
|
187
|
+
// Tab A (inactive): Has toolX
|
|
188
|
+
// Tab B (active): Doesn't have toolX
|
|
189
|
+
|
|
190
|
+
toolX()
|
|
191
|
+
→ Routes to Tab A ✓ (only tab with tool, even though not active!)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Scenario 2: Multiple tabs with tool**
|
|
195
|
+
```typescript
|
|
196
|
+
// Tab A (active): Has toolX
|
|
197
|
+
// Tab B (inactive): Has toolX
|
|
198
|
+
|
|
199
|
+
toolX()
|
|
200
|
+
→ Routes to Tab A ✓ (active tab preferred when multiple available)
|
|
201
|
+
|
|
202
|
+
// Now focus Tab B
|
|
203
|
+
// Tab A (inactive): Has toolX
|
|
204
|
+
// Tab B (active): Has toolX
|
|
205
|
+
|
|
206
|
+
toolX()
|
|
207
|
+
→ Routes to Tab B ✓ (new active tab)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Scenario 3: Explicit targeting**
|
|
211
|
+
```typescript
|
|
212
|
+
// Tab A (active): Has toolX
|
|
213
|
+
// Tab B (inactive): Has toolX
|
|
214
|
+
|
|
215
|
+
toolX({ tabId: "tab-b-id" })
|
|
216
|
+
→ Routes to Tab B ✓ (explicit parameter always wins)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Scenario 4: Active tab loses tool (navigation)**
|
|
220
|
+
```typescript
|
|
221
|
+
// Initial state
|
|
222
|
+
// Tab A (active): Has toolX
|
|
223
|
+
// Tab B (inactive): Has toolX
|
|
224
|
+
|
|
225
|
+
toolX()
|
|
226
|
+
→ Routes to Tab A ✓ (active tab)
|
|
227
|
+
|
|
228
|
+
// User navigates Tab A to different page
|
|
229
|
+
// Tab A (active): No longer has toolX (page changed)
|
|
230
|
+
// Tab B (inactive): Has toolX
|
|
231
|
+
|
|
232
|
+
toolX()
|
|
233
|
+
→ Routes to Tab B ✓ (active tab doesn't have tool, auto-fallback!)
|
|
234
|
+
// No error, seamless transition
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Visual Flow:**
|
|
238
|
+
```
|
|
239
|
+
Before Navigation:
|
|
240
|
+
┌─────────────┐ ┌─────────────┐
|
|
241
|
+
│ Tab A │ Active │ Tab B │
|
|
242
|
+
│ /dashboard │ ✓ │ /dashboard │
|
|
243
|
+
│ has toolX │ │ has toolX │
|
|
244
|
+
└─────────────┘ └─────────────┘
|
|
245
|
+
↑
|
|
246
|
+
toolX() routes here
|
|
247
|
+
|
|
248
|
+
After Navigation (Tab A → /settings):
|
|
249
|
+
┌─────────────┐ ┌─────────────┐
|
|
250
|
+
│ Tab A │ Active │ Tab B │
|
|
251
|
+
│ /settings │ ✓ │ /dashboard │
|
|
252
|
+
│ NO toolX │ │ has toolX │
|
|
253
|
+
└─────────────┘ └─────────────┘
|
|
254
|
+
↑
|
|
255
|
+
toolX() auto-routes here!
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
This ensures tools continue to work even when the user navigates away from pages that provided specific tools.
|
|
259
|
+
|
|
260
|
+
### 2. Built-in Discovery Tool
|
|
261
|
+
|
|
262
|
+
`list_browser_tabs` provides tab discovery:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Always returns current tab state
|
|
266
|
+
const tabs = await list_browser_tabs();
|
|
267
|
+
|
|
268
|
+
[
|
|
269
|
+
{
|
|
270
|
+
tabId: "550e8400-e29b-41d4-a716-446655440000",
|
|
271
|
+
url: "https://app.example.com/dashboard",
|
|
272
|
+
title: "Dashboard - My App",
|
|
273
|
+
isActive: true,
|
|
274
|
+
lastSeen: "2026-02-04T10:30:00.000Z"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
tabId: "6fa459ea-ee8a-3ca4-894e-db77e160355e",
|
|
278
|
+
url: "https://app.example.com/settings",
|
|
279
|
+
title: "Settings - My App",
|
|
280
|
+
isActive: false,
|
|
281
|
+
lastSeen: "2026-02-04T10:29:45.000Z"
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Tool Schema Enhancement
|
|
287
|
+
|
|
288
|
+
All registered tools automatically receive an optional `tabId` parameter:
|
|
289
|
+
|
|
290
|
+
**Original Schema:**
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"type": "object",
|
|
294
|
+
"properties": {
|
|
295
|
+
"username": { "type": "string" }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Enhanced Schema:**
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"type": "object",
|
|
304
|
+
"properties": {
|
|
305
|
+
"username": { "type": "string" },
|
|
306
|
+
"tabId": {
|
|
307
|
+
"type": "string",
|
|
308
|
+
"description": "Optional: Target specific tab by ID. If not provided, uses the currently focused tab. Use list_browser_tabs to discover available tabs."
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The `tabId` parameter is automatically added by the library - you don't need to include it in your schema.
|
|
315
|
+
|
|
316
|
+
## Use Cases
|
|
317
|
+
|
|
318
|
+
### 1. Debugging Specific Tab
|
|
319
|
+
|
|
320
|
+
AI can debug a specific tab while user works in another:
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
User: "Check the state of the Settings tab"
|
|
324
|
+
|
|
325
|
+
AI:
|
|
326
|
+
1. list_browser_tabs()
|
|
327
|
+
→ Finds Settings tab ID: "6fa459ea..."
|
|
328
|
+
|
|
329
|
+
2. get_react_state({ tabId: "6fa459ea..." })
|
|
330
|
+
→ Gets state from Settings tab
|
|
331
|
+
|
|
332
|
+
3. analyze_component({ tabId: "6fa459ea...", component: "UserForm" })
|
|
333
|
+
→ Analyzes specific component
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 2. Cross-Tab Comparison
|
|
337
|
+
|
|
338
|
+
Compare state/data across multiple tabs:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const tabs = await list_browser_tabs();
|
|
342
|
+
|
|
343
|
+
const states = await Promise.all(
|
|
344
|
+
tabs.map(tab =>
|
|
345
|
+
get_react_state({ tabId: tab.tabId })
|
|
346
|
+
)
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Compare states across tabs
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 3. Focus-Driven Interaction
|
|
353
|
+
|
|
354
|
+
Natural interaction with focused tab:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
User: "What's on this page?"
|
|
358
|
+
AI: get_page_info() // No tabId needed
|
|
359
|
+
→ Automatically uses focused tab
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 4. Multi-Screen Workflows
|
|
363
|
+
|
|
364
|
+
User has multiple monitors with different tabs:
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
Monitor 1: Dashboard (focused)
|
|
368
|
+
Monitor 2: Analytics
|
|
369
|
+
Monitor 3: Settings
|
|
370
|
+
|
|
371
|
+
// AI can work with any tab
|
|
372
|
+
get_metrics({ tabId: "analytics-tab" }) // Monitor 2
|
|
373
|
+
update_settings({ tabId: "settings-tab" }) // Monitor 3
|
|
374
|
+
show_alert() // Monitor 1 (focused)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Reference Counting
|
|
378
|
+
|
|
379
|
+
Tools use reference counting to handle multiple registrations:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// Tab 1 registers tool
|
|
383
|
+
workerClient.registerTool('get_data', ...)
|
|
384
|
+
// → Registered with MCP, refCount = 1
|
|
385
|
+
|
|
386
|
+
// Tab 2 registers same tool
|
|
387
|
+
workerClient.registerTool('get_data', ...)
|
|
388
|
+
// → NOT re-registered with MCP, refCount = 2
|
|
389
|
+
|
|
390
|
+
// Tab 1 closes/unregisters
|
|
391
|
+
workerClient.unregisterTool('get_data')
|
|
392
|
+
// → NOT unregistered from MCP, refCount = 1
|
|
393
|
+
|
|
394
|
+
// Tab 2 closes/unregisters
|
|
395
|
+
workerClient.unregisterTool('get_data')
|
|
396
|
+
// → Unregistered from MCP, refCount = 0
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Tab Persistence
|
|
400
|
+
|
|
401
|
+
### SessionStorage Persistence
|
|
402
|
+
|
|
403
|
+
Tab IDs are stored in `sessionStorage`, which means:
|
|
404
|
+
|
|
405
|
+
- ✅ **Page refresh (F5)**: Tab keeps same ID
|
|
406
|
+
- ✅ **Navigation**: Tab keeps same ID
|
|
407
|
+
- ❌ **Duplicate tab**: New tab gets new ID
|
|
408
|
+
- ❌ **New window**: New window gets new ID
|
|
409
|
+
- ❌ **Private/Incognito**: Each session independent
|
|
410
|
+
|
|
411
|
+
### Tab ID Format
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// UUID v4 from crypto.randomUUID()
|
|
415
|
+
"550e8400-e29b-41d4-a716-446655440000"
|
|
416
|
+
|
|
417
|
+
// Fallback if crypto unavailable
|
|
418
|
+
"fallback_1738668000000_xyz123"
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Error Handling
|
|
422
|
+
|
|
423
|
+
### Tab Not Found
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
get_page_info({ tabId: "invalid-id" })
|
|
427
|
+
|
|
428
|
+
// Error response:
|
|
429
|
+
{
|
|
430
|
+
error: "Tool 'get_page_info' not available in tab 'invalid-id'. Available tabs: tab-1, tab-2"
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### No Active Tab
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
// All tabs minimized/backgrounded
|
|
438
|
+
get_page_info() // No tabId, no active tab
|
|
439
|
+
|
|
440
|
+
// Behavior: Uses first available tab + warning log
|
|
441
|
+
// Better than error - tool still works
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Tool Not Registered
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Tab 1: Registers tool A
|
|
448
|
+
// Tab 2: Registers tool B
|
|
449
|
+
|
|
450
|
+
get_tool_b({ tabId: "tab-1" })
|
|
451
|
+
|
|
452
|
+
// Error:
|
|
453
|
+
{
|
|
454
|
+
error: "Tool 'get_tool_b' not available in tab 'tab-1'. Available tabs: tab-2"
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## API Reference
|
|
459
|
+
|
|
460
|
+
### WorkerClient Methods
|
|
461
|
+
|
|
462
|
+
#### `getTabId(): string`
|
|
463
|
+
|
|
464
|
+
Get the unique ID of the current tab.
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
const tabId = workerClient.getTabId();
|
|
468
|
+
console.log(tabId); // "550e8400-e29b-41d4-a716-446655440000"
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
#### `getTabInfo(): TabInfo`
|
|
472
|
+
|
|
473
|
+
Get info about current tab (for debugging).
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
const info = workerClient.getTabInfo();
|
|
477
|
+
// {
|
|
478
|
+
// tabId: "550e8400-...",
|
|
479
|
+
// url: "https://app.example.com/dashboard",
|
|
480
|
+
// title: "Dashboard - My App"
|
|
481
|
+
// }
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
> **Note:** To check which tab is currently active, use the `list_browser_tabs` tool which queries the worker's `TabManager` (the authoritative source).
|
|
485
|
+
|
|
486
|
+
#### `static clearTabId(): void`
|
|
487
|
+
|
|
488
|
+
Clear tab ID from sessionStorage (for testing).
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
WorkerClient.clearTabId();
|
|
492
|
+
// Refresh page to generate new ID
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### MCPController Methods
|
|
496
|
+
|
|
497
|
+
#### `handleRegisterTab(data): void`
|
|
498
|
+
|
|
499
|
+
Register a tab with the worker (called automatically).
|
|
500
|
+
|
|
501
|
+
#### `handleSetActiveTab(data): void`
|
|
502
|
+
|
|
503
|
+
Update active tab tracking (called automatically).
|
|
504
|
+
|
|
505
|
+
## Best Practices
|
|
506
|
+
|
|
507
|
+
### 1. Let Auto-Routing Work
|
|
508
|
+
|
|
509
|
+
Don't pass `tabId` unless you need to target a specific tab:
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// ✅ Good - natural interaction
|
|
513
|
+
get_page_info()
|
|
514
|
+
|
|
515
|
+
// ❌ Unnecessary - harder to use
|
|
516
|
+
get_page_info({ tabId: workerClient.getTabId() })
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### 2. Use Discovery When Needed
|
|
520
|
+
|
|
521
|
+
Use `list_browser_tabs` when you need tab-specific operations:
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
// ✅ Good - explicit discovery
|
|
525
|
+
const tabs = await list_browser_tabs();
|
|
526
|
+
const settingsTab = tabs.find(t => t.url.includes('/settings'));
|
|
527
|
+
await get_state({ tabId: settingsTab.tabId });
|
|
528
|
+
|
|
529
|
+
// ❌ Bad - guessing tab IDs
|
|
530
|
+
await get_state({ tabId: "some-random-id" });
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### 3. Design Stateless Tools
|
|
534
|
+
|
|
535
|
+
Prefer tools that don't depend heavily on local component state:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
// ✅ Good - API calls work from any tab
|
|
539
|
+
registerTool('fetch_user', async (args) => {
|
|
540
|
+
const response = await fetch(`/api/users/${args.id}`);
|
|
541
|
+
return { content: [{ type: 'text', text: await response.text() }] };
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// ⚠️ Be aware - local state might differ per tab
|
|
545
|
+
registerTool('get_form_state', async () => {
|
|
546
|
+
const formState = useFormStore.getState(); // Different per tab
|
|
547
|
+
return { content: [{ type: 'text', text: JSON.stringify(formState) }] };
|
|
548
|
+
});
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 4. Document Tab-Specific Behavior
|
|
552
|
+
|
|
553
|
+
If your tool behaves differently per tab, document it:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
registerTool(
|
|
557
|
+
'get_user_preferences',
|
|
558
|
+
'Get user preferences from current tab context. NOTE: Preferences may differ per tab if user is editing in multiple tabs.',
|
|
559
|
+
schema,
|
|
560
|
+
handler
|
|
561
|
+
);
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
## Migration from Single-Tab
|
|
565
|
+
|
|
566
|
+
Existing single-tab code works without changes:
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
// Before (single-tab)
|
|
570
|
+
await workerClient.registerTool('my_tool', ...);
|
|
571
|
+
|
|
572
|
+
// After (multi-tab) - same code works!
|
|
573
|
+
await workerClient.registerTool('my_tool', ...);
|
|
574
|
+
// Now works across multiple tabs automatically
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
The `tabId` parameter is optional, so existing tools continue to work with the focused tab.
|
|
578
|
+
|
|
579
|
+
## Troubleshooting
|
|
580
|
+
|
|
581
|
+
### Issue: Tool calls go to wrong tab
|
|
582
|
+
|
|
583
|
+
**Solution**: Use `list_browser_tabs` to verify which tab you're targeting:
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
const tabs = await list_browser_tabs();
|
|
587
|
+
console.log(tabs);
|
|
588
|
+
// Find the correct tabId
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Issue: Tab ID changes on refresh
|
|
592
|
+
|
|
593
|
+
**Check**: SessionStorage might be disabled (private mode)
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// Test sessionStorage
|
|
597
|
+
try {
|
|
598
|
+
sessionStorage.setItem('test', '1');
|
|
599
|
+
sessionStorage.removeItem('test');
|
|
600
|
+
console.log('SessionStorage works ✓');
|
|
601
|
+
} catch {
|
|
602
|
+
console.log('SessionStorage blocked ✗');
|
|
603
|
+
// Fallback ID will be used (changes on refresh)
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Issue: Multiple tabs show same tool registered multiple times
|
|
608
|
+
|
|
609
|
+
**This is expected**: Reference counting means:
|
|
610
|
+
- Tool registered once with MCP
|
|
611
|
+
- Multiple tabs can have handlers
|
|
612
|
+
- Worker routes to correct tab automatically
|
|
613
|
+
|
|
614
|
+
## Performance
|
|
615
|
+
|
|
616
|
+
Multi-tab adds minimal overhead:
|
|
617
|
+
|
|
618
|
+
- Tab registration: ~1ms
|
|
619
|
+
- Tab routing: ~0.1ms (Map lookup)
|
|
620
|
+
- Memory per tab: ~100 bytes (tab info)
|
|
621
|
+
|
|
622
|
+
Total overhead for 10 tabs: < 1ms + 1KB memory
|
|
623
|
+
|
|
624
|
+
## Security
|
|
625
|
+
|
|
626
|
+
Tab IDs are not secret:
|
|
627
|
+
- Used for routing only
|
|
628
|
+
- No authentication/authorization
|
|
629
|
+
- All tabs in same browser share worker
|
|
630
|
+
|
|
631
|
+
Do not use tab IDs as security tokens.
|
|
632
|
+
|
|
633
|
+
## See Also
|
|
634
|
+
|
|
635
|
+
- [Architecture](./architecture.md) - Detailed architecture diagrams
|
|
636
|
+
- [Guide](./guide.md) - General usage guide
|
|
637
|
+
- [API Reference](./api.md) - Complete API documentation
|