@mcp-fe/mcp-worker 0.1.3 → 0.1.5

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.
@@ -27808,6 +27808,33 @@ function registerBuiltInTools() {
27808
27808
  toolRegistry.register(definition, handler);
27809
27809
  });
27810
27810
  }
27811
+ function registerTabManagementTool(tabManager) {
27812
+ toolRegistry.register(
27813
+ {
27814
+ name: "list_browser_tabs",
27815
+ description: "List all active browser tabs running this application. Returns tab IDs, URLs, titles, and active status. Use this to discover available tabs before calling tools with specific tabId parameters.",
27816
+ inputSchema: {
27817
+ type: "object",
27818
+ properties: {}
27819
+ }
27820
+ },
27821
+ async () => {
27822
+ const tabs = tabManager.getAllTabs().map((tab) => ({
27823
+ ...tab,
27824
+ isActive: tab.tabId === tabManager.getActiveTabId(),
27825
+ lastSeen: new Date(tab.lastSeen).toISOString()
27826
+ }));
27827
+ return {
27828
+ content: [
27829
+ {
27830
+ type: "text",
27831
+ text: JSON.stringify(tabs, null, 2)
27832
+ }
27833
+ ]
27834
+ };
27835
+ }
27836
+ );
27837
+ }
27811
27838
 
27812
27839
  // libs/mcp-worker/src/lib/mcp-server.ts
27813
27840
  function createMCPServer(options = {}) {
@@ -27912,6 +27939,249 @@ var logger = {
27912
27939
  }
27913
27940
  };
27914
27941
 
