@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/dist/index.js CHANGED
@@ -101,47 +101,511 @@ function validateWithZod(data, validator) {
101
101
  //#endregion
102
102
  //#region src/global.ts
103
103
  /**
104
- * Custom ToolCallEvent implementation
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 (in ms) to detect rapid duplicate registrations
130
- * Registrations within this window are likely due to React Strict Mode
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
- * ModelContext implementation that bridges to MCP SDK
135
- * Implements the W3C Web Model Context API proposal with two-bucket tool management
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
- * Two-Bucket System:
138
- * - Bucket A (provideContextTools): Tools registered via provideContext() - base/app-level tools
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
- * Benefits:
142
- * - provideContext() only clears Bucket A, leaving Bucket B intact
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
- * Add event listener (compatible with ModelContext interface)
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
- * Remove event listener
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
- * Dispatch event
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
- * Provide context (tools) to AI models
181
- * Clears and replaces Bucket A (provideContext tools), leaving Bucket B (dynamic tools) intact
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
- * Register a single tool dynamically (Bucket B)
207
- * Returns an object with an unregister function to remove the tool
208
- * Tools registered via this method persist across provideContext() calls
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
- * Update the bridge tools map with merged tools from both buckets
255
- * Final tool list = Bucket A (provideContext) + Bucket B (dynamic)
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
- * Notify all servers that the tools list has changed
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
- * Execute a tool with hybrid approach:
278
- * 1. Validate input arguments
279
- * 2. Dispatch toolcall event first
280
- * 3. If not prevented, call tool's execute function
281
- * 4. Validate output (permissive mode - warn only)
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
- * Get list of registered tools in MCP format
329
- * Includes full MCP spec: annotations, outputSchema, etc.
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
- * Initialize the MCP bridge with dual-server support
343
- * Creates both TabServer (same-window) and IframeChildServer (parent-child) by default
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
- * Initialize the Web Model Context API (window.navigator.modelContext)
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
- * Cleanup function (for testing/development)
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
  }