@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/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