@mcp-b/webmcp-polyfill 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,5 +1,458 @@
1
- import { coerceWebMcpToolDescriptor, createInvalidStateError, createToolInvocationFailedError, createUnknownError, isPlainObject, parseChromeToolInput, serializeChromeToolResult, serializeInputSchema, toWebMcpAnnotations, validateExecutableOrigin, validatePotentiallyTrustworthyOrigins, validateWebMcpAccess, validateWebMcpToolDescriptor, withAbortSignal } from "./schema.js";
1
+ //#region src/upstream/frames.ts
2
+ const annotationNames = [
3
+ "consequentialHint",
4
+ "debugging",
5
+ "readOnlyHint",
6
+ "untrustedContentHint"
7
+ ];
8
+ const protocol = "webmcp-polyfill";
9
+ const deadline = 500;
10
+ const NativeDOMException$1 = globalThis.DOMException;
11
+ var FrameBridge = class {
12
+ #document;
13
+ #window;
14
+ #handlers;
15
+ #knownPeers = /* @__PURE__ */ new Set();
16
+ #unanswered = /* @__PURE__ */ new Set();
17
+ #started = Promise.withResolvers();
18
+ #handshakes = /* @__PURE__ */ new Map();
19
+ #sessions = /* @__PURE__ */ new Set();
20
+ #monitor;
21
+ #hidden = false;
22
+ constructor(view, handlers) {
23
+ this.#window = view;
24
+ this.#document = view.document;
25
+ this.#handlers = handlers;
26
+ this.#window.addEventListener("message", (event) => this.#receive(event), true);
27
+ this.#window.addEventListener("pagehide", () => {
28
+ this.#hidden = true;
29
+ for (const session of this.#sessions) session.end();
30
+ });
31
+ this.#window.addEventListener("pageshow", (event) => {
32
+ if (event.persisted) {
33
+ this.#hidden = false;
34
+ this.#announce();
35
+ }
36
+ });
37
+ for (const frame of this.#announce()) this.#unanswered.add(frame);
38
+ if (this.#unanswered.size === 0) this.#started.resolve();
39
+ else setTimeout(() => {
40
+ this.#unanswered.clear();
41
+ this.#started.resolve();
42
+ }, deadline);
43
+ }
44
+ async allowed() {
45
+ if (this.#hidden) return false;
46
+ return this.#documentAllowed(this.#document);
47
+ }
48
+ async getTools(fromOrigins) {
49
+ await this.#started.promise;
50
+ const ownOrigin = this.#window.origin;
51
+ const origins = [ownOrigin, ...fromOrigins];
52
+ return (await Promise.all(this.#participants().map(async (peer) => {
53
+ if (peer === this.#window) return this.#handlers.getTools(ownOrigin).map((metadata) => toRegisteredTool(metadata, this.#window, ownOrigin));
54
+ try {
55
+ const reply = await this.#request(peer, origins, { kind: "getTools" });
56
+ if (!Array.isArray(reply.value)) throw new TypeError("Expected a tool list from the frame");
57
+ return reply.value.map((value) => toRegisteredTool(readToolMetadata(value), peer, reply.origin));
58
+ } catch (error) {
59
+ if (!(error instanceof NativeDOMException$1 && error.name === "UnknownError")) console.warn("WebMCP: could not read tools from a frame.", error);
60
+ return [];
61
+ }
62
+ }))).flat();
63
+ }
64
+ async execute(target, expectedOrigin, name, serializedInput, signal) {
65
+ const reply = await this.#request(target, [expectedOrigin], {
66
+ kind: "execute",
67
+ name,
68
+ input: serializedInput
69
+ }, signal);
70
+ if (typeof reply.value !== "string") throw executionError();
71
+ return reply.value;
72
+ }
73
+ async notify(exposedTo) {
74
+ await this.#started.promise;
75
+ const origins = [this.#window.origin, ...exposedTo];
76
+ for (const peer of this.#participants()) {
77
+ if (peer === this.#window) {
78
+ await this.#handlers.changed();
79
+ continue;
80
+ }
81
+ try {
82
+ await this.#request(peer, origins, { kind: "changed" });
83
+ } catch {}
84
+ }
85
+ }
86
+ #request(target, origins, request, signal) {
87
+ return new Promise((resolve, reject) => {
88
+ signal?.throwIfAborted();
89
+ if (this.#hidden || !this.#related(target)) {
90
+ reject(executionError());
91
+ return;
92
+ }
93
+ const id = crypto.randomUUID();
94
+ const { port1, port2 } = new MessageChannel();
95
+ let peerOrigin;
96
+ const close = () => {
97
+ clearTimeout(timer);
98
+ stopWatching();
99
+ signal?.removeEventListener("abort", abort);
100
+ this.#handshakes.delete(id);
101
+ port1.close();
102
+ };
103
+ const fail = (error = executionError()) => {
104
+ port1.postMessage({ kind: "cancel" });
105
+ close();
106
+ reject(error);
107
+ };
108
+ const abort = () => fail(signal?.reason);
109
+ const expire = () => {
110
+ if (peerOrigin === void 0) this.#forget(target);
111
+ fail();
112
+ };
113
+ const connected = (event) => {
114
+ if (event.source !== target) return;
115
+ this.#handshakes.delete(id);
116
+ if (origins && !origins.includes(event.origin)) {
117
+ fail();
118
+ return;
119
+ }
120
+ peerOrigin = event.origin;
121
+ clearTimeout(timer);
122
+ if (request.kind !== "execute" && request.kind !== "changed") timer = setTimeout(expire, deadline);
123
+ port1.postMessage(request);
124
+ };
125
+ let timer = setTimeout(expire, deadline);
126
+ const stopWatching = this.#watch(target, fail);
127
+ port1.onmessage = (event) => {
128
+ const reply = record(event.data);
129
+ if (reply?.kind === "error") {
130
+ fail();
131
+ return;
132
+ }
133
+ if (reply?.kind !== "result" || peerOrigin === void 0) return;
134
+ close();
135
+ resolve({
136
+ value: reply.value,
137
+ origin: peerOrigin
138
+ });
139
+ };
140
+ port1.onmessageerror = () => fail();
141
+ signal?.addEventListener("abort", abort, { once: true });
142
+ this.#handshakes.set(id, connected);
143
+ try {
144
+ target.postMessage(`${protocol}:connect:${id}`, "*", [port2]);
145
+ } catch {
146
+ port2.close();
147
+ fail();
148
+ }
149
+ });
150
+ }
151
+ #receive(event) {
152
+ const message = readWindowMessage(event.data);
153
+ if (!message) return;
154
+ event.stopImmediatePropagation();
155
+ const source = event.source;
156
+ if (!event.isTrusted || !source || !isWindow(source) || !this.#related(source) || event.origin === "null" || this.#hidden) return;
157
+ if (message.kind === "announce") {
158
+ const isNew = !this.#knownPeers.has(source);
159
+ this.#learn(source);
160
+ source.postMessage(`${protocol}:present`, event.origin);
161
+ if (!isNew || this.#window !== this.#window.top) return;
162
+ for (const peer of this.#knownPeers) {
163
+ if (peer === source) continue;
164
+ peer.postMessage(`${protocol}:refresh`, "*");
165
+ }
166
+ return;
167
+ }
168
+ if (message.kind === "present") {
169
+ this.#learn(source);
170
+ return;
171
+ }
172
+ if (message.kind === "refresh") {
173
+ if (source === this.#window.top) this.#announce();
174
+ return;
175
+ }
176
+ if (message.kind === "ready") {
177
+ this.#handshakes.get(message.id)?.(event);
178
+ return;
179
+ }
180
+ const [port] = event.ports;
181
+ if (message.kind !== "connect" || !message.id || !port || event.ports.length !== 1) return;
182
+ this.#learn(source);
183
+ this.#accept(source, event.origin, message.id, port);
184
+ }
185
+ #accept(source, origin, id, port) {
186
+ const controller = new AbortController();
187
+ let requested = false;
188
+ const close = () => {
189
+ clearTimeout(timer);
190
+ stopWatching();
191
+ port.close();
192
+ };
193
+ const cancel = () => {
194
+ controller.abort();
195
+ close();
196
+ };
197
+ const fail = () => {
198
+ port.postMessage({ kind: "error" });
199
+ cancel();
200
+ };
201
+ const timer = setTimeout(fail, deadline);
202
+ const stopWatching = this.#watch(source, fail);
203
+ port.onmessageerror = fail;
204
+ port.onmessage = async (event) => {
205
+ const request = record(event.data);
206
+ if (request?.kind === "cancel") {
207
+ cancel();
208
+ return;
209
+ }
210
+ if (requested) return;
211
+ requested = true;
212
+ clearTimeout(timer);
213
+ try {
214
+ const value = await this.#respond(source, origin, request, controller.signal);
215
+ port.postMessage({
216
+ kind: "result",
217
+ value
218
+ });
219
+ close();
220
+ } catch {
221
+ fail();
222
+ }
223
+ };
224
+ source.postMessage(`${protocol}:ready:${id}`, origin);
225
+ }
226
+ async #respond(source, origin, request, signal) {
227
+ if (!request || !this.#related(source)) throw executionError();
228
+ if (request.kind === "permission") {
229
+ const { childIndex, childOrigin } = request;
230
+ if (typeof childIndex !== "number" || !Number.isInteger(childIndex) || childIndex < 0 || typeof childOrigin !== "string") throw executionError();
231
+ return this.#childPermission(childIndex, childOrigin);
232
+ }
233
+ if (!await this.allowed() || !await this.#sourceAllowed(source, origin)) throw executionError();
234
+ if (!this.#related(source)) throw executionError();
235
+ signal.throwIfAborted();
236
+ if (request.kind === "getTools") return this.#handlers.getTools(origin);
237
+ if (request.kind === "execute" && typeof request.name === "string" && typeof request.input === "string") return this.#handlers.execute(origin, request.name, request.input, signal);
238
+ if (request.kind === "changed") {
239
+ await this.#handlers.changed();
240
+ return;
241
+ }
242
+ throw executionError();
243
+ }
244
+ async #childPermission(childIndex, childOrigin) {
245
+ const child = this.#window[childIndex];
246
+ return child !== void 0 && await this.allowed() && childAllowed(this.#document, child, childOrigin);
247
+ }
248
+ async #documentAllowed(owner) {
249
+ const view = activeWindow(owner);
250
+ if (!view) return false;
251
+ const policy = readToolsPolicy(owner);
252
+ if (policy !== void 0) return policy;
253
+ if (view === view.parent) return true;
254
+ const parentOrigin = view.location.ancestorOrigins?.[0];
255
+ const parentDocument = readDocument(view.parent, parentOrigin === void 0 || parentOrigin === view.origin);
256
+ if (!parentDocument) return this.#parentPermission(view, view.origin);
257
+ return childAllowed(parentDocument, view, view.origin) && await this.#documentAllowed(parentDocument);
258
+ }
259
+ async #sourceAllowed(source, origin) {
260
+ const sourceDocument = readDocument(source, origin === this.#window.origin);
261
+ return sourceDocument ? this.#documentAllowed(sourceDocument) : this.#parentPermission(source, origin);
262
+ }
263
+ async #parentPermission(child, origin) {
264
+ const parent = child.parent;
265
+ if (parent === child) return true;
266
+ let childIndex = -1;
267
+ for (let index = 0; index < parent.length; index++) if (parent[index] === child) {
268
+ childIndex = index;
269
+ break;
270
+ }
271
+ if (childIndex < 0) return false;
272
+ if (parent === this.#window) return await this.allowed() && childAllowed(this.#document, child, origin);
273
+ await this.#started.promise;
274
+ if (!this.#knownPeers.has(parent)) return false;
275
+ try {
276
+ const reply = await this.#request(parent, void 0, {
277
+ kind: "permission",
278
+ childIndex,
279
+ childOrigin: origin
280
+ });
281
+ return reply.value === true && await this.#sourceAllowed(parent, reply.origin);
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+ #related(peer) {
287
+ try {
288
+ return !peer.closed && peer !== this.#window && peer.top === this.#window.top;
289
+ } catch {
290
+ return false;
291
+ }
292
+ }
293
+ #tree() {
294
+ if (this.#window.closed) return [];
295
+ for (const peer of this.#knownPeers) if (!this.#related(peer)) this.#knownPeers.delete(peer);
296
+ const candidates = [this.#window, ...this.#knownPeers];
297
+ for (let ancestor = this.#window.parent; ancestor !== ancestor.parent; ancestor = ancestor.parent) candidates.push(ancestor);
298
+ const tree = /* @__PURE__ */ new Set();
299
+ const pending = [this.#window.top];
300
+ while (pending.length) {
301
+ const frame = pending.pop();
302
+ if (tree.has(frame)) continue;
303
+ tree.add(frame);
304
+ const children = [];
305
+ for (let index = 0; index < frame.length; index++) {
306
+ const child = frame[index];
307
+ if (child) children.push(child);
308
+ }
309
+ for (const candidate of candidates) if (candidate !== frame && candidate.parent === frame && !children.includes(candidate)) children.push(candidate);
310
+ pending.push(...children.reverse());
311
+ }
312
+ return Array.from(tree).filter((frame) => frame === this.#window || this.#related(frame));
313
+ }
314
+ #participants() {
315
+ return this.#tree().filter((frame) => frame === this.#window || this.#knownPeers.has(frame));
316
+ }
317
+ #announce() {
318
+ const others = this.#tree().filter((frame) => frame !== this.#window);
319
+ for (const frame of others) frame.postMessage(`${protocol}:announce`, "*");
320
+ return others;
321
+ }
322
+ #learn(peer) {
323
+ this.#knownPeers.add(peer);
324
+ this.#unanswered.delete(peer);
325
+ if (this.#unanswered.size === 0) this.#started.resolve();
326
+ }
327
+ #forget(peer) {
328
+ this.#knownPeers.delete(peer);
329
+ if (this.#related(peer)) peer.postMessage(`${protocol}:announce`, "*");
330
+ }
331
+ #watch(peer, end) {
332
+ const session = {
333
+ peer,
334
+ end
335
+ };
336
+ this.#sessions.add(session);
337
+ if (this.#monitor === void 0) this.#monitor = this.#window.setInterval(() => {
338
+ for (const open of this.#sessions) if (!this.#related(open.peer)) open.end();
339
+ }, 100);
340
+ return () => {
341
+ this.#sessions.delete(session);
342
+ if (this.#sessions.size === 0) {
343
+ this.#window.clearInterval(this.#monitor);
344
+ this.#monitor = void 0;
345
+ }
346
+ };
347
+ }
348
+ };
349
+ function childAllowed(owner, child, origin) {
350
+ const ownerOrigin = owner.defaultView?.origin;
351
+ const frame = findFrame(owner, child, origin === ownerOrigin);
352
+ if (!frame) return false;
353
+ const directive = (frame.getAttribute("allow") ?? "").split(";").map((part) => part.trim().split(/\s+/u)).find(([name]) => name === "tools");
354
+ if (!directive) return origin === ownerOrigin;
355
+ const sources = directive.length === 1 ? ["'src'"] : directive.slice(1);
356
+ if (sources.includes("'none'")) return false;
357
+ const src = frame.hasAttribute("srcdoc") ? null : frame.getAttribute("src");
358
+ const srcURL = src ? URL.parse(src, owner.baseURI) : null;
359
+ const sourceOrigin = !src || srcURL?.protocol === "about:" ? ownerOrigin : srcURL?.origin;
360
+ return sources.some((source) => source === "*" || source === "'self'" && origin === ownerOrigin || source === "'src'" && origin === sourceOrigin || source === origin);
361
+ }
362
+ function readDocument(view, sameOrigin) {
363
+ if (!sameOrigin) return;
364
+ try {
365
+ return view.document;
366
+ } catch {
367
+ return;
368
+ }
369
+ }
370
+ function findFrame(owner, child, sameOrigin) {
371
+ if (sameOrigin) try {
372
+ const frame = child.frameElement;
373
+ if (frame?.ownerDocument === owner) return frame;
374
+ } catch {}
375
+ return searchFrames(owner, child);
376
+ }
377
+ function searchFrames(root, child) {
378
+ const frames = root.querySelectorAll("iframe, frame");
379
+ for (const frame of frames) if (frame.contentWindow === child) return frame;
380
+ for (const element of root.querySelectorAll("*")) {
381
+ if (!element.shadowRoot) continue;
382
+ const frame = searchFrames(element.shadowRoot, child);
383
+ if (frame) return frame;
384
+ }
385
+ }
386
+ function activeWindow(owner) {
387
+ const view = owner.defaultView;
388
+ return view && !view.closed && view.document === owner ? view : void 0;
389
+ }
390
+ function readToolsPolicy(owner) {
391
+ const source = owner;
392
+ const policy = record(source.permissionsPolicy ?? source.featurePolicy);
393
+ if (typeof policy?.features !== "function" || typeof policy.allowsFeature !== "function") return;
394
+ const features = policy.features();
395
+ return Array.isArray(features) && features.includes("tools") ? Boolean(policy.allowsFeature("tools")) : void 0;
396
+ }
397
+ function toRegisteredTool(metadata, owner, origin) {
398
+ const inputSchema = metadata.serializedSchema === void 0 ? void 0 : JSON.parse(metadata.serializedSchema);
399
+ return {
400
+ ...metadata.annotations !== void 0 && { annotations: { ...metadata.annotations } },
401
+ description: metadata.description,
402
+ ...inputSchema !== void 0 && { inputSchema },
403
+ name: metadata.name,
404
+ origin,
405
+ title: metadata.title,
406
+ window: owner
407
+ };
408
+ }
409
+ function readToolMetadata(value) {
410
+ const tool = record(value);
411
+ if (!tool || typeof tool.name !== "string" || typeof tool.title !== "string" || typeof tool.description !== "string" || tool.serializedSchema !== void 0 && typeof tool.serializedSchema !== "string") throw new TypeError("Invalid tool metadata from the frame");
412
+ const metadata = {
413
+ name: tool.name,
414
+ title: tool.title,
415
+ description: tool.description,
416
+ annotations: void 0,
417
+ serializedSchema: tool.serializedSchema
418
+ };
419
+ if (tool.annotations === void 0) return metadata;
420
+ const hints = record(tool.annotations);
421
+ if (!hints) throw new TypeError("Invalid tool annotations from the frame");
422
+ const annotations = {};
423
+ for (const name of annotationNames) {
424
+ const hint = hints[name];
425
+ if (hint === void 0) continue;
426
+ if (typeof hint !== "boolean") throw new TypeError(`Invalid ${name} annotation from the frame`);
427
+ annotations[name] = hint;
428
+ }
429
+ metadata.annotations = annotations;
430
+ return metadata;
431
+ }
432
+ function readWindowMessage(value) {
433
+ if (typeof value !== "string") return;
434
+ const [namespace, kind = "", id = "", extra] = value.split(":");
435
+ return namespace === protocol && extra === void 0 ? {
436
+ kind,
437
+ id
438
+ } : void 0;
439
+ }
440
+ function record(value) {
441
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return;
442
+ return value;
443
+ }
444
+ function isWindow(value) {
445
+ try {
446
+ return "postMessage" in value && "window" in value;
447
+ } catch {
448
+ return false;
449
+ }
450
+ }
451
+ function executionError() {
452
+ return new NativeDOMException$1("Tool execution failed", "UnknownError");
453
+ }
2
454
 
455
+ //#endregion
3
456
  //#region src/declarative-forms.ts
4
457
  const agentInvokedEvents = /* @__PURE__ */ new WeakSet();
5
458
  const agentResponses = /* @__PURE__ */ new WeakMap();
@@ -37,25 +490,25 @@ function isControl(element) {
37
490
  return element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement;
38
491
  }
39
492
  function getFormControls(form) {
40
- return Reflect.get(HTMLFormElement.prototype, "elements", form);
493
+ return Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, "elements")?.get?.call(form);
41
494
  }
42
495
  function getFormAttribute(form, name) {
43
- return Reflect.apply(Element.prototype.getAttribute, form, [name]);
496
+ return Element.prototype.getAttribute.call(form, name);
44
497
  }
45
498
  function formHasAttribute(form, name) {
46
- return Reflect.apply(Element.prototype.hasAttribute, form, [name]);
499
+ return Element.prototype.hasAttribute.call(form, name);
47
500
  }
48
- function isFormConnected(form) {
49
- return Reflect.get(Node.prototype, "isConnected", form);
501
+ function isConnected(node) {
502
+ return Object.getOwnPropertyDescriptor(Node.prototype, "isConnected")?.get?.call(node);
50
503
  }
51
504
  function getOpenShadowRoot(element) {
52
- return Reflect.get(Element.prototype, "shadowRoot", element);
505
+ return Object.getOwnPropertyDescriptor(Element.prototype, "shadowRoot")?.get?.call(element);
53
506
  }
54
507
  function checkFormValidity(form) {
55
- return Reflect.apply(HTMLFormElement.prototype.checkValidity, form, []);
508
+ return HTMLFormElement.prototype.checkValidity.call(form);
56
509
  }
57
510
  function requestFormSubmit(form, submitter) {
58
- Reflect.apply(HTMLFormElement.prototype.requestSubmit, form, submitter ? [submitter] : []);
511
+ HTMLFormElement.prototype.requestSubmit.call(form, submitter);
59
512
  }
60
513
  function getControls(form) {
61
514
  return [...getFormControls(form)].filter((element) => isControl(element) && !element.matches(":disabled") && !("readOnly" in element && element.readOnly && (element instanceof HTMLTextAreaElement || READONLY_INPUT_TYPES.has(element.type))));
@@ -103,41 +556,42 @@ function validNumberAttribute(input, name) {
103
556
  const value = Number(raw);
104
557
  return Number.isFinite(value) ? value : void 0;
105
558
  }
559
+ function validStep(input, fallback) {
560
+ const step = Number(input.getAttribute("step"));
561
+ return Number.isFinite(step) && step > 0 ? step : fallback;
562
+ }
106
563
  function isStepBaseMultiple(stepBase, step) {
107
564
  const quotient = stepBase / step;
108
565
  return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * 16;
109
566
  }
110
- function validPattern(input) {
567
+ function withPattern(schema, input) {
111
568
  const pattern = input.getAttribute("pattern");
112
- if (pattern === null) return void 0;
569
+ if (pattern === null) return schema;
113
570
  try {
114
571
  new RegExp(pattern, "v");
115
- return pattern;
116
572
  } catch {
117
- return;
573
+ return schema;
118
574
  }
575
+ return {
576
+ ...schema,
577
+ pattern
578
+ };
119
579
  }
120
- function numberSchema(input, includePattern = true) {
580
+ function numberSchema(input) {
121
581
  const schema = { type: "number" };
122
582
  const minimum = validNumberAttribute(input, "min");
123
583
  const maximum = validNumberAttribute(input, "max");
124
584
  if (minimum !== void 0) schema.minimum = minimum;
125
585
  if (maximum !== void 0) schema.maximum = maximum;
126
- const rawStep = input.getAttribute("step");
127
- if (rawStep !== "any") {
128
- const parsedStep = rawStep === null || rawStep === "" ? 1 : Number(rawStep);
129
- const step = Number.isFinite(parsedStep) && parsedStep > 0 ? parsedStep : 1;
586
+ if (input.getAttribute("step") !== "any") {
587
+ const step = validStep(input, 1);
130
588
  const rawValue = Number(input.getAttribute("value"));
131
589
  if (isStepBaseMultiple(minimum ?? (Number.isFinite(rawValue) ? rawValue : 0), step)) schema.multipleOf = step;
132
590
  }
133
- const pattern = includePattern ? validPattern(input) : void 0;
134
- if (pattern !== void 0) schema.pattern = pattern;
135
591
  return schema;
136
592
  }
137
593
  function temporalFormat(input, datePrefix) {
138
- const rawStep = input.getAttribute("step");
139
- const parsedStep = rawStep === null || rawStep === "" ? 60 : Number(rawStep);
140
- const step = Number.isFinite(parsedStep) && parsedStep > 0 ? parsedStep : 60;
594
+ const step = validStep(input, 60);
141
595
  if (step < 1) return `${datePrefix}(:[0-5][0-9](\\.[0-9]{1,3})?)?$`;
142
596
  if (step < 60) return `${datePrefix}(:[0-5][0-9])?$`;
143
597
  return `${datePrefix}$`;
@@ -156,38 +610,79 @@ function groupChoiceSchemas(controls) {
156
610
  return {
157
611
  anyOf: controls.map((control) => {
158
612
  const title = labelText(control);
159
- return {
613
+ const schema = {
160
614
  type: "string",
161
- const: control.value,
162
- ...title ? { title } : {}
615
+ const: control.value
163
616
  };
617
+ if (title) schema.title = title;
618
+ return schema;
164
619
  }),
165
620
  enum: controls.map((control) => control.value)
166
621
  };
167
622
  }
623
+ function isInput(control, type) {
624
+ return control instanceof HTMLInputElement && control.type === type;
625
+ }
626
+ function inputSchema(input) {
627
+ if (TEXT_INPUT_TYPES.has(input.type)) return withPattern({ type: "string" }, input);
628
+ switch (input.type) {
629
+ case "hidden": return input.getAttribute("toolparamdescription") ? { type: "string" } : void 0;
630
+ case "number": return withPattern(numberSchema(input), input);
631
+ case "range": {
632
+ const schema = numberSchema(input);
633
+ schema.minimum ??= 0;
634
+ schema.maximum ??= 100;
635
+ return schema;
636
+ }
637
+ case "checkbox": return { type: "boolean" };
638
+ case "date": return {
639
+ type: "string",
640
+ format: "date"
641
+ };
642
+ case "month": return {
643
+ type: "string",
644
+ format: "^[0-9]{4}-(0[1-9]|1[0-2])$"
645
+ };
646
+ case "week": return {
647
+ type: "string",
648
+ format: "^[0-9]{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$"
649
+ };
650
+ case "time": return {
651
+ type: "string",
652
+ format: temporalFormat(input, "^([01][0-9]|2[0-3]):[0-5][0-9]")
653
+ };
654
+ case "datetime-local": return {
655
+ type: "string",
656
+ format: temporalFormat(input, "^[0-9]{4}-(0[1-9]|1[0-2])-[0-9]{2}T([01][0-9]|2[0-3]):[0-5][0-9]")
657
+ };
658
+ case "color": return {
659
+ type: "string",
660
+ format: "^#[0-9a-zA-Z]{6}$"
661
+ };
662
+ default: return;
663
+ }
664
+ }
168
665
  function parameterSchema(form, controls) {
169
- const first = controls[0];
666
+ const [first] = controls;
170
667
  if (!first) return void 0;
171
- if (controls.length > 1) {
172
- if (!controls.every((control) => control instanceof HTMLInputElement)) return void 0;
173
- if (controls.every((control) => control.type === "checkbox")) return withDescription({
668
+ let schema;
669
+ if (controls.every((control) => isInput(control, "radio"))) schema = {
670
+ type: "string",
671
+ ...groupChoiceSchemas(controls)
672
+ };
673
+ else if (controls.length > 1) {
674
+ if (controls.every((control) => isInput(control, "checkbox"))) schema = {
174
675
  type: "array",
175
676
  items: {
176
677
  type: "string",
177
678
  ...groupChoiceSchemas(controls)
178
679
  },
179
680
  uniqueItems: true
180
- }, form, controls);
181
- if (controls.every((control) => control.type === "radio")) return withDescription({
182
- type: "string",
183
- ...groupChoiceSchemas(controls)
184
- }, form, controls);
185
- return;
186
- }
187
- if (first instanceof HTMLTextAreaElement) return withDescription({ type: "string" }, form, controls);
188
- if (first instanceof HTMLSelectElement) {
681
+ };
682
+ } else if (first instanceof HTMLTextAreaElement) schema = { type: "string" };
683
+ else if (first instanceof HTMLSelectElement) {
189
684
  const choices = optionSchemas([...first.options]);
190
- return withDescription(first.multiple ? {
685
+ schema = first.multiple ? {
191
686
  type: "array",
192
687
  items: {
193
688
  type: "string",
@@ -197,51 +692,10 @@ function parameterSchema(form, controls) {
197
692
  } : {
198
693
  type: "string",
199
694
  ...choices
200
- }, form, controls);
201
- }
202
- if (TEXT_INPUT_TYPES.has(first.type)) {
203
- const schema = { type: "string" };
204
- const pattern = validPattern(first);
205
- if (pattern !== void 0) schema.pattern = pattern;
206
- return withDescription(schema, form, controls);
207
- }
208
- if (first.type === "hidden") return first.getAttribute("toolparamdescription") ? withDescription({ type: "string" }, form, controls) : void 0;
209
- if (first.type === "number") return withDescription(numberSchema(first), form, controls);
210
- if (first.type === "range") {
211
- const schema = numberSchema(first, false);
212
- schema.minimum ??= 0;
213
- schema.maximum ??= 100;
214
- return withDescription(schema, form, controls);
215
- }
216
- if (first.type === "checkbox") return withDescription({ type: "boolean" }, form, controls);
217
- if (first.type === "radio") return withDescription({
218
- type: "string",
219
- ...groupChoiceSchemas([first])
220
- }, form, controls);
221
- if (first.type === "date") return withDescription({
222
- type: "string",
223
- format: "date"
224
- }, form, controls, "Dates MUST be provided in 'YYYY-MM-DD' format.");
225
- if (first.type === "month") return withDescription({
226
- type: "string",
227
- format: "^[0-9]{4}-(0[1-9]|1[0-2])$"
228
- }, form, controls);
229
- if (first.type === "week") return withDescription({
230
- type: "string",
231
- format: "^[0-9]{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$"
232
- }, form, controls);
233
- if (first.type === "time") return withDescription({
234
- type: "string",
235
- format: temporalFormat(first, "^([01][0-9]|2[0-3]):[0-5][0-9]")
236
- }, form, controls);
237
- if (first.type === "datetime-local") return withDescription({
238
- type: "string",
239
- format: temporalFormat(first, "^[0-9]{4}-(0[1-9]|1[0-2])-[0-9]{2}T([01][0-9]|2[0-3]):[0-5][0-9]")
240
- }, form, controls);
241
- if (first.type === "color") return withDescription({
242
- type: "string",
243
- format: "^#[0-9a-zA-Z]{6}$"
244
- }, form, controls);
695
+ };
696
+ } else schema = inputSchema(first);
697
+ const dateHint = first.type === "date" ? "Dates MUST be provided in 'YYYY-MM-DD' format." : void 0;
698
+ return schema && withDescription(schema, form, controls, dateHint);
245
699
  }
246
700
  function synthesizeSchema(form) {
247
701
  const properties = {};
@@ -300,31 +754,19 @@ function inputAcceptsValue(input, value) {
300
754
  probe.value = value;
301
755
  return probe.value !== "";
302
756
  }
303
- function validatesParameter(form, controls, value) {
304
- const first = controls[0];
757
+ function isFormParameterValue(form, controls, value) {
758
+ const [first] = controls;
305
759
  if (!first || !parameterSchema(form, controls)) return false;
306
- if (controls.length > 1) {
307
- if (!controls.every((control) => control instanceof HTMLInputElement)) return false;
308
- if (controls.every((control) => control.type === "checkbox")) return hasUniqueAllowedValues(value, new Set(controls.map((control) => control.value)));
309
- if (controls.every((control) => control.type === "radio")) {
310
- const string = toFormString(value);
311
- return string !== void 0 && controls.some((control) => control.value === string);
312
- }
313
- return false;
314
- }
760
+ const string = toFormString(value);
761
+ if (controls.every((control) => isInput(control, "radio"))) return controls.some((control) => control.value === string);
762
+ if (controls.length > 1) return hasUniqueAllowedValues(value, new Set(controls.map((control) => control.value)));
315
763
  if (first instanceof HTMLSelectElement) {
316
764
  const allowed = new Set([...first.options].map((option) => option.value));
317
765
  if (first.multiple) return hasUniqueAllowedValues(value, allowed);
318
- const string = toFormString(value);
319
766
  return string !== void 0 && allowed.has(string);
320
767
  }
321
- if (first instanceof HTMLTextAreaElement) return toFormString(value) !== void 0;
768
+ if (first instanceof HTMLTextAreaElement) return string !== void 0;
322
769
  if (first.type === "checkbox") return toFormBoolean(value) !== void 0;
323
- if (first.type === "radio") {
324
- const string = toFormString(value);
325
- return string !== void 0 && first.value === string;
326
- }
327
- const string = toFormString(value);
328
770
  return string !== void 0 && inputAcceptsValue(first, string);
329
771
  }
330
772
  function dispatchInputAndChange(control) {
@@ -334,111 +776,78 @@ function dispatchInputAndChange(control) {
334
776
  }));
335
777
  control.dispatchEvent(new Event("change", { bubbles: true }));
336
778
  }
337
- function setNativeValue(control, value) {
779
+ function setValue(control, value) {
780
+ if (control.value === value) return;
338
781
  const prototype = control instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
339
782
  Object.getOwnPropertyDescriptor(prototype, "value")?.set?.call(control, value);
783
+ dispatchInputAndChange(control);
340
784
  }
341
- function setNativeChecked(control, checked) {
785
+ function setChecked(control, checked) {
786
+ if (control.checked === checked) return;
342
787
  Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set?.call(control, checked);
788
+ dispatchInputAndChange(control);
343
789
  }
344
790
  function fillParameter(controls, value) {
345
- const first = controls[0];
346
- if (!first) return;
347
- if (controls.length > 1 && controls.every((control) => control instanceof HTMLInputElement)) {
348
- if (controls.every((control) => control.type === "checkbox") && Array.isArray(value)) {
349
- const checked = new Set(value.map(toFormString));
350
- for (const control of controls) {
351
- const next = checked.has(control.value);
352
- if (control.checked === next) continue;
353
- setNativeChecked(control, next);
354
- dispatchInputAndChange(control);
355
- }
356
- return;
357
- }
358
- const selected = toFormString(value);
359
- const control = controls.find((candidate) => candidate.value === selected);
360
- if (control && !control.checked) {
361
- setNativeChecked(control, true);
362
- dispatchInputAndChange(control);
363
- }
364
- return;
365
- }
366
- if (first instanceof HTMLSelectElement) {
367
- if (first.multiple && Array.isArray(value)) {
368
- const selected = new Set(value.map(toFormString));
791
+ const [first] = controls;
792
+ const string = toFormString(value);
793
+ const values = new Set(Array.isArray(value) ? value.map(toFormString) : []);
794
+ if (controls.every((control) => isInput(control, "radio"))) {
795
+ const control = controls.find((candidate) => candidate.value === string);
796
+ if (control) setChecked(control, true);
797
+ } else if (controls.length > 1) {
798
+ for (const control of controls) if (control instanceof HTMLInputElement) setChecked(control, values.has(control.value));
799
+ } else if (first instanceof HTMLSelectElement) {
800
+ if (first.multiple) {
369
801
  let changed = false;
370
802
  for (const option of first.options) {
371
- const next = selected.has(option.value);
803
+ const next = values.has(option.value);
372
804
  if (option.selected === next) continue;
373
805
  option.selected = next;
374
806
  changed = true;
375
807
  }
376
808
  if (changed) dispatchInputAndChange(first);
377
- return;
378
- }
379
- const next = toFormString(value);
380
- if (next !== void 0 && first.value !== next) {
381
- first.value = next;
809
+ } else if (string !== void 0 && first.value !== string) {
810
+ first.value = string;
382
811
  dispatchInputAndChange(first);
383
812
  }
384
- return;
385
- }
386
- if (first instanceof HTMLInputElement && first.type === "checkbox") {
387
- const next = toFormBoolean(value);
388
- if (next !== void 0 && first.checked !== next) {
389
- setNativeChecked(first, next);
390
- dispatchInputAndChange(first);
391
- }
392
- return;
393
- }
394
- if (first instanceof HTMLInputElement && first.type === "radio") {
395
- if (toFormString(value) === first.value && !first.checked) {
396
- setNativeChecked(first, true);
397
- dispatchInputAndChange(first);
398
- }
399
- return;
400
- }
401
- const next = toFormString(value);
402
- if (next !== void 0 && first.value !== next) {
403
- setNativeValue(first, next);
404
- dispatchInputAndChange(first);
405
- }
813
+ } else if (first && isInput(first, "checkbox")) {
814
+ const checked = toFormBoolean(value);
815
+ if (checked !== void 0) setChecked(first, checked);
816
+ } else if (first && string !== void 0) setValue(first, string);
406
817
  }
407
818
  function fillForm(form, input) {
408
- if (Array.isArray(input)) throw new TypeError("Declarative tool input must be an object");
819
+ if (Array.isArray(input)) throw executionError();
409
820
  const groups = controlGroups(form);
821
+ const parameters = [];
410
822
  for (const [name, value] of Object.entries(input)) {
411
823
  const controls = groups.get(name);
412
- if (!controls || !validatesParameter(form, controls, value)) throw new TypeError(`Invalid value for declarative form parameter "${name}"`);
824
+ if (!controls || !isFormParameterValue(form, controls, value)) throw executionError();
825
+ parameters.push({
826
+ controls,
827
+ value
828
+ });
413
829
  }
414
- for (const [name, value] of Object.entries(input)) fillParameter(groups.get(name) ?? [], value);
830
+ for (const { controls, value } of parameters) fillParameter(controls, value);
415
831
  }
416
832
  function findSubmitter(form) {
417
833
  return [...getFormControls(form)].find((element) => !element.matches(":disabled") && (element instanceof HTMLButtonElement && element.type === "submit" || element instanceof HTMLInputElement && ["image", "submit"].includes(element.type)));
418
834
  }
419
- function validationError(form) {
420
- const failures = [...getFormControls(form)].filter((element) => isControl(element) && element.willValidate && !element.validity.valid).map((control) => `${control.name.trim() || "{unknown}"}: ${control.validationMessage}`).join(". ");
421
- return new DOMException(`Form validation failed: ${failures}`, "UnknownError");
422
- }
423
- function toolActivatedEvent(toolName) {
424
- const event = new Event("toolactivated");
835
+ function lifecycleEvent(type, toolName) {
836
+ const event = new Event(type);
425
837
  Object.defineProperty(event, "toolName", {
426
838
  enumerable: true,
427
839
  value: toolName
428
840
  });
429
841
  return event;
430
842
  }
431
- function waitForSubmission(registration, toolName, autosubmit, submitter) {
843
+ function waitForSubmission(context, registration, definition, submitter, signal) {
432
844
  const { form } = registration;
433
- registration.cancelPending?.(new DOMException("Tool execution cancelled", "UnknownError"));
845
+ registration.cancelPending?.(executionError());
434
846
  return new Promise((resolve, reject) => {
435
847
  let settled = false;
436
848
  const cleanup = () => {
437
- Reflect.apply(EventTarget.prototype.removeEventListener, form, [
438
- "invalid",
439
- onInvalid,
440
- true
441
- ]);
849
+ EventTarget.prototype.removeEventListener.call(form, "invalid", onInvalid, true);
850
+ signal.removeEventListener("abort", onAbort);
442
851
  activeSubmissions.delete(form);
443
852
  if (registration.cancelPending === cancel) delete registration.cancelPending;
444
853
  };
@@ -450,25 +859,32 @@ function waitForSubmission(registration, toolName, autosubmit, submitter) {
450
859
  };
451
860
  const cancel = (reason) => finish(() => reject(reason));
452
861
  const settleResponse = (response) => {
453
- response.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
862
+ if (registration.cancelPending === cancel) delete registration.cancelPending;
863
+ response.then((value) => finish(() => resolve(value ?? null)), (cause) => finish(() => reject(cause)));
454
864
  };
455
865
  const onInvalid = (event) => {
456
866
  if (!event.isTrusted) return;
457
867
  queueMicrotask(() => {
458
- if (!checkFormValidity(form)) cancel(validationError(form));
868
+ if (!checkFormValidity(form)) cancel(executionError());
869
+ });
870
+ };
871
+ const onAbort = () => {
872
+ finish(() => {
873
+ context.dispatchEvent(lifecycleEvent("toolcancel", definition.name));
874
+ reject(signal.reason);
459
875
  });
460
876
  };
461
877
  activeSubmissions.set(form, {
462
878
  complete(event) {
463
- queueMicrotask(() => {
879
+ setTimeout(() => {
464
880
  const response = agentResponses.get(event);
465
881
  if (response) settleResponse(response);
466
- else if (event.defaultPrevented) cancel(new DOMException("preventDefault() requires respondWith()", "UnknownError"));
467
- else finish(() => resolve(void 0));
882
+ else if (event.defaultPrevented) cancel(executionError());
883
+ else finish(() => resolve(null));
468
884
  });
469
885
  },
470
886
  direct() {
471
- finish(() => resolve(void 0));
887
+ finish(() => resolve(null));
472
888
  },
473
889
  respond(event) {
474
890
  queueMicrotask(() => {
@@ -478,27 +894,40 @@ function waitForSubmission(registration, toolName, autosubmit, submitter) {
478
894
  }
479
895
  });
480
896
  registration.cancelPending = cancel;
481
- Reflect.apply(EventTarget.prototype.addEventListener, form, [
482
- "invalid",
483
- onInvalid,
484
- true
485
- ]);
486
- if (!autosubmit) {
897
+ EventTarget.prototype.addEventListener.call(form, "invalid", onInvalid, true);
898
+ signal.addEventListener("abort", onAbort, { once: true });
899
+ if (!definition.autosubmit) {
487
900
  submitter?.focus();
488
- window.dispatchEvent(toolActivatedEvent(toolName));
901
+ context.dispatchEvent(lifecycleEvent("toolactivated", definition.name));
489
902
  return;
490
903
  }
491
904
  try {
492
905
  requestFormSubmit(form, submitter);
493
- window.dispatchEvent(toolActivatedEvent(toolName));
906
+ context.dispatchEvent(lifecycleEvent("toolactivated", definition.name));
494
907
  } catch (error) {
495
908
  cancel(error);
496
909
  }
497
910
  });
498
911
  }
499
- /** Installs the DOM-backed half of the draft Declarative WebMCP API. */
500
- function installDeclarativeForms(document, context) {
501
- let active = true;
912
+ /** Adds declarative tools until the vendored upstream implements them. */
913
+ function installWebMCPDeclarativeExtensions(context) {
914
+ const prototype = SubmitEvent.prototype;
915
+ if ("agentInvoked" in prototype && "respondWith" in prototype) return;
916
+ if (!("agentInvoked" in prototype)) Object.defineProperty(prototype, "agentInvoked", {
917
+ configurable: true,
918
+ enumerable: true,
919
+ get() {
920
+ return isAgentInvokedSubmitEvent(this);
921
+ }
922
+ });
923
+ if (!("respondWith" in prototype)) Object.defineProperty(prototype, "respondWith", {
924
+ configurable: true,
925
+ enumerable: true,
926
+ writable: true,
927
+ value(agentResponse) {
928
+ respondWithAgentSubmitEvent(this, agentResponse);
929
+ }
930
+ });
502
931
  const registrations = /* @__PURE__ */ new Map();
503
932
  const blockedDefinitions = /* @__PURE__ */ new Map();
504
933
  const observers = /* @__PURE__ */ new Map();
@@ -512,9 +941,9 @@ function installDeclarativeForms(document, context) {
512
941
  const onReset = (event) => {
513
942
  if (!event.isTrusted || !(event.target instanceof HTMLFormElement)) return;
514
943
  const form = event.target;
515
- queueMicrotask(() => {
944
+ setTimeout(() => {
516
945
  if (event.defaultPrevented) return;
517
- registrations.get(form)?.cancelPending?.(new DOMException("Tool execution cancelled by form reset", "UnknownError"));
946
+ registrations.get(form)?.cancelPending?.(executionError());
518
947
  });
519
948
  };
520
949
  function stopObservingRoot(root) {
@@ -538,33 +967,30 @@ function installDeclarativeForms(document, context) {
538
967
  }
539
968
  function sync() {
540
969
  observeRoot(document);
541
- for (const root of observers.keys()) if (root instanceof ShadowRoot && !Reflect.get(Node.prototype, "isConnected", root.host)) stopObservingRoot(root);
970
+ for (const root of observers.keys()) if (root instanceof ShadowRoot && !root.host.isConnected) stopObservingRoot(root);
542
971
  const candidates = /* @__PURE__ */ new Set();
543
972
  const selected = /* @__PURE__ */ new Map();
544
973
  const selectedByName = /* @__PURE__ */ new Map();
545
974
  for (const root of observers.keys()) for (const element of root.querySelectorAll("*")) {
546
975
  const shadowRoot = getOpenShadowRoot(element);
547
976
  if (shadowRoot) observeRoot(shadowRoot);
548
- if (!(element instanceof HTMLFormElement) || !formHasAttribute(element, "toolname") || !formHasAttribute(element, "tooldescription") || !isFormConnected(element)) continue;
977
+ if (!(element instanceof HTMLFormElement) || !formHasAttribute(element, "toolname") || !formHasAttribute(element, "tooldescription") || !isConnected(element)) continue;
549
978
  const form = element;
550
979
  candidates.add(form);
551
980
  const definition = toolDefinition(form);
552
981
  const fingerprint = JSON.stringify(definition);
553
982
  const blockedFingerprint = blockedDefinitions.get(form);
554
983
  if (blockedFingerprint === fingerprint) continue;
555
- const retryingChangedDefinition = blockedFingerprint !== void 0;
556
984
  blockedDefinitions.delete(form);
557
985
  const existingForm = selectedByName.get(definition.name);
558
- if (existingForm && !retryingChangedDefinition) {
559
- blockedDefinitions.set(form, fingerprint);
560
- continue;
561
- }
562
986
  if (existingForm) {
563
- const existingSelection = selected.get(existingForm);
564
- if (existingSelection) {
565
- blockedDefinitions.set(existingForm, existingSelection.fingerprint);
566
- selected.delete(existingForm);
987
+ if (blockedFingerprint === void 0) {
988
+ blockedDefinitions.set(form, fingerprint);
989
+ continue;
567
990
  }
991
+ const existingSelection = selected.get(existingForm);
992
+ if (existingSelection) blockedDefinitions.set(existingForm, existingSelection.fingerprint);
993
+ selected.delete(existingForm);
568
994
  }
569
995
  selectedByName.set(definition.name, form);
570
996
  selected.set(form, {
@@ -574,7 +1000,7 @@ function installDeclarativeForms(document, context) {
574
1000
  }
575
1001
  for (const form of blockedDefinitions.keys()) if (!candidates.has(form)) blockedDefinitions.delete(form);
576
1002
  for (const [form, registration] of registrations) if (selected.get(form)?.fingerprint !== registration.fingerprint) {
577
- registration.cancelPending?.(new DOMException("Tool execution cancelled because its form changed", "UnknownError"));
1003
+ registration.cancelPending?.(executionError());
578
1004
  registration.controller.abort();
579
1005
  registrations.delete(form);
580
1006
  }
@@ -592,427 +1018,455 @@ function installDeclarativeForms(document, context) {
592
1018
  title: definition.title,
593
1019
  description: definition.description,
594
1020
  inputSchema: definition.inputSchema,
595
- execute(input) {
1021
+ execute(input, options) {
596
1022
  const submitter = findSubmitter(form);
597
- if (!definition.autosubmit && !submitter) throw new DOMException("A declarative form without toolautosubmit requires a submit button", "UnknownError");
1023
+ if (!definition.autosubmit && !submitter) throw executionError();
598
1024
  fillForm(form, input);
599
- return waitForSubmission(registration, definition.name, definition.autosubmit, submitter);
1025
+ return waitForSubmission(context, registration, definition, submitter, options.signal);
600
1026
  }
601
- }, { signal: controller.signal }).catch((error) => {
1027
+ }, { signal: controller.signal }).catch((cause) => {
602
1028
  controller.abort();
603
- if (error?.name === "AbortError") return;
604
- console.error(`[webmcp] declarative form tool "${definition.name}" was not registered:`, error);
1029
+ if (cause instanceof DOMException && cause.name === "AbortError") return;
1030
+ console.error(`[webmcp] declarative form tool "${definition.name}" was not registered:`, cause);
605
1031
  });
606
1032
  }
607
1033
  }
608
- let restoreAttachShadow = () => {};
609
- const elementPrototype = document.defaultView?.Element.prototype;
610
- const attachShadowDescriptor = elementPrototype ? Object.getOwnPropertyDescriptor(elementPrototype, "attachShadow") : void 0;
611
- if (elementPrototype && attachShadowDescriptor?.configurable) {
612
- const nativeAttachShadow = elementPrototype.attachShadow;
1034
+ const attachShadowDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, "attachShadow");
1035
+ if (attachShadowDescriptor?.configurable) {
1036
+ const nativeAttachShadow = Element.prototype.attachShadow;
613
1037
  const attachShadow = function(init) {
614
1038
  const root = nativeAttachShadow.call(this, init);
615
- if (active && root.mode === "open" && Reflect.get(Node.prototype, "ownerDocument", this) === document && Reflect.get(Node.prototype, "isConnected", this)) observeRoot(root);
1039
+ if (root.mode === "open" && Object.getOwnPropertyDescriptor(Node.prototype, "ownerDocument")?.get?.call(this) === document && isConnected(this)) observeRoot(root);
616
1040
  return root;
617
1041
  };
618
- Object.defineProperty(elementPrototype, "attachShadow", {
1042
+ Object.defineProperty(Element.prototype, "attachShadow", {
619
1043
  ...attachShadowDescriptor,
620
1044
  value: attachShadow
621
1045
  });
622
- restoreAttachShadow = () => {
623
- if (elementPrototype.attachShadow === attachShadow) Object.defineProperty(elementPrototype, "attachShadow", attachShadowDescriptor);
624
- };
625
1046
  }
626
- let restoreFormSubmit = () => {};
627
- const formPrototype = document.defaultView?.HTMLFormElement.prototype;
628
- const submitDescriptor = formPrototype ? Object.getOwnPropertyDescriptor(formPrototype, "submit") : void 0;
629
- if (formPrototype && submitDescriptor?.configurable) {
630
- const nativeSubmit = formPrototype.submit;
1047
+ const submitDescriptor = Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, "submit");
1048
+ if (submitDescriptor?.configurable) {
1049
+ const nativeSubmit = HTMLFormElement.prototype.submit;
631
1050
  const submit = function() {
632
1051
  nativeSubmit.call(this);
633
- if (active) activeSubmissions.get(this)?.direct();
1052
+ activeSubmissions.get(this)?.direct();
634
1053
  };
635
- Object.defineProperty(formPrototype, "submit", {
1054
+ Object.defineProperty(HTMLFormElement.prototype, "submit", {
636
1055
  ...submitDescriptor,
637
1056
  value: submit
638
1057
  });
639
- restoreFormSubmit = () => {
640
- if (formPrototype.submit === submit) Object.defineProperty(formPrototype, "submit", submitDescriptor);
641
- };
642
1058
  }
643
1059
  sync();
644
- return () => {
645
- active = false;
646
- restoreFormSubmit();
647
- restoreAttachShadow();
648
- for (const root of observers.keys()) stopObservingRoot(root);
649
- blockedDefinitions.clear();
650
- for (const registration of registrations.values()) {
651
- registration.cancelPending?.(new DOMException("Tool execution cancelled", "UnknownError"));
652
- registration.controller.abort();
653
- }
654
- registrations.clear();
655
- };
656
1060
  }
657
1061
 
658
1062
  //#endregion
659
- //#region src/index.ts
660
- const POLYFILL_MARKER_PROPERTY = "__isWebMCPPolyfill";
661
- const REGISTERED_INPUT_SCHEMA_SYMBOL = Symbol("registeredInputSchema");
662
- const REGISTRATION_SIGNAL_SYMBOL = Symbol("registrationSignal");
663
- const REGISTRATION_ABORT_SYMBOL = Symbol("registrationAbort");
664
- const installedProperties = [];
665
- let installedContext = null;
666
- let cleanupDeclarativeForms = null;
667
- function currentOrigin() {
668
- return typeof globalThis.origin === "string" ? globalThis.origin : globalThis.location?.origin ?? "";
669
- }
670
- function installProperty(target, key, descriptor) {
671
- const previous = Object.getOwnPropertyDescriptor(target, key);
672
- try {
673
- Object.defineProperty(target, key, descriptor);
674
- } catch (error) {
675
- cleanupWebMCPPolyfill();
676
- throw error;
677
- }
678
- installedProperties.push({
679
- target,
680
- key,
681
- previous
1063
+ //#region src/upstream/index.ts
1064
+ /*!
1065
+ * Copyright (c) 2026 WebMCP polyfill contributors
1066
+ * SPDX-License-Identifier: MIT
1067
+ */
1068
+ const NativeDOMException = globalThis.DOMException;
1069
+ const getWindow = Object.getOwnPropertyDescriptor(globalThis, "window")?.get;
1070
+ const contexts = /* @__PURE__ */ new WeakMap();
1071
+ /**
1072
+ * Install WebMCP in the current window.
1073
+ *
1074
+ * Does nothing outside a secure browser context or when `document.modelContext`
1075
+ * already exists, including partial native implementations. Call before registering
1076
+ * tools in each frame; repeated calls preserve existing contexts and registrations.
1077
+ *
1078
+ * Installing joins cross-frame discovery: the window listens for the polyfill's messages and
1079
+ * announces itself to the other frames of its tree.
1080
+ *
1081
+ * @throws {TypeError} If the window or Document prototype prevents installation.
1082
+ * @example
1083
+ * import { installWebMCP } from "webmcp-polyfill";
1084
+ * installWebMCP();
1085
+ *
1086
+ * @see https://webmachinelearning.github.io/webmcp/#document-extension
1087
+ * @see https://github.com/webmachinelearning/webmcp-polyfill/blob/main/TESTING.md
1088
+ */
1089
+ function installWebMCP$1() {
1090
+ if (typeof document === "undefined" || !globalThis.isSecureContext) return;
1091
+ if ("modelContext" in document) return;
1092
+ const documentPrototype = Document.prototype;
1093
+ const getDefaultView = Object.getOwnPropertyDescriptor(documentPrototype, "defaultView").get;
1094
+ const constructorDescriptor = Object.getOwnPropertyDescriptor(globalThis, "ModelContext");
1095
+ if (!Object.isExtensible(documentPrototype) || constructorDescriptor && !constructorDescriptor.configurable || !constructorDescriptor && !Object.isExtensible(globalThis)) throw new TypeError("Cannot install WebMCP on this realm");
1096
+ Object.defineProperty(globalThis, "ModelContext", {
1097
+ value: modelContextConstructor,
1098
+ configurable: true,
1099
+ writable: true
682
1100
  });
1101
+ const { getModelContext } = { getModelContext() {
1102
+ getDefaultView.call(this);
1103
+ let context = contexts.get(this);
1104
+ if (!context) {
1105
+ context = new ModelContextPolyfill(this);
1106
+ contexts.set(this, context);
1107
+ }
1108
+ return context;
1109
+ } };
1110
+ Object.defineProperty(getModelContext, "name", { value: "get modelContext" });
1111
+ Object.defineProperty(documentPrototype, "modelContext", {
1112
+ configurable: true,
1113
+ enumerable: true,
1114
+ get: getModelContext
1115
+ });
1116
+ document.modelContext;
683
1117
  }
684
- var StrictWebMCPContext = class extends EventTarget {
1118
+ var ModelContextPolyfill = class extends EventTarget {
1119
+ #document;
685
1120
  #tools = /* @__PURE__ */ new Map();
686
- #testingShim = null;
687
- #ontoolchangeHandler = null;
688
- #ontoolchangeListener = (event) => {
689
- this.#ontoolchangeHandler?.call(this, event);
1121
+ #frames;
1122
+ #toolchangeHandler = null;
1123
+ #toolchangeListener = (event) => {
1124
+ const handler = this.#toolchangeHandler;
1125
+ if (typeof handler !== "function") return;
1126
+ if (Reflect.apply(handler, this, [event]) === false) Event.prototype.preventDefault.call(event);
690
1127
  };
691
- #domException;
692
- #ownerDocument;
693
- constructor(ownerDocument) {
1128
+ constructor(owner) {
694
1129
  super();
695
- this.#ownerDocument = ownerDocument;
696
- this.#domException = ownerDocument?.defaultView?.DOMException ?? DOMException;
697
- Object.defineProperty(this, POLYFILL_MARKER_PROPERTY, {
698
- value: true,
699
- enumerable: false,
700
- writable: false,
701
- configurable: false
1130
+ this.#document = owner;
1131
+ const view = activeWindow(owner);
1132
+ if (!view) return;
1133
+ this.#frames = new FrameBridge(view, {
1134
+ getTools: (callerOrigin) => this.#exposedTools(callerOrigin),
1135
+ execute: (callerOrigin, name, serializedInput, signal) => {
1136
+ const ownerOrigin = requireActiveWindow(owner).origin;
1137
+ return this.#executeLocal(name, serializedInput, ownerOrigin, callerOrigin, signal);
1138
+ },
1139
+ changed: () => this.#queueToolChange()
702
1140
  });
703
1141
  }
704
1142
  get ontoolchange() {
705
- return this.#ontoolchangeHandler;
1143
+ return this.#toolchangeHandler;
706
1144
  }
707
1145
  set ontoolchange(handler) {
708
- const listener = typeof handler === "function" ? handler : null;
709
- if (listener === null) {
710
- this.#ontoolchangeHandler = null;
711
- super.removeEventListener("toolchange", this.#ontoolchangeListener);
712
- return;
713
- }
714
- if (this.#ontoolchangeHandler === null) super.addEventListener("toolchange", this.#ontoolchangeListener);
715
- this.#ontoolchangeHandler = listener;
1146
+ const nextHandler = isObject(handler) ? handler : null;
1147
+ if (!this.#toolchangeHandler && nextHandler) this.addEventListener("toolchange", this.#toolchangeListener);
1148
+ if (this.#toolchangeHandler && !nextHandler) this.removeEventListener("toolchange", this.#toolchangeListener);
1149
+ this.#toolchangeHandler = nextHandler;
716
1150
  }
717
1151
  async registerTool(tool, options = {}) {
718
- validateWebMcpAccess(this.#ownerDocument);
719
- const signal = options?.signal;
720
- const normalized = normalizeToolDescriptor(tool, this.#tools);
721
- signal?.throwIfAborted();
722
- validatePotentiallyTrustworthyOrigins(options?.exposedTo);
723
- signal?.throwIfAborted();
724
- if (options?.exposedTo?.length) throw new this.#domException("Cross-document tool exposure requires native WebMCP", "NotSupportedError");
725
- this.#tools.set(normalized.name, normalized);
726
- if (signal) {
727
- const abort = () => {
728
- if (this.#removeTool(normalized.name, normalized)) this.#notifyToolsChanged();
729
- };
730
- normalized[REGISTRATION_SIGNAL_SYMBOL] = signal;
731
- normalized[REGISTRATION_ABORT_SYMBOL] = abort;
732
- signal.addEventListener("abort", abort, { once: true });
733
- }
734
- await this.#notifyToolsChanged();
735
- if (signal?.aborted) throw signal.reason;
1152
+ const ownerDocument = this.#document;
1153
+ const { name, title, description, annotations, inputSchema, execute } = readToolDefinition(tool);
1154
+ const settings = readDictionary(options);
1155
+ const exposedTo = readOriginSequence(settings.exposedTo);
1156
+ const registrationSignal = readAbortSignal(settings.signal);
1157
+ requireActiveWindow(ownerDocument);
1158
+ const requireUnusedName = () => {
1159
+ if (this.#tools.has(name)) throw new NativeDOMException(`A tool named ${name} is already registered`, "InvalidStateError");
1160
+ };
1161
+ requireUnusedName();
1162
+ if (!/^[A-Za-z0-9_.-]{1,128}$/u.test(name)) throw new NativeDOMException(`Tool names are 1 to 128 characters of ASCII alphanumerics, "_", "-" or ".": ${name}`, "InvalidStateError");
1163
+ if (!description) throw new NativeDOMException("A tool description cannot be empty", "InvalidStateError");
1164
+ const serializedSchema = inputSchema === void 0 ? void 0 : serializeJSON(inputSchema);
1165
+ registrationSignal?.throwIfAborted();
1166
+ const exposedOrigins = parseOrigins(exposedTo);
1167
+ const storedTool = {
1168
+ metadata: {
1169
+ name,
1170
+ title,
1171
+ description,
1172
+ annotations,
1173
+ serializedSchema
1174
+ },
1175
+ execute,
1176
+ exposedTo: exposedOrigins
1177
+ };
1178
+ const frames = await this.#requireFrames();
1179
+ registrationSignal?.throwIfAborted();
1180
+ requireUnusedName();
1181
+ return new Promise((resolve, reject) => {
1182
+ registrationSignal?.addEventListener("abort", () => {
1183
+ this.#tools.delete(name);
1184
+ frames.notify(exposedOrigins);
1185
+ reject(registrationSignal.reason);
1186
+ }, { once: true });
1187
+ this.#tools.set(name, storedTool);
1188
+ frames.notify(exposedOrigins).then(resolve, reject);
1189
+ });
736
1190
  }
737
1191
  async getTools(options = {}) {
738
- validateWebMcpAccess(this.#ownerDocument);
739
- validatePotentiallyTrustworthyOrigins(options?.fromOrigins);
740
- if (options?.fromOrigins?.length) throw new this.#domException("Cross-document tool discovery requires native WebMCP", "NotSupportedError");
741
- const tools = [...this.#tools.values()].map((tool) => {
742
- const serialized = tool[REGISTERED_INPUT_SCHEMA_SYMBOL];
743
- const parsed = serialized === void 0 ? void 0 : JSON.parse(serialized);
744
- return {
745
- name: tool.name,
746
- title: tool.title ?? "",
747
- description: tool.description,
748
- ...isPlainObject(parsed) ? { inputSchema: parsed } : {},
749
- origin: currentOrigin(),
750
- window: globalThis.window,
751
- ...tool.annotations ? { annotations: toWebMcpAnnotations(tool.annotations) } : {}
1192
+ const ownerDocument = this.#document;
1193
+ const fromOrigins = readOriginSequence(readDictionary(options).fromOrigins);
1194
+ requireActiveWindow(ownerDocument);
1195
+ const frames = await this.#requireFrames();
1196
+ const requestedOrigins = parseOrigins(fromOrigins);
1197
+ const tools = await frames.getTools(requestedOrigins);
1198
+ tools.sort((left, right) => {
1199
+ if (left.name === right.name) return 0;
1200
+ return left.name < right.name ? -1 : 1;
1201
+ });
1202
+ return new Promise((resolve) => {
1203
+ queueTask(() => resolve(tools));
1204
+ });
1205
+ }
1206
+ async executeTool(tool, inputObject = {}, options = {}) {
1207
+ const ownerDocument = this.#document;
1208
+ const target = readExecutionTarget(tool);
1209
+ const callerSignal = readAbortSignal(readDictionary(options).signal);
1210
+ requireActiveWindow(ownerDocument);
1211
+ const expectedOrigin = URL.parse(target.origin)?.origin;
1212
+ if (!expectedOrigin || expectedOrigin === "null") throw new NativeDOMException("Invalid or opaque origin", "NotSupportedError");
1213
+ if (!isObject(inputObject)) throw new TypeError("inputObject must be an object");
1214
+ const serializedInput = serializeJSON(inputObject);
1215
+ callerSignal?.throwIfAborted();
1216
+ const frames = await this.#requireFrames();
1217
+ callerSignal?.throwIfAborted();
1218
+ const callerWindow = requireActiveWindow(ownerDocument);
1219
+ if (target.window !== callerWindow) return frames.execute(target.window, expectedOrigin, target.name, serializedInput, callerSignal);
1220
+ return this.#executeLocal(target.name, serializedInput, expectedOrigin, callerWindow.origin, callerSignal);
1221
+ }
1222
+ #executeLocal(name, serializedInput, expectedOrigin, callerOrigin, callerSignal) {
1223
+ return new Promise((resolve, reject) => {
1224
+ callerSignal?.throwIfAborted();
1225
+ const callbackController = new AbortController();
1226
+ let callbackFinished = false;
1227
+ const onCallerAbort = () => {
1228
+ reject(callerSignal.reason);
1229
+ queueTask(() => {
1230
+ if (!callbackFinished) callbackController.abort();
1231
+ });
752
1232
  };
753
- }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
754
- await new Promise((resolve) => setTimeout(resolve, 0));
755
- return tools;
756
- }
757
- async executeTool(tool, inputArgsJson, options = {}) {
758
- validateWebMcpAccess(this.#ownerDocument);
759
- if (tool === null || typeof tool !== "object") throw new TypeError("RegisteredTool must be an object");
760
- for (const required of [
761
- "name",
762
- "description",
763
- "window",
764
- "origin"
765
- ]) if (!(required in tool)) throw new TypeError(`RegisteredTool.${required} is required`);
766
- validateExecutableOrigin(tool.origin);
767
- if (tool.window !== globalThis.window || tool.origin !== currentOrigin()) throw createUnknownError(`Tool not found: ${tool.name}`);
768
- return this.#invokeToolByName(tool.name, inputArgsJson, options);
769
- }
770
- /** @internal Used by initializeWebMCPPolyfill */
771
- static getTestingShim(context) {
772
- context.#testingShim ??= new PolyfillTestingShim(context);
773
- return context.#testingShim;
774
- }
775
- /** @internal Used by PolyfillTestingShim */
776
- static getToolInfos(context) {
777
- return [...context.#tools.values()].map((tool) => ({
778
- name: tool.name,
779
- description: tool.description,
780
- ...tool[REGISTERED_INPUT_SCHEMA_SYMBOL] === void 0 ? {} : { inputSchema: tool[REGISTERED_INPUT_SCHEMA_SYMBOL] }
781
- }));
782
- }
783
- /** @internal Used by PolyfillTestingShim */
784
- static executeToolForTesting(context, toolName, inputArgsJson, options) {
785
- validateWebMcpAccess(context.#ownerDocument);
786
- return context.#invokeToolByName(toolName, inputArgsJson, options);
787
- }
788
- /** @internal Used by cleanupWebMCPPolyfill */
789
- static dispose(context) {
790
- for (const name of context.#tools.keys()) context.#removeTool(name);
791
- context.ontoolchange = null;
792
- context.#testingShim?.dispose();
793
- }
794
- #removeTool(name, expected) {
795
- const registered = this.#tools.get(name);
796
- if (!registered || expected && registered !== expected) return false;
797
- const signal = registered[REGISTRATION_SIGNAL_SYMBOL];
798
- const abort = registered[REGISTRATION_ABORT_SYMBOL];
799
- if (signal && abort) signal.removeEventListener("abort", abort);
800
- return this.#tools.delete(name);
801
- }
802
- async #invokeToolByName(toolName, inputArgsJson, options) {
803
- options?.signal?.throwIfAborted();
804
- const tool = this.#tools.get(toolName);
805
- if (!tool) throw createUnknownError(`Tool not found: ${toolName}`);
806
- const args = parseChromeToolInput(inputArgsJson);
807
- options?.signal?.throwIfAborted();
808
- if (tool[REGISTRATION_SIGNAL_SYMBOL]?.aborted) throw createUnknownError("Tool unregistered");
809
- let rawResult;
810
- try {
811
- const registrationSignal = tool[REGISTRATION_SIGNAL_SYMBOL];
812
- rawResult = await withAbortSignal(withAbortSignal(Promise.resolve(tool.execute(args)), registrationSignal, () => createUnknownError("Tool unregistered")), options?.signal);
813
- } catch (error) {
814
- if (options?.signal?.aborted && error === options.signal.reason) throw error;
815
- if (tool[REGISTRATION_SIGNAL_SYMBOL]?.aborted) throw error;
816
- throw createToolInvocationFailedError(error);
1233
+ const rejectExecution = () => {
1234
+ callbackFinished = true;
1235
+ queueTask(() => {
1236
+ callerSignal?.removeEventListener("abort", onCallerAbort);
1237
+ reject(executionError());
1238
+ });
1239
+ };
1240
+ const completeExecution = (value) => {
1241
+ callbackFinished = true;
1242
+ if (callerSignal?.aborted) return;
1243
+ try {
1244
+ const serializedResult = serializeJSON(value);
1245
+ queueTask(() => {
1246
+ callerSignal?.removeEventListener("abort", onCallerAbort);
1247
+ resolve(serializedResult);
1248
+ });
1249
+ } catch {
1250
+ rejectExecution();
1251
+ }
1252
+ };
1253
+ const dispatchTool = () => {
1254
+ if (callerSignal?.aborted) return;
1255
+ try {
1256
+ const ownerWindow = requireActiveWindow(this.#document);
1257
+ const storedTool = this.#tools.get(name);
1258
+ if (!storedTool || expectedOrigin !== ownerWindow.origin || !isExposedTo(storedTool, ownerWindow.origin, callerOrigin)) {
1259
+ rejectExecution();
1260
+ return;
1261
+ }
1262
+ const input = JSON.parse(serializedInput);
1263
+ if (!isObject(input)) {
1264
+ rejectExecution();
1265
+ return;
1266
+ }
1267
+ const execute = storedTool.execute;
1268
+ const callbackResult = execute(input, { signal: callbackController.signal });
1269
+ Promise.resolve(callbackResult).then(completeExecution, rejectExecution);
1270
+ } catch {
1271
+ rejectExecution();
1272
+ }
1273
+ };
1274
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
1275
+ queueTask(dispatchTool);
1276
+ });
1277
+ }
1278
+ async #requireFrames() {
1279
+ const frames = this.#frames;
1280
+ if (!frames || !await frames.allowed()) throw new NativeDOMException("WebMCP is disabled by Permissions Policy", "NotAllowedError");
1281
+ requireActiveWindow(this.#document);
1282
+ return frames;
1283
+ }
1284
+ #exposedTools(callerOrigin) {
1285
+ const ownerOrigin = requireActiveWindow(this.#document).origin;
1286
+ const exposed = [];
1287
+ for (const storedTool of this.#tools.values()) {
1288
+ if (!isExposedTo(storedTool, ownerOrigin, callerOrigin)) continue;
1289
+ exposed.push(storedTool.metadata);
817
1290
  }
818
- return serializeChromeToolResult(rawResult);
1291
+ return exposed;
819
1292
  }
820
- async #notifyToolsChanged() {
821
- await new Promise((resolve) => setTimeout(resolve, 0));
822
- this.dispatchEvent(new Event("toolchange"));
823
- this.#testingShim?.dispatchToolChange();
1293
+ #queueToolChange() {
1294
+ return new Promise((resolve) => {
1295
+ queueTask(() => {
1296
+ this.dispatchEvent(new Event("toolchange"));
1297
+ resolve();
1298
+ });
1299
+ });
824
1300
  }
825
1301
  };
1302
+ function isExposedTo(tool, ownerOrigin, callerOrigin) {
1303
+ return ownerOrigin === callerOrigin || tool.exposedTo.includes(callerOrigin);
1304
+ }
826
1305
  const modelContextConstructor = function ModelContext() {
827
1306
  throw new TypeError("Illegal constructor");
828
1307
  };
829
1308
  Object.defineProperty(modelContextConstructor, "name", { value: "ModelContext" });
830
1309
  Object.defineProperty(modelContextConstructor, "prototype", {
831
- value: StrictWebMCPContext.prototype,
1310
+ value: ModelContextPolyfill.prototype,
832
1311
  writable: false
833
1312
  });
834
1313
  Object.setPrototypeOf(modelContextConstructor, EventTarget);
835
- Object.defineProperty(StrictWebMCPContext.prototype, "constructor", {
836
- configurable: true,
837
- writable: true,
838
- value: modelContextConstructor
839
- });
840
- Object.defineProperty(StrictWebMCPContext.prototype, Symbol.toStringTag, {
841
- configurable: true,
842
- value: "ModelContext"
1314
+ Object.defineProperties(ModelContextPolyfill.prototype, {
1315
+ constructor: {
1316
+ value: modelContextConstructor,
1317
+ configurable: true,
1318
+ writable: true
1319
+ },
1320
+ [Symbol.toStringTag]: {
1321
+ value: "ModelContext",
1322
+ configurable: true
1323
+ },
1324
+ registerTool: { enumerable: true },
1325
+ getTools: { enumerable: true },
1326
+ executeTool: { enumerable: true },
1327
+ ontoolchange: { enumerable: true }
843
1328
  });
844
- for (const member of [
845
- "registerTool",
846
- "getTools",
847
- "executeTool",
848
- "ontoolchange"
849
- ]) {
850
- const descriptor = Object.getOwnPropertyDescriptor(StrictWebMCPContext.prototype, member);
851
- if (descriptor) Object.defineProperty(StrictWebMCPContext.prototype, member, {
852
- ...descriptor,
853
- enumerable: true
854
- });
855
- }
856
- /**
857
- * EventTarget-based testing shim matching the native Chromium ModelContextTesting surface.
858
- *
859
- * Fires `toolchange` events and supports the `ontoolchange` handler property.
860
- */
861
- var PolyfillTestingShim = class extends EventTarget {
862
- _ontoolchange = null;
863
- ontoolchangeListener = (event) => {
864
- this._ontoolchange?.call(this, event);
1329
+ function readToolDefinition(value) {
1330
+ const descriptor = readDictionary(value);
1331
+ const annotations = readAnnotations(descriptor.annotations);
1332
+ const description = toDOMString(requireMember(descriptor.description, "description"));
1333
+ const callback = requireMember(descriptor.execute, "execute");
1334
+ if (typeof callback !== "function") throw new TypeError("execute must be a function");
1335
+ const execute = callback;
1336
+ const inputSchema = readInputSchema(descriptor.inputSchema);
1337
+ const name = toDOMString(requireMember(descriptor.name, "name"));
1338
+ const rawTitle = descriptor.title;
1339
+ return {
1340
+ name,
1341
+ title: rawTitle === void 0 ? "" : toUSVString(rawTitle),
1342
+ description,
1343
+ annotations,
1344
+ inputSchema,
1345
+ execute
865
1346
  };
866
- #context;
867
- constructor(context) {
868
- super();
869
- this.#context = context;
870
- }
871
- listTools() {
872
- return StrictWebMCPContext.getToolInfos(this.#context);
873
- }
874
- executeTool(toolName, inputArgsJson, options) {
875
- return StrictWebMCPContext.executeToolForTesting(this.#context, toolName, inputArgsJson, options);
876
- }
877
- get ontoolchange() {
878
- return this._ontoolchange;
879
- }
880
- set ontoolchange(handler) {
881
- const listener = typeof handler === "function" ? handler : null;
882
- if (listener === null) {
883
- this._ontoolchange = null;
884
- super.removeEventListener("toolchange", this.ontoolchangeListener);
885
- return;
886
- }
887
- if (this._ontoolchange === null) super.addEventListener("toolchange", this.ontoolchangeListener);
888
- this._ontoolchange = listener;
889
- }
890
- /** @internal Called by StrictWebMCPContext when tools change. */
891
- dispatchToolChange() {
892
- this.dispatchEvent(new Event("toolchange"));
893
- }
894
- dispose() {
895
- this.ontoolchange = null;
896
- }
897
- };
898
- function normalizeToolDescriptor(tool, existing) {
899
- if (!tool || typeof tool !== "object") throw new TypeError("registerTool(tool) requires a tool object");
900
- const coerced = coerceWebMcpToolDescriptor(tool);
901
- validateWebMcpToolDescriptor(coerced);
902
- if (existing.has(coerced.name)) throw createInvalidStateError(`Tool already registered: ${coerced.name}`);
903
- const registeredInputSchema = coerced.inputSchema === void 0 ? void 0 : serializeInputSchema(coerced.inputSchema);
1347
+ }
1348
+ function readExecutionTarget(value) {
1349
+ const descriptor = readDictionary(value);
1350
+ readAnnotations(descriptor.annotations);
1351
+ toDOMString(requireMember(descriptor.description, "description"));
1352
+ readInputSchema(descriptor.inputSchema);
1353
+ const name = toDOMString(requireMember(descriptor.name, "name"));
1354
+ const origin = toUSVString(requireMember(descriptor.origin, "origin"));
1355
+ const title = descriptor.title;
1356
+ if (title !== void 0) toDOMString(title);
1357
+ const targetWindow = requireMember(descriptor.window, "window");
1358
+ if (!isObject(targetWindow)) throw new TypeError("window must be a Window");
1359
+ getWindow.call(targetWindow);
904
1360
  return {
905
- name: coerced.name,
906
- ...coerced.title === void 0 ? {} : { title: coerced.title },
907
- description: coerced.description,
908
- ...coerced.inputSchema === void 0 ? {} : { inputSchema: coerced.inputSchema },
909
- ...coerced.annotations === void 0 ? {} : { annotations: coerced.annotations },
910
- execute: (input) => Reflect.apply(coerced.execute, void 0, [input]),
911
- ...registeredInputSchema !== void 0 ? { [REGISTERED_INPUT_SCHEMA_SYMBOL]: registeredInputSchema } : {}
1361
+ name,
1362
+ origin,
1363
+ window: targetWindow
912
1364
  };
913
1365
  }
914
- let navigatorModelContextDeprecationWarned = false;
915
- /**
916
- * Backing store for the `Document.prototype.modelContext` getter.
917
- *
918
- * WebIDL puts `[SameObject] readonly attribute ModelContext modelContext` on
919
- * the Document *interface prototype object*, not on the document instance, and
920
- * Chrome matches that. One shared accessor therefore needs somewhere to keep
921
- * the per-document value, and keying it by document is what makes repeated
922
- * reads return the identical object, as `[SameObject]` requires.
923
- */
924
- const documentModelContexts = /* @__PURE__ */ new WeakMap();
925
- const { modelContext: documentModelContextGetter } = { modelContext() {
926
- if (!(this instanceof Document)) throw new TypeError("Illegal invocation");
927
- return documentModelContexts.get(this);
928
- } };
929
- Object.defineProperty(documentModelContextGetter, "name", { value: "get modelContext" });
930
- function defineDocumentModelContext(target, value) {
931
- documentModelContexts.set(target, value);
932
- if (Object.hasOwn(Document.prototype, "modelContext")) return;
933
- installProperty(Document.prototype, "modelContext", {
934
- configurable: true,
935
- enumerable: true,
936
- get: documentModelContextGetter
937
- });
1366
+ function readInputSchema(value) {
1367
+ if (value !== void 0 && !isObject(value)) throw new TypeError("inputSchema must be an object");
1368
+ return value;
938
1369
  }
939
- function defineDeprecatedNavigatorModelContext(target, value) {
940
- installProperty(target, "modelContext", {
941
- configurable: true,
942
- enumerable: true,
943
- get() {
944
- if (!navigatorModelContextDeprecationWarned) {
945
- navigatorModelContextDeprecationWarned = true;
946
- console.warn("[WebMCPPolyfill] navigator.modelContext is deprecated. The May 27, 2026 WebMCP draft moved the modelContext getter from Navigator to Document — use document.modelContext instead. See https://github.com/webmachinelearning/webmcp/pull/184.");
947
- }
948
- return value;
949
- }
950
- });
1370
+ function isObject(value) {
1371
+ return typeof value === "object" && value !== null || typeof value === "function";
951
1372
  }
952
- function installSubmitEventPolyfill() {
953
- const prototype = SubmitEvent.prototype;
954
- if (!("agentInvoked" in prototype)) installProperty(prototype, "agentInvoked", {
955
- configurable: true,
956
- enumerable: true,
957
- get() {
958
- return isAgentInvokedSubmitEvent(this);
959
- }
960
- });
961
- if (!("respondWith" in prototype)) installProperty(prototype, "respondWith", {
962
- configurable: true,
963
- enumerable: true,
964
- writable: true,
965
- value(agentResponse) {
966
- respondWithAgentSubmitEvent(this, agentResponse);
967
- }
1373
+ function readDictionary(value) {
1374
+ if (value == null) return {};
1375
+ if (!isObject(value)) throw new TypeError("Expected a dictionary");
1376
+ return value;
1377
+ }
1378
+ function readAnnotations(value) {
1379
+ if (value === void 0) return;
1380
+ const dictionary = readDictionary(value);
1381
+ const annotations = {};
1382
+ for (const name of annotationNames) annotations[name] = Boolean(dictionary[name]);
1383
+ return annotations;
1384
+ }
1385
+ function toDOMString(value) {
1386
+ if (typeof value === "symbol") throw new TypeError("Cannot convert a Symbol to a string");
1387
+ return String(value);
1388
+ }
1389
+ function toUSVString(value) {
1390
+ return toDOMString(value).toWellFormed();
1391
+ }
1392
+ function requireMember(value, name) {
1393
+ if (value === void 0) throw new TypeError(`${name} is required`);
1394
+ return value;
1395
+ }
1396
+ function serializeJSON(value) {
1397
+ const result = JSON.stringify(value);
1398
+ if (result === void 0) throw new TypeError("Value is not JSON-serializable");
1399
+ return result;
1400
+ }
1401
+ function readAbortSignal(value) {
1402
+ if (value === void 0) return;
1403
+ return AbortSignal.any([value]);
1404
+ }
1405
+ function readOriginSequence(value) {
1406
+ if (value === void 0) return [];
1407
+ if (!isObject(value)) throw new TypeError("Origins must be a sequence");
1408
+ const getIterator = readDictionary(value)[Symbol.iterator];
1409
+ if (typeof getIterator !== "function") throw new TypeError("Origins must be a sequence");
1410
+ const iterable = { [Symbol.iterator]() {
1411
+ const iterator = Reflect.apply(getIterator, value, []);
1412
+ if (!isObject(iterator)) throw new TypeError("Iterator must be an object");
1413
+ const next = readDictionary(iterator).next;
1414
+ if (typeof next !== "function") throw new TypeError("Iterator next must be a function");
1415
+ return { next() {
1416
+ return Reflect.apply(next, iterator, []);
1417
+ } };
1418
+ } };
1419
+ return Array.from(iterable, toUSVString);
1420
+ }
1421
+ function parseOrigins(origins) {
1422
+ return origins.map((value) => {
1423
+ const origin = URL.parse(value)?.origin;
1424
+ if (origin === void 0) throw new NativeDOMException("Invalid origin", "SecurityError");
1425
+ const url = URL.parse(origin);
1426
+ if (!url || !isPotentiallyTrustworthy(url)) throw new NativeDOMException("Origin is not potentially trustworthy", "SecurityError");
1427
+ return origin;
968
1428
  });
969
1429
  }
970
- function initializeWebMCPPolyfill(options) {
971
- if (globalThis.isSecureContext === false) return;
972
- const nav = typeof navigator === "undefined" ? null : navigator;
973
- const doc = typeof document === "undefined" ? null : document;
974
- if (!nav || !doc || typeof window === "undefined") return;
975
- if (doc.modelContext) return;
976
- const navigatorModelContext = nav.modelContext;
977
- if (installedProperties.length > 0) cleanupWebMCPPolyfill();
978
- if (navigatorModelContext) {
979
- defineDocumentModelContext(doc, navigatorModelContext);
980
- return;
1430
+ function isPotentiallyTrustworthy({ protocol, hostname }) {
1431
+ if (["https:", "wss:"].includes(protocol)) return true;
1432
+ const isLoopback = hostname === "[::1]" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname);
1433
+ const isLocalhost = hostname === "localhost" || hostname === "localhost." || hostname.endsWith(".localhost") || hostname.endsWith(".localhost.");
1434
+ return ["http:", "ws:"].includes(protocol) && (isLoopback || isLocalhost);
1435
+ }
1436
+ function requireActiveWindow(owner) {
1437
+ const view = activeWindow(owner);
1438
+ if (!view) throw new NativeDOMException("The document is not fully active", "InvalidStateError");
1439
+ if (readToolsPolicy(owner) === false) throw new NativeDOMException("WebMCP is disabled by Permissions Policy", "NotAllowedError");
1440
+ return view;
1441
+ }
1442
+ let taskPort;
1443
+ const queuedTasks = [];
1444
+ function queueTask(callback) {
1445
+ if (!taskPort) {
1446
+ const channel = new MessageChannel();
1447
+ channel.port1.onmessage = () => queuedTasks.shift()?.();
1448
+ taskPort = channel.port2;
981
1449
  }
982
- const previousDescriptor = Object.getOwnPropertyDescriptor(window, "ModelContext");
983
- if (previousDescriptor && !previousDescriptor.configurable) {
984
- if (!Object.is(Reflect.get(window, "ModelContext"), modelContextConstructor)) throw new TypeError("Cannot install ModelContext over a non-configurable global");
985
- } else installProperty(window, "ModelContext", {
986
- configurable: true,
987
- enumerable: false,
988
- writable: true,
989
- value: modelContextConstructor
990
- });
991
- const context = new StrictWebMCPContext(doc);
992
- installedContext = context;
993
- defineDocumentModelContext(doc, context);
994
- navigatorModelContextDeprecationWarned = false;
995
- defineDeprecatedNavigatorModelContext(nav, context);
996
- installSubmitEventPolyfill();
997
- if (options?.installTestingShim && !nav.modelContextTesting) installProperty(nav, "modelContextTesting", {
998
- configurable: true,
999
- enumerable: true,
1000
- writable: false,
1001
- value: StrictWebMCPContext.getTestingShim(context)
1002
- });
1003
- cleanupDeclarativeForms = installDeclarativeForms(doc, context);
1004
- }
1005
- function cleanupWebMCPPolyfill() {
1006
- cleanupDeclarativeForms?.();
1007
- cleanupDeclarativeForms = null;
1008
- if (installedContext) StrictWebMCPContext.dispose(installedContext);
1009
- installedContext = null;
1010
- for (const { target, key, previous } of [...installedProperties].reverse()) if (previous) Object.defineProperty(target, key, previous);
1011
- else Reflect.deleteProperty(target, key);
1012
- installedProperties.length = 0;
1013
- navigatorModelContextDeprecationWarned = false;
1450
+ queuedTasks.push(callback);
1451
+ taskPort.postMessage(void 0);
1452
+ }
1453
+
1454
+ //#endregion
1455
+ //#region src/index.ts
1456
+ /**
1457
+ * Installs upstream WebMCP and the temporary declarative tools layer.
1458
+ * Engines missing an API the vendored core calls are left untouched: upstream defines
1459
+ * the document getter before it constructs the context, so a failed construction would
1460
+ * leave `document.modelContext` throwing.
1461
+ */
1462
+ function installWebMCP() {
1463
+ if (typeof document === "undefined" || !globalThis.isSecureContext) return;
1464
+ if (typeof Promise.withResolvers !== "function" || typeof AbortSignal.any !== "function" || typeof String.prototype.toWellFormed !== "function" || typeof URL.parse !== "function") return;
1465
+ installWebMCP$1();
1466
+ const context = document.modelContext;
1467
+ if (context) installWebMCPDeclarativeExtensions(context);
1014
1468
  }
1015
1469
 
1016
1470
  //#endregion
1017
- export { cleanupWebMCPPolyfill, initializeWebMCPPolyfill };
1471
+ export { installWebMCP };
1018
1472
  //# sourceMappingURL=index.js.map