@mcp-b/webmcp-ts-sdk 5.1.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { coerceWebMcpToolDescriptor, createInvalidStateError, createToolInvocationFailedError, createUnknownError, isPlainObject, normalizeInputSchema, normalizeToolResponse, parseChromeToolInput, serializeChromeToolResult, toWebMcpAnnotations, validateExecutableOrigin, validatePotentiallyTrustworthyOrigins, validateWebMcpAccess, validateWebMcpToolDescriptor, withAbortSignal } from "@mcp-b/webmcp-polyfill/schema";
1
+ import { a as createInvalidStateError, i as coerceWebMcpToolDescriptor, n as normalizeInputSchema, o as isPlainObject, r as normalizeToolResponse, s as serializeInputSchema, t as isMcpStandardSchema } from "./schema-FbP7xLdQ.js";
2
+ import { installWebMCP } from "@mcp-b/webmcp-polyfill";
2
3
  import { McpServer, ResourceTemplate, fromJsonSchema, isInputRequiredResult, mergeCapabilities } from "@modelcontextprotocol/server";
3
4
 
4
5
  //#region src/browser-server.ts
@@ -7,48 +8,40 @@ const SERVER_MARKER_PROPERTY = "__isBrowserMcpServer";
7
8
  function isBrowserMcpServer(context) {
8
9
  return typeof context === "object" && context !== null && SERVER_MARKER_PROPERTY in context && context[SERVER_MARKER_PROPERTY] === true;
9
10
  }
11
+ function isPeerOriginTransport(transport) {
12
+ return "clientOrigin" in transport && (transport.clientOrigin === void 0 || typeof transport.clientOrigin === "string") && (!("onclientorigin" in transport) || transport.onclientorigin === void 0 || typeof transport.onclientorigin === "function");
13
+ }
10
14
  function parseNativeToolResult(serialized) {
11
- if (serialized === null) return {
12
- content: [{
13
- type: "text",
14
- text: "Tool execution interrupted by navigation"
15
- }],
16
- isError: true
17
- };
18
15
  let result;
19
16
  try {
20
17
  result = JSON.parse(serialized);
21
18
  } catch {
22
- return normalizeToolResponse(serialized);
19
+ result = serialized;
23
20
  }
24
- return isInputRequiredResult(result) ? result : normalizeToolResponse(result);
25
- }
26
- function toMcpInputSchema(normalized) {
27
- const standardSchema = Object.getOwnPropertyDescriptor(normalized.inputSchema, "~standard");
28
- return standardSchema && !standardSchema.enumerable ? normalized.inputSchema : fromJsonSchema(normalized.inputSchema);
21
+ if (isPlainObject(result)) return result;
22
+ return { content: [{
23
+ type: "text",
24
+ text: typeof result === "string" ? result : serialized
25
+ }] };
29
26
  }
