@mcp-b/global 1.0.15 → 1.1.1

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
@@ -1,4 +1,4 @@
1
- import { TabServerTransport } from "@mcp-b/transports";
1
+ import { IframeChildTransport, TabServerTransport } from "@mcp-b/transports";
2
2
  import { CallToolRequestSchema, ListToolsRequestSchema, Server } from "@mcp-b/webmcp-ts-sdk";
3
3
  import { jsonSchemaToZod } from "@composio/json-schema-to-zod";
4
4
  import { z } from "zod";
@@ -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`);
@@ -200,15 +695,15 @@ var WebModelContext = class {
200
695
  this.provideContextTools.set(tool.name, validatedTool);
201
696
  }
202
697
  this.updateBridgeTools();
203
- if (this.bridge.server.notification) this.bridge.server.notification({
204
- method: "notifications/tools/list_changed",
205
- params: {}
206
- });
698
+ this.notifyToolsListChanged();
207
699
  }
208
700
  /**
209
- * Register a single tool dynamically (Bucket B)
210
- * Returns an object with an unregister function to remove the tool
211
- * 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
212
707
  */
213
708
  registerTool(tool) {
214
709
  console.log(`[Web Model Context] Registering tool dynamically: ${tool.name}`);
@@ -236,10 +731,7 @@ var WebModelContext = class {
236
731
  this.dynamicTools.set(tool.name, validatedTool);
237
732
  this.registrationTimestamps.set(tool.name, now);
238
733
  this.updateBridgeTools();
239
- if (this.bridge.server.notification) this.bridge.server.notification({
240
- method: "notifications/tools/list_changed",
241
- params: {}
242
- });
734
+ this.notifyToolsListChanged();
243
735
  const unregisterFn = () => {
244
736
  console.log(`[Web Model Context] Unregistering tool: ${tool.name}`);
245
737
  if (this.provideContextTools.has(tool.name)) throw new Error(`[Web Model Context] Cannot unregister tool "${tool.name}": This tool was registered via provideContext(). Use provideContext() to update the base tool set.`);
@@ -251,17 +743,52 @@ var WebModelContext = class {
251
743
  this.registrationTimestamps.delete(tool.name);
252
744
  this.unregisterFunctions.delete(tool.name);
253
745
  this.updateBridgeTools();
254
- if (this.bridge.server.notification) this.bridge.server.notification({
255
- method: "notifications/tools/list_changed",
256
- params: {}
257
- });
746
+ this.notifyToolsListChanged();
258
747
  };
259
748
  this.unregisterFunctions.set(tool.name, unregisterFn);
260
749
  return { unregister: unregisterFn };
261
750
  }
262
751
  /**
263
- * Update the bridge tools map with merged tools from both buckets
264
- * 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
265
792
  */
266
793
  updateBridgeTools() {
267
794
  this.bridge.tools.clear();
@@ -270,11 +797,37 @@ var WebModelContext = class {
270
797
  console.log(`[Web Model Context] Updated bridge with ${this.provideContextTools.size} base tools + ${this.dynamicTools.size} dynamic tools = ${this.bridge.tools.size} total`);
271
798
  }
272
799
  /**
273
- * Execute a tool with hybrid approach:
274
- * 1. Validate input arguments
275
- * 2. Dispatch toolcall event first
276
- * 3. If not prevented, call tool's execute function
277
- * 4. Validate output (permissive mode - warn only)
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
804
+ */
805
+ notifyToolsListChanged() {
806
+ if (this.bridge.tabServer.notification) this.bridge.tabServer.notification({
807
+ method: "notifications/tools/list_changed",
808
+ params: {}
809
+ });
810
+ if (this.bridge.iframeServer?.notification) this.bridge.iframeServer.notification({
811
+ method: "notifications/tools/list_changed",
812
+ params: {}
813
+ });
814
+ if (this.testingAPI && "notifyToolsChanged" in this.testingAPI) this.testingAPI.notifyToolsChanged();
815
+ }
816
+ /**
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
278
831
  */
279
832
  async executeTool(toolName, args) {
280
833
  const tool = this.bridge.tools.get(toolName);
@@ -292,6 +845,14 @@ var WebModelContext = class {
292
845
  };
293
846
  }
294
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
+ }
295
856
  const event = new WebToolCallEvent(toolName, validatedArgs);
296
857
  this.dispatchEvent(event);
297
858
  if (event.defaultPrevented && event.hasResponse()) {
@@ -321,8 +882,11 @@ var WebModelContext = class {
321
882
  }
322
883
  }
323
884
  /**
324
- * Get list of registered tools in MCP format
325
- * 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
326
890
  */
327
891
  listTools() {
328
892
  return Array.from(this.bridge.tools.values()).map((tool) => ({
@@ -335,59 +899,171 @@ var WebModelContext = class {
335
899
  }
336
900
  };
337
901
  /**
338
- * Initialize the MCP bridge
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
339
908
  */
340
- function initializeMCPBridge() {
909
+ function initializeMCPBridge(options) {
341
910
  console.log("[Web Model Context] Initializing MCP bridge");
342
- const server = new Server({
343
- name: window.location.hostname || "localhost",
911
+ const hostname = window.location.hostname || "localhost";
912
+ const transportOptions = options?.transport;
913
+ const setupServerHandlers = (server, bridge$1) => {
914
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
915
+ console.log("[MCP Bridge] Handling list_tools request");
916
+ return { tools: bridge$1.modelContext.listTools() };
917
+ });
918
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
919
+ console.log(`[MCP Bridge] Handling call_tool request: ${request.params.name}`);
920
+ const toolName = request.params.name;
921
+ const args = request.params.arguments || {};
922
+ try {
923
+ const response = await bridge$1.modelContext.executeTool(toolName, args);
924
+ return {
925
+ content: response.content,
926
+ isError: response.isError
927
+ };
928
+ } catch (error) {
929
+ console.error(`[MCP Bridge] Error calling tool ${toolName}:`, error);
930
+ throw error;
931
+ }
932
+ });
933
+ };
934
+ const customTransport = transportOptions?.create?.();
935
+ if (customTransport) {
936
+ console.log("[Web Model Context] Using custom transport");
937
+ const server = new Server({
938
+ name: hostname,
939
+ version: "1.0.0"
940
+ }, { capabilities: { tools: { listChanged: true } } });
941
+ const bridge$1 = {
942
+ tabServer: server,
943
+ tools: /* @__PURE__ */ new Map(),
944
+ modelContext: void 0,
945
+ isInitialized: true
946
+ };
947
+ bridge$1.modelContext = new WebModelContext(bridge$1);
948
+ setupServerHandlers(server, bridge$1);
949
+ server.connect(customTransport);
950
+ console.log("[Web Model Context] MCP server connected with custom transport");
951
+ return bridge$1;
952
+ }
953
+ console.log("[Web Model Context] Using dual-server mode");
954
+ const tabServerEnabled = transportOptions?.tabServer !== false;
955
+ const tabServer = new Server({
956
+ name: `${hostname}-tab`,
344
957
  version: "1.0.0"
345
958
  }, { capabilities: { tools: { listChanged: true } } });
346
959
  const bridge = {
347
- server,
960
+ tabServer,
348
961
  tools: /* @__PURE__ */ new Map(),
349
962
  modelContext: void 0,
350
963
  isInitialized: true
351
964
  };
352
965
  bridge.modelContext = new WebModelContext(bridge);
353
- server.setRequestHandler(ListToolsRequestSchema, async () => {
354
- console.log("[MCP Bridge] Handling list_tools request");
355
- return { tools: bridge.modelContext.listTools() };
356
- });
357
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
358
- console.log(`[MCP Bridge] Handling call_tool request: ${request.params.name}`);
359
- const toolName = request.params.name;
360
- const args = request.params.arguments || {};
361
- try {
362
- const response = await bridge.modelContext.executeTool(toolName, args);
363
- return {
364
- content: response.content,
365
- isError: response.isError
366
- };
367
- } catch (error) {
368
- console.error(`[MCP Bridge] Error calling tool ${toolName}:`, error);
369
- throw error;
370
- }
371
- });
372
- const transport = new TabServerTransport({ allowedOrigins: ["*"] });
373
- server.connect(transport);
374
- console.log("[Web Model Context] MCP server connected");
966
+ setupServerHandlers(tabServer, bridge);
967
+ if (tabServerEnabled) {
968
+ const { allowedOrigins,...restTabServerOptions } = typeof transportOptions?.tabServer === "object" ? transportOptions.tabServer : {};
969
+ const tabTransport = new TabServerTransport({
970
+ allowedOrigins: allowedOrigins ?? ["*"],
971
+ ...restTabServerOptions
972
+ });
973
+ tabServer.connect(tabTransport);
974
+ console.log("[Web Model Context] Tab server connected");
975
+ }
976
+ const isInIframe = typeof window !== "undefined" && window.parent !== window;
977
+ const iframeServerConfig = transportOptions?.iframeServer;
978
+ if (iframeServerConfig !== false && (iframeServerConfig !== void 0 || isInIframe)) {
979
+ console.log("[Web Model Context] Enabling iframe server");
980
+ const iframeServer = new Server({
981
+ name: `${hostname}-iframe`,
982
+ version: "1.0.0"
983
+ }, { capabilities: { tools: { listChanged: true } } });
984
+ setupServerHandlers(iframeServer, bridge);
985
+ const { allowedOrigins,...restIframeServerOptions } = typeof iframeServerConfig === "object" ? iframeServerConfig : {};
986
+ const iframeTransport = new IframeChildTransport({
987
+ allowedOrigins: allowedOrigins ?? ["*"],
988
+ ...restIframeServerOptions
989
+ });
990
+ iframeServer.connect(iframeTransport);
991
+ bridge.iframeServer = iframeServer;
992
+ console.log("[Web Model Context] Iframe server connected");
993
+ }
375
994
  return bridge;
376
995
  }
377
996
  /**
378
- * 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
+ * ```
379
1015
  */
