@mcp-fe/mcp-worker 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -26,6 +26,379 @@ var logger = {
26
26
  }
27
27
  };
28
28
 
29
+ // libs/mcp-worker/src/client/tool-registry.ts
30
+ var ToolRegistry = class {
31
+ // Map to store tool handlers in main thread
32
+ toolHandlers = /* @__PURE__ */ new Map();
33
+ // Tool registry for tracking registrations and reference counting
34
+ toolRegistry = /* @__PURE__ */ new Map();
35
+ // Subscribers for tool changes (for React hooks reactivity)
36
+ toolChangeListeners = /* @__PURE__ */ new Map();
37
+ /**
38
+ * Register a tool with handler (or increment ref count if already exists)
39
+ *
40
+ * @param name - Tool name
41
+ * @param description - Tool description
42
+ * @param inputSchema - JSON Schema for tool inputs
43
+ * @param handler - Async function that handles the tool execution
44
+ * @param options - Additional tool options
45
+ * @returns true if newly registered, false if ref count incremented
46
+ */
47
+ register(name, description, inputSchema, handler, options) {
48
+ const existing = this.toolRegistry.get(name);
49
+ if (existing) {
50
+ existing.refCount++;
51
+ logger.log(
52
+ `[ToolRegistry] Incremented ref count for '${name}': ${existing.refCount}`
53
+ );
54
+ this.toolHandlers.set(name, handler);
55
+ this.notifyToolChange(name);
56
+ return false;
57
+ }
58
+ this.toolHandlers.set(name, handler);
59
+ this.toolRegistry.set(name, {
60
+ name,
61
+ description,
62
+ inputSchema,
63
+ outputSchema: options?.outputSchema,
64
+ annotations: options?.annotations,
65
+ execution: options?.execution,
66
+ _meta: options?._meta,
67
+ icons: options?.icons,
68
+ title: options?.title,
69
+ refCount: 1,
70
+ isRegistered: true
71
+ });
72
+ logger.log(`[ToolRegistry] Registered tool '${name}'`);
73
+ this.notifyToolChange(name);
74
+ return true;
75
+ }
76
+ /**
77
+ * Unregister a tool (decrement ref count, remove if count reaches 0)
78
+ *
79
+ * @param name - Tool name to unregister
80
+ * @returns true if tool was removed (ref count reached 0), false if ref count decremented, null if not found
81
+ */
82
+ unregister(name) {
83
+ const existing = this.toolRegistry.get(name);
84
+ if (!existing) {
85
+ logger.warn(`[ToolRegistry] Cannot unregister '${name}': not found`);
86
+ return null;
87
+ }
88
+ existing.refCount--;
89
+ logger.log(
90
+ `[ToolRegistry] Decremented ref count for '${name}': ${existing.refCount}`
91
+ );
92
+ if (existing.refCount <= 0) {
93
+ this.toolHandlers.delete(name);
94
+ this.toolRegistry.delete(name);
95
+ logger.log(`[ToolRegistry] Removed tool '${name}' from registry`);
96
+ this.notifyToolChange(name);
97
+ return true;
98
+ }
99
+ this.notifyToolChange(name);
100
+ return false;
101
+ }
102
+ /**
103
+ * Get tool handler function
104
+ */
105
+ getHandler(name) {
106
+ return this.toolHandlers.get(name);
107
+ }
108
+ /**
109
+ * Get tool info (ref count and registration status)
110
+ */
111
+ getInfo(name) {
112
+ const info = this.toolRegistry.get(name);
113
+ if (!info)
114
+ return null;
115
+ return {
116
+ refCount: info.refCount,
117
+ isRegistered: info.isRegistered
118
+ };
119
+ }
120
+ /**
121
+ * Get complete tool details
122
+ */
123
+ getDetails(name) {
124
+ return this.toolRegistry.get(name) ?? null;
125
+ }
126
+ /**
127
+ * Get all registered tool names
128
+ */
129
+ getRegisteredTools() {
130
+ return Array.from(this.toolRegistry.keys()).filter(
131
+ (name) => this.toolRegistry.get(name)?.isRegistered
132
+ );
133
+ }
134
+ /**
135
+ * Check if a tool is registered
136
+ */
137
+ isRegistered(name) {
138
+ return this.toolRegistry.get(name)?.isRegistered ?? false;
139
+ }
140
+ /**
141
+ * Subscribe to tool changes for a specific tool
142
+ * Returns unsubscribe function
143
+ */
144
+ onToolChange(toolName, callback) {
145
+ if (!this.toolChangeListeners.has(toolName)) {
146
+ this.toolChangeListeners.set(toolName, /* @__PURE__ */ new Set());
147
+ }
148
+ const listeners = this.toolChangeListeners.get(toolName);
149
+ listeners.add(callback);
150
+ try {
151
+ const current = this.toolRegistry.get(toolName);
152
+ if (current) {
153
+ callback({
154
+ refCount: current.refCount,
155
+ isRegistered: current.isRegistered
156
+ });
157
+ } else {
158
+ callback(null);
159
+ }
160
+ } catch (error) {
161
+ logger.error(
162
+ "[ToolRegistry] Error in tool change listener (initial call):",
163
+ error
164
+ );
165
+ }
166
+ return () => {
167
+ listeners.delete(callback);
168
+ if (listeners.size === 0) {
169
+ this.toolChangeListeners.delete(toolName);
170
+ }
171
+ };
172
+ }
173
+ /**
174
+ * Notify all listeners about tool changes
175
+ * @private
176
+ */
177
+ notifyToolChange(toolName) {
178
+ const listeners = this.toolChangeListeners.get(toolName);
179
+ if (!listeners || listeners.size === 0)
180
+ return;
181
+ const info = this.toolRegistry.get(toolName);
182
+ const payload = info ? {
183
+ refCount: info.refCount,
184
+ isRegistered: info.isRegistered
185
+ } : null;
186
+ listeners.forEach((callback) => {
187
+ try {
188
+ callback(payload);
189
+ } catch (error) {
190
+ logger.error("[ToolRegistry] Error in tool change listener:", error);
191
+ }
192
+ });
193
+ }
194
+ /**
195
+ * Clear all tools (useful for cleanup/testing)
196
+ */
197
+ clear() {
198
+ this.toolHandlers.clear();
199
+ this.toolRegistry.clear();
200
+ this.toolChangeListeners.clear();
201
+ logger.log("[ToolRegistry] Cleared all tools");
202
+ }
203
+ /**
204
+ * Get all tool names (including those with refCount > 0)
205
+ */
206
+ getAllToolNames() {
207
+ return Array.from(this.toolRegistry.keys());
208
+ }
209
+ };
210
+
211
+ // libs/mcp-worker/src/client/web-mcp-adapter.ts
212
+ var WebMcpAdapter = class _WebMcpAdapter {
213
+ /** Tracks names of tools currently registered via WebMCP API */
214
+ registeredTools = /* @__PURE__ */ new Set();
215
+ /** Whether the adapter is enabled (enabled by default — auto-detects browser support) */
216
+ enabled = true;
217
+ // --------------------------------------------------------------------------
218
+ // Feature detection
219
+ // --------------------------------------------------------------------------
220
+ /**
221
+ * Check if the browser exposes the WebMCP API (`navigator.modelContext`).
222
+ *
223
+ * Safe to call in any environment (SSR, workers, older browsers).
224
+ *
225
+ * @see https://webmachinelearning.github.io/webmcp/#navigator-extension
226
+ */
227
+ static isSupported() {
228
+ try {
229
+ return typeof navigator !== "undefined" && "modelContext" in navigator && navigator.modelContext != null && typeof navigator.modelContext.registerTool === "function";
230
+ } catch {
231
+ return false;
232
+ }
233
+ }
234
+ /**
235
+ * Convenience: instance-level check that also respects the `enabled` flag.
236
+ */
237
+ isAvailable() {
238
+ return this.enabled && _WebMcpAdapter.isSupported();
239
+ }
240
+ // --------------------------------------------------------------------------
241
+ // Configuration
242
+ // --------------------------------------------------------------------------
243
+ /**
244
+ * Enable or disable the WebMCP adapter.
245
+ *
246
+ * When disabled, `registerTool` / `unregisterTool` become silent no-ops
247
+ * even if the browser supports the API.
248
+ */
249
+ setEnabled(value) {
250
+ this.enabled = value;
251
+ logger.log(`[WebMcpAdapter] Enabled: ${value}`);
252
+ }
253
+ // --------------------------------------------------------------------------
254
+ // Registration
255
+ // --------------------------------------------------------------------------
256
+ /**
257
+ * Register a tool with the WebMCP API (`navigator.modelContext.registerTool()`).
258
+ *
259
+ * Silently returns if the API is unavailable or the adapter is disabled.
260
+ * Per spec, `registerTool()` throws if a tool with the same name already exists,
261
+ * so we unregister first if needed (idempotent update).
262
+ *
263
+ * @param name - Tool name
264
+ * @param description - Human-readable description (required by spec)
265
+ * @param inputSchema - JSON Schema for tool inputs
266
+ * @param handler - Async handler executed in the main thread
267
+ * @param options - Additional MCP tool options (annotations, …)
268
+ */
269
+ registerTool(name, description, inputSchema, handler, options) {
270
+ if (!this.isAvailable()) {
271
+ return;
272
+ }
273
+ if (this.registeredTools.has(name)) {
274
+ this.unregisterTool(name);
275
+ }
276
+ try {
277
+ const modelContext = navigator.modelContext;
278
+ const tool = {
279
+ name,
280
+ description: description ?? name,
281
+ inputSchema,
282
+ execute: async (input) => {
283
+ return handler(input);
284
+ },
285
+ annotations: this.mapAnnotations(options?.annotations)
286
+ };
287
+ modelContext.registerTool(tool);
288
+ this.registeredTools.add(name);
289
+ logger.log(
290
+ `[WebMcpAdapter] Registered tool '${name}' via navigator.modelContext`
291
+ );
292
+ } catch (error) {
293
+ logger.warn(`[WebMcpAdapter] Failed to register '${name}':`, error);
294
+ }
295
+ }
296
+ /**
297
+ * Unregister a tool from the WebMCP API (`navigator.modelContext.unregisterTool()`).
298
+ *
299
+ * Safe to call even if the tool was never registered via WebMCP.
300
+ *
301
+ * @param name - Tool name to unregister
302
+ * @returns `true` if the tool was found and unregistered, `false` otherwise
303
+ */
304
+ unregisterTool(name) {
305
+ if (!this.registeredTools.has(name)) {
306
+ return false;
307
+ }
308
+ try {
309
+ if (_WebMcpAdapter.isSupported()) {
310
+ const modelContext = navigator.modelContext;
311
+ modelContext.unregisterTool(name);
312
+ }
313
+ this.registeredTools.delete(name);
314
+ logger.log(
315
+ `[WebMcpAdapter] Unregistered tool '${name}' from navigator.modelContext`
316
+ );
317
+ return true;
318
+ } catch (error) {
319
+ logger.warn(`[WebMcpAdapter] Failed to unregister '${name}':`, error);
320
+ this.registeredTools.delete(name);
321
+ return false;
322
+ }
323
+ }
324
+ /**
325
+ * Unregister ALL tools registered via WebMCP.
326
+ *
327
+ * Uses `navigator.modelContext.clearContext()` when available (per spec, clears
328
+ * all tools at once), otherwise falls back to individual unregisterTool calls.
329
+ */
330
+ clearAll() {
331
+ if (this.registeredTools.size === 0)
332
+ return;
333
+ logger.log(`[WebMcpAdapter] Clearing ${this.registeredTools.size} tool(s)`);
334
+ try {
335
+ if (_WebMcpAdapter.isSupported()) {
336
+ const modelContext = navigator.modelContext;
337
+ modelContext.clearContext();
338
+ }
339
+ } catch (error) {
340
+ logger.warn(
341
+ "[WebMcpAdapter] clearContext() failed, falling back to individual unregister:",
342
+ error
343
+ );
344
+ const names = Array.from(this.registeredTools);
345
+ names.forEach((name) => {
346
+ try {
347
+ if (_WebMcpAdapter.isSupported()) {
348
+ navigator.modelContext.unregisterTool(name);
349
+ }
350
+ } catch {
351
+ }
352
+ });
353
+ }
354
+ this.registeredTools.clear();
355
+ }
356
+ // --------------------------------------------------------------------------
357
+ // Query
358
+ // --------------------------------------------------------------------------
359
+ /**
360
+ * Check if a tool is registered via WebMCP.
361
+ */
362
+ isRegistered(name) {
363
+ return this.registeredTools.has(name);
364
+ }
365
+ /**
366
+ * Get names of all tools registered via WebMCP.
367
+ */
368
+ getRegisteredTools() {
369
+ return Array.from(this.registeredTools);
370
+ }
371
+ // --------------------------------------------------------------------------
372
+ // Mapping helpers
373
+ // --------------------------------------------------------------------------
374
+ /**
375
+ * Map internal ToolAnnotations to WebMCP ToolAnnotations.
376
+ * The spec currently only defines `readOnlyHint`.
377
+ */
378
+ mapAnnotations(annotations) {
379
+ if (!annotations)
380
+ return void 0;
381
+ return {
382
+ readOnlyHint: annotations.readOnlyHint
383
+ };
384
+ }
385
+ /**
386
+ * Convert internal ToolDefinition to ModelContextTool format (utility).
387
+ * Requires an execute callback to be provided separately.
388
+ */
389
+ static toModelContextTool(tool, handler) {
390
+ return {
391
+ name: tool.name,
392
+ description: tool.description ?? tool.name,
393
+ inputSchema: tool.inputSchema,
394
+ execute: async (input) => {
395
+ return handler(input);
396
+ },
397
+ annotations: tool.annotations ? { readOnlyHint: tool.annotations.readOnlyHint } : void 0
398
+ };
399
+ }
400
+ };
401
+
29
402
  // libs/mcp-worker/src/client/worker-client.ts
