@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/docs/index.md +1 -0
- package/docs/native-webmcp.md +232 -0
- package/index.js +539 -153
- package/mcp-service-worker.js +10 -0
- package/mcp-shared-worker.js +10 -0
- 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/tool-registry.d.ts +117 -0
- package/src/client/tool-registry.d.ts.map +1 -0
- 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 +41 -37
- package/src/client/worker-client.d.ts.map +1 -1
- package/src/worker/mcp-controller.d.ts.map +1 -1
- package/src/worker/mcp-server.d.ts +5 -0
- package/src/worker/mcp-server.d.ts.map +1 -1
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
|
-
//
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
178
|
-
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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.
|
|
282
|
-
name,
|
|
283
|
-
description,
|
|
284
|
-
inputSchema,
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1250
|
+
* Register tool in worker only (assumes already registered in ToolRegistry)
|
|
842
1251
|
* @private
|
|
843
1252
|
*/
|
|
844
|
-
async
|
|
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
|
|
897
|
-
if (
|
|
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
|
-
|
|
902
|
-
|
|
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.
|
|
1289
|
+
this.webMcpAdapter.unregisterTool(name);
|
|
912
1290
|
logger.log(`[WorkerClient] Unregistered tool '${name}'`);
|
|
913
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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,
|