auto-webmcp 0.3.15 → 0.3.16

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.
@@ -1,83 +1,3 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
- };
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
-
11
- // src/registry.ts
12
- var registry_exports = {};
13
- __export(registry_exports, {
14
- getAllRegisteredTools: () => getAllRegisteredTools,
15
- getRegisteredToolName: () => getRegisteredToolName,
16
- isWebMCPSupported: () => isWebMCPSupported,
17
- registerFormTool: () => registerFormTool,
18
- unregisterAll: () => unregisterAll,
19
- unregisterFormTool: () => unregisterFormTool
20
- });
21
- function isWebMCPSupported() {
22
- return typeof navigator !== "undefined" && typeof navigator.modelContext !== "undefined";
23
- }
24
- async function registerFormTool(form, metadata, execute) {
25
- if (!isWebMCPSupported())
26
- return;
27
- const existing = registeredTools.get(form);
28
- if (existing) {
29
- await unregisterFormTool(form);
30
- }
31
- const toolDef = {
32
- name: metadata.name,
33
- description: metadata.description,
34
- inputSchema: metadata.inputSchema,
35
- execute
36
- };
37
- if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
38
- toolDef.annotations = metadata.annotations;
39
- }
40
- try {
41
- await navigator.modelContext.registerTool(toolDef);
42
- } catch {
43
- try {
44
- await navigator.modelContext.unregisterTool(metadata.name);
45
- await navigator.modelContext.registerTool(toolDef);
46
- } catch {
47
- }
48
- }
49
- registeredTools.set(form, metadata.name);
50
- }
51
- async function unregisterFormTool(form) {
52
- if (!isWebMCPSupported())
53
- return;
54
- const name = registeredTools.get(form);
55
- if (!name)
56
- return;
57
- try {
58
- await navigator.modelContext.unregisterTool(name);
59
- } catch {
60
- }
61
- registeredTools.delete(form);
62
- }
63
- function getRegisteredToolName(form) {
64
- return registeredTools.get(form);
65
- }
66
- function getAllRegisteredTools() {
67
- return Array.from(registeredTools.entries()).map(([form, name]) => ({ form, name }));
68
- }
69
- async function unregisterAll() {
70
- const entries = Array.from(registeredTools.entries());
71
- await Promise.all(entries.map(([form]) => unregisterFormTool(form)));
72
- }
73
- var registeredTools;
74
- var init_registry = __esm({
75
- "src/registry.ts"() {
76
- "use strict";
77
- registeredTools = /* @__PURE__ */ new Map();
78
- }
79
- });
80
-
81
1
  // src/config.ts