30
403
  var WorkerClient = class _WorkerClient {
31
404
  // Configurable worker script URLs (defaults kept for backward compatibility)
@@ -45,14 +418,10 @@ var WorkerClient = class _WorkerClient {
45
418
  // Initialization state
46
419
  isInitialized = false;
47
420
  initResolvers = [];
48
- // Queue for operations that need to wait for initialization
49
- pendingRegistrations = [];
50
- // Map to store tool handlers in main thread
51
- toolHandlers = /* @__PURE__ */ new Map();
52
- // Tool registry for tracking registrations and reference counting
53
- toolRegistry = /* @__PURE__ */ new Map();
54
- // Subscribers for tool changes (for React hooks reactivity)
55
- toolChangeListeners = /* @__PURE__ */ new Map();
421
+ // Tool registry for managing tool lifecycle
422
+ toolRegistry = new ToolRegistry();
423
+ // WebMCP adapter for browser-level tool registration via navigator.modelContext
424
+ webMcpAdapter = new WebMcpAdapter();
56
425
  // Tab tracking for multi-tab support
57
426
  tabId;
58
427
  static TAB_ID_STORAGE_KEY = "mcp_fe_tab_id";
@@ -156,26 +525,27 @@ var WorkerClient = class _WorkerClient {
156
525
  * @private
157
526
  */
158
527
  cleanupAllTools() {
159
- const toolNames = Array.from(this.toolRegistry.keys());
528
+ const toolNames = this.toolRegistry.getAllToolNames();
160
529
  if (toolNames.length === 0) {
161
530
  return;
162
531
  }
163
532
  logger.log(
164
533
  `[WorkerClient] Page unloading, cleaning up ${toolNames.length} tool(s)`
165
534
  );
535
+ this.webMcpAdapter.clearAll();
166
536
  toolNames.forEach((toolName) => {
167
- const existing = this.toolRegistry.get(toolName);
537
+ const existing = this.toolRegistry.getInfo(toolName);
168
538
  if (!existing)
169
539
  return;
170
- existing.refCount = 1;
171
540
  try {
172
541
  this.post("UNREGISTER_TOOL", {
173
542
  name: toolName,
174
543
  tabId: this.tabId
175
544
  }).catch(() => {
176
545
  });
177
- this.toolHandlers.delete(toolName);
178
- this.toolRegistry.delete(toolName);
546
+ while (this.toolRegistry.isRegistered(toolName)) {
547
+ this.toolRegistry.unregister(toolName);
548
+ }
179
549
  } catch (error) {
180
550
  logger.warn(
181
551
  `[WorkerClient] Failed to unregister '${toolName}' during cleanup:`,
@@ -207,6 +577,14 @@ var WorkerClient = class _WorkerClient {
207
577
  this.serviceWorkerUrl = opts.serviceWorkerUrl;
208
578
  if (opts.backendWsUrl)
209
579
  this.backendWsUrl = opts.backendWsUrl;
580
+ if (opts.enableWebMcp !== void 0) {
581
+ this.webMcpAdapter.setEnabled(opts.enableWebMcp);
582
+ }
583
+ }
584
+ if (this.webMcpAdapter.isAvailable()) {
585
+ logger.log(
586
+ "[WorkerClient] WebMCP: auto-enabled (navigator.modelContext detected)"
587
+ );
210
588
  }
211
589
  if (this.initPromise) {
212
590
  return this.initPromise.then(async () => {
@@ -253,43 +631,74 @@ var WorkerClient = class _WorkerClient {
253
631
  return this.initPromise;
254
632
  }
255
633
  /**
256
- * Mark worker as initialized and process pending registrations
634
+ * Mark worker as initialized and sync all registered tools to worker
257
635
  * @private
258
636
  */
259
637
  markAsInitialized() {
260
638
  this.isInitialized = true;
261
- logger.log(
262
- "[WorkerClient] Worker initialized, processing pending operations"
263
- );
639
+ logger.log("[WorkerClient] Worker initialized, syncing tools to worker");
264
640
  this.registerTab();
265
641
  this.setActiveTab();
266
642
  this.initResolvers.forEach((resolve) => resolve());
267
643
  this.initResolvers = [];
268
- const pending = [...this.pendingRegistrations];
269
- this.pendingRegistrations = [];
270
- pending.forEach(
271
- async ({
272
- name,
273
- description,
274
- inputSchema,
275
- handler,
276
- options,
277
- resolve,
278
- reject
279
- }) => {
644
+ this.syncToolsToWorker().catch((error) => {
645
+ logger.error("[WorkerClient] Error syncing tools to worker:", error);
646
+ });
647
+ }
648
+ /**
649
+ * Synchronize all locally registered tools to worker
650
+ * @private
651
+ */
652
+ async syncToolsToWorker() {
653
+ const toolNames = this.toolRegistry.getRegisteredTools();
654
+ if (toolNames.length === 0) {
655
+ logger.log("[WorkerClient] No tools to sync");
656
+ return;
657
+ }
658
+ logger.log(`[WorkerClient] Syncing ${toolNames.length} tool(s) to worker`);
659
+ await Promise.all(
660
+ toolNames.map(async (toolName) => {
661
+ const details = this.toolRegistry.getDetails(toolName);
662
+ if (!details)
663
+ return;
664
+ const handler = this.toolRegistry.getHandler(toolName);
280
665
  try {
281
- await this.registerToolInternal(
282
- name,
283
- description,
284
- inputSchema,
285
- handler,
286
- options
666
+ await this.registerToolInWorker(
667
+ details.name,
668
+ details.description,
669
+ details.inputSchema,
670
+ {
671
+ outputSchema: details.outputSchema,
672
+ annotations: details.annotations,
673
+ execution: details.execution,
674
+ _meta: details._meta,
675
+ icons: details.icons,
676
+ title: details.title
677
+ }
287
678
  );
288
- resolve();
679
+ if (handler) {
680
+ this.webMcpAdapter.registerTool(
681
+ details.name,
682
+ details.description,
683
+ details.inputSchema,
684
+ handler,
685
+ {
686
+ outputSchema: details.outputSchema,
687
+ annotations: details.annotations,
688
+ execution: details.execution,
689
+ _meta: details._meta,
690
+ icons: details.icons,
691
+ title: details.title
692
+ }
693
+ );
694
+ }
289
695
  } catch (error) {
290
- reject(error instanceof Error ? error : new Error(String(error)));
696
+ logger.error(
697
+ `[WorkerClient] Failed to sync tool '${toolName}' to worker:`,
698
+ error
699
+ );
291
700
  }
292
- }
701
+ })
293
702
  );
294
703
  }
295
704
  /**
@@ -798,29 +1207,29 @@ var WorkerClient = class _WorkerClient {
798
1207
  * ```
799
1208
  */
800
1209
  async registerTool(name, description, inputSchema, handler, options) {
801
- if (!this.isInitialized) {
802
- logger.log(
803
- `[WorkerClient] Queueing tool registration '${name}' (worker not initialized yet)`
804
- );
805
- return new Promise((resolve, reject) => {
806
- this.pendingRegistrations.push({
807
- name,
808
- description,
809
- inputSchema,
810
- handler,
811
- options,
812
- resolve,
813
- reject
814
- });
815
- });
816
- }
817
- return this.registerToolInternal(
1210
+ const isNew = this.toolRegistry.register(
818
1211
  name,
819
1212
  description,
820
1213
  inputSchema,
821
1214
  handler,
822
1215
  options
823
1216
  );
1217
+ if (!this.isInitialized) {
1218
+ logger.log(
1219
+ `[WorkerClient] Registered '${name}' locally (will sync to worker after init)`
1220
+ );
1221
+ return;
1222
+ }
1223
+ if (isNew) {
1224
+ await this.registerToolInWorker(name, description, inputSchema, options);
1225
+ this.webMcpAdapter.registerTool(
1226
+ name,
1227
+ description,
1228
+ inputSchema,
1229
+ handler,
1230
+ options
1231
+ );
1232
+ }
824
1233
  }
825
1234
  /**
826
1235
  * Enhance tool input schema with optional tabId parameter for multi-tab support
@@ -838,21 +1247,10 @@ var WorkerClient = class _WorkerClient {
838
1247
  return enhanced;
839
1248
  }
840
1249
  /**
841
- * Internal method to register tool (assumes worker is initialized)
1250
+ * Register tool in worker only (assumes already registered in ToolRegistry)
842
1251
  * @private
843
1252
  */
844
- async registerToolInternal(name, description, inputSchema, handler, options) {
845
- const existing = this.toolRegistry.get(name);
846
- if (existing) {
847
- existing.refCount++;
848
- logger.log(
849
- `[WorkerClient] Incremented ref count for '${name}': ${existing.refCount}`
850
- );
851
- this.toolHandlers.set(name, handler);
852
- this.notifyToolChange(name);
853
- return;
854
- }
855
- this.toolHandlers.set(name, handler);
1253
+ async registerToolInWorker(name, description, inputSchema, options) {
856
1254
  const enhancedSchema = this.enhanceSchemaWithTabId(inputSchema);
857
1255
  await this.request("REGISTER_TOOL", {
858
1256
  name,
@@ -869,23 +1267,9 @@ var WorkerClient = class _WorkerClient {
869
1267
  tabId: this.tabId
870
1268
  // Tell worker which tab registered this
871
1269
  });
872
- this.toolRegistry.set(name, {
873
- name,
874
- description,
875
- inputSchema: enhancedSchema,
876
- outputSchema: options?.outputSchema,
877
- annotations: options?.annotations,
878
- execution: options?.execution,
879
- _meta: options?._meta,
880
- icons: options?.icons,
881
- title: options?.title,
882
- refCount: 1,
883
- isRegistered: true
884
- });
885
1270
  logger.log(
886
1271
  `[WorkerClient] Registered tool '${name}' with main-thread handler`
887
1272
  );
888
- this.notifyToolChange(name);
889
1273
  }
890
1274
  /**
891
1275
  * Unregister a previously registered MCP tool
@@ -893,27 +1277,19 @@ var WorkerClient = class _WorkerClient {
893
1277
  * @returns Promise that resolves to true if tool was found and removed
894
1278
  */
895
1279
  async unregisterTool(name) {
896
- const existing = this.toolRegistry.get(name);
897
- if (!existing) {
898
- logger.warn(`[WorkerClient] Cannot unregister '${name}': not found`);
1280
+ const result = this.toolRegistry.unregister(name);
1281
+ if (result === null) {
899
1282
  return false;
900
1283
  }
901
- existing.refCount--;
902
- logger.log(
903
- `[WorkerClient] Decremented ref count for '${name}': ${existing.refCount}`
904
- );
905
- if (existing.refCount <= 0) {
906
- this.toolHandlers.delete(name);
907
- const result = await this.request(
1284
+ if (result) {
1285
+ const workerResult = await this.request(
908
1286
  "UNREGISTER_TOOL",
909
1287
  { name, tabId: this.tabId }
910
1288
  );
911
- this.toolRegistry.delete(name);
1289
+ this.webMcpAdapter.unregisterTool(name);
912
1290
  logger.log(`[WorkerClient] Unregistered tool '${name}'`);
913
- this.notifyToolChange(name);
914
- return result?.success ?? false;
1291
+ return workerResult?.success ?? false;
915
1292
  }
916
- this.notifyToolChange(name);
917
1293
  return true;
918
1294
  }
919
1295
  /**
@@ -921,82 +1297,91 @@ var WorkerClient = class _WorkerClient {
921
1297
  * Returns unsubscribe function
922
1298
  */
923
1299
  onToolChange(toolName, callback) {
924
- if (!this.toolChangeListeners.has(toolName)) {
925
- this.toolChangeListeners.set(toolName, /* @__PURE__ */ new Set());
926
- }
927
- const listeners = this.toolChangeListeners.get(toolName);
928
- listeners.add(callback);
929
- const current = this.toolRegistry.get(toolName);
930
- if (current) {
931
- callback({
932
- refCount: current.refCount,
933
- isRegistered: current.isRegistered
934
- });
935
- } else {
936
- callback(null);
937
- }
938
- return () => {
939
- listeners.delete(callback);
940
- if (listeners.size === 0) {
941
- this.toolChangeListeners.delete(toolName);
942
- }
943
- };
944
- }
945
- /**
946
- * Notify all listeners about tool changes
947
- * @private
948
- */
949
- notifyToolChange(toolName) {
950
- const listeners = this.toolChangeListeners.get(toolName);
951
- if (!listeners || listeners.size === 0)
952
- return;
953
- const info = this.toolRegistry.get(toolName);
954
- const payload = info ? {
955
- refCount: info.refCount,
956
- isRegistered: info.isRegistered
957
- } : null;
958
- listeners.forEach((callback) => {
959
- try {
960
- callback(payload);
961
- } catch (error) {
962
- logger.error("[WorkerClient] Error in tool change listener:", error);
963
- }
964
- });
1300
+ return this.toolRegistry.onToolChange(toolName, callback);
965
1301
  }
966
1302
  /**
967
1303
  * Get tool info from registry
968
1304
  */
969
1305
  getToolInfo(toolName) {
970
- const info = this.toolRegistry.get(toolName);
971
- if (!info)
972
- return null;
973
- return {
974
- refCount: info.refCount,
975
- isRegistered: info.isRegistered
976
- };
1306
+ return this.toolRegistry.getInfo(toolName);
977
1307
  }
978
1308
  /**
979
1309
  * Get complete tool details from registry
980
1310
  */
981
1311
  getToolDetails(toolName) {
982
- const info = this.toolRegistry.get(toolName);
983
- if (!info)
984
- return null;
985
- return info;
1312
+ return this.toolRegistry.getDetails(toolName);
986
1313
  }
987
1314
  /**
988
1315
  * Get all registered tool names
989
1316
  */
990
1317
  getRegisteredTools() {
991
- return Array.from(this.toolRegistry.keys()).filter(
992
- (name) => this.toolRegistry.get(name)?.isRegistered
993
- );
1318
+ return this.toolRegistry.getRegisteredTools();
994
1319
  }
995
1320
  /**
996
1321
  * Check if a tool is registered
997
1322
  */
998
1323
  isToolRegistered(toolName) {
999
- return this.toolRegistry.get(toolName)?.isRegistered ?? false;
1324
+ return this.toolRegistry.isRegistered(toolName);
1325
+ }
1326
+ // --------------------------------------------------------------------------
1327
+ // WebMCP API
1328
+ // --------------------------------------------------------------------------
1329
+ /**
1330
+ * Check if the browser supports the WebMCP API (`navigator.modelContext`).
1331
+ */
1332
+ static isWebMcpSupported() {
1333
+ return WebMcpAdapter.isSupported();
1334
+ }
1335
+ /**
1336
+ * Check if WebMCP is both enabled AND supported.
1337
+ */
1338
+ isWebMcpAvailable() {
1339
+ return this.webMcpAdapter.isAvailable();
1340
+ }
1341
+ /**
1342
+ * Enable or disable WebMCP registration at runtime.
1343
+ * When enabled and the browser supports it, newly registered tools will also
1344
+ * be advertised via `navigator.modelContext`. Existing tools are synced immediately.
1345
+ */
1346
+ setWebMcpEnabled(enabled) {
1347
+ this.webMcpAdapter.setEnabled(enabled);
1348
+ if (enabled && WebMcpAdapter.isSupported() && this.isInitialized) {
1349
+ const toolNames = this.toolRegistry.getRegisteredTools();
1350
+ toolNames.forEach((toolName) => {
1351
+ const details = this.toolRegistry.getDetails(toolName);
1352
+ const handler = this.toolRegistry.getHandler(toolName);
1353
+ if (!details || !handler)
1354
+ return;
1355
+ this.webMcpAdapter.registerTool(
1356
+ details.name,
1357
+ details.description,
1358
+ details.inputSchema,
1359
+ handler,
1360
+ {
1361
+ outputSchema: details.outputSchema,
1362
+ annotations: details.annotations,
1363
+ execution: details.execution,
1364
+ _meta: details._meta,
1365
+ icons: details.icons,
1366
+ title: details.title
1367
+ }
1368
+ );
1369
+ });
1370
+ } else if (!enabled) {
1371
+ this.webMcpAdapter.clearAll();
1372
+ }
1373
+ }
1374
+ /**
1375
+ * Check if a specific tool is registered via the WebMCP API.
1376
+ */
1377
+ isToolRegisteredViaWebMcp(toolName) {
1378
+ return this.webMcpAdapter.isRegistered(toolName);
1379
+ }
1380
+ /**
1381
+ * Get names of tools registered via the WebMCP API.
1382
+ */
1383
+ getWebMcpRegisteredTools() {
1384
+ return this.webMcpAdapter.getRegisteredTools();
1000
1385
  }
1001
1386
  /**
1002
1387
  * Handle tool call from worker - execute handler in main thread and return result
@@ -1008,7 +1393,7 @@ var WorkerClient = class _WorkerClient {
1008
1393
  args
1009
1394
  });
1010
1395
  try {
1011
- const handler = this.toolHandlers.get(toolName);
1396
+ const handler = this.toolRegistry.getHandler(toolName);
1012
1397
  if (!handler) {
1013
1398
  throw new Error(`Tool handler not found: ${toolName}`);
1014
1399
  }
@@ -1137,6 +1522,7 @@ async function queryEvents(filters) {
1137
1522
  });
1138
1523
  }
1139
1524
  export {
1525
+ WebMcpAdapter,
1140
1526
  WorkerClient,
1141
1527
  logger,
1142
1528
  queryEvents,