@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/README.md +486 -0
- package/dist/index.d.ts +189 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.iife.js +4 -4
- package/dist/index.js +848 -79
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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
|
-
*
|
|
104
|
+
* Detect if the native Chromium Web Model Context API is available.
|
|
105
|
+
* Checks for both navigator.modelContext and navigator.modelContextTesting,
|
|
106
|
+
* and verifies they are native implementations (not polyfills) by examining
|
|
107
|
+
* the constructor name.
|
|
108
|
+
*
|
|
109
|
+
* @returns Detection result with flags for native context and testing API availability
|
|
110
|
+
*/
|
|
111
|
+
function detectNativeAPI() {
|
|
112
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") return {
|
|
113
|
+
hasNativeContext: false,
|
|
114
|
+
hasNativeTesting: false
|
|
115
|
+
};
|
|
116
|
+
const modelContext = navigator.modelContext;
|
|
117
|
+
const modelContextTesting = navigator.modelContextTesting;
|
|
118
|
+
if (!modelContext || !modelContextTesting) return {
|
|
119
|
+
hasNativeContext: false,
|
|
120
|
+
hasNativeTesting: false
|
|
121
|
+
};
|
|
122
|
+
if ((modelContextTesting.constructor?.name || "").includes("WebModelContext")) return {
|
|
123
|
+
hasNativeContext: false,
|
|
124
|
+
hasNativeTesting: false
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
hasNativeContext: true,
|
|
128
|
+
hasNativeTesting: true
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Adapter that wraps the native Chromium Web Model Context API.
|
|
133
|
+
* Synchronizes tool changes from the native API to the MCP bridge,
|
|
134
|
+
* enabling MCP clients to stay in sync with the native tool registry.
|
|
135
|
+
*
|
|
136
|
+
* Key features:
|
|
137
|
+
* - Listens to native tool changes via registerToolsChangedCallback()
|
|
138
|
+
* - Syncs native tools to MCP bridge automatically
|
|
139
|
+
* - Delegates tool execution to native API
|
|
140
|
+
* - Converts native results to MCP ToolResponse format
|
|
141
|
+
*
|
|
142
|
+
* @class NativeModelContextAdapter
|
|
143
|
+
* @implements {InternalModelContext}
|
|
144
|
+
*/
|
|
145
|
+
var NativeModelContextAdapter = class {
|
|
146
|
+
nativeContext;
|
|
147
|
+
nativeTesting;
|
|
148
|
+
bridge;
|
|
149
|
+
syncInProgress = false;
|
|
150
|
+
/**
|
|
151
|
+
* Creates a new NativeModelContextAdapter.
|
|
152
|
+
*
|
|
153
|
+
* @param {MCPBridge} bridge - The MCP bridge instance
|
|
154
|
+
* @param {ModelContext} nativeContext - The native navigator.modelContext
|
|
155
|
+
* @param {ModelContextTesting} nativeTesting - The native navigator.modelContextTesting
|
|
156
|
+
*/
|
|
157
|
+
constructor(bridge, nativeContext, nativeTesting) {
|
|
158
|
+
this.bridge = bridge;
|
|
159
|
+
this.nativeContext = nativeContext;
|
|
160
|
+
this.nativeTesting = nativeTesting;
|
|
161
|
+
this.nativeTesting.registerToolsChangedCallback(() => {
|
|
162
|
+
console.log("[Native Adapter] Tool change detected from native API");
|
|
163
|
+
this.syncToolsFromNative();
|
|
164
|
+
});
|
|
165
|
+
this.syncToolsFromNative();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Synchronizes tools from the native API to the MCP bridge.
|
|
169
|
+
* Fetches all tools from navigator.modelContextTesting.listTools()
|
|
170
|
+
* and updates the bridge's tool registry.
|
|
171
|
+
*
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
syncToolsFromNative() {
|
|
175
|
+
if (this.syncInProgress) return;
|
|
176
|
+
this.syncInProgress = true;
|
|
177
|
+
try {
|
|
178
|
+
const nativeTools = this.nativeTesting.listTools();
|
|
179
|
+
console.log(`[Native Adapter] Syncing ${nativeTools.length} tools from native API`);
|
|
180
|
+
this.bridge.tools.clear();
|
|
181
|
+
for (const toolInfo of nativeTools) try {
|
|
182
|
+
const inputSchema = JSON.parse(toolInfo.inputSchema);
|
|
183
|
+
const validatedTool = {
|
|
184
|
+
name: toolInfo.name,
|
|
185
|
+
description: toolInfo.description,
|
|
186
|
+
inputSchema,
|
|
187
|
+
execute: async (args) => {
|
|
188
|
+
const result = await this.nativeTesting.executeTool(toolInfo.name, JSON.stringify(args));
|
|
189
|
+
return this.convertToToolResponse(result);
|
|
190
|
+
},
|
|
191
|
+
inputValidator: jsonSchemaToZod$1(inputSchema)
|
|
192
|
+
};
|
|
193
|
+
this.bridge.tools.set(toolInfo.name, validatedTool);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(`[Native Adapter] Failed to sync tool "${toolInfo.name}":`, error);
|
|
196
|
+
}
|
|
197
|
+
this.notifyMCPServers();
|
|
198
|
+
} finally {
|
|
199
|
+
this.syncInProgress = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Converts native API result to MCP ToolResponse format.
|
|
204
|
+
* Native API returns simplified values (string, number, object, etc.)
|
|
205
|
+
* which need to be wrapped in the MCP CallToolResult format.
|
|
206
|
+
*
|
|
207
|
+
* @param {unknown} result - The result from native executeTool()
|
|
208
|
+
* @returns {ToolResponse} Formatted MCP ToolResponse
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
convertToToolResponse(result) {
|
|
212
|
+
if (typeof result === "string") return { content: [{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: result
|
|
215
|
+
}] };
|
|
216
|
+
if (result === void 0 || result === null) return { content: [{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: ""
|
|
219
|
+
}] };
|
|
220
|
+
if (typeof result === "object") return {
|
|
221
|
+
content: [{
|
|
222
|
+
type: "text",
|
|
223
|
+
text: JSON.stringify(result, null, 2)
|
|
224
|
+
}],
|
|
225
|
+
structuredContent: result
|
|
226
|
+
};
|
|
227
|
+
return { content: [{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: String(result)
|
|
230
|
+
}] };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Notifies all connected MCP servers that the tools list has changed.
|
|
234
|
+
*
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
notifyMCPServers() {
|
|
238
|
+
if (this.bridge.tabServer?.notification) this.bridge.tabServer.notification({
|
|
239
|
+
method: "notifications/tools/list_changed",
|
|
240
|
+
params: {}
|
|
241
|
+
});
|
|
242
|
+
if (this.bridge.iframeServer?.notification) this.bridge.iframeServer.notification({
|
|
243
|
+
method: "notifications/tools/list_changed",
|
|
244
|
+
params: {}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Provides context (tools) to AI models via the native API.
|
|
249
|
+
* Delegates to navigator.modelContext.provideContext().
|
|
250
|
+
* Tool change callback will fire and trigger sync automatically.
|
|
251
|
+
*
|
|
252
|
+
* @param {ModelContextInput} context - Context containing tools to register
|
|
253
|
+
*/
|
|
254
|
+
provideContext(context) {
|
|
255
|
+
console.log("[Native Adapter] Delegating provideContext to native API");
|
|
256
|
+
this.nativeContext.provideContext(context);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Registers a single tool dynamically via the native API.
|
|
260
|
+
* Delegates to navigator.modelContext.registerTool().
|
|
261
|
+
* Tool change callback will fire and trigger sync automatically.
|
|
262
|
+
*
|
|
263
|
+
* @param {ToolDescriptor} tool - The tool descriptor to register
|
|
264
|
+
* @returns {{unregister: () => void}} Object with unregister function
|
|
265
|
+
*/
|
|
266
|
+
registerTool(tool) {
|
|
267
|
+
console.log(`[Native Adapter] Delegating registerTool("${tool.name}") to native API`);
|
|
268
|
+
return this.nativeContext.registerTool(tool);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Unregisters a tool by name via the native API.
|
|
272
|
+
* Delegates to navigator.modelContext.unregisterTool().
|
|
273
|
+
*
|
|
274
|
+
* @param {string} name - Name of the tool to unregister
|
|
275
|
+
*/
|
|
276
|
+
unregisterTool(name) {
|
|
277
|
+
console.log(`[Native Adapter] Delegating unregisterTool("${name}") to native API`);
|
|
278
|
+
this.nativeContext.unregisterTool(name);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Clears all registered tools via the native API.
|
|
282
|
+
* Delegates to navigator.modelContext.clearContext().
|
|
283
|
+
*/
|
|
284
|
+
clearContext() {
|
|
285
|
+
console.log("[Native Adapter] Delegating clearContext to native API");
|
|
286
|
+
this.nativeContext.clearContext();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Executes a tool via the native API.
|
|
290
|
+
* Delegates to navigator.modelContextTesting.executeTool() with JSON string args.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} toolName - Name of the tool to execute
|
|
293
|
+
* @param {Record<string, unknown>} args - Arguments to pass to the tool
|
|
294
|
+
* @returns {Promise<ToolResponse>} The tool's response in MCP format
|
|
295
|
+
* @internal
|
|
296
|
+
*/
|
|
297
|
+
async executeTool(toolName, args) {
|
|
298
|
+
console.log(`[Native Adapter] Executing tool "${toolName}" via native API`);
|
|
299
|
+
try {
|
|
300
|
+
const result = await this.nativeTesting.executeTool(toolName, JSON.stringify(args));
|
|
301
|
+
return this.convertToToolResponse(result);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(`[Native Adapter] Error executing tool "${toolName}":`, error);
|
|
304
|
+
return {
|
|
305
|
+
content: [{
|
|
306
|
+
type: "text",
|
|
307
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
308
|
+
}],
|
|
309
|
+
isError: true
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Lists all registered tools from the MCP bridge.
|
|
315
|
+
* Returns tools synced from the native API.
|
|
316
|
+
*
|
|
317
|
+
* @returns {Array<{name: string, description: string, inputSchema: InputSchema}>} Array of tool descriptors
|
|
318
|
+
*/
|
|
319
|
+
listTools() {
|
|
320
|
+
return Array.from(this.bridge.tools.values()).map((tool) => ({
|
|
321
|
+
name: tool.name,
|
|
322
|
+
description: tool.description,
|
|
323
|
+
inputSchema: tool.inputSchema,
|
|
324
|
+
...tool.outputSchema && { outputSchema: tool.outputSchema },
|
|
325
|
+
...tool.annotations && { annotations: tool.annotations }
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Adds an event listener for tool call events.
|
|
330
|
+
* Delegates to the native API's addEventListener.
|
|
331
|
+
*
|
|
332
|
+
* @param {'toolcall'} type - Event type
|
|
333
|
+
* @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler
|
|
334
|
+
* @param {boolean | AddEventListenerOptions} [options] - Event listener options
|
|
335
|
+
*/
|
|
336
|
+
addEventListener(type, listener, options) {
|
|
337
|
+
this.nativeContext.addEventListener(type, listener, options);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Removes an event listener for tool call events.
|
|
341
|
+
* Delegates to the native API's removeEventListener.
|
|
342
|
+
*
|
|
343
|
+
* @param {'toolcall'} type - Event type
|
|
344
|
+
* @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler
|
|
345
|
+
* @param {boolean | EventListenerOptions} [options] - Event listener options
|
|
346
|
+
*/
|
|
347
|
+
removeEventListener(type, listener, options) {
|
|
348
|
+
this.nativeContext.removeEventListener(type, listener, options);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Dispatches a tool call event.
|
|
352
|
+
* Delegates to the native API's dispatchEvent.
|
|
353
|
+
*
|
|
354
|
+
* @param {Event} event - The event to dispatch
|
|
355
|
+
* @returns {boolean} False if event was cancelled, true otherwise
|
|
356
|
+
*/
|
|
357
|
+
dispatchEvent(event) {
|
|
358
|
+
return this.nativeContext.dispatchEvent(event);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* ToolCallEvent implementation for the Web Model Context API.
|
|
363
|
+
* Represents an event fired when a tool is called, allowing event listeners
|
|
364
|
+
* to intercept and provide custom responses.
|
|
365
|
+
*
|
|
366
|
+
* @class WebToolCallEvent
|
|
367
|
+
* @extends {Event}
|
|
368
|
+
* @implements {ToolCallEvent}
|
|
105
369
|
*/
|
|
106
370
|
var WebToolCallEvent = class extends Event {
|
|
107
371
|
name;
|
|
108
372
|
arguments;
|
|
109
373
|
_response = null;
|
|
110
374
|
_responded = false;
|
|
375
|
+
/**
|
|
376
|
+
* Creates a new ToolCallEvent.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} toolName - Name of the tool being called
|
|
379
|
+
* @param {Record<string, unknown>} args - Validated arguments for the tool
|
|
380
|
+
*/
|
|
111
381
|
constructor(toolName, args) {
|
|
112
382
|
super("toolcall", { cancelable: true });
|
|
113
383
|
this.name = toolName;
|
|
114
384
|
this.arguments = args;
|
|
115
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Provides a response for this tool call, preventing the default tool execution.
|
|
388
|
+
*
|
|
389
|
+
* @param {ToolResponse} response - The response to use instead of executing the tool
|
|
390
|
+
* @throws {Error} If a response has already been provided
|
|
391
|
+
*/
|
|
116
392
|
respondWith(response) {
|
|
117
393
|
if (this._responded) throw new Error("Response already provided for this tool call");
|
|
118
394
|
this._response = response;
|
|
119
395
|
this._responded = true;
|
|
120
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Gets the response provided via respondWith().
|
|
399
|
+
*
|
|
400
|
+
* @returns {ToolResponse | null} The response, or null if none provided
|
|
401
|
+
*/
|
|
121
402
|
getResponse() {
|
|
122
403
|
return this._response;
|
|
123
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Checks whether a response has been provided for this tool call.
|
|
407
|
+
*
|
|
408
|
+
* @returns {boolean} True if respondWith() was called
|
|
409
|
+
*/
|
|
124
410
|
hasResponse() {
|
|
125
411
|
return this._responded;
|
|
126
412
|
}
|
|
127
413
|
};
|
|
128
414
|
/**
|
|
129
|
-
* Time window
|
|
130
|
-
*
|
|
415
|
+
* Time window in milliseconds to detect rapid duplicate tool registrations.
|
|
416
|
+
* Used to filter out double-registrations caused by React Strict Mode.
|
|
131
417
|
*/
|
|
132
418
|
const RAPID_DUPLICATE_WINDOW_MS = 50;
|
|
133
419
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
420
|
+
* Testing API implementation for the Model Context Protocol.
|
|
421
|
+
* Provides debugging, mocking, and testing capabilities for tool execution.
|
|
422
|
+
* Implements both Chromium native methods and polyfill-specific extensions.
|
|
423
|
+
*
|
|
424
|
+
* @class WebModelContextTesting
|
|
425
|
+
* @implements {ModelContextTesting}
|
|
426
|
+
*/
|
|
427
|
+
var WebModelContextTesting = class {
|
|
428
|
+
toolCallHistory = [];
|
|
429
|
+
mockResponses = /* @__PURE__ */ new Map();
|
|
430
|
+
toolsChangedCallbacks = /* @__PURE__ */ new Set();
|
|
431
|
+
bridge;
|
|
432
|
+
/**
|
|
433
|
+
* Creates a new WebModelContextTesting instance.
|
|
434
|
+
*
|
|
435
|
+
* @param {MCPBridge} bridge - The MCP bridge instance to test
|
|
436
|
+
*/
|
|
437
|
+
constructor(bridge) {
|
|
438
|
+
this.bridge = bridge;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Records a tool call in the history.
|
|
442
|
+
* Called internally by WebModelContext when tools are executed.
|
|
443
|
+
*
|
|
444
|
+
* @param {string} toolName - Name of the tool that was called
|
|
445
|
+
* @param {Record<string, unknown>} args - Arguments passed to the tool
|
|
446
|
+
* @internal
|
|
447
|
+
*/
|
|
448
|
+
recordToolCall(toolName, args) {
|
|
449
|
+
this.toolCallHistory.push({
|
|
450
|
+
toolName,
|
|
451
|
+
arguments: args,
|
|
452
|
+
timestamp: Date.now()
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Checks if a mock response is registered for a specific tool.
|
|
457
|
+
*
|
|
458
|
+
* @param {string} toolName - Name of the tool to check
|
|
459
|
+
* @returns {boolean} True if a mock response exists
|
|
460
|
+
* @internal
|
|
461
|
+
*/
|
|
462
|
+
hasMockResponse(toolName) {
|
|
463
|
+
return this.mockResponses.has(toolName);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Retrieves the mock response for a specific tool.
|
|
467
|
+
*
|
|
468
|
+
* @param {string} toolName - Name of the tool
|
|
469
|
+
* @returns {ToolResponse | undefined} The mock response, or undefined if none exists
|
|
470
|
+
* @internal
|
|
471
|
+
*/
|
|
472
|
+
getMockResponse(toolName) {
|
|
473
|
+
return this.mockResponses.get(toolName);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Notifies all registered callbacks that the tools list has changed.
|
|
477
|
+
* Called internally when tools are registered, unregistered, or cleared.
|
|
478
|
+
*
|
|
479
|
+
* @internal
|
|
480
|
+
*/
|
|
481
|
+
notifyToolsChanged() {
|
|
482
|
+
for (const callback of this.toolsChangedCallbacks) try {
|
|
483
|
+
callback();
|
|
484
|
+
} catch (error) {
|
|
485
|
+
console.error("[Model Context Testing] Error in tools changed callback:", error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Executes a tool directly with JSON string input (Chromium native API).
|
|
490
|
+
* Parses the JSON input, validates it, and executes the tool.
|
|
491
|
+
*
|
|
492
|
+
* @param {string} toolName - Name of the tool to execute
|
|
493
|
+
* @param {string} inputArgsJson - JSON string of input arguments
|
|
494
|
+
* @returns {Promise<unknown>} The tool's result, or undefined on error
|
|
495
|
+
* @throws {SyntaxError} If the input JSON is invalid
|
|
496
|
+
* @throws {Error} If the tool does not exist
|
|
497
|
+
*/
|
|
498
|
+
async executeTool(toolName, inputArgsJson) {
|
|
499
|
+
console.log(`[Model Context Testing] Executing tool: ${toolName}`);
|
|
500
|
+
let args;
|
|
501
|
+
try {
|
|
502
|
+
args = JSON.parse(inputArgsJson);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
throw new SyntaxError(`Invalid JSON input: ${error instanceof Error ? error.message : String(error)}`);
|
|
505
|
+
}
|
|
506
|
+
if (!this.bridge.tools.get(toolName)) throw new Error(`Tool not found: ${toolName}`);
|
|
507
|
+
const result = await this.bridge.modelContext.executeTool(toolName, args);
|
|
508
|
+
if (result.isError) return;
|
|
509
|
+
if (result.structuredContent) return result.structuredContent;
|
|
510
|
+
if (result.content && result.content.length > 0) {
|
|
511
|
+
const firstContent = result.content[0];
|
|
512
|
+
if (firstContent && firstContent.type === "text") return firstContent.text;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Lists all registered tools with inputSchema as JSON string (Chromium native API).
|
|
517
|
+
* Returns an array of ToolInfo objects where inputSchema is stringified.
|
|
518
|
+
*
|
|
519
|
+
* @returns {Array<{name: string, description: string, inputSchema: string}>} Array of tool information
|
|
520
|
+
*/
|
|
521
|
+
listTools() {
|
|
522
|
+
return this.bridge.modelContext.listTools().map((tool) => ({
|
|
523
|
+
name: tool.name,
|
|
524
|
+
description: tool.description,
|
|
525
|
+
inputSchema: JSON.stringify(tool.inputSchema)
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Registers a callback that fires when the tools list changes (Chromium native API).
|
|
530
|
+
* The callback will be invoked on registerTool, unregisterTool, provideContext, and clearContext.
|
|
531
|
+
*
|
|
532
|
+
* @param {() => void} callback - Function to call when tools change
|
|
533
|
+
*/
|
|
534
|
+
registerToolsChangedCallback(callback) {
|
|
535
|
+
this.toolsChangedCallbacks.add(callback);
|
|
536
|
+
console.log("[Model Context Testing] Tools changed callback registered");
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Gets all tool calls that have been recorded (polyfill extension).
|
|
540
|
+
*
|
|
541
|
+
* @returns {Array<{toolName: string, arguments: Record<string, unknown>, timestamp: number}>} Tool call history
|
|
542
|
+
*/
|
|
543
|
+
getToolCalls() {
|
|
544
|
+
return [...this.toolCallHistory];
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Clears the tool call history (polyfill extension).
|
|
548
|
+
*/
|
|
549
|
+
clearToolCalls() {
|
|
550
|
+
this.toolCallHistory = [];
|
|
551
|
+
console.log("[Model Context Testing] Tool call history cleared");
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Sets a mock response for a specific tool (polyfill extension).
|
|
555
|
+
* When set, the tool's execute function will be bypassed.
|
|
556
|
+
*
|
|
557
|
+
* @param {string} toolName - Name of the tool to mock
|
|
558
|
+
* @param {ToolResponse} response - The mock response to return
|
|
559
|
+
*/
|
|
560
|
+
setMockToolResponse(toolName, response) {
|
|
561
|
+
this.mockResponses.set(toolName, response);
|
|
562
|
+
console.log(`[Model Context Testing] Mock response set for tool: ${toolName}`);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Clears the mock response for a specific tool (polyfill extension).
|
|
566
|
+
*
|
|
567
|
+
* @param {string} toolName - Name of the tool
|
|
568
|
+
*/
|
|
569
|
+
clearMockToolResponse(toolName) {
|
|
570
|
+
this.mockResponses.delete(toolName);
|
|
571
|
+
console.log(`[Model Context Testing] Mock response cleared for tool: ${toolName}`);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Clears all mock tool responses (polyfill extension).
|
|
575
|
+
*/
|
|
576
|
+
clearAllMockToolResponses() {
|
|
577
|
+
this.mockResponses.clear();
|
|
578
|
+
console.log("[Model Context Testing] All mock responses cleared");
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Gets the current tools registered in the system (polyfill extension).
|
|
582
|
+
*
|
|
583
|
+
* @returns {ReturnType<InternalModelContext['listTools']>} Array of registered tools
|
|
584
|
+
*/
|
|
585
|
+
getRegisteredTools() {
|
|
586
|
+
return this.bridge.modelContext.listTools();
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Resets the entire testing state (polyfill extension).
|
|
590
|
+
* Clears both tool call history and all mock responses.
|
|
591
|
+
*/
|
|
592
|
+
reset() {
|
|
593
|
+
this.clearToolCalls();
|
|
594
|
+
this.clearAllMockToolResponses();
|
|
595
|
+
console.log("[Model Context Testing] Testing state reset");
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* ModelContext implementation that bridges to the Model Context Protocol SDK.
|
|
600
|
+
* Implements the W3C Web Model Context API proposal with two-bucket tool management:
|
|
601
|
+
* - Bucket A (provideContextTools): Tools registered via provideContext()
|
|
602
|
+
* - Bucket B (dynamicTools): Tools registered via registerTool()
|
|
136
603
|
*
|
|
137
|
-
*
|
|
138
|
-
* -
|
|
139
|
-
* - Bucket B (dynamicTools): Tools registered via registerTool() - component-scoped tools
|
|
604
|
+
* This separation ensures that component-scoped dynamic tools persist across
|
|
605
|
+
* app-level provideContext() calls.
|
|
140
606
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* - Components can manage their own tool lifecycle independently
|
|
144
|
-
* - Final tool list = Bucket A + Bucket B (merged, with collision detection)
|
|
607
|
+
* @class WebModelContext
|
|
608
|
+
* @implements {InternalModelContext}
|
|
145
609
|
*/
|
|
146
610
|
var WebModelContext = class {
|
|
147
611
|
bridge;
|
|
@@ -150,6 +614,12 @@ var WebModelContext = class {
|
|
|
150
614
|
dynamicTools;
|
|
151
615
|
registrationTimestamps;
|
|
152
616
|
unregisterFunctions;
|
|
617
|
+
testingAPI;
|
|
618
|
+
/**
|
|
619
|
+
* Creates a new WebModelContext instance.
|
|
620
|
+
*
|
|
621
|
+
* @param {MCPBridge} bridge - The MCP bridge to use for communication
|
|
622
|
+
*/
|
|
153
623
|
constructor(bridge) {
|
|
154
624
|
this.bridge = bridge;
|
|
155
625
|
this.eventTarget = new EventTarget();
|
|
@@ -159,26 +629,51 @@ var WebModelContext = class {
|
|
|
159
629
|
this.unregisterFunctions = /* @__PURE__ */ new Map();
|
|
160
630
|
}
|
|
161
631
|
/**
|
|
162
|
-
*
|
|
632
|
+
* Sets the testing API instance.
|
|
633
|
+
* Called during initialization to enable testing features.
|
|
634
|
+
*
|
|
635
|
+
* @param {WebModelContextTesting} testingAPI - The testing API instance
|
|
636
|
+
* @internal
|
|
637
|
+
*/
|
|
638
|
+
setTestingAPI(testingAPI) {
|
|
639
|
+
this.testingAPI = testingAPI;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Adds an event listener for tool call events.
|
|
643
|
+
*
|
|
644
|
+
* @param {'toolcall'} type - Event type (only 'toolcall' is supported)
|
|
645
|
+
* @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler function
|
|
646
|
+
* @param {boolean | AddEventListenerOptions} [options] - Event listener options
|
|
163
647
|
*/
|
|
164
648
|
addEventListener(type, listener, options) {
|
|
165
649
|
this.eventTarget.addEventListener(type, listener, options);
|
|
166
650
|
}
|
|
167
651
|
/**
|
|
168
|
-
*
|
|
652
|
+
* Removes an event listener for tool call events.
|
|
653
|
+
*
|
|
654
|
+
* @param {'toolcall'} type - Event type (only 'toolcall' is supported)
|
|
655
|
+
* @param {(event: ToolCallEvent) => void | Promise<void>} listener - Event handler function
|
|
656
|
+
* @param {boolean | EventListenerOptions} [options] - Event listener options
|
|
169
657
|
*/
|
|
170
658
|
removeEventListener(type, listener, options) {
|
|
171
659
|
this.eventTarget.removeEventListener(type, listener, options);
|
|
172
660
|
}
|
|
173
661
|
/**
|
|
174
|
-
*
|
|
662
|
+
* Dispatches a tool call event to all registered listeners.
|
|
663
|
+
*
|
|
664
|
+
* @param {Event} event - The event to dispatch
|
|
665
|
+
* @returns {boolean} False if event was cancelled, true otherwise
|
|
175
666
|
*/
|
|
176
667
|
dispatchEvent(event) {
|
|
177
668
|
return this.eventTarget.dispatchEvent(event);
|
|
178
669
|
}
|
|
179
670
|
/**
|
|
180
|
-
*
|
|
181
|
-
* Clears and replaces
|
|
671
|
+
* Provides context (tools) to AI models by registering base tools (Bucket A).
|
|
672
|
+
* Clears and replaces all previously registered base tools while preserving
|
|
673
|
+
* dynamic tools registered via registerTool().
|
|
674
|
+
*
|
|
675
|
+
* @param {ModelContextInput} context - Context containing tools to register
|
|
676
|
+
* @throws {Error} If a tool name collides with an existing dynamic tool
|
|
182
677
|
*/
|
|
183
678
|
provideContext(context) {
|
|
184
679
|
console.log(`[Web Model Context] Registering ${context.tools.length} tools via provideContext`);
|
|
@@ -200,15 +695,15 @@ var WebModelContext = class {
|
|
|
200
695
|
this.provideContextTools.set(tool.name, validatedTool);
|
|
201
696
|
}
|
|
202
697
|
this.updateBridgeTools();
|
|
203
|
-
|
|
204
|
-
method: "notifications/tools/list_changed",
|
|
205
|
-
params: {}
|
|
206
|
-
});
|
|
698
|
+
this.notifyToolsListChanged();
|
|
207
699
|
}
|
|
208
700
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
264
|
-
*
|
|
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
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
|
|
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
|
-
*
|
|
325
|
-
*
|
|
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
|
-
*
|
|
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
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|