@mcp-b/global 2.0.3-canary.4 → 2.0.5

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,1851 +1,150 @@
1
1
  import { IframeChildTransport, TabServerTransport } from "@mcp-b/transports";
2
- import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, Server } from "@mcp-b/webmcp-ts-sdk";
3
- import { jsonSchemaToZod } from "@n8n/json-schema-to-zod";
4
- import { z } from "zod";
5
- import { zodToJsonSchema as zodToJsonSchema$1 } from "zod-to-json-schema";
2
+ import { initializeWebMCPPolyfill } from "@mcp-b/webmcp-polyfill";
3
+ import { BrowserMcpServer } from "@mcp-b/webmcp-ts-sdk";
6
4
 
7
- //#region src/logger.ts
8
- /**
9
- * @license
10
- * Copyright 2025 Google LLC
11
- * SPDX-License-Identifier: Apache-2.0
12
- */
13
- /**
14
- * Lightweight logging system for @mcp-b/global
15
- *
16
- * Design Decision: This implements a custom logger instead of using the 'debug'
17
- * package to reduce bundle size and eliminate external dependencies in the
18
- * browser build. The API is intentionally simpler, focusing on the specific
19
- * needs of browser-based MCP implementations.
20
- *
21
- * Configuration via localStorage:
22
- * - localStorage.setItem('WEBMCP_DEBUG', '*') - enable all debug logging
23
- * - localStorage.setItem('WEBMCP_DEBUG', 'WebModelContext') - enable specific namespace
24
- * - localStorage.setItem('WEBMCP_DEBUG', 'WebModelContext,NativeAdapter') - multiple namespaces
25
- * - localStorage.setItem('WEBMCP_DEBUG', 'WebModelContext:') - enable namespace and sub-namespaces
26
- * - localStorage.removeItem('WEBMCP_DEBUG') - disable debug logging (default)
27
- *
28
- * Environment Support:
29
- * - Automatically detects localStorage availability
30
- * - Gracefully degrades to "disabled" state when localStorage is inaccessible
31
- * - Never throws errors from configuration checks (safe for private browsing mode)
32
- */
33
- /** localStorage key for debug configuration */
34
- const DEBUG_CONFIG_KEY = "WEBMCP_DEBUG";
5
+ //#region src/global.ts
6
+ let runtime = null;
7
+ function isBrowserEnvironment() {
8
+ return typeof window !== "undefined" && typeof window.navigator !== "undefined";
9
+ }
35
10
  /**
36
- * Check if debug logging is enabled for a namespace
37
- *
38
- * Supports namespace hierarchy via colons. Setting 'WebModelContext' will match
39
- * both 'WebModelContext' and 'WebModelContext:init', but NOT 'WebModelContextTesting'.
11
+ * Replace navigator.modelContext with the given value.
12
+ * Tries an own-property on the navigator instance first. If the native browser
13
+ * defines modelContext as a non-configurable property (common in Chromium), this
14
+ * will throw in that case we fall back to redefining the getter on
15
+ * Navigator.prototype so that `navigator.modelContext` resolves to our value.
40
16
  */