30
- function toMcpAnnotations(annotations) {
31
- if (!annotations) return void 0;
32
- const { untrustedContentHint: _untrustedContentHint, ...mcpAnnotations } = annotations;
33
- return mcpAnnotations;
27
+ /** Runs `operation` in a microtask; rejects with the reason as soon as `signal` aborts. */
28
+ function withAbortSignal(operation, signal) {
29
+ return new Promise((resolve, reject) => {
30
+ signal.throwIfAborted();
31
+ const onAbort = () => reject(signal.reason);
32
+ signal.addEventListener("abort", onAbort, { once: true });
33
+ Promise.resolve().then(operation).then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
34
+ });
34
35
  }
35
- function toNativeTool(tool, inputSchema, execute) {
36
- const annotations = tool.annotations ? toWebMcpAnnotations(tool.annotations) : void 0;
37
- return {
38
- name: tool.name,
39
- ...tool.title !== void 0 ? { title: tool.title } : {},
40
- description: tool.description,
41
- ...inputSchema !== void 0 ? { inputSchema } : {},
42
- ...annotations ? { annotations } : {},
43
- execute
44
- };
36
+ function toMcpInputSchema(normalized) {
37
+ if (isMcpStandardSchema(normalized.inputSchema)) return normalized.inputSchema;
38
+ return fromJsonSchema(normalized.inputSchema);
45
39
  }
46
40
  /**
47
41
  * Thin WebMCP-to-MCP v2 adapter.
48
42
  *
49
43
  * The official v2 `McpServer` owns MCP registration, validation, and transport
50
- * behavior. This class only adds the document-facing WebMCP contract and native
51
- * Chrome mirroring.
44
+ * behavior. This class extends the upstream WebMCP context with MCP capabilities.
52
45
  */
53
46
  var BrowserMcpServer = class extends EventTarget {
54
47
  [SERVER_MARKER_PROPERTY] = true;
@@ -68,13 +61,23 @@ var BrowserMcpServer = class extends EventTarget {
68
61
  nativeToolChangeListener;
69
62
  closed = false;
70
63
  closePromise;
71
- ontoolchangeHandler = null;
72
- ontoolchangeListener = (event) => {
73
- this.ontoolchangeHandler?.call(this, event);
64
+ eventHandlers = /* @__PURE__ */ new Map();
65
+ eventHandlerListener = (event) => {
66
+ this.eventHandlers.get(event.type)?.call(this, event);
67
+ };
68
+ nativeLifecycleListener = (event) => {
69
+ if (this.closed) return;
70
+ const mirrored = new Event(event.type);
71
+ if ("toolName" in event) Object.defineProperty(mirrored, "toolName", {
72
+ enumerable: true,
73
+ value: event.toolName
74
+ });
75
+ this.dispatchEvent(mirrored);
74
76
  };
75
77
  constructor(serverInfo, options = {}) {
76
78
  super();
77
- const { native, ...serverOptions } = options;
79
+ const { native: suppliedContext, ...serverOptions } = options;
80
+ if (!suppliedContext && !globalThis.document?.modelContext) installWebMCP();
78
81
  this.mcpServer = new McpServer(serverInfo, {
79
82
  ...serverOptions,
80
83
  capabilities: mergeCapabilities(serverOptions.capabilities ?? {}, {
@@ -83,59 +86,84 @@ var BrowserMcpServer = class extends EventTarget {
83
86
  prompts: { listChanged: true }
84
87
  })
85
88
  });
86
- this.native = native;
89
+ this.native = suppliedContext ?? globalThis.document?.modelContext;
87
90
  this.ownerDocument = globalThis.document ?? null;
88
- if (native && typeof native.addEventListener === "function" && typeof native.removeEventListener === "function") {
89
- this.nativeToolChangeListener = () => {
90
- if (this.closed) return;
91
- this.nativeToolChangeQueue = this.nativeToolChangeQueue.then(async () => {
92
- try {
93
- await this.syncNativeTools();
94
- } catch (error) {
95
- console.warn("[BrowserMcpServer] Native WebMCP tool reconciliation failed:", error);
96
- }
97
- await this.notifyProducerToolsChanged();
98
- });
99
- };
100
- native.addEventListener("toolchange", this.nativeToolChangeListener);
101
- }
91
+ this.nativeToolChangeListener = () => {
92
+ if (this.closed) return;
93
+ this.nativeToolChangeQueue = this.nativeToolChangeQueue.then(async () => {
94
+ try {
95
+ await this.syncNativeTools();
96
+ } catch (error) {
97
+ console.warn("[BrowserMcpServer] Native WebMCP tool reconciliation failed:", error);
98
+ }
99
+ await this.notifyProducerToolsChanged();
100
+ });
101
+ };
102
+ this.native?.addEventListener("toolchange", this.nativeToolChangeListener);
103
+ this.native?.addEventListener("toolactivated", this.nativeLifecycleListener);
104
+ this.native?.addEventListener("toolcancel", this.nativeLifecycleListener);
105
+ }
106
+ getEventHandler(type) {
107
+ return this.eventHandlers.get(type) ?? null;
108
+ }
109
+ setEventHandler(type, handler) {
110
+ if (typeof handler === "function") {
111
+ if (!this.eventHandlers.has(type)) super.addEventListener(type, this.eventHandlerListener);
112
+ this.eventHandlers.set(type, handler);
113
+ } else if (this.eventHandlers.delete(type)) super.removeEventListener(type, this.eventHandlerListener);
102
114
  }
103
115
  get ontoolchange() {
104
- return this.ontoolchangeHandler;
116
+ return this.getEventHandler("toolchange");
105
117
  }
106
118
  set ontoolchange(handler) {
107
- const listener = typeof handler === "function" ? handler : null;
108
- if (listener === null) {
109
- this.ontoolchangeHandler = null;
110
- super.removeEventListener("toolchange", this.ontoolchangeListener);
111
- return;
112
- }
113
- if (this.ontoolchangeHandler === null) super.addEventListener("toolchange", this.ontoolchangeListener);
114
- this.ontoolchangeHandler = listener;
119
+ this.setEventHandler("toolchange", handler);
120
+ }
121
+ get ontoolactivated() {
122
+ return this.getEventHandler("toolactivated");
123
+ }
124
+ set ontoolactivated(handler) {
125
+ this.setEventHandler("toolactivated", handler);
126
+ }
127
+ get ontoolcancel() {
128
+ return this.getEventHandler("toolcancel");
129
+ }
130
+ set ontoolcancel(handler) {
131
+ this.setEventHandler("toolcancel", handler);
115
132
  }
116
133
  async notifyProducerToolsChanged() {
117
134
  if (this.closed) return;
118
135
  await new Promise((resolve) => setTimeout(resolve, 0));
119
136
  if (!this.closed) this.dispatchEvent(new Event("toolchange"));
120
137
  }
121
- getNativeStandardToolsApi() {
122
- const candidate = this.native;
123
- return candidate && typeof candidate.executeTool === "function" ? candidate : void 0;
124
- }
125
138
  async registerNativeToolMirror(tool, normalized, options, execute, controller) {
126
- if (!this.native) return;
127
- const nativeRegister = this.native.registerTool;
139
+ const native = this.native;
140
+ if (!native) return;
128
141
  const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
129
- const nativeInputSchema = normalized.registeredInputSchema === void 0 ? void 0 : JSON.parse(normalized.registeredInputSchema);
142
+ const nativeTool = {
143
+ name: tool.name,
144
+ description: tool.description,
145
+ execute
146
+ };
147
+ if (tool.title !== void 0) nativeTool.title = tool.title;
148
+ if (normalized.registeredInputSchema !== void 0) {
149
+ const inputSchema = JSON.parse(normalized.registeredInputSchema);
150
+ if (!isPlainObject(inputSchema)) throw new TypeError("inputSchema must be an object");
151
+ nativeTool.inputSchema = inputSchema;
152
+ }
153
+ if (tool.annotations) nativeTool.annotations = tool.annotations;
130
154
  this.nativeToolAbortControllers.set(tool.name, controller);
131
155
  try {
132
- await nativeRegister.call(this.native, toNativeTool(tool, nativeInputSchema, (input) => execute(input)), {
156
+ await native.registerTool(nativeTool, {
133
157
  ...options,
134
158
  signal
135
159
  });
136
160
  } catch (error) {
137
161
  controller.abort();
138
162
  if (this.nativeToolAbortControllers.get(tool.name) === controller) this.nativeToolAbortControllers.delete(tool.name);
163
+ if (error instanceof DOMException && error.name === "NotAllowedError") {
164
+ console.warn(`[BrowserMcpServer] Tool "${tool.name}" is available over MCP only: ${error.message}. Grant allow="tools" on the iframe to expose it to WebMCP.`);
165
+ return;
166
+ }
139
167
  throw error;
140
168
  }
141
169
  if (this.nativeToolAbortControllers.get(tool.name) === controller) this.removingNativeTools.delete(tool.name);
@@ -146,79 +174,74 @@ var BrowserMcpServer = class extends EventTarget {
146
174
  controller.abort();
147
175
  this.nativeToolAbortControllers.delete(name);
148
176
  }
149
- validateToolDescriptor(tool) {
150
- validateWebMcpToolDescriptor(tool);
151
- if (this.tools.has(tool.name) || this.pendingTools.has(tool.name)) throw createInvalidStateError(`Tool already registered: ${tool.name}`);
152
- return normalizeInputSchema(tool.inputSchema);
153
- }
154
177
  registerToolInMcp(tool, normalized, executeTool, exposedTo) {
155
- const outputSchema = tool.outputSchema === void 0 ? void 0 : structuredClone(tool.outputSchema);
156
- const mcpOutputSchema = outputSchema === void 0 ? void 0 : fromJsonSchema(outputSchema);
157
- const mcpAnnotations = toMcpAnnotations(tool.annotations);
158
- const mcpCompatibleInput = normalized.inputSchema.type === void 0 || normalized.inputSchema.type === "object";
159
- const mcpHandle = mcpCompatibleInput ? this.mcpServer.registerTool(tool.name, {
160
- ...tool.title !== void 0 ? { title: tool.title } : {},
178
+ const item = {
179
+ name: tool.name,
161
180
  description: tool.description,
162
- inputSchema: toMcpInputSchema(normalized),
163
- ...mcpOutputSchema ? { outputSchema: mcpOutputSchema } : {},
164
- ...mcpAnnotations ? { annotations: mcpAnnotations } : {}
165
- }, async (args, context) => {
166
- const result = await executeTool(args, context.mcpReq.signal);
167
- if (isInputRequiredResult(result)) throw new Error(`WebMCP tool "${tool.name}" returned input_required. Multi-round tool flows require BrowserMcpServer.mcpServer.registerTool().`);
168
- return normalizeToolResponse(result);
169
- }) : void 0;
170
- if (exposedTo?.length) mcpHandle?.disable();
171
- if (!mcpCompatibleInput) console.warn(`[BrowserMcpServer] Tool "${tool.name}" remains available through WebMCP but cannot be exposed over MCP because MCP input schemas require an object root.`);
172
- return {
173
- item: {
174
- name: tool.name,
175
- ...tool.title !== void 0 ? { title: tool.title } : {},
176
- description: tool.description,
177
- inputSchema: normalized.inputSchema,
178
- ...outputSchema === void 0 ? {} : { outputSchema },
179
- ...tool.annotations ? { annotations: tool.annotations } : {}
180
- },
181
- ...normalized.registeredInputSchema !== void 0 ? { registeredInputSchema: normalized.registeredInputSchema } : {},
182
- execute: executeTool,
183
- mcpHandle,
184
- ...exposedTo?.length ? { exposedTo } : {}
181
+ inputSchema: normalized.inputSchema
185
182
  };
183
+ if (tool.title !== void 0) item.title = tool.title;
184
+ if (tool.outputSchema !== void 0) item.outputSchema = structuredClone(tool.outputSchema);
185
+ if (tool.annotations) item.annotations = tool.annotations;
186
+ const config = { description: tool.description };
187
+ if (item.outputSchema !== void 0) config.outputSchema = fromJsonSchema(item.outputSchema);
188
+ if (tool.title !== void 0) config.title = tool.title;
189
+ if (tool.annotations) {
190
+ const { untrustedContentHint: _untrustedContentHint, ...annotations } = tool.annotations;
191
+ config.annotations = annotations;
192
+ }
193
+ let mcpHandle;
194
+ if (normalized.inputSchema.type === void 0 || normalized.inputSchema.type === "object") {
195
+ config.inputSchema = toMcpInputSchema(normalized);
196
+ mcpHandle = this.mcpServer.registerTool(tool.name, config, async (args, context) => {
197
+ const result = await executeTool(args, { signal: context.mcpReq.signal });
198
+ if (isInputRequiredResult(result)) throw new Error(`WebMCP tool "${tool.name}" returned input_required. Multi-round tool flows require BrowserMcpServer.mcpServer.registerTool().`);
199
+ return normalizeToolResponse(result);
200
+ });
201
+ mcpHandle.disable();
202
+ } else console.warn(`[BrowserMcpServer] Tool "${tool.name}" remains available through WebMCP but cannot be exposed over MCP because MCP input schemas require an object root.`);
203
+ const registered = {
204
+ item,
205
+ mcpHandle
206
+ };
207
+ if (exposedTo?.length) registered.exposedTo = exposedTo;
208
+ return registered;
186
209
  }
187
210
  registerTool = async (toolValue, optionsValue = {}) => {
188
211
  const { signal, exposedTo } = optionsValue ?? {};
189
- const options = {
190
- ...signal ? { signal } : {},
191
- ...exposedTo ? { exposedTo: [...exposedTo] } : {}
192
- };
193
- validateWebMcpAccess(this.ownerDocument);
212
+ const options = {};
213
+ if (signal) options.signal = signal;
214
+ if (exposedTo) options.exposedTo = [...exposedTo];
194
215
  if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
195
216
  if (!isPlainObject(toolValue)) throw new TypeError("registerTool(tool) requires a tool object");
196
217
  const tool = coerceWebMcpToolDescriptor(toolValue);
197
- const normalized = this.validateToolDescriptor(tool);
198
- options.signal?.throwIfAborted();
199
- validatePotentiallyTrustworthyOrigins(options.exposedTo);
218
+ if (this.tools.has(tool.name) && !this.nativeBackfilledTools.has(tool.name) || this.pendingTools.has(tool.name)) throw createInvalidStateError(`Tool already registered: ${tool.name}`);
219
+ const normalized = normalizeInputSchema(tool.inputSchema);
200
220
  options.signal?.throwIfAborted();
201
- const execute = async (args, signal) => {
202
- signal?.throwIfAborted();
203
- return withAbortSignal(Promise.resolve().then(() => Reflect.apply(tool.execute, void 0, [args])), signal);
204
- };
221
+ const callback = tool.execute;
222
+ const execute = (args, options) => withAbortSignal(() => callback(args, options), options.signal);
205
223
  const controller = new AbortController();
224
+ const exposedOrigins = options.exposedTo?.map((origin) => URL.parse(origin)?.origin ?? origin);
206
225
  let registered;
207
226
  const abort = () => {
208
227
  if (this.pendingTools.get(tool.name) === controller) this.pendingTools.delete(tool.name);
209
228
  if (registered && this.tools.get(tool.name) === registered) this.removeTool(tool.name);
210
- else this.abortNativeToolMirror(tool.name, controller);
229
+ else {
230
+ registered?.mcpHandle?.remove();
231
+ this.abortNativeToolMirror(tool.name, controller);
232
+ }
233
+ registered = void 0;
211
234
  };
212
235
  options.signal?.addEventListener("abort", abort, { once: true });
213
236
  this.pendingTools.set(tool.name, controller);
214
237
  try {
238
+ if (!this.nativeBackfilledTools.has(tool.name)) registered = this.registerToolInMcp(tool, normalized, execute, exposedOrigins);
239
+ options.signal?.throwIfAborted();
215
240
  await this.registerNativeToolMirror(tool, normalized, options, execute, controller);
216
241
  if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
217
242
  options.signal?.throwIfAborted();
218
- registered = this.registerToolInMcp(tool, normalized, execute, options.exposedTo?.map((origin) => {
219
- const parsedOrigin = new URL(origin).origin;
220
- return parsedOrigin === "null" ? origin : parsedOrigin;
221
- }));
243
+ this.removeTool(tool.name, { skipNative: true });
244
+ registered ??= this.registerToolInMcp(tool, normalized, execute, exposedOrigins);
222
245
  this.tools.set(tool.name, registered);
223
246
  this.applyToolExposure();
224
247
  if (options.signal?.aborted) {
@@ -226,6 +249,7 @@ var BrowserMcpServer = class extends EventTarget {
226
249
  options.signal.throwIfAborted();
227
250
  }
228
251
  } catch (error) {
252
+ registered?.mcpHandle?.remove();
229
253
  this.abortNativeToolMirror(tool.name, controller);
230
254
  options.signal?.removeEventListener("abort", abort);
231
255
  options.signal?.throwIfAborted();
@@ -237,8 +261,7 @@ var BrowserMcpServer = class extends EventTarget {
237
261
  registered.abortSignal = options.signal;
238
262
  registered.abortListener = abort;
239
263
  }
240
- if (this.tools.get(tool.name) === registered) if (this.native) await this.nativeToolChangeQueue;
241
- else await this.notifyProducerToolsChanged();
264
+ if (this.tools.get(tool.name) === registered) await this.nativeToolChangeQueue;
242
265
  if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
243
266
  options.signal?.throwIfAborted();
244
267
  };
@@ -249,46 +272,52 @@ var BrowserMcpServer = class extends EventTarget {
249
272
  this.tools.delete(name);
250
273
  this.nativeBackfilledTools.delete(name);
251
274
  registered.mcpHandle?.remove();
252
- if ((options?.notify ?? true) && !this.native) this.notifyProducerToolsChanged();
253
275
  }
254
276
  if (!options?.skipNative) this.abortNativeToolMirror(name);
255
277
  }
256
278
  syncNativeTools() {
279
+ const native = this.native;
257
280
  const sync = this.nativeSyncQueue.then(async () => {
258
- if (this.closed) return;
259
- const native = this.getNativeStandardToolsApi();
260
- if (native) await this.backfillNativeStandardTools(native);
281
+ if (this.closed || !native) return;
282
+ await this.backfillNativeStandardTools(native);
261
283
  });
262
284
  this.nativeSyncQueue = sync.then(() => void 0, () => void 0);
263
285
  return sync;
264
286
  }
265
287
  async backfillNativeStandardTools(native) {
266
- const tools = await native.getTools();
288
+ const ownerWindow = this.ownerDocument?.defaultView;
289
+ const tools = (await native.getTools()).filter((tool) => {
290
+ if (tool.window === ownerWindow) return true;
291
+ if (ownerWindow?.parent !== ownerWindow) return false;
292
+ let frame = tool.window;
293
+ while (frame) {
294
+ if (frame === ownerWindow) return true;
295
+ const parent = frame.parent;
296
+ if (parent === frame) break;
297
+ frame = parent;
298
+ }
299
+ return false;
300
+ });
267
301
  if (this.closed) return;
302
+ for (const name of this.removingNativeTools) if (!tools.some((tool) => tool.name === name && tool.window === ownerWindow)) this.removingNativeTools.delete(name);
268
303
  const nextTools = /* @__PURE__ */ new Map();
269
- const nativeNames = new Set(tools.map(({ name }) => name));
270
- for (const name of this.removingNativeTools) if (!nativeNames.has(name)) this.removingNativeTools.delete(name);
271
304
  for (const tool of tools) {
272
305
  if (nextTools.has(tool.name) || this.pendingTools.has(tool.name) || this.removingNativeTools.has(tool.name)) continue;
273
- let inputSchema = DEFAULT_INPUT_SCHEMA;
274
- if (tool.inputSchema !== void 0) try {
275
- const serialized = typeof tool.inputSchema === "string" ? tool.inputSchema : JSON.stringify(tool.inputSchema);
276
- const parsed = JSON.parse(serialized);
277
- inputSchema = isPlainObject(parsed) ? parsed : void 0;
278
- } catch {
279
- inputSchema = void 0;
280
- }
281
- if (!inputSchema) {
306
+ let inputSchema;
307
+ try {
308
+ inputSchema = JSON.parse(serializeInputSchema(tool.inputSchema ?? DEFAULT_INPUT_SCHEMA));
309
+ } catch {}
310
+ if (!isPlainObject(inputSchema)) {
282
311
  console.warn(`[BrowserMcpServer] Native tool "${tool.name}" was not exposed over MCP because its input schema is malformed.`);
283
312
  continue;
284
313
  }
285
314
  const item = {
286
315
  name: tool.name,
287
- ...tool.title !== void 0 ? { title: tool.title } : {},
288
316
  description: tool.description,
289
- inputSchema,
290
- ...tool.annotations ? { annotations: tool.annotations } : {}
317
+ inputSchema
291
318
  };
319
+ if (tool.title !== void 0) item.title = tool.title;
320
+ if (tool.annotations) item.annotations = tool.annotations;
292
321
  nextTools.set(tool.name, {
293
322
  source: tool,
294
323
  item,
@@ -297,47 +326,41 @@ var BrowserMcpServer = class extends EventTarget {
297
326
  }
298
327
  for (const [name, current] of this.nativeBackfilledTools) {
299
328
  const next = nextTools.get(name);
300
- if (!next || next.fingerprint !== current.fingerprint) {
301
- this.removeTool(name, {
302
- skipNative: true,
303
- notify: false
304
- });
305
- continue;
306
- }
307
- this.nativeBackfilledTools.set(name, next);
329
+ if (next?.fingerprint === current.fingerprint) this.nativeBackfilledTools.set(name, next);
330
+ else this.removeTool(name, { skipNative: true });
308
331
  }
309
332
  for (const [name, next] of nextTools) {
310
333
  if (this.tools.has(name)) continue;
311
- const execute = async (args, signal) => {
334
+ const execute = async (args, options) => {
312
335
  const currentTool = this.nativeBackfilledTools.get(name)?.source;
313
336
  if (!currentTool) throw new Error(`Native tool not found: ${name}`);
314
- const input = JSON.stringify(args);
315
- return parseNativeToolResult(signal ? await native.executeTool.call(this.native, currentTool, input, { signal }) : await native.executeTool.call(this.native, currentTool, input));
337
+ try {
338
+ return parseNativeToolResult(await native.executeTool(currentTool, args, options));
339
+ } catch (error) {
340
+ if (currentTool.window.closed) this.nativeToolChangeListener();
341
+ throw error;
342
+ }
316
343
  };
317
344
  const tool = {
318
- name,
319
- ...next.item.title !== void 0 ? { title: next.item.title } : {},
320
- description: next.item.description,
321
- inputSchema: next.item.inputSchema,
322
- ...next.item.annotations ? { annotations: next.item.annotations } : {},
323
- execute: (args) => execute(args)
345
+ ...next.item,
346
+ execute
324
347
  };
325
348
  try {
326
- const normalized = this.validateToolDescriptor(tool);
327
- this.tools.set(tool.name, this.registerToolInMcp(tool, normalized, execute, void 0));
349
+ const normalized = normalizeInputSchema(tool.inputSchema);
350
+ this.tools.set(name, this.registerToolInMcp(tool, normalized, execute));
328
351
  this.nativeBackfilledTools.set(name, next);
329
352
  } catch (error) {
330
353
  console.warn(`[BrowserMcpServer] Native tool "${name}" was not exposed over MCP because its schema could not be compiled:`, error);
331
354
  }
332
355
  }
356
+ this.applyToolExposure();
333
357
  }
334
358
  registerResource(descriptor) {
335
359
  if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
336
360
  const registeredDescriptor = { ...descriptor };
337
- const config = {
338
- ...registeredDescriptor.description !== void 0 ? { description: registeredDescriptor.description } : {},
339
- ...registeredDescriptor.mimeType !== void 0 ? { mimeType: registeredDescriptor.mimeType } : {}
340
- };
361
+ const config = {};
362
+ if (registeredDescriptor.description !== void 0) config.description = registeredDescriptor.description;
363
+ if (registeredDescriptor.mimeType !== void 0) config.mimeType = registeredDescriptor.mimeType;
341
364
  const template = registeredDescriptor.uri.includes("{") ? new ResourceTemplate(registeredDescriptor.uri, { list: void 0 }) : void 0;
342
365
  const mcpHandle = template ? this.mcpServer.registerResource(registeredDescriptor.name, template, config, async (uri, variables) => registeredDescriptor.read(uri, variables)) : this.mcpServer.registerResource(registeredDescriptor.name, registeredDescriptor.uri, config, async (uri) => registeredDescriptor.read(uri));
343
366
  this.registrations.add(mcpHandle);
@@ -347,15 +370,13 @@ var BrowserMcpServer = class extends EventTarget {
347
370
  }
348
371
  registerPrompt(descriptor) {
349
372
  if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
350
- const registeredDescriptor = {
351
- ...descriptor,
352
- ...descriptor.argsSchema === void 0 ? {} : { argsSchema: structuredClone(descriptor.argsSchema) }
353
- };
373
+ const registeredDescriptor = { ...descriptor };
374
+ if (descriptor.argsSchema !== void 0) registeredDescriptor.argsSchema = structuredClone(descriptor.argsSchema);
354
375
  const argsSchema = registeredDescriptor.argsSchema === void 0 ? void 0 : fromJsonSchema(registeredDescriptor.argsSchema);
355
- const mcpHandle = this.mcpServer.registerPrompt(registeredDescriptor.name, {
356
- ...registeredDescriptor.description !== void 0 ? { description: registeredDescriptor.description } : {},
357
- ...argsSchema ? { argsSchema } : {}
358
- }, async (args) => registeredDescriptor.get(args ?? {}));
376
+ const config = {};
377
+ if (registeredDescriptor.description !== void 0) config.description = registeredDescriptor.description;
378
+ if (argsSchema) config.argsSchema = argsSchema;
379
+ const mcpHandle = this.mcpServer.registerPrompt(registeredDescriptor.name, config, async (args) => registeredDescriptor.get(args ?? {}));
359
380
  this.registrations.add(mcpHandle);
360
381
  return { unregister: () => {
361
382
  if (this.registrations.delete(mcpHandle)) mcpHandle.remove();
@@ -365,69 +386,28 @@ var BrowserMcpServer = class extends EventTarget {
365
386
  return structuredClone([...this.tools.values()].map(({ item }) => item));
366
387
  }
367
388
  async getTools(options) {
368
- validateWebMcpAccess(this.ownerDocument);
369
389
  if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
370
- if (this.native) return this.native.getTools(options);
371
- validatePotentiallyTrustworthyOrigins(options?.fromOrigins);
372
- if (options?.fromOrigins?.length) throw new DOMException("Cross-document tool discovery requires native WebMCP", "NotSupportedError");
373
- const origin = globalThis.location?.origin ?? "";
374
- const currentWindow = globalThis.window;
375
- const tools = [...this.tools.values()].map(({ item, registeredInputSchema }) => {
376
- const parsed = registeredInputSchema === void 0 ? void 0 : JSON.parse(registeredInputSchema);
377
- return {
378
- name: item.name,
379
- title: item.title ?? "",
380
- description: item.description,
381
- ...isPlainObject(parsed) ? { inputSchema: parsed } : {},
382
- origin,
383
- window: currentWindow,
384
- ...item.annotations ? { annotations: toWebMcpAnnotations(item.annotations) } : {}
385
- };
386
- }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
387
- await new Promise((resolve) => setTimeout(resolve, 0));
388
- return tools;
390
+ if (!this.native) throw createInvalidStateError("BrowserMcpServer has no WebMCP context");
391
+ return this.native.getTools(options);
389
392
  }
390
- async executeTool(tool, inputArgsJson, options) {
391
- validateWebMcpAccess(this.ownerDocument);
393
+ async executeTool(...args) {
392
394
  if (this.closed) throw createInvalidStateError("BrowserMcpServer is closed");
393
- const native = this.getNativeStandardToolsApi();
394
- if (native) return native.executeTool.call(this.native, tool, inputArgsJson, options);
395
- if (tool === null || typeof tool !== "object") throw new TypeError("RegisteredTool must be an object");
396
- for (const required of [
397
- "name",
398
- "description",
399
- "window",
400
- "origin"
401
- ]) if (!(required in tool)) throw new TypeError(`RegisteredTool.${required} is required`);
402
- validateExecutableOrigin(tool.origin);
403
- const origin = globalThis.location?.origin ?? "";
404
- const currentWindow = globalThis.window;
405
- if (tool.origin !== origin || tool.window !== currentWindow) throw createUnknownError(`Native tool not found: ${tool.name}`);
406
- options?.signal?.throwIfAborted();
407
- const registered = this.tools.get(tool.name);
408
- if (!registered) throw createUnknownError(`Tool not found: ${tool.name}`);
409
- const args = parseChromeToolInput(inputArgsJson);
410
- try {
411
- return serializeChromeToolResult(await withAbortSignal(registered.execute(args, options?.signal), registered.abortSignal, () => createUnknownError("Tool unregistered")));
412
- } catch (error) {
413
- if (options?.signal?.aborted && error === options.signal.reason) throw error;
414
- if (registered.abortSignal?.aborted) throw error;
415
- throw createToolInvocationFailedError(error);
416
- }
395
+ if (!this.native) throw createInvalidStateError("BrowserMcpServer has no WebMCP context");
396
+ return this.native.executeTool(...args);
417
397
  }
418
398
  /**
419
399
  * Re-evaluate which registered tools may appear on the wire.
420
400
  *
421
- * `exposedTo` narrows a tool to named embedder origins. The peer origin is only known
422
- * once a transport that reports one has heard from its peer, so a restricted tool stays
423
- * disabled until then — and stays disabled forever on transports that cannot name a peer
424
- * at all. Failing closed keeps a tool that asked for a narrow audience from reaching a
425
- * wider one; unrestricted tools are untouched and behave exactly as before.
401
+ * A published tool without `exposedTo` is enabled. `exposedTo` narrows a tool to named
402
+ * embedder origins. The peer origin is only known once a transport that reports one has
403
+ * heard from its peer, so a restricted tool stays disabled until then — and stays
404
+ * disabled forever on transports that cannot name a peer at all. Failing closed keeps a
405
+ * tool that asked for a narrow audience from reaching a wider one.
426
406
  */
427
407
  applyToolExposure() {
428
408
  for (const { exposedTo, mcpHandle } of this.tools.values()) {
429
- if (!exposedTo?.length || !mcpHandle) continue;
430
- const visible = this.peerOrigin !== void 0 && this.peerOrigin !== "null" && exposedTo.includes(this.peerOrigin);
409
+ if (!mcpHandle) continue;
410
+ const visible = !exposedTo?.length || this.peerOrigin !== void 0 && this.peerOrigin !== "null" && exposedTo.includes(this.peerOrigin);
431
411
  if (visible === mcpHandle.enabled) continue;
432
412
  if (visible) mcpHandle.enable();
433
413
  else mcpHandle.disable();
@@ -444,7 +424,7 @@ var BrowserMcpServer = class extends EventTarget {
444
424
  this.stopObservingPeerOrigin = void 0;
445
425
  this.peerOrigin = void 0;
446
426
  this.applyToolExposure();
447
- if (!("clientOrigin" in transport)) return;
427
+ if (!isPeerOriginTransport(transport)) return;
448
428
  const peered = transport;
449
429
  this.peerOrigin = peered.clientOrigin;
450
430
  this.applyToolExposure();
@@ -486,12 +466,11 @@ var BrowserMcpServer = class extends EventTarget {
486
466
  this.stopObservingPeerOrigin?.();
487
467
  this.stopObservingPeerOrigin = void 0;
488
468
  this.closePromise = (async () => {
489
- if (this.native && this.nativeToolChangeListener) {
490
- this.native.removeEventListener("toolchange", this.nativeToolChangeListener);
491
- this.nativeToolChangeListener = void 0;
492
- }
469
+ this.native?.removeEventListener("toolchange", this.nativeToolChangeListener);
470
+ this.native?.removeEventListener("toolactivated", this.nativeLifecycleListener);
471
+ this.native?.removeEventListener("toolcancel", this.nativeLifecycleListener);
493
472
  for (const name of this.nativeToolAbortControllers.keys()) this.abortNativeToolMirror(name);
494
- for (const name of this.tools.keys()) this.removeTool(name, { notify: false });
473
+ for (const name of this.tools.keys()) this.removeTool(name);
495
474
  for (const handle of this.registrations) handle.remove();
496
475
  this.registrations.clear();
497
476
  this.nativeBackfilledTools.clear();