27942
+ // libs/mcp-worker/src/lib/tab-manager.ts
27943
+ var TabManager = class {
27944
+ // Registry of all active tabs
27945
+ tabRegistry = /* @__PURE__ */ new Map();
27946
+ // Currently active/focused tab
27947
+ activeTabId = null;
27948
+ // Tool handlers per tab: Map<ToolName, Set<TabId>>
27949
+ toolHandlersByTab = /* @__PURE__ */ new Map();
27950
+ /**
27951
+ * Register a new tab
27952
+ */
27953
+ registerTab(tabId, url2, title) {
27954
+ if (!tabId) {
27955
+ logger.warn("[TabManager] Cannot register tab: missing tabId");
27956
+ return;
27957
+ }
27958
+ this.tabRegistry.set(tabId, {
27959
+ url: url2 || "",
27960
+ title: title || "",
27961
+ lastSeen: Date.now()
27962
+ });
27963
+ logger.log(`[TabManager] Registered tab: ${tabId} (${title})`);
27964
+ }
27965
+ /**
27966
+ * Set the active/focused tab
27967
+ */
27968
+ setActiveTab(tabId) {
27969
+ if (!tabId) {
27970
+ logger.warn("[TabManager] Cannot set active tab: missing tabId");
27971
+ return;
27972
+ }
27973
+ this.activeTabId = tabId;
27974
+ logger.log(`[TabManager] Active tab changed: ${tabId}`);
27975
+ const tab = this.tabRegistry.get(tabId);
27976
+ if (tab) {
27977
+ tab.lastSeen = Date.now();
27978
+ }
27979
+ }
27980
+ /**
27981
+ * Get the currently active tab ID
27982
+ */
27983
+ getActiveTabId() {
27984
+ return this.activeTabId;
27985
+ }
27986
+ /**
27987
+ * Get all registered tabs
27988
+ */
27989
+ getAllTabs() {
27990
+ return Array.from(this.tabRegistry.entries()).map(([tabId, info]) => ({
27991
+ tabId,
27992
+ ...info
27993
+ }));
27994
+ }
27995
+ /**
27996
+ * Get tab info by ID
27997
+ */
27998
+ getTabInfo(tabId) {
27999
+ return this.tabRegistry.get(tabId);
28000
+ }
28001
+ /**
28002
+ * Check if tab exists
28003
+ */
28004
+ hasTab(tabId) {
28005
+ return this.tabRegistry.has(tabId);
28006
+ }
28007
+ /**
28008
+ * Remove a tab from registry
28009
+ */
28010
+ removeTab(tabId) {
28011
+ const existed = this.tabRegistry.delete(tabId);
28012
+ if (existed) {
28013
+ logger.log(`[TabManager] Removed tab: ${tabId}`);
28014
+ if (this.activeTabId === tabId) {
28015
+ this.activeTabId = null;
28016
+ }
28017
+ this.cleanupTabTools(tabId);
28018
+ }
28019
+ return existed;
28020
+ }
28021
+ /**
28022
+ * Register a tool for a specific tab
28023
+ */
28024
+ registerToolForTab(toolName, tabId) {
28025
+ if (!this.toolHandlersByTab.has(toolName)) {
28026
+ this.toolHandlersByTab.set(toolName, /* @__PURE__ */ new Set());
28027
+ }
28028
+ const tabHandlers = this.toolHandlersByTab.get(toolName);
28029
+ const isNewTab = !tabHandlers.has(tabId);
28030
+ tabHandlers.add(tabId);
28031
+ if (isNewTab) {
28032
+ logger.log(
28033
+ `[TabManager] Tab ${tabId} registered tool '${toolName}' (${tabHandlers.size} tab(s) total)`
28034
+ );
28035
+ } else {
28036
+ logger.log(
28037
+ `[TabManager] Tab ${tabId} re-registered tool '${toolName}' (already tracked)`
28038
+ );
28039
+ }
28040
+ return isNewTab;
28041
+ }
28042
+ /**
28043
+ * Unregister a tool from a specific tab
28044
+ */
28045
+ unregisterToolFromTab(toolName, tabId) {
28046
+ const tabHandlers = this.toolHandlersByTab.get(toolName);
28047
+ if (!tabHandlers || !tabHandlers.has(tabId)) {
28048
+ return { wasRemoved: false, remainingTabs: 0, wasActiveTab: false };
28049
+ }
28050
+ const wasActiveTab = tabId === this.activeTabId;
28051
+ tabHandlers.delete(tabId);
28052
+ const remainingTabs = tabHandlers.size;
28053
+ logger.log(
28054
+ `[TabManager] Removed tab ${tabId} from tool '${toolName}' (${remainingTabs} tab(s) remaining)`
28055
+ );
28056
+ if (remainingTabs === 0) {
28057
+ this.toolHandlersByTab.delete(toolName);
28058
+ }
28059
+ return { wasRemoved: true, remainingTabs, wasActiveTab };
28060
+ }
28061
+ /**
28062
+ * Get all tabs that have a specific tool
28063
+ */
28064
+ getTabsForTool(toolName) {
28065
+ return this.toolHandlersByTab.get(toolName) || /* @__PURE__ */ new Set();
28066
+ }
28067
+ /**
28068
+ * Check if a tab has a specific tool
28069
+ */
28070
+ tabHasTool(toolName, tabId) {
28071
+ const tabHandlers = this.toolHandlersByTab.get(toolName);
28072
+ return tabHandlers ? tabHandlers.has(tabId) : false;
28073
+ }
28074
+ /**
28075
+ * Smart routing: determine which tab should handle a tool call
28076
+ *
28077
+ * Priority:
28078
+ * 1. Explicit tabId parameter (if provided and valid)
28079
+ * 2. Only one tab has tool -> use it (regardless of focus)
28080
+ * 3. Active tab has tool -> use it
28081
+ * 4. Active tab doesn't have tool -> use first available
28082
+ * 5. No active tab -> use first available
28083
+ */
28084
+ routeToolCall(toolName, explicitTabId) {
28085
+ const tabHandlers = this.toolHandlersByTab.get(toolName);
28086
+ if (!tabHandlers || tabHandlers.size === 0) {
28087
+ return null;
28088
+ }
28089
+ if (explicitTabId) {
28090
+ if (tabHandlers.has(explicitTabId)) {
28091
+ return {
28092
+ targetTabId: explicitTabId,
28093
+ reason: "explicit tabId parameter"
28094
+ };
28095
+ } else {
28096
+ return null;
28097
+ }
28098
+ }
28099
+ if (tabHandlers.size === 1) {
28100
+ const targetTabId = tabHandlers.values().next().value;
28101
+ return {
28102
+ targetTabId,
28103
+ reason: "only one tab has tool"
28104
+ };
28105
+ }
28106
+ if (this.activeTabId && tabHandlers.has(this.activeTabId)) {
28107
+ return {
28108
+ targetTabId: this.activeTabId,
28109
+ reason: "active tab has tool"
28110
+ };
28111
+ }
28112
+ const firstTab = tabHandlers.values().next().value;
28113
+ const reason = this.activeTabId ? "active tab lacks tool, using first available" : "no active tab, using first available";
28114
+ return {
28115
+ targetTabId: firstTab,
28116
+ reason
28117
+ };
28118
+ }
28119
+ /**
28120
+ * Get routing statistics for debugging
28121
+ */
28122
+ getRoutingInfo(toolName) {
28123
+ const tabHandlers = this.toolHandlersByTab.get(toolName);
28124
+ const tabsWithTool = tabHandlers ? Array.from(tabHandlers) : [];
28125
+ return {
28126
+ toolExists: tabsWithTool.length > 0,
28127
+ tabsWithTool,
28128
+ activeTabHasTool: this.activeTabId ? this.tabHasTool(toolName, this.activeTabId) : false,
28129
+ recommendedTab: this.routeToolCall(toolName)?.targetTabId || null
28130
+ };
28131
+ }
28132
+ /**
28133
+ * Clean up all tools for a specific tab
28134
+ * @private
28135
+ */
28136
+ cleanupTabTools(tabId) {
28137
+ const removedTools = [];
28138
+ for (const [toolName, tabHandlers] of this.toolHandlersByTab.entries()) {
28139
+ if (tabHandlers.has(tabId)) {
28140
+ tabHandlers.delete(tabId);
28141
+ removedTools.push(toolName);
28142
+ if (tabHandlers.size === 0) {
28143
+ this.toolHandlersByTab.delete(toolName);
28144
+ }
28145
+ }
28146
+ }
28147
+ if (removedTools.length > 0) {
28148
+ logger.log(
28149
+ `[TabManager] Cleaned up ${removedTools.length} tool(s) for tab ${tabId}: ${removedTools.join(", ")}`
28150
+ );
28151
+ }
28152
+ }
28153
+ /**
28154
+ * Get statistics for monitoring
28155
+ */
28156
+ getStats() {
28157
+ const toolsPerTab = {};
28158
+ for (const [tabId] of this.tabRegistry) {
28159
+ let toolCount = 0;
28160
+ for (const tabHandlers of this.toolHandlersByTab.values()) {
28161
+ if (tabHandlers.has(tabId)) {
28162
+ toolCount++;
28163
+ }
28164
+ }
28165
+ toolsPerTab[tabId] = toolCount;
28166
+ }
28167
+ return {
28168
+ totalTabs: this.tabRegistry.size,
28169
+ activeTabId: this.activeTabId,
28170
+ totalTools: this.toolHandlersByTab.size,
28171
+ toolsPerTab
28172
+ };
28173
+ }
28174
+ /**
28175
+ * Clear all data (for testing)
28176
+ */
28177
+ clear() {
28178
+ this.tabRegistry.clear();
28179
+ this.activeTabId = null;
28180
+ this.toolHandlersByTab.clear();
28181
+ logger.log("[TabManager] Cleared all data");
28182
+ }
28183
+ };
28184
+
27915
28185
  // libs/mcp-worker/src/lib/mcp-controller.ts
