@mcp-b/global 1.1.0 → 1.1.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/README.md +375 -0
- package/dist/index.d.ts +137 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.iife.js +4 -4
- package/dist/index.js +678 -34
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -101,47 +101,511 @@ function validateWithZod(data, validator) {
|
|
|
101
101
|
//#endregion
|
|
102
102
|
//#region src/global.ts
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
104
|
+
* Detect if the native Chromium Web Model Context API is available.
|
|
105
|
+
* Checks for both navigator.modelContext and navigator.modelContextTesting,
|
|
106
|
+
* and verifies they are native implementations (not polyfills) by examining
|
|
107
|
+
* the constructor name.
|
|
108
|
+
*
|
|
109
|
+
* @returns Detection result with flags for native context and testing API availability
|
|
110
|
+
*/
|
|
111
|
+
function detectNativeAPI() {
|
|
112
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") return {
|
|
113
|
+
hasNativeContext: false,
|
|
114
|
+
hasNativeTesting: false
|
|
115
|
+
};
|
|
116
|
+
const modelContext = navigator.modelContext;
|
|
117
|
+
const modelContextTesting = navigator.modelContextTesting;
|
|
118
|
+
if (!modelContext || !modelContextTesting) return {
|
|
119
|
+
hasNativeContext: false,
|
|
120
|
+
hasNativeTesting: false
|
|
121
|
+
};
|
|
122
|
+
if ((modelContextTesting.constructor?.name || "").includes("WebModelContext")) return {
|
|
123
|
+
hasNativeContext: false,
|
|
124
|
+
hasNativeTesting: false
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
hasNativeContext: true,
|
|
128
|
+
hasNativeTesting: true
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Adapter that wraps the native Chromium Web Model Context API.
|
|
133
|
+
* Synchronizes tool changes from the native API to the MCP bridge,
|
|
134
|
+
* enabling MCP clients to stay in sync with the native tool registry.
|
|
135
|
+
*
|
|
136
|
+
* Key features:
|
|
137
|
+
* - Listens to native tool changes via registerToolsChangedCallback()
|
|
138
|
+
* - Syncs native tools to MCP bridge automatically
|
|
139
|
+
* - Delegates tool execution to native API
|
|
140
|
+
* - Converts native results to MCP ToolResponse format
|
|
141
|
+
*
|
|
142
|
+
* @class NativeModelContextAdapter
|
|
143
|
+
* @implements {InternalModelContext}
|
|
144
|
+
*/
|
|
145
|
+
var NativeModelContextAdapter = class {
|
|
146
|
+
nativeContext;
|
|
147
|
+
nativeTesting;
|
|
148
|
+
bridge;
|
|
149
|
+
syncInProgress = false;
|
|
150
|
+
/**
|
|
151
|
+
* Creates a new NativeModelContextAdapter.
|
|
152
|
+
*
|
|
153
|
+
* @param {MCPBridge} bridge - The MCP bridge instance
|
|
154
|
+
* @param {ModelContext} nativeContext - The native navigator.modelContext
|
|
155
|
+
* @param {ModelContextTesting} nativeTesting - The native navigator.modelContextTesting
|
|
156
|
+
*/
|
|
157
|
+
constructor(bridge, nativeContext, nativeTesting) {
|
|
158
|
+
this.bridge = bridge;
|
|
159
|
+
this.nativeContext = nativeContext;
|
|
160
|
+
this.nativeTesting = nativeTesting;
|
|
161
|
+
this.nativeTesting.registerToolsChangedCallback(() => {
|
|
162
|
+
console.log("[Native Adapter] Tool change detected from native API");
|
|
163
|
+
this.syncToolsFromNative();
|
|
164
|
+
});
|
|
165
|
+
this.syncToolsFromNative();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Synchronizes tools from the native API to the MCP bridge.
|
|
169
|
+
* Fetches all tools from navigator.modelContextTesting.listTools()
|
|
170
|
+
* and updates the bridge's tool registry.
|
|
171
|
+
*
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
syncToolsFromNative() {
|
|
175
|
+
if (this.syncInProgress) return;
|
|
176
|
+
this.syncInProgress = true;
|
|
177
|
+
try {
|
|
178
|
+
const nativeTools = this.nativeTesting.listTools();
|
|
179
|
+
console.log(`[Native Adapter] Syncing ${nativeTools.length} tools from native API`);
|
|
180
|
+
this.bridge.tools.clear();
|
|
181
|
+
for (const toolInfo of nativeTools) try {
|
|
182
|
+
const inputSchema = JSON.parse(toolInfo.inputSchema);
|
|
183
|
+
const validatedTool = {
|
|
184
|
+
name: toolInfo.name,
|
|
185
|
+
description: toolInfo.description,
|
|
186
|
+
inputSchema,
|
|
187
|
+
execute: async (args) => {
|
|
188
|
+
const result = await this.nativeTesting.executeTool(toolInfo.name, JSON.stringify(args));
|
|
189
|
+
return this.convertToToolResponse(result);
|
|
190
|
+
},
|
|
191
|
+
inputValidator: jsonSchemaToZod$1(inputSchema)
|
|
192
|
+
};
|
|
193
|
+
this.bridge.tools.set(toolInfo.name, validatedTool);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(`[Native Adapter] Failed to sync tool "${toolInfo.name}":`, error);
|
|
196
|
+
}
|
|
197
|
+
this.notifyMCPServers();
|
|
198
|
+
} finally {
|
|
199
|
+
this.syncInProgress = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Converts native API result to MCP ToolResponse format.
|
|
204
|
+
* Native API returns simplified values (string, number, object, etc.)
|
|
205
|
+
* which need to be wrapped in the MCP CallToolResult format.
|
|
206
|
+
*
|
|
207
|
+
* @param {unknown} result - The result from native executeTool()
|
|
208
|
+
* @returns {ToolResponse} Formatted MCP ToolResponse
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
convertToToolResponse(result) {
|
|
212
|
+
if (typeof result === "string") return { content: [{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: result
|
|
215
|
+
}] };
|
|
216
|
+
if (result === void 0 || result === null) return { content: [{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: ""
|
|
219
|
+
}] };
|
|
220
|
+
if (typeof result === "object") return {
|
|
221
|
+
content: [{
|
|
222
|
+
type: "text",
|
|
223
|
+
text: JSON.stringify(result, null, 2)
|
|
224
|
+
}],
|
|
225
|
+
structuredContent: result
|
|
226
|
+
};
|
|
227
|
+
return { content: [{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: String(result)
|
|
230
|
+
}] };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Notifies all connected MCP servers that the tools list has changed.
|
|
234
|
+
*
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
notifyMCPServers() {
|
|
238
|
+
if (this.bridge.tabServer?.notification) this.bridge.tabServer.notification({
|
|
239
|
+
method: "notifications/tools/list_changed",
|
|
240
|
+
params: {}
|
|
241
|
+
});
|
|
242
|
+
if (this.bridge.iframeServer?.notification) this.bridge.iframeServer.notification({
|
|
243
|
+
method: "notifications/tools/list_changed",
|
|
244
|
+
params: {}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Provides context (tools) to AI models via the native API.
|
|
249
|
+
* Delegates to navigator.modelContext.provideContext().
|
|
250
|
+
* Tool change callback will fire and trigger sync automatically.
|
|
251
|
+
*
|
|
252
|
+
* @param {ModelContextInput} context - Context containing tools to register
|
|
253
|
+
*/
|
|
254
|
+
provideContext(context) {
|
|
255
|
+
console.log("[Native Adapter] Delegating provideContext to native API");
|
|
256
|
+
this.nativeContext.provideContext(context);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Registers a single tool dynamically via the native API.
|
|
260
|
+
* Delegates to navigator.modelContext.registerTool().
|
|
261
|
+
* Tool change callback will fire and trigger sync automatically.
|
|
262
|
+
*
|
|
263
|
+
* @param {ToolDescriptor} tool - The tool descriptor to register
|
|
264
|
+
* @returns {{unregister: () => void}} Object with unregister function
|
|
265
|
+
*/
|
|
266
|
+
registerTool(tool) {
|
|
267
|
+
console.log(`[Native Adapter] Delegating registerTool("${tool.name}") to native API`);
|
|
268
|
+
return this.nativeContext.registerTool(tool);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Unregisters a tool by name via the native API.
|
|
272
|
+
* Delegates to navigator.modelContext.unregisterTool().
|
|
273
|
+
*
|
|
274
|
+
* @param {string} name - Name of the tool to unregister
|
|
275
|
+
*/
|
|
276
|
+
unregisterTool(name) {
|
|
277
|
+
console.log(`[Native Adapter] Delegating unregisterTool("${name}") to native API`);
|
|
278
|
+
this.nativeContext.unregisterTool(name);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Clears all registered tools via the native API.
|
|
282
|
+
* Delegates to navigator.modelContext.clearContext().
|
|
283
|
+
*/
|
|
284
|
+
clearContext() {
|
|
285
|
+
console.log("[Native Adapter] Delegating clearContext to native API");
|
|
286
|
+
this.nativeContext.clearContext();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Executes a tool via the native API.
|
|
290
|
+
* Delegates to navigator.modelContextTesting.executeTool() with JSON string args.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} toolName - Name of the tool to execute
|
|
293
|
+
* @param {Record<string, unknown>} args - Arguments to pass to the tool
|
|
294
|
+
* @returns {Promise<ToolResponse>} The tool's response in MCP format
|
|
295
|
+
* @internal
|
|
296
|
+
*/
|
|
297
|
+
async executeTool(toolName, args) {
|
|
298
|
+
console.log(`[Native Adapter] Executing tool "${toolName}" via native API`);
|
|
299
|
+
try {
|
|
300
|
+
const result = await this.nativeTesting.executeTool(toolName, JSON.stringify(args));
|
|
301
|
+
return this.convertToToolResponse(result);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(`[Native Adapter] Error executing tool "${toolName}":`, error);
|
|
304
|
+
return {
|
|
305
|
+
content: [{
|
|
306
|
+
type: "text",
|
|
307
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
308
|
+
}],
|
|
309
|
+
isError: true
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Lists all registered tools from the MCP bridge.
|
|
315
|
+
* Returns tools synced from the native API.
|
|
316
|
+
*
|
|
317
|
+
* @returns {Array<{name: string, description: string, inputSchema: InputSchema}>} Array of tool descriptors
|
|
318
|
+
*/
|
|
319
|
+
listTools() {
|
|
320
|
+
return Array.from(this.bridge.tools.values()).map((tool) => ({
|
|
321
|
+
name: tool.name,
|
|
322
|
+
description: tool.description,
|
|
323
|
+
inputSchema: tool.inputSchema,
|
|
324
|
+
...tool.outputSchema && { outputSchema: tool.outputSchema },
|
|
325
|
+
...tool.annotations && { annotations: tool.annotations }
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Adds an event listener for tool call events.
|
|
330
|
+
* Delegates to the native API's addEventListener.
|
|
331
|
+
*
|
|
332
|
+
* @param {'toolcall'} type - Event type
|
|
333
|
+
* @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler
|
|
334
|
+
* @param {boolean | AddEventListenerOptions} [options] - Event listener options
|
|
335
|
+
*/
|
|
336
|
+
addEventListener(type, listener, options) {
|
|
337
|
+
this.nativeContext.addEventListener(type, listener, options);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Removes an event listener for tool call events.
|
|
341
|
+
* Delegates to the native API's removeEventListener.
|
|
342
|
+
*
|
|
343
|
+
* @param {'toolcall'} type - Event type
|
|
344
|
+
* @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler
|
|
345
|
+
* @param {boolean | EventListenerOptions} [options] - Event listener options
|
|
346
|
+
*/
|
|
347
|
+
removeEventListener(type, listener, options) {
|
|
348
|
+
this.nativeContext.removeEventListener(type, listener, options);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Dispatches a tool call event.
|
|
352
|
+
* Delegates to the native API's dispatchEvent.
|
|
353
|
+
*
|
|
354
|
+
* @param {Event} event - The event to dispatch
|
|
355
|
+
* @returns {boolean} False if event was cancelled, true otherwise
|
|
356
|
+
*/
|
|
357
|
+
dispatchEvent(event) {
|
|
358
|
+
return this.nativeContext.dispatchEvent(event);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* ToolCallEvent implementation for the Web Model Context API.
|
|
363
|
+
* Represents an event fired when a tool is called, allowing event listeners
|
|
364
|
+
* to intercept and provide custom responses.
|
|
365
|
+
*
|
|
366
|
+
* @class WebToolCallEvent
|
|
367
|
+
* @extends {Event}
|
|
368
|
+
* @implements {ToolCallEvent}
|
|
105
369
|
*/
|
|
106
370
|
var WebToolCallEvent = class extends Event {
|
|
107
371
|
name;
|
|
108
372
|
arguments;
|
|
109
373
|
_response = null;
|
|
110
374
|
_responded = false;
|
|
375
|
+
/**
|
|
376
|
+
* Creates a new ToolCallEvent.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} toolName - Name of the tool being called
|
|
379
|
+
* @param {Record<string, unknown>} args - Validated arguments for the tool
|
|
380
|
+
*/
|
|
111
381
|
constructor(toolName, args) {
|
|
112
382
|
super("toolcall", { cancelable: true });
|
|
113
383
|
this.name = toolName;
|
|
114
384
|
this.arguments = args;
|
|
115
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Provides a response for this tool call, preventing the default tool execution.
|
|
388
|
+
*
|
|
389
|
+
* @param {ToolResponse} response - The response to use instead of executing the tool
|
|
390
|
+
* @throws {Error} If a response has already been provided
|
|
391
|
+
*/
|
|
116
392
|
respondWith(response) {
|
|
117
393
|
if (this._responded) throw new Error("Response already provided for this tool call");
|
|
118
394
|
this._response = response;
|
|
119
395
|
this._responded = true;
|
|
120
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Gets the response provided via respondWith().
|
|
399
|
+
*
|
|
400
|
+
* @returns {ToolResponse | null} The response, or null if none provided
|
|
401
|
+
*/
|
|
121
402
|
getResponse() {
|
|
122
403
|
return this._response;
|
|
123
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Checks whether a response has been provided for this tool call.
|
|
407
|
+
*
|
|
408
|
+
* @returns {boolean} True if respondWith() was called
|
|
409
|
+
*/
|
|
124
410
|
hasResponse() {
|
|
125
411
|
return this._responded;
|
|
126
412
|
}
|
|
127
413
|
};
|
|
128
414
|
/**
|
|
129
|
-
* Time window
|
|
130
|
-
*
|
|
415
|
+
* Time window in milliseconds to detect rapid duplicate tool registrations.
|
|
416
|
+
* Used to filter out double-registrations caused by React Strict Mode.
|
|
131
417
|
*/
|
|
132
418
|
const RAPID_DUPLICATE_WINDOW_MS = 50;
|
|
133
419
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
420
|
+
* Testing API implementation for the Model Context Protocol.
|
|
421
|
+
* Provides debugging, mocking, and testing capabilities for tool execution.
|
|
422
|
+
* Implements both Chromium native methods and polyfill-specific extensions.
|
|
423
|
+
*
|
|
424
|
+
* @class WebModelContextTesting
|
|
425
|
+
* @implements {ModelContextTesting}
|
|
426
|
+
*/
|
|
427
|
+
var WebModelContextTesting = class {
|
|
428
|
+
toolCallHistory = [];
|
|
429
|
+
mockResponses = /* @__PURE__ */ new Map();
|
|
430
|
+
toolsChangedCallbacks = /* @__PURE__ */ new Set();
|
|
431
|
+
bridge;
|
|
432
|
+
/**
|
|
433
|
+
* Creates a new WebModelContextTesting instance.
|
|
434
|
+
*
|
|
435
|
+
* @param {MCPBridge} bridge - The MCP bridge instance to test
|
|
436
|
+
*/
|
|
437
|
+
constructor(bridge) {
|
|
438
|
+
this.bridge = bridge;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Records a tool call in the history.
|
|
442
|
+
* Called internally by WebModelContext when tools are executed.
|
|
443
|
+
*
|
|
444
|
+
* @param {string} toolName - Name of the tool that was called
|
|
445
|
+
* @param {Record<string, unknown>} args - Arguments passed to the tool
|
|
446
|
+
* @internal
|
|
447
|
+
*/
|
|
448
|
+
recordToolCall(toolName, args) {
|
|
449
|
+
this.toolCallHistory.push({
|
|
450
|
+
toolName,
|
|
451
|
+
arguments: args,
|
|
452
|
+
timestamp: Date.now()
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Checks if a mock response is registered for a specific tool.
|
|
457
|
+
*
|
|
458
|
+
* @param {string} toolName - Name of the tool to check
|
|
459
|
+
* @returns {boolean} True if a mock response exists
|
|
460
|
+
* @internal
|
|
461
|
+
*/
|
|
462
|
+
hasMockResponse(toolName) {
|
|
463
|
+
return this.mockResponses.has(toolName);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Retrieves the mock response for a specific tool.
|
|
467
|
+
*
|
|
468
|
+
* @param {string} toolName - Name of the tool
|
|
469
|
+
* @returns {ToolResponse | undefined} The mock response, or undefined if none exists
|
|
470
|
+
* @internal
|
|
471
|
+
*/
|
|
472
|
+
getMockResponse(toolName) {
|
|
473
|
+
return this.mockResponses.get(toolName);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Notifies all registered callbacks that the tools list has changed.
|
|
477
|
+
* Called internally when tools are registered, unregistered, or cleared.
|
|
478
|
+
*
|
|
479
|
+
* @internal
|
|
480
|
+
*/
|
|
481
|
+
notifyToolsChanged() {
|
|
482
|
+
for (const callback of this.toolsChangedCallbacks) try {
|
|
483
|
+
callback();
|
|
484
|
+
} catch (error) {
|
|
485
|
+
console.error("[Model Context Testing] Error in tools changed callback:", error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Executes a tool directly with JSON string input (Chromium native API).
|
|
490
|
+
* Parses the JSON input, validates it, and executes the tool.
|
|
491
|
+
*
|
|
492
|
+
* @param {string} toolName - Name of the tool to execute
|
|
493
|
+
* @param {string} inputArgsJson - JSON string of input arguments
|
|
494
|
+
* @returns {Promise<unknown>} The tool's result, or undefined on error
|
|
495
|
+
* @throws {SyntaxError} If the input JSON is invalid
|
|
496
|
+
* @throws {Error} If the tool does not exist
|
|
497
|
+
*/
|
|
498
|
+
async executeTool(toolName, inputArgsJson) {
|
|
499
|
+
console.log(`[Model Context Testing] Executing tool: ${toolName}`);
|
|
500
|
+
let args;
|
|
501
|
+
try {
|
|
502
|
+
args = JSON.parse(inputArgsJson);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
throw new SyntaxError(`Invalid JSON input: ${error instanceof Error ? error.message : String(error)}`);
|
|
505
|
+
}
|
|
506
|
+
if (!this.bridge.tools.get(toolName)) throw new Error(`Tool not found: ${toolName}`);
|
|
507
|
+
const result = await this.bridge.modelContext.executeTool(toolName, args);
|
|
508
|
+
if (result.isError) return;
|
|
509
|
+
if (result.structuredContent) return result.structuredContent;
|
|
510
|
+
if (result.content && result.content.length > 0) {
|
|
511
|
+
const firstContent = result.content[0];
|
|
512
|
+
if (firstContent && firstContent.type === "text") return firstContent.text;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Lists all registered tools with inputSchema as JSON string (Chromium native API).
|
|
517
|
+
* Returns an array of ToolInfo objects where inputSchema is stringified.
|
|
518
|
+
*
|
|
519
|
+
* @returns {Array<{name: string, description: string, inputSchema: string}>} Array of tool information
|
|
520
|
+
*/
|
|
521
|
+
listTools() {
|
|
522
|
+
return this.bridge.modelContext.listTools().map((tool) => ({
|
|
523
|
+
name: tool.name,
|
|
524
|
+
description: tool.description,
|
|
525
|
+
inputSchema: JSON.stringify(tool.inputSchema)
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Registers a callback that fires when the tools list changes (Chromium native API).
|
|
530
|
+
* The callback will be invoked on registerTool, unregisterTool, provideContext, and clearContext.
|
|
531
|
+
*
|
|
532
|
+
* @param {() => void} callback - Function to call when tools change
|
|
533
|
+
*/
|
|
534
|
+
registerToolsChangedCallback(callback) {
|
|
535
|
+
this.toolsChangedCallbacks.add(callback);
|
|
536
|
+
console.log("[Model Context Testing] Tools changed callback registered");
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Gets all tool calls that have been recorded (polyfill extension).
|
|
540
|
+
*
|
|
541
|
+
* @returns {Array<{toolName: string, arguments: Record<string, unknown>, timestamp: number}>} Tool call history
|
|
542
|
+
*/
|
|
543
|
+
getToolCalls() {
|
|
544
|
+
return [...this.toolCallHistory];
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Clears the tool call history (polyfill extension).
|
|
548
|
+
*/
|
|
549
|
+
clearToolCalls() {
|
|
550
|
+
this.toolCallHistory = [];
|
|
551
|
+
console.log("[Model Context Testing] Tool call history cleared");
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Sets a mock response for a specific tool (polyfill extension).
|
|
555
|
+
* When set, the tool's execute function will be bypassed.
|
|
556
|
+
*
|
|
557
|
+
* @param {string} toolName - Name of the tool to mock
|
|
558
|
+
* @param {ToolResponse} response - The mock response to return
|
|
559
|
+
*/
|
|
560
|
+
setMockToolResponse(toolName, response) {
|
|
561
|
+
this.mockResponses.set(toolName, response);
|
|
562
|
+
console.log(`[Model Context Testing] Mock response set for tool: ${toolName}`);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Clears the mock response for a specific tool (polyfill extension).
|
|
566
|
+
*
|
|
567
|
+
* @param {string} toolName - Name of the tool
|
|
568
|
+
*/
|
|
569
|
+
clearMockToolResponse(toolName) {
|
|
570
|
+
this.mockResponses.delete(toolName);
|
|
571
|
+
console.log(`[Model Context Testing] Mock response cleared for tool: ${toolName}`);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Clears all mock tool responses (polyfill extension).
|
|
575
|
+
*/
|
|
576
|
+
clearAllMockToolResponses() {
|
|
577
|
+
this.mockResponses.clear();
|
|
578
|
+
console.log("[Model Context Testing] All mock responses cleared");
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Gets the current tools registered in the system (polyfill extension).
|
|
582
|
+
*
|
|
583
|
+
* @returns {ReturnType<InternalModelContext['listTools']>} Array of registered tools
|
|
584
|
+
*/
|
|
585
|
+
getRegisteredTools() {
|
|
586
|
+
return this.bridge.modelContext.listTools();
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Resets the entire testing state (polyfill extension).
|
|
590
|
+
* Clears both tool call history and all mock responses.
|
|
591
|
+
*/
|
|
592
|
+
reset() {
|
|
593
|
+
this.clearToolCalls();
|
|
594
|
+
this.clearAllMockToolResponses();
|
|
595
|
+
console.log("[Model Context Testing] Testing state reset");
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* ModelContext implementation that bridges to the Model Context Protocol SDK.
|
|
600
|
+
* Implements the W3C Web Model Context API proposal with two-bucket tool management:
|
|
601
|
+
* - Bucket A (provideContextTools): Tools registered via provideContext()
|
|
602
|
+
* - Bucket B (dynamicTools): Tools registered via registerTool()
|
|
136
603
|
*
|
|
137
|
-
*
|
|
138
|
-
* -
|
|
139
|
-
* - Bucket B (dynamicTools): Tools registered via registerTool() - component-scoped tools
|
|
604
|
+
* This separation ensures that component-scoped dynamic tools persist across
|
|
605
|
+
* app-level provideContext() calls.
|
|
140
606
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* - Components can manage their own tool lifecycle independently
|
|
144
|
-
* - Final tool list = Bucket A + Bucket B (merged, with collision detection)
|
|
607
|
+
* @class WebModelContext
|
|
608
|
+
* @implements {InternalModelContext}
|
|
145
609
|
*/
|
|
146
610
|
var WebModelContext = class {
|
|
147
611
|
bridge;
|
|
@@ -150,6 +614,12 @@ var WebModelContext = class {
|
|
|
150
614
|
dynamicTools;
|
|
151
615
|
registrationTimestamps;
|
|
152
616
|
unregisterFunctions;
|
|
617
|
+
testingAPI;
|
|
618
|
+
/**
|
|
619
|
+
* Creates a new WebModelContext instance.
|
|
620
|
+
*
|
|
621
|
+
* @param {MCPBridge} bridge - The MCP bridge to use for communication
|
|
622
|
+
*/
|
|
153
623
|
constructor(bridge) {
|
|
154
624
|
this.bridge = bridge;
|
|
155
625
|
this.eventTarget = new EventTarget();
|
|
@@ -159,26 +629,51 @@ var WebModelContext = class {
|
|
|
159
629
|
this.unregisterFunctions = /* @__PURE__ */ new Map();
|
|
160
630
|
}
|
|
161
631
|
/**
|
|
162
|
-
*
|
|
632
|
+
* Sets the testing API instance.
|
|
633
|
+
* Called during initialization to enable testing features.
|
|
634
|
+
*
|
|
635
|
+
* @param {WebModelContextTesting} testingAPI - The testing API instance
|
|
636
|
+
* @internal
|
|
637
|
+
*/
|
|
638
|
+
setTestingAPI(testingAPI) {
|
|
639
|
+
this.testingAPI = testingAPI;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Adds an event listener for tool call events.
|
|
643
|
+
*
|
|
644
|
+
* @param {'toolcall'} type - Event type (only 'toolcall' is supported)
|
|
645
|
+
* @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler function
|
|
646
|
+
* @param {boolean | AddEventListenerOptions} [options] - Event listener options
|
|
163
647
|
*/
|
|
164
648
|
addEventListener(type, listener, options) {
|
|
165
649
|
this.eventTarget.addEventListener(type, listener, options);
|
|
166
650
|
}
|
|
167
651
|
/**
|
|
168
|
-
*
|
|
652
|
+
* Removes an event listener for tool call events.
|
|
653
|
+
*
|
|
654
|
+
* @param {'toolcall'} type - Event type (only 'toolcall' is supported)
|
|
655
|
+
* @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler function
|
|
656
|
+
* @param {boolean | EventListenerOptions} [options] - Event listener options
|
|
169
657
|
*/
|
|
170
658
|
removeEventListener(type, listener, options) {
|
|
171
659
|
this.eventTarget.removeEventListener(type, listener, options);
|
|
172
660
|
}
|
|
173
661
|
/**
|
|
174
|
-
*
|
|
662
|
+
* Dispatches a tool call event to all registered listeners.
|
|
663
|
+
*
|
|
664
|
+
* @param {Event} event - The event to dispatch
|
|
665
|
+
* @returns {boolean} False if event was cancelled, true otherwise
|
|
175
666
|
*/
|
|
176
667
|
dispatchEvent(event) {
|
|
177
668
|
return this.eventTarget.dispatchEvent(event);
|
|
178
669
|
}
|
|
179
670
|
/**
|
|
180
|
-
*
|
|
181
|
-
* Clears and replaces
|
|
671
|
+
* Provides context (tools) to AI models by registering base tools (Bucket A).
|
|
672
|
+
* Clears and replaces all previously registered base tools while preserving
|
|
673
|
+
* dynamic tools registered via registerTool().
|
|
674
|
+
*
|
|
675
|
+
* @param {ModelContextInput} context - Context containing tools to register
|
|
676
|
+
* @throws {Error} If a tool name collides with an existing dynamic tool
|
|
182
677
|
*/
|
|
183
678
|
provideContext(context) {
|
|
184
679
|
console.log(`[Web Model Context] Registering ${context.tools.length} tools via provideContext`);
|
|
@@ -203,9 +698,12 @@ var WebModelContext = class {
|
|
|
203
698
|
this.notifyToolsListChanged();
|
|
204
699
|
}
|
|
205
700
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
701
|
+
* Registers a single tool dynamically (Bucket B).
|
|
702
|
+
* Dynamic tools persist across provideContext() calls and can be independently managed.
|
|
703
|
+
*
|
|
704
|
+
* @param {ToolDescriptor} tool - The tool descriptor to register
|
|
705
|
+
* @returns {{unregister: () => void}} Object with unregister function
|
|
706
|
+
* @throws {Error} If tool name collides with existing tools
|
|
209
707
|
*/
|
|
210
708
|
registerTool(tool) {
|
|
211
709
|
console.log(`[Web Model Context] Registering tool dynamically: ${tool.name}`);
|
|
@@ -251,8 +749,46 @@ var WebModelContext = class {
|
|
|
251
749
|
return { unregister: unregisterFn };
|
|
252
750
|
}
|
|
253
751
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
752
|
+
* Unregisters a tool by name (Chromium native API).
|
|
753
|
+
* Can unregister tools from either Bucket A (provideContext) or Bucket B (registerTool).
|
|
754
|
+
*
|
|
755
|
+
* @param {string} name - Name of the tool to unregister
|
|
756
|
+
*/
|
|
757
|
+
unregisterTool(name) {
|
|
758
|
+
console.log(`[Web Model Context] Unregistering tool: ${name}`);
|
|
759
|
+
const inProvideContext = this.provideContextTools.has(name);
|
|
760
|
+
const inDynamic = this.dynamicTools.has(name);
|
|
761
|
+
if (!inProvideContext && !inDynamic) {
|
|
762
|
+
console.warn(`[Web Model Context] Tool "${name}" is not registered, ignoring unregister call`);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (inProvideContext) this.provideContextTools.delete(name);
|
|
766
|
+
if (inDynamic) {
|
|
767
|
+
this.dynamicTools.delete(name);
|
|
768
|
+
this.registrationTimestamps.delete(name);
|
|
769
|
+
this.unregisterFunctions.delete(name);
|
|
770
|
+
}
|
|
771
|
+
this.updateBridgeTools();
|
|
772
|
+
this.notifyToolsListChanged();
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Clears all registered tools from both buckets (Chromium native API).
|
|
776
|
+
* Removes all tools registered via provideContext() and registerTool().
|
|
777
|
+
*/
|
|
778
|
+
clearContext() {
|
|
779
|
+
console.log("[Web Model Context] Clearing all tools");
|
|
780
|
+
this.provideContextTools.clear();
|
|
781
|
+
this.dynamicTools.clear();
|
|
782
|
+
this.registrationTimestamps.clear();
|
|
783
|
+
this.unregisterFunctions.clear();
|
|
784
|
+
this.updateBridgeTools();
|
|
785
|
+
this.notifyToolsListChanged();
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Updates the bridge tools map with merged tools from both buckets.
|
|
789
|
+
* The final tool list is the union of Bucket A (provideContext) and Bucket B (dynamic).
|
|
790
|
+
*
|
|
791
|
+
* @private
|
|
256
792
|
*/
|
|
257
793
|
updateBridgeTools() {
|
|
258
794
|
this.bridge.tools.clear();
|
|
@@ -261,7 +797,10 @@ var WebModelContext = class {
|
|
|
261
797
|
console.log(`[Web Model Context] Updated bridge with ${this.provideContextTools.size} base tools + ${this.dynamicTools.size} dynamic tools = ${this.bridge.tools.size} total`);
|
|
262
798
|
}
|
|
263
799
|
/**
|
|
264
|
-
*
|
|
800
|
+
* Notifies all servers and testing callbacks that the tools list has changed.
|
|
801
|
+
* Sends MCP notifications to connected servers and invokes registered testing callbacks.
|
|
802
|
+
*
|
|
803
|
+
* @private
|
|
265
804
|
*/
|
|
266
805
|
notifyToolsListChanged() {
|
|
267
806
|
if (this.bridge.tabServer.notification) this.bridge.tabServer.notification({
|
|
@@ -272,13 +811,23 @@ var WebModelContext = class {
|
|
|
272
811
|
method: "notifications/tools/list_changed",
|
|
273
812
|
params: {}
|
|
274
813
|
});
|
|
814
|
+
if (this.testingAPI && "notifyToolsChanged" in this.testingAPI) this.testingAPI.notifyToolsChanged();
|
|
275
815
|
}
|
|
276
816
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
817
|
+
* Executes a tool with validation and event dispatch.
|
|
818
|
+
* Follows this sequence:
|
|
819
|
+
* 1. Validates input arguments against schema
|
|
820
|
+
* 2. Records tool call in testing API (if available)
|
|
821
|
+
* 3. Checks for mock response (if testing)
|
|
822
|
+
* 4. Dispatches 'toolcall' event to listeners
|
|
823
|
+
* 5. Executes tool function if not prevented
|
|
824
|
+
* 6. Validates output (permissive mode - warns only)
|
|
825
|
+
*
|
|
826
|
+
* @param {string} toolName - Name of the tool to execute
|
|
827
|
+
* @param {Record<string, unknown>} args - Arguments to pass to the tool
|
|
828
|
+
* @returns {Promise<ToolResponse>} The tool's response
|
|
829
|
+
* @throws {Error} If tool is not found
|
|
830
|
+
* @internal
|
|
282
831
|
*/
|
|
283
832
|
async executeTool(toolName, args) {
|
|
284
833
|
const tool = this.bridge.tools.get(toolName);
|
|
@@ -296,6 +845,14 @@ var WebModelContext = class {
|
|
|
296
845
|
};
|
|
297
846
|
}
|
|
298
847
|
const validatedArgs = validation.data;
|
|
848
|
+
if (this.testingAPI) this.testingAPI.recordToolCall(toolName, validatedArgs);
|
|
849
|
+
if (this.testingAPI?.hasMockResponse(toolName)) {
|
|
850
|
+
const mockResponse = this.testingAPI.getMockResponse(toolName);
|
|
851
|
+
if (mockResponse) {
|
|
852
|
+
console.log(`[Web Model Context] Returning mock response for tool: ${toolName}`);
|
|
853
|
+
return mockResponse;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
299
856
|
const event = new WebToolCallEvent(toolName, validatedArgs);
|
|
300
857
|
this.dispatchEvent(event);
|
|
301
858
|
if (event.defaultPrevented && event.hasResponse()) {
|
|
@@ -325,8 +882,11 @@ var WebModelContext = class {
|
|
|
325
882
|
}
|
|
326
883
|
}
|
|
327
884
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
885
|
+
* Lists all registered tools in MCP format.
|
|
886
|
+
* Returns tools from both buckets with full MCP specification including
|
|
887
|
+
* annotations and output schemas.
|
|
888
|
+
*
|
|
889
|
+
* @returns {Array<{name: string, description: string, inputSchema: InputSchema, outputSchema?: InputSchema, annotations?: ToolAnnotations}>} Array of tool descriptors
|
|
330
890
|
*/
|
|
331
891
|
listTools() {
|
|
332
892
|
return Array.from(this.bridge.tools.values()).map((tool) => ({
|
|
@@ -339,8 +899,12 @@ var WebModelContext = class {
|
|
|
339
899
|
}
|
|
340
900
|
};
|
|
341
901
|
/**
|
|
342
|
-
*
|
|
343
|
-
* Creates
|
|
902
|
+
* Initializes the MCP bridge with dual-server support.
|
|
903
|
+
* Creates TabServer for same-window communication and optionally IframeChildServer
|
|
904
|
+
* for parent-child iframe communication.
|
|
905
|
+
*
|
|
906
|
+
* @param {WebModelContextInitOptions} [options] - Configuration options
|
|
907
|
+
* @returns {MCPBridge} The initialized MCP bridge
|
|
344
908
|
*/
|
|
345
909
|
function initializeMCPBridge(options) {
|
|
346
910
|
console.log("[Web Model Context] Initializing MCP bridge");
|
|
@@ -430,7 +994,24 @@ function initializeMCPBridge(options) {
|
|
|
430
994
|
return bridge;
|
|
431
995
|
}
|
|
432
996
|
/**
|
|
433
|
-
*
|
|
997
|
+
* Initializes the Web Model Context API on window.navigator.
|
|
998
|
+
* Creates and exposes navigator.modelContext and navigator.modelContextTesting.
|
|
999
|
+
* Automatically detects and uses native Chromium implementation if available.
|
|
1000
|
+
*
|
|
1001
|
+
* @param {WebModelContextInitOptions} [options] - Configuration options
|
|
1002
|
+
* @throws {Error} If initialization fails
|
|
1003
|
+
* @example
|
|
1004
|
+
* ```typescript
|
|
1005
|
+
* import { initializeWebModelContext } from '@mcp-b/global';
|
|
1006
|
+
*
|
|
1007
|
+
* initializeWebModelContext({
|
|
1008
|
+
* transport: {
|
|
1009
|
+
* tabServer: {
|
|
1010
|
+
* allowedOrigins: ['https://example.com']
|
|
1011
|
+
* }
|
|
1012
|
+
* }
|
|
1013
|
+
* });
|
|
1014
|
+
* ```
|
|
434
1015
|
*/
|
|
435
1016
|
function initializeWebModelContext(options) {
|
|
436
1017
|
if (typeof window === "undefined") {
|
|
@@ -438,10 +1019,49 @@ function initializeWebModelContext(options) {
|
|
|
438
1019
|
return;
|
|
439
1020
|
}
|
|
440
1021
|
const effectiveOptions = options ?? window.__webModelContextOptions;
|
|
1022
|
+
const native = detectNativeAPI();
|
|
1023
|
+
if (native.hasNativeContext && native.hasNativeTesting) {
|
|
1024
|
+
const nativeContext = window.navigator.modelContext;
|
|
1025
|
+
const nativeTesting = window.navigator.modelContextTesting;
|
|
1026
|
+
if (!nativeContext || !nativeTesting) {
|
|
1027
|
+
console.error("[Web Model Context] Native API detection mismatch");
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
console.log("✅ [Web Model Context] Native Chromium API detected");
|
|
1031
|
+
console.log(" Using native implementation with MCP bridge synchronization");
|
|
1032
|
+
console.log(" Native API will automatically collect tools from embedded iframes");
|
|
1033
|
+
try {
|
|
1034
|
+
const bridge = initializeMCPBridge(effectiveOptions);
|
|
1035
|
+
bridge.modelContext = new NativeModelContextAdapter(bridge, nativeContext, nativeTesting);
|
|
1036
|
+
bridge.modelContextTesting = nativeTesting;
|
|
1037
|
+
Object.defineProperty(window, "__mcpBridge", {
|
|
1038
|
+
value: bridge,
|
|
1039
|
+
writable: false,
|
|
1040
|
+
configurable: true
|
|
1041
|
+
});
|
|
1042
|
+
console.log("✅ [Web Model Context] MCP bridge synced with native API");
|
|
1043
|
+
console.log(" MCP clients will receive automatic tool updates from native registry");
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
console.error("[Web Model Context] Failed to initialize native adapter:", error);
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (native.hasNativeContext && !native.hasNativeTesting) {
|
|
1051
|
+
console.warn("[Web Model Context] Partial native API detected");
|
|
1052
|
+
console.warn(" navigator.modelContext exists but navigator.modelContextTesting is missing");
|
|
1053
|
+
console.warn(" Cannot sync with native API. Please enable experimental features:");
|
|
1054
|
+
console.warn(" - Navigate to chrome://flags");
|
|
1055
|
+
console.warn(" - Enable \"Experimental Web Platform Features\"");
|
|
1056
|
+
console.warn(" - Or launch with: --enable-experimental-web-platform-features");
|
|
1057
|
+
console.warn(" Skipping initialization to avoid conflicts");
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
441
1060
|
if (window.navigator.modelContext) {
|
|
442
1061
|
console.warn("[Web Model Context] window.navigator.modelContext already exists, skipping initialization");
|
|
443
1062
|
return;
|
|
444
1063
|
}
|
|
1064
|
+
console.log("[Web Model Context] Native API not detected, installing polyfill");
|
|
445
1065
|
try {
|
|
446
1066
|
const bridge = initializeMCPBridge(effectiveOptions);
|
|
447
1067
|
Object.defineProperty(window.navigator, "modelContext", {
|
|
@@ -455,13 +1075,36 @@ function initializeWebModelContext(options) {
|
|
|
455
1075
|
configurable: true
|
|
456
1076
|
});
|
|
457
1077
|
console.log("✅ [Web Model Context] window.navigator.modelContext initialized successfully");
|
|
1078
|
+
console.log("[Model Context Testing] Installing polyfill");
|
|
1079
|
+
console.log(" 💡 To use the native implementation in Chromium:");
|
|
1080
|
+
console.log(" - Navigate to chrome://flags");
|
|
1081
|
+
console.log(" - Enable \"Experimental Web Platform Features\"");
|
|
1082
|
+
console.log(" - Or launch with: --enable-experimental-web-platform-features");
|
|
1083
|
+
const testingAPI = new WebModelContextTesting(bridge);
|
|
1084
|
+
bridge.modelContextTesting = testingAPI;
|
|
1085
|
+
bridge.modelContext.setTestingAPI(testingAPI);
|
|
1086
|
+
Object.defineProperty(window.navigator, "modelContextTesting", {
|
|
1087
|
+
value: testingAPI,
|
|
1088
|
+
writable: false,
|
|
1089
|
+
configurable: true
|
|
1090
|
+
});
|
|
1091
|
+
console.log("✅ [Model Context Testing] Polyfill installed at window.navigator.modelContextTesting");
|
|
458
1092
|
} catch (error) {
|
|
459
1093
|
console.error("[Web Model Context] Failed to initialize:", error);
|
|
460
1094
|
throw error;
|
|
461
1095
|
}
|
|
462
1096
|
}
|
|
463
1097
|
/**
|
|
464
|
-
*
|
|
1098
|
+
* Cleans up the Web Model Context API.
|
|
1099
|
+
* Closes all MCP servers and removes API from window.navigator.
|
|
1100
|
+
* Useful for testing and hot module replacement.
|
|
1101
|
+
*
|
|
1102
|
+
* @example
|
|
1103
|
+
* ```typescript
|
|
1104
|
+
* import { cleanupWebModelContext } from '@mcp-b/global';
|
|
1105
|
+
*
|
|
1106
|
+
* cleanupWebModelContext();
|
|
1107
|
+
* ```
|
|
465
1108
|
*/
|
|
466
1109
|
function cleanupWebModelContext() {
|
|
467
1110
|
if (typeof window === "undefined") return;
|
|
@@ -472,6 +1115,7 @@ function cleanupWebModelContext() {
|
|
|
472
1115
|
console.warn("[Web Model Context] Error closing MCP servers:", error);
|
|
473
1116
|
}
|
|
474
1117
|
delete window.navigator.modelContext;
|
|
1118
|
+
delete window.navigator.modelContextTesting;
|
|
475
1119
|
delete window.__mcpBridge;
|
|
476
1120
|
console.log("[Web Model Context] Cleaned up");
|
|
477
1121
|
}
|