41
- function isDebugEnabled(namespace) {
42
- if (typeof window === "undefined" || !window.localStorage) return false;
17
+ function replaceModelContext(value) {
43
18
  try {
44
- const debugConfig = localStorage.getItem(DEBUG_CONFIG_KEY);
45
- if (!debugConfig) return false;
46
- if (debugConfig === "*") return true;
47
- return debugConfig.split(",").map((p) => p.trim()).some((pattern) => namespace === pattern || namespace.startsWith(`${pattern}:`));
48
- } catch (err) {
49
- if (typeof console !== "undefined" && console.warn) {
50
- const message = err instanceof Error ? err.message : String(err);
51
- console.warn(`[WebMCP] localStorage access failed, debug logging disabled: ${message}`);
52
- }
53
- return false;
19
+ Object.defineProperty(navigator, "modelContext", {
20
+ configurable: true,
21
+ enumerable: true,
22
+ writable: false,
23
+ value
24
+ });
25
+ } catch {
26
+ Object.defineProperty(Object.getPrototypeOf(navigator), "modelContext", {
27
+ configurable: true,
28
+ enumerable: true,
29
+ get() {
30
+ return value;
31
+ }
32
+ });
54
33
  }
34
+ if (navigator.modelContext !== value) console.error("[WebModelContext] Failed to replace navigator.modelContext.", "Descriptor:", Object.getOwnPropertyDescriptor(navigator, "modelContext"));
55
35
  }
56
- /**
57
- * No-op function for disabled log levels
58
- */
59
- const noop = () => {};
60
- /**
61
- * Create a namespaced logger
62
- *
63
- * Uses .bind() to prepend namespace prefixes to console methods without manual
64
- * string concatenation. Debug enablement is determined at logger creation time
65
- * for performance - changes to localStorage after creation won't affect existing
66
- * loggers. Refresh the page to apply new WEBMCP_DEBUG settings.
67
- *
68
- * @param namespace - Namespace for the logger (e.g., 'WebModelContext', 'NativeAdapter')
69
- * @returns Logger instance with debug, info, warn, error methods
70
- *
71
- * @example
72
- * ```typescript
73
- * const logger = createLogger('WebModelContext');
74
- * logger.debug('Tool registered:', toolName); // Only shown if WEBMCP_DEBUG includes 'WebModelContext'
75
- * logger.error('Execution failed:', error); // Always enabled
76
- * ```
77
- */
78
- function createLogger(namespace) {
79
- const prefix = `[${namespace}]`;
80
- const isDebug = isDebugEnabled(namespace);
81
- const boundWarn = console.warn.bind(console, prefix);
82
- const boundError = console.error.bind(console, prefix);
83
- const boundLog = console.log.bind(console, prefix);
84
- return {
85
- warn: boundWarn,
86
- error: boundError,
87
- debug: isDebug ? boundLog : noop,
88
- info: isDebug ? boundLog : noop
89
- };
36
+ function createTransport(config) {
37
+ if (window.parent !== window && config?.iframeServer !== false) {
38
+ const { allowedOrigins: allowedOrigins$1,...rest$1 } = typeof config?.iframeServer === "object" ? config.iframeServer : {};
39
+ return new IframeChildTransport({
40
+ allowedOrigins: allowedOrigins$1 ?? ["*"],
41
+ ...rest$1
42
+ });
43
+ }
44
+ if (config?.tabServer === false) throw new Error("tabServer transport is disabled and iframe transport was not selected");
45
+ const { allowedOrigins,...rest } = typeof config?.tabServer === "object" ? config.tabServer : {};
46
+ return new TabServerTransport({
47
+ allowedOrigins: allowedOrigins ?? ["*"],
48
+ ...rest
49
+ });
90
50
  }
91
-
92
- //#endregion
93
- //#region src/validation.ts
94
- const logger$2 = createLogger("WebModelContext");
95
- const isRecord = (value) => typeof value === "object" && value !== null;
96
- const stripSchemaMeta = (schema) => {
97
- const { $schema: _,...rest } = schema;
98
- return rest;
99
- };
100
- const isOptionalSchema = (schema) => {
101
- const typeName = schema._def?.typeName;
102
- return typeName === "ZodOptional" || typeName === "ZodDefault";
103
- };
104
- /**
105
- * Detect if schema is a Zod schema object (Record<string, ZodType>).
106
- * Only supports Zod 3.x schemas.
107
- */
108
- function isZodSchema(schema) {
109
- if (!isRecord(schema)) return false;
110
- if ("type" in schema && typeof schema.type === "string") return false;
111
- const values = Object.values(schema);
112
- if (values.length === 0) return false;
113
- return values.some((v) => isRecord(v) && "_def" in v);
114
- }
115
- /**
116
- * Convert Zod 3 schema object to JSON Schema.
117
- * Only supports Zod 3.x - Zod 4 is not supported.
118
- */
119
- function zodToJsonSchema(schema) {
120
- const properties = {};
121
- const required = [];
122
- for (const [key, zodSchema] of Object.entries(schema)) {
123
- properties[key] = stripSchemaMeta(zodToJsonSchema$1(zodSchema, {
124
- strictUnions: true,
125
- $refStrategy: "none"
126
- }));
127
- if (!isOptionalSchema(zodSchema)) required.push(key);
128
- }
129
- const result = {
130
- type: "object",
131
- properties
132
- };
133
- if (required.length > 0) result.required = required;
134
- return result;
135
- }
136
- function jsonSchemaToZod$1(jsonSchema) {
51
+ function parseTestingInputSchema(inputSchema) {
52
+ if (!inputSchema) return;
137
53
  try {
138
- return jsonSchemaToZod(jsonSchema);
54
+ const parsed = JSON.parse(inputSchema);
55
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
56
+ return parsed;
139
57
  } catch (error) {
140
- logger$2.warn("jsonSchemaToZod failed:", error);
141
- return z.object({}).passthrough();
142
- }
143
- }
144
- function normalizeSchema(schema) {
145
- if (isZodSchema(schema)) {
146
- const jsonSchema = zodToJsonSchema(schema);
147
- return {
148
- jsonSchema,
149
- zodValidator: jsonSchemaToZod$1(jsonSchema)
150
- };
58
+ console.warn("[WebMCP] Failed to parse testing inputSchema JSON:", error);
59
+ return;
151
60
  }
152
- return {
153
- jsonSchema: schema,
154
- zodValidator: jsonSchemaToZod$1(schema)
155
- };
156
- }
157
- function validateWithZod(data, validator) {
158
- const result = validator.safeParse(data);
159
- if (result.success) return {
160
- success: true,
161
- data: result.data
162
- };
163
- return {
164
- success: false,
165
- error: `Validation failed:\n${result.error.issues.map((err) => ` - ${err.path.join(".") || "root"}: ${err.message}`).join("\n")}`
166
- };
167
61
  }
168
-
169
- //#endregion
170
- //#region src/global.ts
171
- const logger$1 = createLogger("WebModelContext");
172
- const nativeLogger = createLogger("NativeAdapter");
173
- const bridgeLogger = createLogger("MCPBridge");
174
- const testingLogger = createLogger("ModelContextTesting");
175
- /**
176
- * Marker property name used to identify polyfill implementations.
177
- * This constant ensures single source of truth for the marker used in
178
- * both detection (detectNativeAPI) and definition (WebModelContextTesting).
179
- */
180
- const POLYFILL_MARKER_PROPERTY = "__isWebMCPPolyfill";
181
- /**
182
- * Detect if the native Chromium Web Model Context API is available.
183
- * Checks for both navigator.modelContext and navigator.modelContextTesting,
184
- * and verifies they are native implementations (not polyfills).
185
- *
186
- * Detection uses a marker property (`__isWebMCPPolyfill`) on the testing API
187
- * to reliably distinguish polyfills from native implementations. This approach
188
- * works correctly even when class names are minified in production builds.
189
- *
190
- * @returns Detection result with flags for native context and testing API availability
191
- */
192
- function detectNativeAPI() {
193
- /* c8 ignore next 2 */
194
- if (typeof window === "undefined" || typeof navigator === "undefined") return {
195
- hasNativeContext: false,
196
- hasNativeTesting: false
197
- };
198
- const modelContext = navigator.modelContext;
199
- const modelContextTesting = navigator.modelContextTesting;
200
- if (!modelContext || !modelContextTesting) return {
201
- hasNativeContext: Boolean(modelContext),
202
- hasNativeTesting: Boolean(modelContextTesting)
203
- };
204
- if (POLYFILL_MARKER_PROPERTY in modelContextTesting && modelContextTesting[POLYFILL_MARKER_PROPERTY] === true) return {
205
- hasNativeContext: false,
206
- hasNativeTesting: false
62
+ function getTestingShimTools() {
63
+ const testingShim = navigator.modelContextTesting;
64
+ if (!testingShim) return;
65
+ if (typeof testingShim.getRegisteredTools === "function") return {
66
+ testingShim,
67
+ tools: testingShim.getRegisteredTools()
207
68
  };
69
+ if (typeof testingShim.listTools !== "function") return;
208
70
  return {
209
- hasNativeContext: true,
210
- hasNativeTesting: true
71
+ testingShim,
72
+ tools: testingShim.listTools().map((tool) => ({
73
+ name: tool.name,
74
+ description: tool.description ?? "",
75
+ inputSchema: parseTestingInputSchema(tool.inputSchema) ?? {
76
+ type: "object",
77
+ properties: {}
78
+ }
79
+ }))
211
80
  };
212
81
  }
213
- /**
214
- * Adapter that wraps the native Chromium Web Model Context API.
215
- * Synchronizes tool changes from the native API to the MCP bridge,
216
- * enabling MCP clients to stay in sync with the native tool registry.
217
- *
218
- * Key features:
219
- * - Listens to native tool changes via registerToolsChangedCallback()
220
- * - Syncs native tools to MCP bridge automatically
221
- * - Delegates tool execution to native API
222
- * - Converts native results to MCP ToolResponse format
223
- *
224
- * @class NativeModelContextAdapter
225
- * @implements {InternalModelContext}
226
- */
227
- var NativeModelContextAdapter = class {
228
- nativeContext;
229
- nativeTesting;
230
- bridge;
231
- syncInProgress = false;
232
- /**
233
- * Creates a new NativeModelContextAdapter.
234
- *
235
- * @param {MCPBridge} bridge - The MCP bridge instance
236
- * @param {ModelContext} nativeContext - The native navigator.modelContext
237
- * @param {ModelContextTesting} nativeTesting - The native navigator.modelContextTesting
238
- */
239
- constructor(bridge, nativeContext, nativeTesting) {
240
- this.bridge = bridge;
241
- this.nativeContext = nativeContext;
242
- this.nativeTesting = nativeTesting;
243
- this.nativeTesting.registerToolsChangedCallback(() => {
244
- this.syncToolsFromNative();
245
- });
246
- this.syncToolsFromNative();
247
- }
248
- /**
249
- * Synchronizes tools from the native API to the MCP bridge.
250
- * Fetches all tools from navigator.modelContextTesting.listTools()
251
- * and updates the bridge's tool registry.
252
- *
253
- * @private
254
- */
255
- syncToolsFromNative() {
256
- if (this.syncInProgress) return;
257
- this.syncInProgress = true;
258
- try {
259
- const nativeTools = this.nativeTesting.listTools();
260
- this.bridge.tools.clear();
261
- for (const toolInfo of nativeTools) try {
262
- const inputSchema = JSON.parse(toolInfo.inputSchema);
263
- const validatedTool = {
264
- name: toolInfo.name,
265
- description: toolInfo.description,
266
- inputSchema,
267
- execute: async (args) => {
268
- const result = await this.nativeTesting.executeTool(toolInfo.name, JSON.stringify(args));
269
- return this.convertToToolResponse(result);
270
- },
271
- inputValidator: jsonSchemaToZod$1(inputSchema)
272
- };
273
- this.bridge.tools.set(toolInfo.name, validatedTool);
274
- } catch (error) {
275
- nativeLogger.error(`Failed to sync tool "${toolInfo.name}":`, error);
276
- }
277
- this.notifyMCPServers();
278
- } finally {
279
- this.syncInProgress = false;
280
- }
281
- }
282
- /**
283
- * Converts native API result to MCP ToolResponse format.
284
- * Native API returns simplified values (string, number, object, etc.)
285
- * which need to be wrapped in the MCP CallToolResult format.
286
- *
287
- * @param {unknown} result - The result from native executeTool()
288
- * @returns {ToolResponse} Formatted MCP ToolResponse
289
- * @private
290
- */
291
- convertToToolResponse(result) {
292
- if (typeof result === "string") return { content: [{
293
- type: "text",
294
- text: result
295
- }] };
296
- if (result === void 0 || result === null) return { content: [{
297
- type: "text",
298
- text: ""
299
- }] };
300
- if (typeof result === "object") return {
82
+ function syncToolsFromTestingShim(server) {
83
+ const shimState = getTestingShimTools();
84
+ if (!shimState) return 0;
85
+ const { testingShim, tools } = shimState;
86
+ return server.backfillTools(tools, async (name, args) => {
87
+ const serialized = await testingShim.executeTool(name, JSON.stringify(args ?? {}));
88
+ if (serialized === null) return {
301
89
  content: [{
302
90
  type: "text",
303
- text: JSON.stringify(result, null, 2)
91
+ text: "Tool execution interrupted by navigation"
304
92
  }],
305
- structuredContent: result
93
+ isError: true
306
94
  };
307
- return { content: [{
308
- type: "text",
309
- text: String(result)
310
- }] };
311
- }
312
- /**
313
- * Notifies all connected MCP servers that the tools list has changed.
314
- *
315
- * @private
316
- */
317
- notifyMCPServers() {
318
- if (this.bridge.tabServer?.notification) this.bridge.tabServer.notification({
319
- method: "notifications/tools/list_changed",
320
- params: {}
321
- });
322
- if (this.bridge.iframeServer?.notification) this.bridge.iframeServer.notification({
323
- method: "notifications/tools/list_changed",
324
- params: {}
325
- });
326
- }
327
- /**
328
- * Provides context (tools) to AI models via the native API.
329
- * Delegates to navigator.modelContext.provideContext().
330
- * Tool change callback will fire and trigger sync automatically.
331
- *
332
- * @param {ModelContextInput} context - Context containing tools to register
333
- */
334
- provideContext(context) {
335
- this.nativeContext.provideContext(context);
336
- }
337
- /**
338
- * Registers a single tool dynamically via the native API.
339
- * Delegates to navigator.modelContext.registerTool().
340
- * Tool change callback will fire and trigger sync automatically.
341
- *
342
- * @param {ToolDescriptor} tool - The tool descriptor to register
343
- * @returns {{unregister: () => void}} Object with unregister function
344
- */
345
- registerTool(tool) {
346
- return this.nativeContext.registerTool(tool);
347
- }
348
- /**
349
- * Unregisters a tool by name via the native API.
350
- * Delegates to navigator.modelContext.unregisterTool().
351
- *
352
- * @param {string} name - Name of the tool to unregister
353
- */
354
- unregisterTool(name) {
355
- this.nativeContext.unregisterTool(name);
356
- }
357
- /**
358
- * Clears all registered tools via the native API.
359
- * Delegates to navigator.modelContext.clearContext().
360
- */
361
- clearContext() {
362
- this.nativeContext.clearContext();
363
- }
364
- /**
365
- * Executes a tool via the native API.
366
- * Delegates to navigator.modelContextTesting.executeTool() with JSON string args.
367
- *
368
- * @param {string} toolName - Name of the tool to execute
369
- * @param {Record<string, unknown>} args - Arguments to pass to the tool
370
- * @returns {Promise<ToolResponse>} The tool's response in MCP format
371
- * @internal
372
- */
373
- async executeTool(toolName, args) {
95
+ let parsed;
374
96
  try {
375
- const result = await this.nativeTesting.executeTool(toolName, JSON.stringify(args));
376
- return this.convertToToolResponse(result);
377
- } catch (error) {
378
- nativeLogger.error(`Error executing tool "${toolName}":`, error);
379
- return {
380
- content: [{
381
- type: "text",
382
- text: `Error: ${error instanceof Error ? error.message : String(error)}`
383
- }],
384
- isError: true
385
- };
97
+ parsed = JSON.parse(serialized);
98
+ } catch (parseError) {
99
+ throw new Error(`Failed to parse serialized tool response for ${name}: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
386
100
  }
387
- }
388
- /**
389
- * Lists all registered tools from the MCP bridge.
390
- * Returns tools synced from the native API.
391
- *
392
- * @returns {Array<{name: string, description: string, inputSchema: InputSchema}>} Array of tool descriptors
393
- */
394
- listTools() {
395
- return Array.from(this.bridge.tools.values()).map((tool) => ({
396
- name: tool.name,
397
- description: tool.description,
398
- inputSchema: tool.inputSchema,
399
- ...tool.outputSchema && { outputSchema: tool.outputSchema },
400
- ...tool.annotations && { annotations: tool.annotations }
401
- }));
402
- }
403
- /**
404
- * Registers a resource dynamically.
405
- * Note: Native Chromium API does not yet support resources.
406
- * This is a polyfill-only feature.
407
- */
408
- registerResource(_resource) {
409
- nativeLogger.warn("registerResource is not supported by native API");
410
- return { unregister: () => {} };
411
- }
412
- /**
413
- * Unregisters a resource by URI.
414
- * Note: Native Chromium API does not yet support resources.
415
- */
416
- unregisterResource(_uri) {
417
- nativeLogger.warn("unregisterResource is not supported by native API");
418
- }
419
- /**
420
- * Lists all registered resources.
421
- * Note: Native Chromium API does not yet support resources.
422
- */
423
- listResources() {
424
- return [];
425
- }
426
- /**
427
- * Lists all resource templates.
428
- * Note: Native Chromium API does not yet support resources.
429
- */
430
- listResourceTemplates() {
431
- return [];
432
- }
433
- /**
434
- * Reads a resource by URI.
435
- * Note: Native Chromium API does not yet support resources.
436
- * @internal
437
- */
438
- async readResource(_uri) {
439
- throw new Error("[Native Adapter] readResource is not supported by native API");
440
- }
441
- /**
442
- * Registers a prompt dynamically.
443
- * Note: Native Chromium API does not yet support prompts.
444
- * This is a polyfill-only feature.
445
- */
446
- registerPrompt(_prompt) {
447
- nativeLogger.warn("registerPrompt is not supported by native API");
448
- return { unregister: () => {} };
449
- }
450
- /**
451
- * Unregisters a prompt by name.
452
- * Note: Native Chromium API does not yet support prompts.
453
- */
454
- unregisterPrompt(_name) {
455
- nativeLogger.warn("unregisterPrompt is not supported by native API");
456
- }
457
- /**
458
- * Lists all registered prompts.
459
- * Note: Native Chromium API does not yet support prompts.
460
- */
461
- listPrompts() {
462
- return [];
463
- }
464
- /**
465
- * Gets a prompt with arguments.
466
- * Note: Native Chromium API does not yet support prompts.
467
- * @internal
468
- */
469
- async getPrompt(_name, _args) {
470
- throw new Error("[Native Adapter] getPrompt is not supported by native API");
471
- }
472
- /**
473
- * Adds an event listener for tool call events.
474
- * Delegates to the native API's addEventListener.
475
- *
476
- * @param {'toolcall'} type - Event type
477
- * @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler
478
- * @param {boolean | AddEventListenerOptions} [options] - Event listener options
479
- */
480
- addEventListener(type, listener, options) {
481
- this.nativeContext.addEventListener(type, listener, options);
482
- }
483
- /**
484
- * Removes an event listener for tool call events.
485
- * Delegates to the native API's removeEventListener.
486
- *
487
- * @param {'toolcall'} type - Event type
488
- * @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler
489
- * @param {boolean | EventListenerOptions} [options] - Event listener options
490
- */
491
- removeEventListener(type, listener, options) {
492
- this.nativeContext.removeEventListener(type, listener, options);
493
- }
494
- /**
495
- * Dispatches a tool call event.
496
- * Delegates to the native API's dispatchEvent.
497
- *
498
- * @param {Event} event - The event to dispatch
499
- * @returns {boolean} False if event was cancelled, true otherwise
500
- */
501
- dispatchEvent(event) {
502
- return this.nativeContext.dispatchEvent(event);
503
- }
504
- /**
505
- * Request an LLM completion from the connected client.
506
- * Note: Native Chromium API does not yet support sampling.
507
- * This is handled by the polyfill.
508
- */
509
- async createMessage(params) {
510
- const underlyingServer = this.bridge.tabServer.server;
511
- if (!underlyingServer?.createMessage) throw new Error("Sampling is not supported: no connected client with sampling capability");
512
- return underlyingServer.createMessage(params);
513
- }
514
- /**
515
- * Request user input from the connected client.
516
- * Note: Native Chromium API does not yet support elicitation.
517
- * This is handled by the polyfill.
518
- */
519
- async elicitInput(params) {
520
- const underlyingServer = this.bridge.tabServer.server;
521
- if (!underlyingServer?.elicitInput) throw new Error("Elicitation is not supported: no connected client with elicitation capability");
522
- return underlyingServer.elicitInput(params);
523
- }
524
- };
525
- /**
526
- * ToolCallEvent implementation for the Web Model Context API.
527
- * Represents an event fired when a tool is called, allowing event listeners
528
- * to intercept and provide custom responses.
529
- *
530
- * @class WebToolCallEvent
531
- * @extends {Event}
532
- * @implements {ToolCallEvent}
533
- */
534
- var WebToolCallEvent = class extends Event {
535
- name;
536
- arguments;
537
- _response = null;
538
- _responded = false;
539
- /**
540
- * Creates a new ToolCallEvent.
541
- *
542
- * @param {string} toolName - Name of the tool being called
543
- * @param {Record<string, unknown>} args - Validated arguments for the tool
544
- */
545
- constructor(toolName, args) {
546
- super("toolcall", { cancelable: true });
547
- this.name = toolName;
548
- this.arguments = args;
549
- }
550
- /**
551
- * Provides a response for this tool call, preventing the default tool execution.
552
- *
553
- * @param {ToolResponse} response - The response to use instead of executing the tool
554
- * @throws {Error} If a response has already been provided
555
- */
556
- respondWith(response) {
557
- if (this._responded) throw new Error("Response already provided for this tool call");
558
- this._response = response;
559
- this._responded = true;
560
- }
561
- /**
562
- * Gets the response provided via respondWith().
563
- *
564
- * @returns {ToolResponse | null} The response, or null if none provided
565
- */
566
- getResponse() {
567
- return this._response;
568
- }
569
- /**
570
- * Checks whether a response has been provided for this tool call.
571
- *
572
- * @returns {boolean} True if respondWith() was called
573
- */
574
- hasResponse() {
575
- return this._responded;
576
- }
577
- };
578
- /**
579
- * Time window in milliseconds to detect rapid duplicate tool registrations.
580
- * Used to filter out double-registrations caused by React Strict Mode.
581
- */
582
- const RAPID_DUPLICATE_WINDOW_MS = 50;
583
- /**
584
- * Testing API implementation for the Model Context Protocol.
585
- * Provides debugging, mocking, and testing capabilities for tool execution.
586
- * Implements both Chromium native methods and polyfill-specific extensions.
587
- *
588
- * @class WebModelContextTesting
589
- * @implements {ModelContextTesting}
590
- */
591
- var WebModelContextTesting = class {
592
- /**
593
- * Marker property to identify this as a polyfill implementation.
594
- * Used by detectNativeAPI() to distinguish polyfill from native Chromium API.
595
- * This approach works reliably even when class names are minified in production builds.
596
- *
597
- * @see POLYFILL_MARKER_PROPERTY - The constant defining this property name
598
- * @see MayHavePolyfillMarker - The interface for type-safe detection
599
- */
600
- [POLYFILL_MARKER_PROPERTY] = true;
601
- toolCallHistory = [];
602
- mockResponses = /* @__PURE__ */ new Map();
603
- toolsChangedCallbacks = /* @__PURE__ */ new Set();
604
- bridge;
605
- /**
606
- * Creates a new WebModelContextTesting instance.
607
- *
608
- * @param {MCPBridge} bridge - The MCP bridge instance to test
609
- */
610
- constructor(bridge) {
611
- this.bridge = bridge;
612
- }
613
- /**
614
- * Records a tool call in the history.
615
- * Called internally by WebModelContext when tools are executed.
616
- *
617
- * @param {string} toolName - Name of the tool that was called
618
- * @param {Record<string, unknown>} args - Arguments passed to the tool
619
- * @internal
620
- */
621
- recordToolCall(toolName, args) {
622
- this.toolCallHistory.push({
623
- toolName,
624
- arguments: args,
625
- timestamp: Date.now()
626
- });
627
- }
628
- /**
629
- * Checks if a mock response is registered for a specific tool.
630
- *
631
- * @param {string} toolName - Name of the tool to check
632
- * @returns {boolean} True if a mock response exists
633
- * @internal
634
- */
635
- hasMockResponse(toolName) {
636
- return this.mockResponses.has(toolName);
637
- }
638
- /**
639
- * Retrieves the mock response for a specific tool.
640
- *
641
- * @param {string} toolName - Name of the tool
642
- * @returns {ToolResponse | undefined} The mock response, or undefined if none exists
643
- * @internal
644
- */
645
- getMockResponse(toolName) {
646
- return this.mockResponses.get(toolName);
647
- }
648
- /**
649
- * Notifies all registered callbacks that the tools list has changed.
650
- * Called internally when tools are registered, unregistered, or cleared.
651
- *
652
- * @internal
653
- */
654
- notifyToolsChanged() {
655
- for (const callback of this.toolsChangedCallbacks) try {
656
- callback();
657
- } catch (error) {
658
- testingLogger.error("Error in tools changed callback:", error);
659
- }
660
- }
661
- /**
662
- * Executes a tool directly with JSON string input (Chromium native API).
663
- * Parses the JSON input, validates it, and executes the tool.
664
- *
665
- * @param {string} toolName - Name of the tool to execute
666
- * @param {string} inputArgsJson - JSON string of input arguments
667
- * @returns {Promise<unknown>} The tool's result, or undefined on error
668
- * @throws {SyntaxError} If the input JSON is invalid
669
- * @throws {Error} If the tool does not exist
670
- */
671
- async executeTool(toolName, inputArgsJson) {
672
- let args;
673
- try {
674
- args = JSON.parse(inputArgsJson);
675
- } catch (error) {
676
- throw new SyntaxError(`Invalid JSON input: ${error instanceof Error ? error.message : String(error)}`);
677
- }
678
- if (!this.bridge.tools.get(toolName)) throw new Error(`Tool not found: ${toolName}`);
679
- const result = await this.bridge.modelContext.executeTool(toolName, args);
680
- if (result.isError) return;
681
- if (result.structuredContent) return result.structuredContent;
682
- if (result.content && result.content.length > 0) {
683
- const firstContent = result.content[0];
684
- if (firstContent && firstContent.type === "text") return firstContent.text;
685
- }
686
- }
687
- /**
688
- * Lists all registered tools with inputSchema as JSON string (Chromium native API).
689
- * Returns an array of ToolInfo objects where inputSchema is stringified.
690
- *
691
- * @returns {Array<{name: string, description: string, inputSchema: string}>} Array of tool information
692
- */
693
- listTools() {
694
- return this.bridge.modelContext.listTools().map((tool) => ({
695
- name: tool.name,
696
- description: tool.description,
697
- inputSchema: JSON.stringify(tool.inputSchema)
698
- }));
699
- }
700
- /**
701
- * Registers a callback that fires when the tools list changes (Chromium native API).
702
- * The callback will be invoked on registerTool, unregisterTool, provideContext, and clearContext.
703
- *
704
- * @param {() => void} callback - Function to call when tools change
705
- */
706
- registerToolsChangedCallback(callback) {
707
- this.toolsChangedCallbacks.add(callback);
708
- }
709
- /**
710
- * Gets all tool calls that have been recorded (polyfill extension).
711
- *
712
- * @returns {Array<{toolName: string, arguments: Record<string, unknown>, timestamp: number}>} Tool call history
713
- */
714
- getToolCalls() {
715
- return [...this.toolCallHistory];
716
- }
717
- /**
718
- * Clears the tool call history (polyfill extension).
719
- */
720
- clearToolCalls() {
721
- this.toolCallHistory = [];
722
- }
723
- /**
724
- * Sets a mock response for a specific tool (polyfill extension).
725
- * When set, the tool's execute function will be bypassed.
726
- *
727
- * @param {string} toolName - Name of the tool to mock
728
- * @param {ToolResponse} response - The mock response to return
729
- */
730
- setMockToolResponse(toolName, response) {
731
- this.mockResponses.set(toolName, response);
732
- }
733
- /**
734
- * Clears the mock response for a specific tool (polyfill extension).
735
- *
736
- * @param {string} toolName - Name of the tool
737
- */
738
- clearMockToolResponse(toolName) {
739
- this.mockResponses.delete(toolName);
740
- }
741
- /**
742
- * Clears all mock tool responses (polyfill extension).
743
- */
744
- clearAllMockToolResponses() {
745
- this.mockResponses.clear();
746
- }
747
- /**
748
- * Gets the current tools registered in the system (polyfill extension).
749
- *
750
- * @returns {ReturnType<InternalModelContext['listTools']>} Array of registered tools
751
- */
752
- getRegisteredTools() {
753
- return this.bridge.modelContext.listTools();
754
- }
755
- /**
756
- * Resets the entire testing state (polyfill extension).
757
- * Clears both tool call history and all mock responses.
758
- */
759
- reset() {
760
- this.clearToolCalls();
761
- this.clearAllMockToolResponses();
762
- }
763
- };
764
- /**
765
- * ModelContext implementation that bridges to the Model Context Protocol SDK.
766
- * Implements the W3C Web Model Context API proposal with two-bucket tool management:
767
- * - Bucket A (provideContextTools): Tools registered via provideContext()
768
- * - Bucket B (dynamicTools): Tools registered via registerTool()
769
- *
770
- * This separation ensures that component-scoped dynamic tools persist across
771
- * app-level provideContext() calls.
772
- *
773
- * @class WebModelContext
774
- * @implements {InternalModelContext}
775
- */
776
- var WebModelContext = class {
777
- bridge;
778
- eventTarget;
779
- provideContextTools;
780
- dynamicTools;
781
- provideContextResources;
782
- dynamicResources;
783
- provideContextPrompts;
784
- dynamicPrompts;
785
- toolRegistrationTimestamps;
786
- resourceRegistrationTimestamps;
787
- promptRegistrationTimestamps;
788
- toolUnregisterFunctions;
789
- resourceUnregisterFunctions;
790
- promptUnregisterFunctions;
791
- /**
792
- * Tracks which list change notifications are pending.
793
- * Uses microtask-based batching to coalesce rapid registrations
794
- * (e.g., React mount phase) into a single notification per list type.
795
- */
796
- pendingNotifications = /* @__PURE__ */ new Set();
797
- testingAPI;
798
- /**
799
- * Creates a new WebModelContext instance.
800
- *
801
- * @param {MCPBridge} bridge - The MCP bridge to use for communication
802
- */
803
- constructor(bridge) {
804
- this.bridge = bridge;
805
- this.eventTarget = new EventTarget();
806
- this.provideContextTools = /* @__PURE__ */ new Map();
807
- this.dynamicTools = /* @__PURE__ */ new Map();
808
- this.toolRegistrationTimestamps = /* @__PURE__ */ new Map();
809
- this.toolUnregisterFunctions = /* @__PURE__ */ new Map();
810
- this.provideContextResources = /* @__PURE__ */ new Map();
811
- this.dynamicResources = /* @__PURE__ */ new Map();
812
- this.resourceRegistrationTimestamps = /* @__PURE__ */ new Map();
813
- this.resourceUnregisterFunctions = /* @__PURE__ */ new Map();
814
- this.provideContextPrompts = /* @__PURE__ */ new Map();
815
- this.dynamicPrompts = /* @__PURE__ */ new Map();
816
- this.promptRegistrationTimestamps = /* @__PURE__ */ new Map();
817
- this.promptUnregisterFunctions = /* @__PURE__ */ new Map();
818
- }
819
- /**
820
- * Sets the testing API instance.
821
- * Called during initialization to enable testing features.
822
- *
823
- * @param {WebModelContextTesting} testingAPI - The testing API instance
824
- * @internal
825
- */
826
- setTestingAPI(testingAPI) {
827
- this.testingAPI = testingAPI;
828
- }
829
- /**
830
- * Adds an event listener for tool call events.
831
- *
832
- * @param {'toolcall'} type - Event type (only 'toolcall' is supported)
833
- * @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler function
834
- * @param {boolean | AddEventListenerOptions} [options] - Event listener options
835
- */
836
- addEventListener(type, listener, options) {
837
- this.eventTarget.addEventListener(type, listener, options);
838
- }
839
- /**
840
- * Removes an event listener for tool call events.
841
- *
842
- * @param {'toolcall'} type - Event type (only 'toolcall' is supported)
843
- * @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler function
844
- * @param {boolean | EventListenerOptions} [options] - Event listener options
845
- */
846
- removeEventListener(type, listener, options) {
847
- this.eventTarget.removeEventListener(type, listener, options);
848
- }
849
- /**
850
- * Dispatches a tool call event to all registered listeners.
851
- *
852
- * @param {Event} event - The event to dispatch
853
- * @returns {boolean} False if event was cancelled, true otherwise
854
- */
855
- dispatchEvent(event) {
856
- return this.eventTarget.dispatchEvent(event);
857
- }
858
- /**
859
- * Provides context (tools, resources, prompts) to AI models by registering base items (Bucket A).
860
- * Clears and replaces all previously registered base items while preserving
861
- * dynamic items registered via register* methods.
862
- *
863
- * @param {ModelContextInput} context - Context containing tools, resources, and prompts to register
864
- * @throws {Error} If a name/uri collides with existing dynamic items
865
- */
866
- provideContext(context) {
867
- this.provideContextTools.clear();
868
- this.provideContextResources.clear();
869
- this.provideContextPrompts.clear();
870
- for (const tool of context.tools ?? []) {
871
- if (tool.name.startsWith("_")) logger$1.warn(`⚠️ Warning: Tool name "${tool.name}" starts with underscore. This may cause compatibility issues with some MCP clients. Consider using a letter as the first character.`);
872
- if (/^[0-9]/.test(tool.name)) logger$1.warn(`⚠️ Warning: Tool name "${tool.name}" starts with a number. This may cause compatibility issues. Consider using a letter as the first character.`);
873
- if (tool.name.startsWith("-")) logger$1.warn(`⚠️ Warning: Tool name "${tool.name}" starts with hyphen. This may cause compatibility issues. Consider using a letter as the first character.`);
874
- if (this.dynamicTools.has(tool.name)) throw new Error(`[Web Model Context] Tool name collision: "${tool.name}" is already registered via registerTool(). Please use a different name or unregister the dynamic tool first.`);
875
- const { jsonSchema: inputJson, zodValidator: inputZod } = normalizeSchema(tool.inputSchema);
876
- const normalizedOutput = tool.outputSchema ? normalizeSchema(tool.outputSchema) : null;
877
- const validatedTool = {
878
- name: tool.name,
879
- description: tool.description,
880
- inputSchema: inputJson,
881
- ...normalizedOutput && { outputSchema: normalizedOutput.jsonSchema },
882
- ...tool.annotations && { annotations: tool.annotations },
883
- execute: tool.execute,
884
- inputValidator: inputZod,
885
- ...normalizedOutput && { outputValidator: normalizedOutput.zodValidator }
886
- };
887
- this.provideContextTools.set(tool.name, validatedTool);
888
- }
889
- for (const resource of context.resources ?? []) {
890
- if (this.dynamicResources.has(resource.uri)) throw new Error(`[Web Model Context] Resource URI collision: "${resource.uri}" is already registered via registerResource(). Please use a different URI or unregister the dynamic resource first.`);
891
- const validatedResource = this.validateResource(resource);
892
- this.provideContextResources.set(resource.uri, validatedResource);
893
- }
894
- for (const prompt of context.prompts ?? []) {
895
- if (this.dynamicPrompts.has(prompt.name)) throw new Error(`[Web Model Context] Prompt name collision: "${prompt.name}" is already registered via registerPrompt(). Please use a different name or unregister the dynamic prompt first.`);
896
- const validatedPrompt = this.validatePrompt(prompt);
897
- this.provideContextPrompts.set(prompt.name, validatedPrompt);
898
- }
899
- this.updateBridgeTools();
900
- this.updateBridgeResources();
901
- this.updateBridgePrompts();
902
- this.scheduleListChanged("tools");
903
- this.scheduleListChanged("resources");
904
- this.scheduleListChanged("prompts");
905
- }
906
- /**
907
- * Validates and normalizes a resource descriptor.
908
- * @private
909
- */
910
- validateResource(resource) {
911
- const templateParamRegex = /\{([^}]{1,100})\}/g;
912
- const templateParams = [];
913
- for (const match of resource.uri.matchAll(templateParamRegex)) {
914
- const paramName = match[1];
915
- templateParams.push(paramName);
916
- }
917
- return {
918
- uri: resource.uri,
919
- name: resource.name,
920
- description: resource.description,
921
- mimeType: resource.mimeType,
922
- read: resource.read,
923
- isTemplate: templateParams.length > 0,
924
- templateParams
925
- };
926
- }
927
- /**
928
- * Validates and normalizes a prompt descriptor.
929
- * @private
930
- */
931
- validatePrompt(prompt) {
932
- let argsSchema;
933
- let argsValidator;
934
- if (prompt.argsSchema) {
935
- const normalized = normalizeSchema(prompt.argsSchema);
936
- argsSchema = normalized.jsonSchema;
937
- argsValidator = normalized.zodValidator;
938
- }
939
- return {
940
- name: prompt.name,
941
- description: prompt.description,
942
- argsSchema,
943
- get: prompt.get,
944
- argsValidator
945
- };
946
- }
947
- /**
948
- * Registers a single tool dynamically (Bucket B).
949
- * Dynamic tools persist across provideContext() calls and can be independently managed.
950
- *
951
- * @param {ToolDescriptor} tool - The tool descriptor to register
952
- * @returns {{unregister: () => void}} Object with unregister function
953
- * @throws {Error} If tool name collides with existing tools
954
- */
955
- registerTool(tool) {
956
- if (tool.name.startsWith("_")) logger$1.warn(`⚠️ Warning: Tool name "${tool.name}" starts with underscore. This may cause compatibility issues with some MCP clients. Consider using a letter as the first character.`);
957
- if (/^[0-9]/.test(tool.name)) logger$1.warn(`⚠️ Warning: Tool name "${tool.name}" starts with a number. This may cause compatibility issues. Consider using a letter as the first character.`);
958
- if (tool.name.startsWith("-")) logger$1.warn(`⚠️ Warning: Tool name "${tool.name}" starts with hyphen. This may cause compatibility issues. Consider using a letter as the first character.`);
959
- const now = Date.now();
960
- const lastRegistration = this.toolRegistrationTimestamps.get(tool.name);
961
- if (lastRegistration && now - lastRegistration < RAPID_DUPLICATE_WINDOW_MS) {
962
- logger$1.warn(`Tool "${tool.name}" registered multiple times within ${RAPID_DUPLICATE_WINDOW_MS}ms. This is likely due to React Strict Mode double-mounting. Ignoring duplicate registration.`);
963
- const existingUnregister = this.toolUnregisterFunctions.get(tool.name);
964
- if (existingUnregister) return { unregister: existingUnregister };
965
- }
966
- if (this.provideContextTools.has(tool.name)) throw new Error(`[Web Model Context] Tool name collision: "${tool.name}" is already registered via provideContext(). Please use a different name or update your provideContext() call.`);
967
- if (this.dynamicTools.has(tool.name)) throw new Error(`[Web Model Context] Tool name collision: "${tool.name}" is already registered via registerTool(). Please unregister it first or use a different name.`);
968
- const { jsonSchema: inputJson, zodValidator: inputZod } = normalizeSchema(tool.inputSchema);
969
- const normalizedOutput = tool.outputSchema ? normalizeSchema(tool.outputSchema) : null;
970
- const validatedTool = {
971
- name: tool.name,
972
- description: tool.description,
973
- inputSchema: inputJson,
974
- ...normalizedOutput && { outputSchema: normalizedOutput.jsonSchema },
975
- ...tool.annotations && { annotations: tool.annotations },
976
- execute: tool.execute,
977
- inputValidator: inputZod,
978
- ...normalizedOutput && { outputValidator: normalizedOutput.zodValidator }
979
- };
980
- this.dynamicTools.set(tool.name, validatedTool);
981
- this.toolRegistrationTimestamps.set(tool.name, now);
982
- this.updateBridgeTools();
983
- this.scheduleListChanged("tools");
984
- const unregisterFn = () => {
985
- 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.`);
986
- if (!this.dynamicTools.has(tool.name)) {
987
- logger$1.warn(`Tool "${tool.name}" is not registered, ignoring unregister call`);
988
- return;
989
- }
990
- this.dynamicTools.delete(tool.name);
991
- this.toolRegistrationTimestamps.delete(tool.name);
992
- this.toolUnregisterFunctions.delete(tool.name);
993
- this.updateBridgeTools();
994
- this.scheduleListChanged("tools");
995
- };
996
- this.toolUnregisterFunctions.set(tool.name, unregisterFn);
997
- return { unregister: unregisterFn };
998
- }
999
- /**
1000
- * Registers a single resource dynamically (Bucket B).
1001
- * Dynamic resources persist across provideContext() calls and can be independently managed.
1002
- *
1003
- * @param {ResourceDescriptor} resource - The resource descriptor to register
1004
- * @returns {{unregister: () => void}} Object with unregister function
1005
- * @throws {Error} If resource URI collides with existing resources
1006
- */
1007
- registerResource(resource) {
1008
- const now = Date.now();
1009
- const lastRegistration = this.resourceRegistrationTimestamps.get(resource.uri);
1010
- if (lastRegistration && now - lastRegistration < RAPID_DUPLICATE_WINDOW_MS) {
1011
- logger$1.warn(`Resource "${resource.uri}" registered multiple times within ${RAPID_DUPLICATE_WINDOW_MS}ms. This is likely due to React Strict Mode double-mounting. Ignoring duplicate registration.`);
1012
- const existingUnregister = this.resourceUnregisterFunctions.get(resource.uri);
1013
- if (existingUnregister) return { unregister: existingUnregister };
1014
- }
1015
- if (this.provideContextResources.has(resource.uri)) throw new Error(`[Web Model Context] Resource URI collision: "${resource.uri}" is already registered via provideContext(). Please use a different URI or update your provideContext() call.`);
1016
- if (this.dynamicResources.has(resource.uri)) throw new Error(`[Web Model Context] Resource URI collision: "${resource.uri}" is already registered via registerResource(). Please unregister it first or use a different URI.`);
1017
- const validatedResource = this.validateResource(resource);
1018
- this.dynamicResources.set(resource.uri, validatedResource);
1019
- this.resourceRegistrationTimestamps.set(resource.uri, now);
1020
- this.updateBridgeResources();
1021
- this.scheduleListChanged("resources");
1022
- const unregisterFn = () => {
1023
- if (this.provideContextResources.has(resource.uri)) throw new Error(`[Web Model Context] Cannot unregister resource "${resource.uri}": This resource was registered via provideContext(). Use provideContext() to update the base resource set.`);
1024
- if (!this.dynamicResources.has(resource.uri)) {
1025
- logger$1.warn(`Resource "${resource.uri}" is not registered, ignoring unregister call`);
1026
- return;
1027
- }
1028
- this.dynamicResources.delete(resource.uri);
1029
- this.resourceRegistrationTimestamps.delete(resource.uri);
1030
- this.resourceUnregisterFunctions.delete(resource.uri);
1031
- this.updateBridgeResources();
1032
- this.scheduleListChanged("resources");
1033
- };
1034
- this.resourceUnregisterFunctions.set(resource.uri, unregisterFn);
1035
- return { unregister: unregisterFn };
1036
- }
1037
- /**
1038
- * Unregisters a resource by URI.
1039
- * Can unregister resources from either Bucket A (provideContext) or Bucket B (registerResource).
1040
- *
1041
- * @param {string} uri - URI of the resource to unregister
1042
- */
1043
- unregisterResource(uri) {
1044
- const inProvideContext = this.provideContextResources.has(uri);
1045
- const inDynamic = this.dynamicResources.has(uri);
1046
- if (!inProvideContext && !inDynamic) {
1047
- logger$1.warn(`Resource "${uri}" is not registered, ignoring unregister call`);
1048
- return;
1049
- }
1050
- if (inProvideContext) this.provideContextResources.delete(uri);
1051
- if (inDynamic) {
1052
- this.dynamicResources.delete(uri);
1053
- this.resourceRegistrationTimestamps.delete(uri);
1054
- this.resourceUnregisterFunctions.delete(uri);
1055
- }
1056
- this.updateBridgeResources();
1057
- this.scheduleListChanged("resources");
1058
- }
1059
- /**
1060
- * Lists all registered resources in MCP format.
1061
- * Returns static resources from both buckets (not templates).
1062
- *
1063
- * @returns {Resource[]} Array of resource descriptors
1064
- */
1065
- listResources() {
1066
- return Array.from(this.bridge.resources.values()).filter((r) => !r.isTemplate).map((resource) => ({
1067
- uri: resource.uri,
1068
- name: resource.name,
1069
- description: resource.description,
1070
- mimeType: resource.mimeType
1071
- }));
1072
- }
1073
- /**
1074
- * Lists all registered resource templates.
1075
- * Returns only resources with URI templates (dynamic resources).
1076
- *
1077
- * @returns {Array<{uriTemplate: string, name: string, description?: string, mimeType?: string}>}
1078
- */
1079
- listResourceTemplates() {
1080
- return Array.from(this.bridge.resources.values()).filter((r) => r.isTemplate).map((resource) => ({
1081
- uriTemplate: resource.uri,
1082
- name: resource.name,
1083
- ...resource.description !== void 0 && { description: resource.description },
1084
- ...resource.mimeType !== void 0 && { mimeType: resource.mimeType }
1085
- }));
1086
- }
1087
- /**
1088
- * Registers a single prompt dynamically (Bucket B).
1089
- * Dynamic prompts persist across provideContext() calls and can be independently managed.
1090
- *
1091
- * @param {PromptDescriptor} prompt - The prompt descriptor to register
1092
- * @returns {{unregister: () => void}} Object with unregister function
1093
- * @throws {Error} If prompt name collides with existing prompts
1094
- */
1095
- registerPrompt(prompt) {
1096
- const now = Date.now();
1097
- const lastRegistration = this.promptRegistrationTimestamps.get(prompt.name);
1098
- if (lastRegistration && now - lastRegistration < RAPID_DUPLICATE_WINDOW_MS) {
1099
- logger$1.warn(`Prompt "${prompt.name}" registered multiple times within ${RAPID_DUPLICATE_WINDOW_MS}ms. This is likely due to React Strict Mode double-mounting. Ignoring duplicate registration.`);
1100
- const existingUnregister = this.promptUnregisterFunctions.get(prompt.name);
1101
- if (existingUnregister) return { unregister: existingUnregister };
1102
- }
1103
- if (this.provideContextPrompts.has(prompt.name)) throw new Error(`[Web Model Context] Prompt name collision: "${prompt.name}" is already registered via provideContext(). Please use a different name or update your provideContext() call.`);
1104
- if (this.dynamicPrompts.has(prompt.name)) throw new Error(`[Web Model Context] Prompt name collision: "${prompt.name}" is already registered via registerPrompt(). Please unregister it first or use a different name.`);
1105
- const validatedPrompt = this.validatePrompt(prompt);
1106
- this.dynamicPrompts.set(prompt.name, validatedPrompt);
1107
- this.promptRegistrationTimestamps.set(prompt.name, now);
1108
- this.updateBridgePrompts();
1109
- this.scheduleListChanged("prompts");
1110
- const unregisterFn = () => {
1111
- if (this.provideContextPrompts.has(prompt.name)) throw new Error(`[Web Model Context] Cannot unregister prompt "${prompt.name}": This prompt was registered via provideContext(). Use provideContext() to update the base prompt set.`);
1112
- if (!this.dynamicPrompts.has(prompt.name)) {
1113
- logger$1.warn(`Prompt "${prompt.name}" is not registered, ignoring unregister call`);
1114
- return;
1115
- }
1116
- this.dynamicPrompts.delete(prompt.name);
1117
- this.promptRegistrationTimestamps.delete(prompt.name);
1118
- this.promptUnregisterFunctions.delete(prompt.name);
1119
- this.updateBridgePrompts();
1120
- this.scheduleListChanged("prompts");
1121
- };
1122
- this.promptUnregisterFunctions.set(prompt.name, unregisterFn);
1123
- return { unregister: unregisterFn };
1124
- }
1125
- /**
1126
- * Unregisters a prompt by name.
1127
- * Can unregister prompts from either Bucket A (provideContext) or Bucket B (registerPrompt).
1128
- *
1129
- * @param {string} name - Name of the prompt to unregister
1130
- */
1131
- unregisterPrompt(name) {
1132
- const inProvideContext = this.provideContextPrompts.has(name);
1133
- const inDynamic = this.dynamicPrompts.has(name);
1134
- if (!inProvideContext && !inDynamic) {
1135
- logger$1.warn(`Prompt "${name}" is not registered, ignoring unregister call`);
1136
- return;
1137
- }
1138
- if (inProvideContext) this.provideContextPrompts.delete(name);
1139
- if (inDynamic) {
1140
- this.dynamicPrompts.delete(name);
1141
- this.promptRegistrationTimestamps.delete(name);
1142
- this.promptUnregisterFunctions.delete(name);
1143
- }
1144
- this.updateBridgePrompts();
1145
- this.scheduleListChanged("prompts");
1146
- }
1147
- /**
1148
- * Lists all registered prompts in MCP format.
1149
- * Returns prompts from both buckets.
1150
- *
1151
- * @returns {Prompt[]} Array of prompt descriptors
1152
- */
1153
- listPrompts() {
1154
- return Array.from(this.bridge.prompts.values()).map((prompt) => ({
1155
- name: prompt.name,
1156
- description: prompt.description,
1157
- arguments: prompt.argsSchema?.properties ? Object.entries(prompt.argsSchema.properties).map(([name, schema]) => ({
1158
- name,
1159
- description: schema.description,
1160
- required: prompt.argsSchema?.required?.includes(name) ?? false
1161
- })) : void 0
1162
- }));
1163
- }
1164
- /**
1165
- * Unregisters a tool by name (Chromium native API).
1166
- * Can unregister tools from either Bucket A (provideContext) or Bucket B (registerTool).
1167
- *
1168
- * @param {string} name - Name of the tool to unregister
1169
- */
1170
- unregisterTool(name) {
1171
- const inProvideContext = this.provideContextTools.has(name);
1172
- const inDynamic = this.dynamicTools.has(name);
1173
- if (!inProvideContext && !inDynamic) {
1174
- logger$1.warn(`Tool "${name}" is not registered, ignoring unregister call`);
1175
- return;
1176
- }
1177
- if (inProvideContext) this.provideContextTools.delete(name);
1178
- if (inDynamic) {
1179
- this.dynamicTools.delete(name);
1180
- this.toolRegistrationTimestamps.delete(name);
1181
- this.toolUnregisterFunctions.delete(name);
1182
- }
1183
- this.updateBridgeTools();
1184
- this.scheduleListChanged("tools");
1185
- }
1186
- /**
1187
- * Clears all registered context from both buckets (Chromium native API).
1188
- * Removes all tools, resources, and prompts registered via provideContext() and register* methods.
1189
- */
1190
- clearContext() {
1191
- this.provideContextTools.clear();
1192
- this.dynamicTools.clear();
1193
- this.toolRegistrationTimestamps.clear();
1194
- this.toolUnregisterFunctions.clear();
1195
- this.provideContextResources.clear();
1196
- this.dynamicResources.clear();
1197
- this.resourceRegistrationTimestamps.clear();
1198
- this.resourceUnregisterFunctions.clear();
1199
- this.provideContextPrompts.clear();
1200
- this.dynamicPrompts.clear();
1201
- this.promptRegistrationTimestamps.clear();
1202
- this.promptUnregisterFunctions.clear();
1203
- this.updateBridgeTools();
1204
- this.updateBridgeResources();
1205
- this.updateBridgePrompts();
1206
- this.scheduleListChanged("tools");
1207
- this.scheduleListChanged("resources");
1208
- this.scheduleListChanged("prompts");
1209
- }
1210
- /**
1211
- * Updates the bridge tools map with merged tools from both buckets.
1212
- * The final tool list is the union of Bucket A (provideContext) and Bucket B (dynamic).
1213
- *
1214
- * @private
1215
- */
1216
- updateBridgeTools() {
1217
- this.bridge.tools.clear();
1218
- for (const [name, tool] of this.provideContextTools) this.bridge.tools.set(name, tool);
1219
- for (const [name, tool] of this.dynamicTools) this.bridge.tools.set(name, tool);
1220
- }
1221
- /**
1222
- * Notifies all servers and testing callbacks that the tools list has changed.
1223
- * Sends MCP notifications to connected servers and invokes registered testing callbacks.
1224
- *
1225
- * @private
1226
- */
1227
- notifyToolsListChanged() {
1228
- if (this.bridge.tabServer.notification) this.bridge.tabServer.notification({
1229
- method: "notifications/tools/list_changed",
1230
- params: {}
1231
- });
1232
- if (this.bridge.iframeServer?.notification) this.bridge.iframeServer.notification({
1233
- method: "notifications/tools/list_changed",
1234
- params: {}
1235
- });
1236
- if (this.testingAPI && "notifyToolsChanged" in this.testingAPI) this.testingAPI.notifyToolsChanged();
1237
- }
1238
- /**
1239
- * Updates the bridge resources map with merged resources from both buckets.
1240
- *
1241
- * @private
1242
- */
1243
- updateBridgeResources() {
1244
- this.bridge.resources.clear();
1245
- for (const [uri, resource] of this.provideContextResources) this.bridge.resources.set(uri, resource);
1246
- for (const [uri, resource] of this.dynamicResources) this.bridge.resources.set(uri, resource);
1247
- }
1248
- /**
1249
- * Notifies all servers that the resources list has changed.
1250
- *
1251
- * @private
1252
- */
1253
- notifyResourcesListChanged() {
1254
- if (this.bridge.tabServer.notification) this.bridge.tabServer.notification({
1255
- method: "notifications/resources/list_changed",
1256
- params: {}
1257
- });
1258
- if (this.bridge.iframeServer?.notification) this.bridge.iframeServer.notification({
1259
- method: "notifications/resources/list_changed",
1260
- params: {}
1261
- });
1262
- }
1263
- /**
1264
- * Updates the bridge prompts map with merged prompts from both buckets.
1265
- *
1266
- * @private
1267
- */
1268
- updateBridgePrompts() {
1269
- this.bridge.prompts.clear();
1270
- for (const [name, prompt] of this.provideContextPrompts) this.bridge.prompts.set(name, prompt);
1271
- for (const [name, prompt] of this.dynamicPrompts) this.bridge.prompts.set(name, prompt);
1272
- }
1273
- /**
1274
- * Notifies all servers that the prompts list has changed.
1275
- *
1276
- * @private
1277
- */
1278
- notifyPromptsListChanged() {
1279
- if (this.bridge.tabServer.notification) this.bridge.tabServer.notification({
1280
- method: "notifications/prompts/list_changed",
1281
- params: {}
1282
- });
1283
- if (this.bridge.iframeServer?.notification) this.bridge.iframeServer.notification({
1284
- method: "notifications/prompts/list_changed",
1285
- params: {}
1286
- });
1287
- }
1288
- /**
1289
- * Schedules a list changed notification using microtask batching.
1290
- * Multiple calls for the same list type within the same task are coalesced
1291
- * into a single notification. This dramatically reduces notification spam
1292
- * during React mount/unmount cycles.
1293
- *
1294
- * @param listType - The type of list that changed ('tools' | 'resources' | 'prompts')
1295
- * @private
1296
- */
1297
- scheduleListChanged(listType) {
1298
- if (this.pendingNotifications.has(listType)) return;
1299
- this.pendingNotifications.add(listType);
1300
- queueMicrotask(() => {
1301
- this.pendingNotifications.delete(listType);
1302
- switch (listType) {
1303
- case "tools":
1304
- this.notifyToolsListChanged();
1305
- break;
1306
- case "resources":
1307
- this.notifyResourcesListChanged();
1308
- break;
1309
- case "prompts":
1310
- this.notifyPromptsListChanged();
1311
- break;
1312
- default: {
1313
- const _exhaustive = listType;
1314
- logger$1.error(`Unknown list type: ${_exhaustive}`);
1315
- }
1316
- }
1317
- });
1318
- }
1319
- /**
1320
- * Reads a resource by URI (internal use only by MCP bridge).
1321
- * Handles both static resources and URI templates.
1322
- *
1323
- * @param {string} uri - The URI of the resource to read
1324
- * @returns {Promise<{contents: ResourceContents[]}>} The resource contents
1325
- * @throws {Error} If resource is not found
1326
- * @internal
1327
- */
1328
- async readResource(uri) {
1329
- const staticResource = this.bridge.resources.get(uri);
1330
- if (staticResource && !staticResource.isTemplate) try {
1331
- let parsedUri;
1332
- try {
1333
- parsedUri = new URL(uri);
1334
- } catch {
1335
- parsedUri = new URL(`custom-scheme:///${encodeURIComponent(uri)}`);
1336
- parsedUri.originalUri = uri;
1337
- }
1338
- return await staticResource.read(parsedUri);
1339
- } catch (error) {
1340
- logger$1.error(`Error reading resource ${uri}:`, error);
1341
- throw error;
1342
- }
1343
- for (const resource of this.bridge.resources.values()) {
1344
- if (!resource.isTemplate) continue;
1345
- const params = this.matchUriTemplate(resource.uri, uri);
1346
- if (params) try {
1347
- let parsedUri;
1348
- try {
1349
- parsedUri = new URL(uri);
1350
- } catch {
1351
- parsedUri = new URL(`custom-scheme:///${encodeURIComponent(uri)}`);
1352
- parsedUri.originalUri = uri;
1353
- }
1354
- return await resource.read(parsedUri, params);
1355
- } catch (error) {
1356
- logger$1.error(`Error reading resource ${uri}:`, error);
1357
- throw error;
1358
- }
1359
- }
1360
- throw new Error(`Resource not found: ${uri}`);
1361
- }
1362
- /**
1363
- * Matches a URI against a URI template and extracts parameters.
1364
- *
1365
- * @param {string} template - The URI template (e.g., "file://{path}")
1366
- * @param {string} uri - The actual URI to match
1367
- * @returns {Record<string, string> | null} Extracted parameters or null if no match
1368
- * @private
1369
- */
1370
- matchUriTemplate(template, uri) {
1371
- const paramNames = [];
1372
- let regexPattern = template.replace(/[.*+?^${}()|[\]\\]/g, (char) => {
1373
- if (char === "{" || char === "}") return char;
1374
- return `\\${char}`;
1375
- });
1376
- regexPattern = regexPattern.replace(/\{([^}]+)\}/g, (_, paramName) => {
1377
- paramNames.push(paramName);
1378
- return "(.+)";
1379
- });
1380
- const regex = /* @__PURE__ */ new RegExp(`^${regexPattern}$`);
1381
- const match = uri.match(regex);
1382
- if (!match) return null;
1383
- const params = {};
1384
- for (let i = 0; i < paramNames.length; i++) {
1385
- const paramName = paramNames[i];
1386
- params[paramName] = match[i + 1];
1387
- }
1388
- return params;
1389
- }
1390
- /**
1391
- * Gets a prompt with arguments (internal use only by MCP bridge).
1392
- *
1393
- * @param {string} name - Name of the prompt
1394
- * @param {Record<string, unknown>} args - Arguments to pass to the prompt
1395
- * @returns {Promise<{messages: PromptMessage[]}>} The prompt messages
1396
- * @throws {Error} If prompt is not found
1397
- * @internal
1398
- */
1399
- async getPrompt(name, args) {
1400
- const prompt = this.bridge.prompts.get(name);
1401
- if (!prompt) throw new Error(`Prompt not found: ${name}`);
1402
- if (prompt.argsValidator && args) {
1403
- const validation = validateWithZod(args, prompt.argsValidator);
1404
- if (!validation.success) {
1405
- logger$1.error(`Argument validation failed for prompt ${name}:`, validation.error);
1406
- throw new Error(`Argument validation error for prompt "${name}":\n${validation.error}`);
1407
- }
1408
- }
1409
- try {
1410
- return await prompt.get(args ?? {});
1411
- } catch (error) {
1412
- logger$1.error(`Error getting prompt ${name}:`, error);
1413
- throw error;
1414
- }
1415
- }
1416
- /**
1417
- * Executes a tool with validation and event dispatch.
1418
- * Follows this sequence:
1419
- * 1. Validates input arguments against schema
1420
- * 2. Records tool call in testing API (if available)
1421
- * 3. Checks for mock response (if testing)
1422
- * 4. Dispatches 'toolcall' event to listeners
1423
- * 5. Executes tool function if not prevented
1424
- * 6. Validates output (permissive mode - warns only)
1425
- *
1426
- * @param {string} toolName - Name of the tool to execute
1427
- * @param {Record<string, unknown>} args - Arguments to pass to the tool
1428
- * @returns {Promise<ToolResponse>} The tool's response
1429
- * @throws {Error} If tool is not found
1430
- * @internal
1431
- */
1432
- async executeTool(toolName, args) {
1433
- const tool = this.bridge.tools.get(toolName);
1434
- if (!tool) throw new Error(`Tool not found: ${toolName}`);
1435
- const validation = validateWithZod(args, tool.inputValidator);
1436
- if (!validation.success) {
1437
- logger$1.error(`Input validation failed for ${toolName}:`, validation.error);
1438
- return {
1439
- content: [{
1440
- type: "text",
1441
- text: `Input validation error for tool "${toolName}":\n${validation.error}`
1442
- }],
1443
- isError: true
1444
- };
1445
- }
1446
- const validatedArgs = validation.data;
1447
- if (this.testingAPI) this.testingAPI.recordToolCall(toolName, validatedArgs);
1448
- if (this.testingAPI?.hasMockResponse(toolName)) {
1449
- const mockResponse = this.testingAPI.getMockResponse(toolName);
1450
- if (mockResponse) return mockResponse;
1451
- }
1452
- const event = new WebToolCallEvent(toolName, validatedArgs);
1453
- this.dispatchEvent(event);
1454
- if (event.defaultPrevented && event.hasResponse()) {
1455
- const response = event.getResponse();
1456
- if (response) return response;
1457
- }
1458
- try {
1459
- const response = await tool.execute(validatedArgs);
1460
- if (tool.outputValidator && response.structuredContent) {
1461
- const outputValidation = validateWithZod(response.structuredContent, tool.outputValidator);
1462
- if (!outputValidation.success) logger$1.warn(`Output validation failed for ${toolName}:`, outputValidation.error);
1463
- }
1464
- if (response.metadata && typeof response.metadata === "object" && "willNavigate" in response.metadata) logger$1.info(`Tool "${toolName}" will trigger navigation`, response.metadata);
1465
- return response;
1466
- } catch (error) {
1467
- logger$1.error(`Error executing tool ${toolName}:`, error);
1468
- return {
1469
- content: [{
1470
- type: "text",
1471
- text: `Error: ${error instanceof Error ? error.message : String(error)}`
1472
- }],
1473
- isError: true
1474
- };
1475
- }
1476
- }
1477
- /**
1478
- * Lists all registered tools in MCP format.
1479
- * Returns tools from both buckets with full MCP specification including
1480
- * annotations and output schemas.
1481
- *
1482
- * @returns {Array<{name: string, description: string, inputSchema: InputSchema, outputSchema?: InputSchema, annotations?: ToolAnnotations}>} Array of tool descriptors
1483
- */
1484
- listTools() {
1485
- return Array.from(this.bridge.tools.values()).map((tool) => ({
1486
- name: tool.name,
1487
- description: tool.description,
1488
- inputSchema: tool.inputSchema,
1489
- ...tool.outputSchema && { outputSchema: tool.outputSchema },
1490
- ...tool.annotations && { annotations: tool.annotations }
1491
- }));
1492
- }
1493
- /**
1494
- * Request an LLM completion from the connected client.
1495
- * This sends a sampling request to the connected MCP client.
1496
- *
1497
- * @param {SamplingRequestParams} params - Parameters for the sampling request
1498
- * @returns {Promise<SamplingResult>} The LLM completion result
1499
- */
1500
- async createMessage(params) {
1501
- const underlyingServer = this.bridge.tabServer.server;
1502
- if (!underlyingServer?.createMessage) throw new Error("Sampling is not supported: no connected client with sampling capability");
1503
- return underlyingServer.createMessage(params);
1504
- }
1505
- /**
1506
- * Request user input from the connected client.
1507
- * This sends an elicitation request to the connected MCP client.
1508
- *
1509
- * @param {ElicitationParams} params - Parameters for the elicitation request
1510
- * @returns {Promise<ElicitationResult>} The user's response
1511
- */
1512
- async elicitInput(params) {
1513
- const underlyingServer = this.bridge.tabServer.server;
1514
- if (!underlyingServer?.elicitInput) throw new Error("Elicitation is not supported: no connected client with elicitation capability");
1515
- return underlyingServer.elicitInput(params);
1516
- }
1517
- };
1518
- /**
1519
- * Initializes the MCP bridge with dual-server support.
1520
- * Creates TabServer for same-window communication and optionally IframeChildServer
1521
- * for parent-child iframe communication.
1522
- *
1523
- * @param {WebModelContextInitOptions} [options] - Configuration options
1524
- * @returns {MCPBridge} The initialized MCP bridge
1525
- */
1526
- function initializeMCPBridge(options) {
1527
- const hostname = window.location.hostname || "localhost";
1528
- const transportOptions = options?.transport;
1529
- const setupServerHandlers = (server, bridge$1) => {
1530
- server.setRequestHandler(ListToolsRequestSchema, async () => {
1531
- return { tools: bridge$1.modelContext.listTools() };
1532
- });
1533
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1534
- const toolName = request.params.name;
1535
- const args = request.params.arguments || {};
1536
- try {
1537
- const response = await bridge$1.modelContext.executeTool(toolName, args);
1538
- return {
1539
- content: response.content,
1540
- isError: response.isError,
1541
- ...response.structuredContent && { structuredContent: response.structuredContent }
1542
- };
1543
- } catch (error) {
1544
- bridgeLogger.error(`Error calling tool ${toolName}:`, error);
1545
- throw error;
1546
- }
1547
- });
1548
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
1549
- return { resources: bridge$1.modelContext.listResources() };
1550
- });
1551
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1552
- try {
1553
- return await bridge$1.modelContext.readResource(request.params.uri);
1554
- } catch (error) {
1555
- bridgeLogger.error(`Error reading resource ${request.params.uri}:`, error);
1556
- throw error;
1557
- }
1558
- });
1559
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
1560
- return { prompts: bridge$1.modelContext.listPrompts() };
1561
- });
1562
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1563
- try {
1564
- return await bridge$1.modelContext.getPrompt(request.params.name, request.params.arguments);
1565
- } catch (error) {
1566
- bridgeLogger.error(`Error getting prompt ${request.params.name}:`, error);
1567
- throw error;
1568
- }
1569
- });
1570
- };
1571
- const customTransport = transportOptions?.create?.();
1572
- if (customTransport) {
1573
- const server = new Server({
1574
- name: hostname,
1575
- version: "1.0.0"
1576
- }, { capabilities: {
1577
- tools: { listChanged: true },
1578
- resources: { listChanged: true },
1579
- prompts: { listChanged: true }
1580
- } });
1581
- const bridge$1 = {
1582
- tabServer: server,
1583
- tools: /* @__PURE__ */ new Map(),
1584
- resources: /* @__PURE__ */ new Map(),
1585
- prompts: /* @__PURE__ */ new Map(),
1586
- modelContext: void 0,
1587
- isInitialized: true
1588
- };
1589
- bridge$1.modelContext = new WebModelContext(bridge$1);
1590
- setupServerHandlers(server, bridge$1);
1591
- server.connect(customTransport);
1592
- return bridge$1;
1593
- }
1594
- const tabServerEnabled = transportOptions?.tabServer !== false;
1595
- const tabServer = new Server({
1596
- name: `${hostname}-tab`,
1597
- version: "1.0.0"
1598
- }, { capabilities: {
1599
- tools: { listChanged: true },
1600
- resources: { listChanged: true },
1601
- prompts: { listChanged: true }
1602
- } });
1603
- const bridge = {
1604
- tabServer,
1605
- tools: /* @__PURE__ */ new Map(),
1606
- resources: /* @__PURE__ */ new Map(),
1607
- prompts: /* @__PURE__ */ new Map(),
1608
- modelContext: void 0,
1609
- isInitialized: true
1610
- };
1611
- bridge.modelContext = new WebModelContext(bridge);
1612
- setupServerHandlers(tabServer, bridge);
1613
- if (tabServerEnabled) {
1614
- const { allowedOrigins,...restTabServerOptions } = typeof transportOptions?.tabServer === "object" ? transportOptions.tabServer : {};
1615
- const tabTransport = new TabServerTransport({
1616
- allowedOrigins: allowedOrigins ?? ["*"],
1617
- ...restTabServerOptions
1618
- });
1619
- tabServer.connect(tabTransport);
1620
- }
1621
- const isInIframe = typeof window !== "undefined" && window.parent !== window;
1622
- const iframeServerConfig = transportOptions?.iframeServer;
1623
- if (iframeServerConfig !== false && (iframeServerConfig !== void 0 || isInIframe)) {
1624
- const iframeServer = new Server({
1625
- name: `${hostname}-iframe`,
1626
- version: "1.0.0"
1627
- }, { capabilities: {
1628
- tools: { listChanged: true },
1629
- resources: { listChanged: true },
1630
- prompts: { listChanged: true }
1631
- } });
1632
- setupServerHandlers(iframeServer, bridge);
1633
- const { allowedOrigins,...restIframeServerOptions } = typeof iframeServerConfig === "object" ? iframeServerConfig : {};
1634
- const iframeTransport = new IframeChildTransport({
1635
- allowedOrigins: allowedOrigins ?? ["*"],
1636
- ...restIframeServerOptions
1637
- });
1638
- iframeServer.connect(iframeTransport);
1639
- bridge.iframeServer = iframeServer;
1640
- }
1641
- return bridge;
101
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Invalid serialized tool response for ${name}`);
102
+ return parsed;
103
+ });
1642
104
  }
1643
- /**
1644
- * Initializes the Web Model Context API on window.navigator.
1645
- * Creates and exposes navigator.modelContext and navigator.modelContextTesting.
1646
- * Automatically detects and uses native Chromium implementation if available.
1647
- *
1648
- * @param {WebModelContextInitOptions} [options] - Configuration options
1649
- * @throws {Error} If initialization fails
1650
- * @example
1651
- * ```typescript
1652
- * import { initializeWebModelContext } from '@mcp-b/global';
1653
- *
1654
- * initializeWebModelContext({
1655
- * transport: {
1656
- * tabServer: {
1657
- * allowedOrigins: ['https://example.com']
1658
- * }
1659
- * }
1660
- * });
1661
- * ```
1662
- */
1663
105
  function initializeWebModelContext(options) {
1664
- /* c8 ignore next 4 */
1665
- if (typeof window === "undefined") {
1666
- logger$1.warn("Not in browser environment, skipping initialization");
1667
- return;
1668
- }
1669
- const effectiveOptions = options ?? window.__webModelContextOptions;
1670
- const native = detectNativeAPI();
1671
- if (native.hasNativeContext && native.hasNativeTesting) {
1672
- const nativeContext = window.navigator.modelContext;
1673
- const nativeTesting = window.navigator.modelContextTesting;
1674
- if (!nativeContext || !nativeTesting) {
1675
- logger$1.error("Native API detection mismatch");
1676
- return;
1677
- }
1678
- logger$1.info("✅ Native Chromium API detected");
1679
- logger$1.info(" Using native implementation with MCP bridge synchronization");
1680
- logger$1.info(" Native API will automatically collect tools from embedded iframes");
1681
- try {
1682
- const bridge = initializeMCPBridge(effectiveOptions);
1683
- bridge.modelContext = new NativeModelContextAdapter(bridge, nativeContext, nativeTesting);
1684
- bridge.modelContextTesting = nativeTesting;
1685
- Object.defineProperty(window, "__mcpBridge", {
1686
- value: bridge,
1687
- writable: false,
1688
- configurable: true
1689
- });
1690
- logger$1.info("✅ MCP bridge synced with native API");
1691
- logger$1.info(" MCP clients will receive automatic tool updates from native registry");
1692
- } catch (error) {
1693
- logger$1.error("Failed to initialize native adapter:", error);
1694
- throw error;
1695
- }
1696
- return;
1697
- }
1698
- if (native.hasNativeContext && !native.hasNativeTesting) {
1699
- logger$1.warn("Partial native API detected");
1700
- logger$1.warn(" navigator.modelContext exists but navigator.modelContextTesting is missing");
1701
- logger$1.warn(" Cannot sync with native API. Please enable experimental features:");
1702
- logger$1.warn(" - Navigate to chrome://flags");
1703
- logger$1.warn(" - Enable \"Experimental Web Platform Features\"");
1704
- logger$1.warn(" - Or launch with: --enable-experimental-web-platform-features");
1705
- logger$1.warn(" Skipping initialization to avoid conflicts");
1706
- return;
1707
- }
1708
- if (window.navigator.modelContext) {
1709
- logger$1.warn("window.navigator.modelContext already exists, skipping initialization");
1710
- return;
1711
- }
1712
- logger$1.info("Native API not detected, installing polyfill");
1713
- try {
1714
- const bridge = initializeMCPBridge(effectiveOptions);
1715
- Object.defineProperty(window.navigator, "modelContext", {
1716
- value: bridge.modelContext,
1717
- writable: false,
1718
- configurable: false
1719
- });
1720
- Object.defineProperty(window, "__mcpBridge", {
1721
- value: bridge,
1722
- writable: false,
1723
- configurable: true
1724
- });
1725
- logger$1.info("✅ window.navigator.modelContext initialized successfully");
1726
- testingLogger.info("Installing polyfill");
1727
- testingLogger.info(" 💡 To use the native implementation in Chromium:");
1728
- testingLogger.info(" - Navigate to chrome://flags");
1729
- testingLogger.info(" - Enable \"Experimental Web Platform Features\"");
1730
- testingLogger.info(" - Or launch with: --enable-experimental-web-platform-features");
1731
- const testingAPI = new WebModelContextTesting(bridge);
1732
- bridge.modelContextTesting = testingAPI;
1733
- bridge.modelContext.setTestingAPI(testingAPI);
1734
- Object.defineProperty(window.navigator, "modelContextTesting", {
1735
- value: testingAPI,
1736
- writable: false,
1737
- configurable: true
1738
- });
1739
- testingLogger.info("✅ Polyfill installed at window.navigator.modelContextTesting");
1740
- } catch (error) {
1741
- logger$1.error("Failed to initialize:", error);
1742
- throw error;
1743
- }
106
+ if (!isBrowserEnvironment()) return;
107
+ if (runtime) return;
108
+ initializeWebMCPPolyfill({ installTestingShim: options?.installTestingShim ?? "if-missing" });
109
+ const native = navigator.modelContext;
110
+ if (!native) throw new Error("navigator.modelContext is not available");
111
+ const server = new BrowserMcpServer({
112
+ name: `${window.location.hostname || "localhost"}-webmcp`,
113
+ version: "1.0.0"
114
+ }, { native });
115
+ server.syncNativeTools();
116
+ syncToolsFromTestingShim(server);
117
+ replaceModelContext(server);
118
+ const transport = createTransport(options?.transport);
119
+ runtime = {
120
+ native,
121
+ server,
122
+ transport
123
+ };
124
+ server.connect(transport).catch((error) => {
125
+ console.error("[WebModelContext] Failed to connect MCP transport:", error);
126
+ });
1744
127
  }
1745
- /**
1746
- * Cleans up the Web Model Context API.
1747
- * Closes all MCP servers and removes API from window.navigator.
1748
- * Useful for testing and hot module replacement.
1749
- *
1750
- * @example
1751
- * ```typescript
1752
- * import { cleanupWebModelContext } from '@mcp-b/global';
1753
- *
1754
- * cleanupWebModelContext();
1755
- * ```
1756
- */
1757
128
  function cleanupWebModelContext() {
1758
- /* c8 ignore next */
1759
- if (typeof window === "undefined") return;
1760
- if (window.__mcpBridge) try {
1761
- window.__mcpBridge.tabServer.close();
1762
- if (window.__mcpBridge.iframeServer) window.__mcpBridge.iframeServer.close();
1763
- } catch (error) {
1764
- logger$1.warn("Error closing MCP servers:", error);
1765
- }
1766
- delete window.navigator.modelContext;
1767
- delete window.navigator.modelContextTesting;
1768
- delete window.__mcpBridge;
1769
- logger$1.info("Cleaned up");
129
+ if (!runtime) return;
130
+ const { native, server, transport } = runtime;
131
+ runtime = null;
132
+ server.close();
133
+ transport.close();
134
+ replaceModelContext(native);
1770
135
  }
1771
136
 
1772
137
  //#endregion
1773
138
  //#region src/index.ts
1774
- const logger = createLogger("WebModelContext");
1775
- function mergeTransportOptions(base, override) {
1776
- if (!base) return override;
1777
- if (!override) return base;
1778
- return {
1779
- ...base,
1780
- ...override,
1781
- tabServer: {
1782
- ...base.tabServer ?? {},
1783
- ...override.tabServer ?? {}
1784
- }
1785
- };
1786
- }
1787
- function mergeInitOptions(base, override) {
1788
- if (!base) return override;
1789
- if (!override) return base;
1790
- return {
1791
- ...base,
1792
- ...override,
1793
- transport: mergeTransportOptions(base.transport ?? {}, override.transport ?? {})
1794
- };
1795
- }
1796
- function parseScriptTagOptions(script) {
1797
- if (!script || !script.dataset) return;
1798
- const { dataset } = script;
1799
- if (dataset.webmcpOptions) try {
1800
- return JSON.parse(dataset.webmcpOptions);
1801
- } catch (error) {
1802
- logger.error("Invalid JSON in data-webmcp-options:", error);
1803
- return;
1804
- }
1805
- const options = {};
1806
- let hasOptions = false;
1807
- if (dataset.webmcpAutoInitialize !== void 0) {
1808
- options.autoInitialize = dataset.webmcpAutoInitialize !== "false";
1809
- hasOptions = true;
1810
- }
1811
- const tabServerOptions = {};
1812
- let hasTabServerOptions = false;
1813
- if (dataset.webmcpAllowedOrigins) {
1814
- const origins = dataset.webmcpAllowedOrigins.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
1815
- if (origins.length > 0) {
1816
- tabServerOptions.allowedOrigins = origins;
1817
- hasOptions = true;
1818
- hasTabServerOptions = true;
1819
- }
1820
- }
1821
- if (dataset.webmcpChannelId) {
1822
- tabServerOptions.channelId = dataset.webmcpChannelId;
1823
- hasOptions = true;
1824
- hasTabServerOptions = true;
1825
- }
1826
- if (hasTabServerOptions) options.transport = {
1827
- ...options.transport ?? {},
1828
- tabServer: {
1829
- ...options.transport?.tabServer ?? {},
1830
- ...tabServerOptions
1831
- }
1832
- };
1833
- return hasOptions ? options : void 0;
1834
- }
1835
139
  if (typeof window !== "undefined" && typeof document !== "undefined") {
1836
- const globalOptions = window.__webModelContextOptions;
1837
- const scriptElement = document.currentScript;
1838
- const scriptOptions = parseScriptTagOptions(scriptElement);
1839
- const mergedOptions = mergeInitOptions(globalOptions, scriptOptions) ?? globalOptions ?? scriptOptions;
1840
- if (mergedOptions) window.__webModelContextOptions = mergedOptions;
1841
- const shouldAutoInitialize = mergedOptions?.autoInitialize !== false;
1842
- try {
1843
- if (shouldAutoInitialize) initializeWebModelContext(mergedOptions);
140
+ const options = window.__webModelContextOptions;
141
+ if (options?.autoInitialize !== false) try {
142
+ initializeWebModelContext(options);
1844
143
  } catch (error) {
1845
- logger.error("Auto-initialization failed:", error);
144
+ console.error("[WebModelContext] Auto-initialization failed:", error);
1846
145
  }
1847
146
  }
1848
147
 
1849
148
  //#endregion
1850
- export { cleanupWebModelContext, createLogger, initializeWebModelContext, zodToJsonSchema };
149
+ export { cleanupWebModelContext, initializeWebModelContext };
1851
150
  //# sourceMappingURL=index.js.map