27916
28186
  var MAX_RECONNECT_DELAY = 3e4;
27917
28187
  var INITIAL_RECONNECT_DELAY = 1e3;
@@ -27920,6 +28190,7 @@ var MCPController = class _MCPController {
27920
28190
  this.backendUrl = backendUrl2;
27921
28191
  this.broadcastFn = broadcastFn;
27922
28192
  this.requireAuth = requireAuth;
28193
+ registerTabManagementTool(this.tabManager);
27923
28194
  }
27924
28195
  socket = null;
27925
28196
  transport = null;
@@ -27931,6 +28202,40 @@ var MCPController = class _MCPController {
27931
28202
  // Queue for tool registrations that arrive before MCP server is ready
27932
28203
  pendingToolRegistrations = [];
27933
28204
  isMCPServerReady = false;
28205
+ // Multi-tab support via TabManager
28206
+ tabManager = new TabManager();
28207
+ // Map to track pending tool calls
28208
+ pendingToolCalls = /* @__PURE__ */ new Map();
28209
+ /**
28210
+ * Handle tab registration from WorkerClient
28211
+ * @public
28212
+ */
28213
+ handleRegisterTab(data) {
28214
+ const tabId = data["tabId"];
28215
+ const url2 = data["url"];
28216
+ const title = data["title"];
28217
+ if (!tabId) {
28218
+ logger.warn("[MCPController] REGISTER_TAB missing tabId");
28219
+ return;
28220
+ }
28221
+ this.tabManager.registerTab(tabId, url2, title);
28222
+ this.broadcastFn({
28223
+ type: "TAB_LIST_UPDATED",
28224
+ tabs: this.tabManager.getAllTabs()
28225
+ });
28226
+ }
28227
+ /**
28228
+ * Handle active tab change from WorkerClient
28229
+ * @public
28230
+ */
28231
+ handleSetActiveTab(data) {
28232
+ const tabId = data["tabId"];
28233
+ if (!tabId) {
28234
+ logger.warn("[MCPController] SET_ACTIVE_TAB missing tabId");
28235
+ return;
28236
+ }
28237
+ this.tabManager.setActiveTab(tabId);
28238
+ }
27934
28239
  startKeepAlive() {
27935
28240
  if (this.keepAliveInterval) {
27936
28241
  clearInterval(this.keepAliveInterval);
@@ -28122,6 +28427,7 @@ var MCPController = class _MCPController {
28122
28427
  const description = toolData["description"];
28123
28428
  const inputSchema = toolData["inputSchema"];
28124
28429
  const handlerType = toolData["handlerType"];
28430
+ const tabId = toolData["tabId"];
28125
28431
  if (!name || !description || !inputSchema) {
28126
28432
  throw new Error(
28127
28433
  "Missing required tool fields: name, description, inputSchema"
@@ -28132,49 +28438,72 @@ var MCPController = class _MCPController {
28132
28438
  `Unsupported handler type: ${handlerType}. Only 'proxy' handlers are supported.`
28133
28439
  );
28134
28440
  }
28135
- const handler = async (args) => {
28136
- const callId = `call_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
28137
- logger.log(`[MCPController] Proxying tool call to main thread: ${name}`, {
28138
- callId,
28139
- args
28140
- });
28141
- return new Promise((resolve, reject) => {
28142
- const pendingCall = {
28143
- resolve,
28144
- reject,
28145
- timeout: setTimeout(() => {
28146
- reject(new Error(`Tool call timeout: ${name}`));
28147
- }, 3e4)
28148
- // 30 second timeout
28149
- };
28150
- if (!this.pendingToolCalls) {
28151
- this.pendingToolCalls = /* @__PURE__ */ new Map();
28152
- }
28153
- this.pendingToolCalls.set(callId, pendingCall);
28154
- this.broadcastFn({
28155
- type: "CALL_TOOL",
28156
- toolName: name,
28157
- args,
28158
- callId
28441
+ const isNewTab = this.tabManager.registerToolForTab(name, tabId);
28442
+ if (!isNewTab) {
28443
+ return;
28444
+ }
28445
+ const tabsWithTool = this.tabManager.getTabsForTool(name);
28446
+ if (tabsWithTool.size === 1) {
28447
+ const handler = async (args) => {
28448
+ const argsObj = args;
28449
+ const explicitTabId = argsObj["tabId"];
28450
+ const routingResult = this.tabManager.routeToolCall(
28451
+ name,
28452
+ explicitTabId
28453
+ );
28454
+ if (!routingResult) {
28455
+ const available = Array.from(this.tabManager.getTabsForTool(name));
28456
+ if (explicitTabId) {
28457
+ throw new Error(
28458
+ `Tool '${name}' not available in tab '${explicitTabId}'. Available tabs: ${available.join(", ")}`
28459
+ );
28460
+ } else {
28461
+ throw new Error(
28462
+ `Tool '${name}' has no registered tabs. Please specify tabId parameter.`
28463
+ );
28464
+ }
28465
+ }
28466
+ const { targetTabId, reason } = routingResult;
28467
+ logger.log(
28468
+ `[MCPController] Routing '${name}' to tab ${targetTabId}: ${reason}`
28469
+ );
28470
+ const callId = `call_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
28471
+ return new Promise((resolve, reject) => {
28472
+ const pendingCall = {
28473
+ resolve,
28474
+ reject,
28475
+ timeout: setTimeout(() => {
28476
+ this.pendingToolCalls.delete(callId);
28477
+ reject(
28478
+ new Error(`Tool call timeout: ${name} (tab: ${targetTabId})`)
28479
+ );
28480
+ }, 3e4)
28481
+ // 30 second timeout
28482
+ };
28483
+ this.pendingToolCalls.set(callId, pendingCall);
28484
+ this.broadcastFn({
28485
+ type: "CALL_TOOL",
28486
+ toolName: name,
28487
+ args,
28488
+ callId,
28489
+ targetTabId
28490
+ });
28159
28491
  });
28160
- });
28161
- };
28162
- toolRegistry.register(
28163
- {
28164
- name,
28165
- description,
28166
- inputSchema
28167
- },
28168
- handler
28169
- );
28170
- logger.log(
28171
- `[MCPController] Registered proxy tool: ${name} (forwards to main thread)`
28172
- );
28492
+ };
28493
+ toolRegistry.register(
28494
+ {
28495
+ name,
28496
+ description,
28497
+ inputSchema
28498
+ },
28499
+ handler
28500
+ );
28501
+ logger.log(
28502
+ `[MCPController] Registered proxy tool: ${name} with smart multi-tab routing`
28503
+ );
28504
+ }
28173
28505
  }