380
- function initializeWebModelContext() {
1016
+ function initializeWebModelContext(options) {
381
1017
  if (typeof window === "undefined") {
382
1018
  console.warn("[Web Model Context] Not in browser environment, skipping initialization");
383
1019
  return;
384
1020
  }
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
+ }
385
1060
  if (window.navigator.modelContext) {
386
1061
  console.warn("[Web Model Context] window.navigator.modelContext already exists, skipping initialization");
387
1062
  return;
388
1063
  }
1064
+ console.log("[Web Model Context] Native API not detected, installing polyfill");
389
1065
  try {
390
- const bridge = initializeMCPBridge();
1066
+ const bridge = initializeMCPBridge(effectiveOptions);
391
1067
  Object.defineProperty(window.navigator, "modelContext", {
392
1068
  value: bridge.modelContext,
393
1069
  writable: false,
@@ -399,32 +1075,125 @@ function initializeWebModelContext() {
399
1075
  configurable: true
400
1076
  });
401
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");
402
1092
  } catch (error) {
403
1093
  console.error("[Web Model Context] Failed to initialize:", error);
404
1094
  throw error;
405
1095
  }
406
1096
  }
407
1097
  /**
408
- * 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
+ * ```
409
1108
  */
410
1109
  function cleanupWebModelContext() {
411
1110
  if (typeof window === "undefined") return;
412
1111
  if (window.__mcpBridge) try {
413
- window.__mcpBridge.server.close();
1112
+ window.__mcpBridge.tabServer.close();
1113
+ if (window.__mcpBridge.iframeServer) window.__mcpBridge.iframeServer.close();
414
1114
  } catch (error) {
415
- console.warn("[Web Model Context] Error closing MCP server:", error);
1115
+ console.warn("[Web Model Context] Error closing MCP servers:", error);
416
1116
  }
417
1117
  delete window.navigator.modelContext;
1118
+ delete window.navigator.modelContextTesting;
418
1119
  delete window.__mcpBridge;
419
1120
  console.log("[Web Model Context] Cleaned up");
420
1121
  }
421
1122
 
422
1123
  //#endregion
423
1124
  //#region src/index.ts
424
- if (typeof window !== "undefined" && typeof document !== "undefined") try {
425
- initializeWebModelContext();
426
- } catch (error) {
427
- console.error("[Web Model Context] Auto-initialization failed:", error);
1125
+ function mergeTransportOptions(base, override) {
1126
+ if (!base) return override;
1127
+ if (!override) return base;
1128
+ return {
1129
+ ...base,
1130
+ ...override,
1131
+ tabServer: {
1132
+ ...base.tabServer ?? {},
1133
+ ...override.tabServer ?? {}
1134
+ }
1135
+ };
1136
+ }
1137
+ function mergeInitOptions(base, override) {
1138
+ if (!base) return override;
1139
+ if (!override) return base;
1140
+ return {
1141
+ ...base,
1142
+ ...override,
1143
+ transport: mergeTransportOptions(base.transport ?? {}, override.transport ?? {})
1144
+ };
1145
+ }
1146
+ function parseScriptTagOptions(script) {
1147
+ if (!script || !script.dataset) return;
1148
+ const { dataset } = script;
1149
+ if (dataset.webmcpOptions) try {
1150
+ return JSON.parse(dataset.webmcpOptions);
1151
+ } catch (error) {
1152
+ console.error("[Web Model Context] Invalid JSON in data-webmcp-options:", error);
1153
+ return;
1154
+ }
1155
+ const options = {};
1156
+ let hasOptions = false;
1157
+ if (dataset.webmcpAutoInitialize !== void 0) {
1158
+ options.autoInitialize = dataset.webmcpAutoInitialize !== "false";
1159
+ hasOptions = true;
1160
+ }
1161
+ const tabServerOptions = {};
1162
+ let hasTabServerOptions = false;
1163
+ if (dataset.webmcpAllowedOrigins) {
1164
+ const origins = dataset.webmcpAllowedOrigins.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
1165
+ if (origins.length > 0) {
1166
+ tabServerOptions.allowedOrigins = origins;
1167
+ hasOptions = true;
1168
+ hasTabServerOptions = true;
1169
+ }
1170
+ }
1171
+ if (dataset.webmcpChannelId) {
1172
+ tabServerOptions.channelId = dataset.webmcpChannelId;
1173
+ hasOptions = true;
1174
+ hasTabServerOptions = true;
1175
+ }
1176
+ if (hasTabServerOptions) options.transport = {
1177
+ ...options.transport ?? {},
1178
+ tabServer: {
1179
+ ...options.transport?.tabServer ?? {},
1180
+ ...tabServerOptions
1181
+ }
1182
+ };
1183
+ return hasOptions ? options : void 0;
1184
+ }
1185
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
1186
+ const globalOptions = window.__webModelContextOptions;
1187
+ const scriptElement = document.currentScript;
1188
+ const scriptOptions = parseScriptTagOptions(scriptElement);
1189
+ const mergedOptions = mergeInitOptions(globalOptions, scriptOptions) ?? globalOptions ?? scriptOptions;
1190
+ if (mergedOptions) window.__webModelContextOptions = mergedOptions;
1191
+ const shouldAutoInitialize = mergedOptions?.autoInitialize !== false;
1192
+ try {
1193
+ if (shouldAutoInitialize) initializeWebModelContext(mergedOptions);
1194
+ } catch (error) {
1195
+ console.error("[Web Model Context] Auto-initialization failed:", error);
1196
+ }
428
1197
  }
429
1198
 
430
1199
  //#endregion