82
2
  function resolveConfig(userConfig) {
83
3
  return {
@@ -231,19 +151,19 @@ function mapSelectElement(select) {
231
151
  return { type: "string", enum: enumValues, oneOf };
232
152
  }
233
153
  function collectCheckboxEnum(form, name) {
234
- return Array.from(
235
- form.querySelectorAll(`input[type="checkbox"][name="${CSS.escape(name)}"]`)
154
+ return Array.from(form.elements).filter(
155
+ (el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === name
236
156
  ).map((cb) => cb.value).filter((v) => v !== "" && v !== "on");
237
157
  }
238
158
  function collectRadioEnum(form, name) {
239
- const radios = Array.from(
240
- form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
159
+ const radios = Array.from(form.elements).filter(
160
+ (el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === name
241
161
  );
242
162
  return radios.map((r) => r.value).filter((v) => v !== "");
243
163
  }
244
164
  function collectRadioOneOf(form, name) {
245
- const radios = Array.from(
246
- form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
165
+ const radios = Array.from(form.elements).filter(
166
+ (el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === name
247
167
  ).filter((r) => r.value !== "");
248
168
  return radios.map((r) => {
249
169
  const title = getRadioLabelText(r);
@@ -503,20 +423,26 @@ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
503
423
  }
504
424
  return results;
505
425
  }
426
+ function collectFormAssociatedControls(form) {
427
+ const controls = Array.from(form.elements).filter(
428
+ (el) => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement
429
+ );
430
+ const seen = new Set(controls);
431
+ for (const shadowControl of collectShadowControls(form)) {
432
+ if (!seen.has(shadowControl)) {
433
+ controls.push(shadowControl);
434
+ seen.add(shadowControl);
435
+ }
436
+ }
437
+ return controls;
438
+ }
506
439
  function buildSchema(form) {
507
440
  const properties = {};
508
441
  const required = [];
509
442
  const fieldElements = /* @__PURE__ */ new Map();
510
443
  const processedRadioGroups = /* @__PURE__ */ new Set();
511
444
  const processedCheckboxGroups = /* @__PURE__ */ new Set();
512
- const controls = [
513
- ...Array.from(
514
- form.querySelectorAll(
515
- "input, textarea, select"
516
- )
517
- ),
518
- ...collectShadowControls(form)
519
- ];
445
+ const controls = collectFormAssociatedControls(form);
520
446
  for (const control of controls) {
521
447
  const name = control.name;
522
448
  const fieldKey = name || resolveNativeControlFallbackKey(control);
@@ -549,8 +475,8 @@ function buildSchema(form) {
549
475
  const radioOneOf = collectRadioOneOf(form, fieldKey);
550
476
  if (radioOneOf.length > 0)
551
477
  schemaProp.oneOf = radioOneOf;
552
- const checkedRadio = form.querySelector(
553
- `input[type="radio"][name="${CSS.escape(fieldKey)}"]:checked`
478
+ const checkedRadio = Array.from(form.elements).find(
479
+ (el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === fieldKey && el.checked
554
480
  );
555
481
  if (checkedRadio?.value)
556
482
  schemaProp.default = checkedRadio.value;
@@ -565,10 +491,8 @@ function buildSchema(form) {
565
491
  };
566
492
  if (schemaProp.description)
567
493
  arrayProp.description = schemaProp.description;
568
- const checkedBoxes = Array.from(
569
- form.querySelectorAll(
570
- `input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
571
- )
494
+ const checkedBoxes = Array.from(form.elements).filter(
495
+ (el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === fieldKey && el.checked
572
496
  ).map((b) => b.value);
573
497
  if (checkedBoxes.length > 0)
574
498
  arrayProp.default = checkedBoxes;
@@ -1038,8 +962,68 @@ function buildSchemaFromInputs(inputs) {
1038
962
  return { schema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object", properties, required }, fieldElements };
1039
963
  }
1040
964
 
1041
- // src/discovery.ts
1042
- init_registry();
965
+ // src/registry.ts
966
+ var registeredTools = /* @__PURE__ */ new Map();
967
+ var registrationControllers = /* @__PURE__ */ new Map();
968
+ function isWebMCPSupported() {
969
+ return typeof navigator !== "undefined" && typeof navigator.modelContext !== "undefined";
970
+ }
971
+ async function registerFormTool(form, metadata, execute) {
972
+ if (!isWebMCPSupported())
973
+ return;
974
+ const existing = registeredTools.get(form);
975
+ if (existing) {
976
+ await unregisterFormTool(form);
977
+ }
978
+ const toolDef = {
979
+ name: metadata.name,
980
+ description: metadata.description,
981
+ inputSchema: metadata.inputSchema,
982
+ execute
983
+ };
984
+ if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
985
+ toolDef.annotations = metadata.annotations;
986
+ }
987
+ const controller = new AbortController();
988
+ registrationControllers.set(form, controller);
989
+ try {
990
+ await navigator.modelContext.registerTool(toolDef, { signal: controller.signal });
991
+ } catch {
992
+ try {
993
+ await navigator.modelContext.unregisterTool?.(metadata.name);
994
+ await navigator.modelContext.registerTool(toolDef, { signal: controller.signal });
995
+ } catch {
996
+ }
997
+ }
998
+ registeredTools.set(form, metadata.name);
999
+ }
1000
+ async function unregisterFormTool(form) {
1001
+ if (!isWebMCPSupported())
1002
+ return;
1003
+ const name = registeredTools.get(form);
1004
+ if (!name)
1005
+ return;
1006
+ const controller = registrationControllers.get(form);
1007
+ if (controller) {
1008
+ controller.abort();
1009
+ registrationControllers.delete(form);
1010
+ }
1011
+ try {
1012
+ await navigator.modelContext.unregisterTool?.(name);
1013
+ } catch {
1014
+ }
1015
+ registeredTools.delete(form);
1016
+ }
1017
+ function getRegisteredToolName(form) {
1018
+ return registeredTools.get(form);
1019
+ }
1020
+ function getAllRegisteredTools() {
1021
+ return Array.from(registeredTools.entries()).map(([form, name]) => ({ form, name }));
1022
+ }
1023
+ async function unregisterAll() {
1024
+ const entries = Array.from(registeredTools.entries());
1025
+ await Promise.all(entries.map(([form]) => unregisterFormTool(form)));
1026
+ }
1043
1027
 
1044
1028
  // src/interceptor.ts
1045
1029
  var pendingExecutions = /* @__PURE__ */ new WeakMap();
@@ -1056,7 +1040,20 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1056
1040
  formFieldElements.set(form, metadata.fieldElements);
1057
1041
  }
1058
1042
  attachSubmitInterceptor(form, toolName);
1059
- return async (params) => {
1043
+ return async (params, client) => {
1044
+ const modelContextClient = client;
1045
+ if (config.autoSubmit && metadata?.annotations?.destructiveHint === true && typeof modelContextClient?.requestUserInteraction === "function") {
1046
+ const approved = await modelContextClient.requestUserInteraction(async () => {
1047
+ return new Promise((resolve) => {
1048
+ const ok = window.confirm(`Agent requested a destructive action via "${toolName}". Continue?`);
1049
+ resolve(ok);
1050
+ });
1051
+ });
1052
+ if (!approved) {
1053
+ window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
1054
+ return { content: [{ type: "text", text: `Cancelled "${toolName}" by user.` }] };
1055
+ }
1056
+ }
1060
1057
  pendingFillWarnings.set(form, []);
1061
1058
  pendingWarnings.delete(form);
1062
1059
  fillFormFields(form, params);
@@ -1193,7 +1190,23 @@ function findInShadowRoots(root, selector) {
1193
1190
  }
1194
1191
  return null;
1195
1192
  }
1193
+ function getAssociatedInputsByName(form, type, name) {
1194
+ return Array.from(form.elements).filter(
1195
+ (el) => el instanceof HTMLInputElement && el.type === type && el.name === name
1196
+ );
1197
+ }
1196
1198
  function findNativeField(form, key) {
1199
+ const named = form.elements.namedItem(key);
1200
+ if (typeof named === "object" && named !== null && (named instanceof HTMLInputElement || named instanceof HTMLTextAreaElement || named instanceof HTMLSelectElement)) {
1201
+ return named;
1202
+ }
1203
+ if (named instanceof RadioNodeList) {
1204
+ const first = named[0];
1205
+ const firstObj = first;
1206
+ if (firstObj instanceof HTMLInputElement || firstObj instanceof HTMLTextAreaElement || firstObj instanceof HTMLSelectElement) {
1207
+ return firstObj;
1208
+ }
1209
+ }
1197
1210
  const esc = CSS.escape(key);
1198
1211
  const light = form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
1199
1212
  `input#${esc}, textarea#${esc}, select#${esc}`
@@ -1213,10 +1226,7 @@ function fillFormFields(form, params) {
1213
1226
  fillInput(input, form, key, value);
1214
1227
  if (input.type === "checkbox") {
1215
1228
  if (Array.isArray(value)) {
1216
- const esc = CSS.escape(key);
1217
- snapshot[key] = Array.from(
1218
- form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`)
1219
- ).filter((b) => b.checked).map((b) => b.value);
1229
+ snapshot[key] = getAssociatedInputsByName(form, "checkbox", key).filter((b) => b.checked).map((b) => b.value);
1220
1230
  } else {
1221
1231
  snapshot[key] = input.checked;
1222
1232
  }
@@ -1265,8 +1275,7 @@ function fillInput(input, form, key, value) {
1265
1275
  const type = input.type.toLowerCase();
1266
1276
  if (type === "checkbox") {
1267
1277
  if (Array.isArray(value)) {
1268
- const esc = CSS.escape(key);
1269
- const allBoxes = form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`);
1278
+ const allBoxes = getAssociatedInputsByName(form, "checkbox", key);
1270
1279
  for (const box of allBoxes) {
1271
1280
  setReactChecked(box, value.map(String).includes(box.value));
1272
1281
  }
@@ -1307,10 +1316,7 @@ function fillInput(input, form, key, value) {
1307
1316
  return;
1308
1317
  }
1309
1318
  if (type === "radio") {
1310
- const esc = CSS.escape(key);
1311
- const radios = form.querySelectorAll(
1312
- `input[type="radio"][name="${esc}"]`
1313
- );
1319
+ const radios = getAssociatedInputsByName(form, "radio", key);
1314
1320
  for (const radio of radios) {
1315
1321
  if (radio.value === String(value)) {
1316
1322
  if (_checkedSetter) {
@@ -1633,9 +1639,35 @@ function isExcluded(form, config) {
1633
1639
  }
1634
1640
  return false;
1635
1641
  }
1642
+ function withNumericSuffix(baseName, n) {
1643
+ const suffix = `_${n}`;
1644
+ return `${baseName.slice(0, Math.max(1, 64 - suffix.length))}${suffix}`;
1645
+ }
1646
+ function getUsedToolNames(excludeForm) {
1647
+ const names = new Set(registeredOrphanToolNames);
1648
+ for (const { form, name } of getAllRegisteredTools()) {
1649
+ if (excludeForm && form === excludeForm)
1650
+ continue;
1651
+ names.add(name);
1652
+ }
1653
+ return names;
1654
+ }
1655
+ function ensureUniqueToolName(baseName, excludeForm) {
1656
+ const used = getUsedToolNames(excludeForm);
1657
+ if (!used.has(baseName))
1658
+ return baseName;
1659
+ let i = 2;
1660
+ let candidate = withNumericSuffix(baseName, i);
1661
+ while (used.has(candidate)) {
1662
+ i++;
1663
+ candidate = withNumericSuffix(baseName, i);
1664
+ }
1665
+ return candidate;
1666
+ }
1636
1667
  async function registerForm(form, config) {
1637
1668
  if (isExcluded(form, config))
1638
1669
  return;
1670
+ const previousName = getRegisteredToolName(form);
1639
1671
  let override;
1640
1672
  for (const [selector, ovr] of Object.entries(config.overrides)) {
1641
1673
  try {
@@ -1647,6 +1679,11 @@ async function registerForm(form, config) {
1647
1679
  }
1648
1680
  }
1649
1681
  const metadata = analyzeForm(form, override);
1682
+ const resolvedName = ensureUniqueToolName(metadata.name, form);
1683
+ if (resolvedName !== metadata.name && config.debug) {
1684
+ console.warn(`[auto-webmcp] tool name collision: "${metadata.name}" renamed to "${resolvedName}"`);
1685
+ }
1686
+ metadata.name = resolvedName;
1650
1687
  if (config.debug) {
1651
1688
  warnToolQuality(metadata.name, metadata.description);
1652
1689
  }
@@ -1658,6 +1695,9 @@ async function registerForm(form, config) {
1658
1695
  '[type="submit"], button[data-variant="primary"], button:not([type])'
1659
1696
  ) ?? null;
1660
1697
  const pendingBtns = window["__pendingSubmitBtns"] ??= {};
1698
+ if (previousName && previousName !== metadata.name) {
1699
+ delete pendingBtns[previousName];
1700
+ }
1661
1701
  pendingBtns[metadata.name] = formSubmitBtn;
1662
1702
  if (config.debug) {
1663
1703
  console.log(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
@@ -1665,12 +1705,14 @@ async function registerForm(form, config) {
1665
1705
  emit("form:registered", form, metadata.name);
1666
1706
  }
1667
1707
  async function unregisterForm(form, config) {
1668
- const { getRegisteredToolName: getRegisteredToolName2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
1669
- const name = getRegisteredToolName2(form);
1708
+ const name = getRegisteredToolName(form);
1670
1709
  if (!name)
1671
1710
  return;
1672
1711
  await unregisterFormTool(form);
1673
1712
  registeredForms.delete(form);
1713
+ const pendingBtns = window["__pendingSubmitBtns"];
1714
+ if (pendingBtns)
1715
+ delete pendingBtns[name];
1674
1716
  if (config.debug) {
1675
1717
  console.log(`[auto-webmcp] Unregistered: ${name}`);
1676
1718
  }
@@ -1719,11 +1761,47 @@ function scheduleReAnalysis(form, config) {
1719
1761
  }, RE_ANALYSIS_DEBOUNCE_MS)
1720
1762
  );
1721
1763
  }
1764
+ function scheduleFormReAnalysisById(formId, config) {
1765
+ const owner = document.getElementById(formId);
1766
+ if (owner instanceof HTMLFormElement && registeredForms.has(owner)) {
1767
+ scheduleReAnalysis(owner, config);
1768
+ }
1769
+ }
1770
+ function resolveOwnerForm(el) {
1771
+ const closest = el.closest("form");
1772
+ if (closest instanceof HTMLFormElement)
1773
+ return closest;
1774
+ const explicitOwner = el.form;
1775
+ if (explicitOwner instanceof HTMLFormElement)
1776
+ return explicitOwner;
1777
+ const formId = el.getAttribute("form");
1778
+ if (formId) {
1779
+ const byId = document.getElementById(formId);
1780
+ if (byId instanceof HTMLFormElement)
1781
+ return byId;
1782
+ }
1783
+ return null;
1784
+ }
1722
1785
  function startObserver(config) {
1723
1786
  if (observer)
1724
1787
  return;
1725
1788
  observer = new MutationObserver((mutations) => {
1726
1789
  for (const mutation of mutations) {
1790
+ if (mutation.type === "attributes" && mutation.target instanceof Element) {
1791
+ const target = mutation.target;
1792
+ const ownerForm = resolveOwnerForm(target);
1793
+ if (ownerForm && registeredForms.has(ownerForm)) {
1794
+ scheduleReAnalysis(ownerForm, config);
1795
+ } else if (target instanceof HTMLFormElement) {
1796
+ void registerForm(target, config);
1797
+ } else if (isInterestingNode(target) && !target.closest("form")) {
1798
+ scheduleOrphanRescan(config);
1799
+ }
1800
+ if (mutation.attributeName === "form" && mutation.oldValue) {
1801
+ scheduleFormReAnalysisById(mutation.oldValue, config);
1802
+ }
1803
+ continue;
1804
+ }
1727
1805
  for (const node of mutation.addedNodes) {
1728
1806
  if (!(node instanceof Element))
1729
1807
  continue;
@@ -1752,7 +1830,12 @@ function startObserver(config) {
1752
1830
  }
1753
1831
  }
1754
1832
  });
1755
- observer.observe(document.body, { childList: true, subtree: true });
1833
+ observer.observe(document.body, {
1834
+ childList: true,
1835
+ subtree: true,
1836
+ attributes: true,
1837
+ attributeOldValue: true
1838
+ });
1756
1839
  }
1757
1840
  function listenForRouteChanges(config) {
1758
1841
  window.addEventListener("hashchange", () => scanForms(config));
@@ -1896,6 +1979,15 @@ async function scanOrphanInputs(config) {
1896
1979
  }
1897
1980
  console.log(`[auto-webmcp] orphan: submit button for group:`, submitBtn ? `"${submitBtn.textContent?.trim()}" disabled=${submitBtn.disabled}` : "none");
1898
1981
  const metadata = analyzeOrphanInputGroup(container, inputs, submitBtn);
1982
+ if (registeredOrphanToolNames.has(metadata.name)) {
1983
+ console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
1984
+ continue;
1985
+ }
1986
+ const orphanName = ensureUniqueToolName(metadata.name);
1987
+ if (orphanName !== metadata.name && config.debug) {
1988
+ console.warn(`[auto-webmcp] orphan tool name collision: "${metadata.name}" renamed to "${orphanName}"`);
1989
+ }
1990
+ metadata.name = orphanName;
1899
1991
  console.log(`[auto-webmcp] orphan: tool="${metadata.name}" schema keys:`, Object.keys(metadata.inputSchema.properties));
1900
1992
  const inputPairs = [];
1901
1993
  const schemaProps = metadata.inputSchema.properties;
@@ -1916,7 +2008,7 @@ async function scanOrphanInputs(config) {
1916
2008
  continue;
1917
2009
  }
1918
2010
  const toolName = metadata.name;
1919
- const execute = async (params) => {
2011
+ const execute = async (params, _client) => {
1920
2012
  console.log(`[auto-webmcp] orphan execute: tool="${toolName}" params=`, params);
1921
2013
  console.log(`[auto-webmcp] orphan execute: inputPairs=`, inputPairs.map((p) => p.key));
1922
2014
  for (const { key, el } of inputPairs) {
@@ -1985,10 +2077,6 @@ async function scanOrphanInputs(config) {
1985
2077
  return { content: [{ type: "text", text: "Fields filled and form submitted." }] };
1986
2078
  };
1987
2079
  try {
1988
- if (registeredOrphanToolNames.has(metadata.name)) {
1989
- console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
1990
- continue;
1991
- }
1992
2080
  const toolDef = {
1993
2081
  name: metadata.name,
1994
2082
  description: metadata.description,
@@ -2039,7 +2127,6 @@ function stopDiscovery() {
2039
2127
  }
2040
2128
 
2041
2129
  // src/index.ts
2042
- init_registry();
2043
2130
  async function autoWebMCP(config) {
2044
2131
  const resolved = resolveConfig(config);
2045
2132
  if (resolved.debug) {