28174
- pendingToolCalls;
28175
28506
  handleToolCallResult(callId, result) {
28176
- if (!this.pendingToolCalls)
28177
- return;
28178
28507
  const pendingCall = this.pendingToolCalls.get(callId);
28179
28508
  if (!pendingCall) {
28180
28509
  logger.warn(
@@ -28191,14 +28520,35 @@ var MCPController = class _MCPController {
28191
28520
  pendingCall.reject(new Error(resultData.error || "Tool call failed"));
28192
28521
  }
28193
28522
  }
28194
- async handleUnregisterTool(toolName) {
28195
- const success2 = toolRegistry.unregister(toolName);
28196
- if (success2) {
28197
- logger.log(`[MCPController] Unregistered tool: ${toolName}`);
28198
- } else {
28199
- logger.log(`[MCPController] Tool not found: ${toolName}`);
28523
+ async handleUnregisterTool(toolName, tabId) {
28524
+ if (!tabId) {
28525
+ logger.warn(
28526
+ `[MCPController] UNREGISTER_TOOL missing tabId for '${toolName}'`
28527
+ );
28528
+ return false;
28529
+ }
28530
+ const result = this.tabManager.unregisterToolFromTab(toolName, tabId);
28531
+ if (!result.wasRemoved) {
28532
+ logger.warn(
28533
+ `[MCPController] Tool '${toolName}' not found in tab ${tabId}`
28534
+ );
28535
+ return false;
28536
+ }
28537
+ if (result.wasActiveTab && result.remainingTabs > 0) {
28538
+ logger.log(
28539
+ `[MCPController] Active tab ${tabId} unregistered '${toolName}', but ${result.remainingTabs} other tab(s) still have it. Future calls will route to available tabs.`
28540
+ );
28541
+ }
28542
+ if (result.remainingTabs === 0) {
28543
+ const success2 = toolRegistry.unregister(toolName);
28544
+ if (success2) {
28545
+ logger.log(
28546
+ `[MCPController] Unregistered tool from MCP: ${toolName} (no tabs remaining)`
28547
+ );
28548
+ }
28549
+ return success2;
28200
28550
  }
28201
- return success2;
28551
+ return true;
28202
28552
  }
28203
28553
  getConnectionStatus() {
28204
28554
  return this.socket?.readyState === WebSocket.OPEN;
@@ -28232,27 +28582,44 @@ var getController = () => {
28232
28582
  return controller;
28233
28583
  };
28234
28584
  var setBackendUrl = (url2) => {
28235
- backendUrl = url2;
28236
- controller = null;
28237
- controller = MCPController.create(url2, (message) => {
28238
- self.clients.matchAll().then((clients) => {
28239
- clients.forEach((client) => {
28240
- try {
28241
- client.postMessage(message);
28242
- } catch (e) {
28243
- logger.error(
28244
- "[ServiceWorker] Failed to post message to client:",
28245
- e
28246
- );
28247
- }
28585
+ if (controller && backendUrl === url2) {
28586
+ logger.log(
28587
+ "[ServiceWorker] Controller already initialized with same URL, reusing"
28588
+ );
28589
+ return controller;
28590
+ }
28591
+ if (backendUrl !== url2) {
28592
+ logger.log(
28593
+ `[ServiceWorker] Initializing/updating controller with URL: ${url2}`
28594
+ );
28595
+ backendUrl = url2;
28596
+ if (controller) {
28597
+ try {
28598
+ controller.dispose();
28599
+ } catch (e) {
28600
+ logger.warn("[ServiceWorker] Failed to dispose old controller:", e);
28601
+ }
28602
+ }
28603
+ controller = MCPController.create(url2, (message) => {
28604
+ self.clients.matchAll().then((clients) => {
28605
+ clients.forEach((client) => {
28606
+ try {
28607
+ client.postMessage(message);
28608
+ } catch (e) {
28609
+ logger.error(
28610
+ "[ServiceWorker] Failed to post message to client:",
28611
+ e
28612
+ );
28613
+ }
28614
+ });
28615
+ }).catch((err) => {
28616
+ logger.error(
28617
+ "[ServiceWorker] Failed to match clients for broadcast:",
28618
+ err
28619
+ );
28248
28620
  });
28249
- }).catch((err) => {
28250
- logger.error(
28251
- "[ServiceWorker] Failed to match clients for broadcast:",
28252
- err
28253
- );
28254
28621
  });
28255
- });
28622
+ }
28256
28623
  return controller;
28257
28624
  };
28258
28625
  self.addEventListener("message", async (event) => {
@@ -28372,6 +28739,26 @@ self.addEventListener("message", async (event) => {
28372
28739
  }
28373
28740
  return;
28374
28741
  }
28742
+ if (msg["type"] === "REGISTER_TAB") {
28743
+ try {
28744
+ if (controller) {
28745
+ getController().handleRegisterTab(msg);
28746
+ }
28747
+ } catch (error48) {
28748
+ logger.error("[ServiceWorker] Failed to register tab:", error48);
28749
+ }
28750
+ return;
28751
+ }
28752
+ if (msg["type"] === "SET_ACTIVE_TAB") {
28753
+ try {
28754
+ if (controller) {
28755
+ getController().handleSetActiveTab(msg);
28756
+ }
28757
+ } catch (error48) {
28758
+ logger.error("[ServiceWorker] Failed to set active tab:", error48);
28759
+ }
28760
+ return;
28761
+ }
28375
28762
  if (msg["type"] === "REGISTER_TOOL") {
28376
28763
  event.waitUntil(
28377
28764
  (async () => {
@@ -28416,6 +28803,7 @@ self.addEventListener("message", async (event) => {
28416
28803
  return;
28417
28804
  }
28418
28805
  const toolName = msg["name"];
28806
+ const tabId = msg["tabId"];
28419
28807
  if (!toolName) {
28420
28808
  if (event.ports && event.ports[0]) {
28421
28809
  event.ports[0].postMessage({
@@ -28425,7 +28813,10 @@ self.addEventListener("message", async (event) => {
28425
28813
  }
28426
28814
  return;
28427
28815
  }
28428
- const success2 = await getController().handleUnregisterTool(toolName);
28816
+ const success2 = await getController().handleUnregisterTool(
28817
+ toolName,
28818
+ tabId
28819
+ );
28429
28820
  if (event.ports && event.ports[0]) {
28430
28821
  event.ports[0].postMessage({ success: success2 });
28431
28822
  }