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 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAmJ,MAAM,aAAa,CAAC;AAC1L,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,6FAA6F;IAC7F,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAKD,iDAAiD;AACjD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAOxF;AA4uBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,WAAW,CAAC,EACvF,SAAS,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,GACrD,YAAY,CAMd"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAmJ,MAAM,aAAa,CAAC;AAC1L,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,6FAA6F;IAC7F,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAKD,iDAAiD;AACjD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAOxF;AAuwBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,WAAW,CAAC,EACvF,SAAS,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,GACrD,YAAY,CAMd"}
@@ -3,9 +3,6 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __esm = (fn, res) => function __init() {
7
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
- };
9
6
  var __export = (target, all) => {
10
7
  for (var name in all)
11
8
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -20,76 +17,6 @@ var __copyProps = (to, from, except, desc) => {
20
17
  };
21
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
19
 
23
- // src/registry.ts
24
- var registry_exports = {};
25
- __export(registry_exports, {
26
- getAllRegisteredTools: () => getAllRegisteredTools,
27
- getRegisteredToolName: () => getRegisteredToolName,
28
- isWebMCPSupported: () => isWebMCPSupported,
29
- registerFormTool: () => registerFormTool,
30
- unregisterAll: () => unregisterAll,
31
- unregisterFormTool: () => unregisterFormTool
32
- });
33
- function isWebMCPSupported() {
34
- return typeof navigator !== "undefined" && typeof navigator.modelContext !== "undefined";
35
- }
36
- async function registerFormTool(form, metadata, execute) {
37
- if (!isWebMCPSupported())
38
- return;
39
- const existing = registeredTools.get(form);
40
- if (existing) {
41
- await unregisterFormTool(form);
42
- }
43
- const toolDef = {
44
- name: metadata.name,
45
- description: metadata.description,
46
- inputSchema: metadata.inputSchema,
47
- execute
48
- };
49
- if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
50
- toolDef.annotations = metadata.annotations;
51
- }
52
- try {
53
- await navigator.modelContext.registerTool(toolDef);
54
- } catch {
55
- try {
56
- await navigator.modelContext.unregisterTool(metadata.name);
57
- await navigator.modelContext.registerTool(toolDef);
58
- } catch {
59
- }
60
- }
61
- registeredTools.set(form, metadata.name);
62
- }
63
- async function unregisterFormTool(form) {
64
- if (!isWebMCPSupported())
65
- return;
66
- const name = registeredTools.get(form);
67
- if (!name)
68
- return;
69
- try {
70
- await navigator.modelContext.unregisterTool(name);
71
- } catch {
72
- }
73
- registeredTools.delete(form);
74
- }
75
- function getRegisteredToolName(form) {
76
- return registeredTools.get(form);
77
- }
78
- function getAllRegisteredTools() {
79
- return Array.from(registeredTools.entries()).map(([form, name]) => ({ form, name }));
80
- }
81
- async function unregisterAll() {
82
- const entries = Array.from(registeredTools.entries());
83
- await Promise.all(entries.map(([form]) => unregisterFormTool(form)));
84
- }
85
- var registeredTools;
86
- var init_registry = __esm({
87
- "src/registry.ts"() {
88
- "use strict";
89
- registeredTools = /* @__PURE__ */ new Map();
90
- }
91
- });
92
-
93
20
  // src/index.ts
94
21
  var src_exports = {};
95
22
  __export(src_exports, {
@@ -250,19 +177,19 @@ function mapSelectElement(select) {
250
177
  return { type: "string", enum: enumValues, oneOf };
251
178
  }
252
179
  function collectCheckboxEnum(form, name) {
253
- return Array.from(
254
- form.querySelectorAll(`input[type="checkbox"][name="${CSS.escape(name)}"]`)
180
+ return Array.from(form.elements).filter(
181
+ (el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === name
255
182
  ).map((cb) => cb.value).filter((v) => v !== "" && v !== "on");
256
183
  }
257
184
  function collectRadioEnum(form, name) {
258
- const radios = Array.from(
259
- form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
185
+ const radios = Array.from(form.elements).filter(
186
+ (el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === name
260
187
  );
261
188
  return radios.map((r) => r.value).filter((v) => v !== "");
262
189
  }
263
190
  function collectRadioOneOf(form, name) {
264
- const radios = Array.from(
265
- form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
191
+ const radios = Array.from(form.elements).filter(
192
+ (el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === name
266
193
  ).filter((r) => r.value !== "");
267
194
  return radios.map((r) => {
268
195
  const title = getRadioLabelText(r);
@@ -522,20 +449,26 @@ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
522
449
  }
523
450
  return results;
524
451
  }
452
+ function collectFormAssociatedControls(form) {
453
+ const controls = Array.from(form.elements).filter(
454
+ (el) => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement
455
+ );
456
+ const seen = new Set(controls);
457
+ for (const shadowControl of collectShadowControls(form)) {
458
+ if (!seen.has(shadowControl)) {
459
+ controls.push(shadowControl);
460
+ seen.add(shadowControl);
461
+ }
462
+ }
463
+ return controls;
464
+ }
525
465
  function buildSchema(form) {
526
466
  const properties = {};
527
467
  const required = [];
528
468
  const fieldElements = /* @__PURE__ */ new Map();
529
469
  const processedRadioGroups = /* @__PURE__ */ new Set();
530
470
  const processedCheckboxGroups = /* @__PURE__ */ new Set();
531
- const controls = [
532
- ...Array.from(
533
- form.querySelectorAll(
534
- "input, textarea, select"
535
- )
536
- ),
537
- ...collectShadowControls(form)
538
- ];
471
+ const controls = collectFormAssociatedControls(form);
539
472
  for (const control of controls) {
540
473
  const name = control.name;
541
474
  const fieldKey = name || resolveNativeControlFallbackKey(control);
@@ -568,8 +501,8 @@ function buildSchema(form) {
568
501
  const radioOneOf = collectRadioOneOf(form, fieldKey);
569
502
  if (radioOneOf.length > 0)
570
503
  schemaProp.oneOf = radioOneOf;
571
- const checkedRadio = form.querySelector(
572
- `input[type="radio"][name="${CSS.escape(fieldKey)}"]:checked`
504
+ const checkedRadio = Array.from(form.elements).find(
505
+ (el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === fieldKey && el.checked
573
506
  );
574
507
  if (checkedRadio?.value)
575
508
  schemaProp.default = checkedRadio.value;
@@ -584,10 +517,8 @@ function buildSchema(form) {
584
517
  };
585
518
  if (schemaProp.description)
586
519
  arrayProp.description = schemaProp.description;
587
- const checkedBoxes = Array.from(
588
- form.querySelectorAll(
589
- `input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
590
- )
520
+ const checkedBoxes = Array.from(form.elements).filter(
521
+ (el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === fieldKey && el.checked
591
522
  ).map((b) => b.value);
592
523
  if (checkedBoxes.length > 0)
593
524
  arrayProp.default = checkedBoxes;
@@ -1057,8 +988,68 @@ function buildSchemaFromInputs(inputs) {
1057
988
  return { schema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object", properties, required }, fieldElements };
1058
989
  }
1059
990
 
1060
- // src/discovery.ts
1061
- init_registry();
991
+ // src/registry.ts
992
+ var registeredTools = /* @__PURE__ */ new Map();
993
+ var registrationControllers = /* @__PURE__ */ new Map();
994
+ function isWebMCPSupported() {
995
+ return typeof navigator !== "undefined" && typeof navigator.modelContext !== "undefined";
996
+ }
997
+ async function registerFormTool(form, metadata, execute) {
998
+ if (!isWebMCPSupported())
999
+ return;
1000
+ const existing = registeredTools.get(form);
1001
+ if (existing) {
1002
+ await unregisterFormTool(form);
1003
+ }
1004
+ const toolDef = {
1005
+ name: metadata.name,
1006
+ description: metadata.description,
1007
+ inputSchema: metadata.inputSchema,
1008
+ execute
1009
+ };
1010
+ if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
1011
+ toolDef.annotations = metadata.annotations;
1012
+ }
1013
+ const controller = new AbortController();
1014
+ registrationControllers.set(form, controller);
1015
+ try {
1016
+ await navigator.modelContext.registerTool(toolDef, { signal: controller.signal });
1017
+ } catch {
1018
+ try {
1019
+ await navigator.modelContext.unregisterTool?.(metadata.name);
1020
+ await navigator.modelContext.registerTool(toolDef, { signal: controller.signal });
1021
+ } catch {
1022
+ }
1023
+ }
1024
+ registeredTools.set(form, metadata.name);
1025
+ }
1026
+ async function unregisterFormTool(form) {
1027
+ if (!isWebMCPSupported())
1028
+ return;
1029
+ const name = registeredTools.get(form);
1030
+ if (!name)
1031
+ return;
1032
+ const controller = registrationControllers.get(form);
1033
+ if (controller) {
1034
+ controller.abort();
1035
+ registrationControllers.delete(form);
1036
+ }
1037
+ try {
1038
+ await navigator.modelContext.unregisterTool?.(name);
1039
+ } catch {
1040
+ }
1041
+ registeredTools.delete(form);
1042
+ }
1043
+ function getRegisteredToolName(form) {
1044
+ return registeredTools.get(form);
1045
+ }
1046
+ function getAllRegisteredTools() {
1047
+ return Array.from(registeredTools.entries()).map(([form, name]) => ({ form, name }));
1048
+ }
1049
+ async function unregisterAll() {
1050
+ const entries = Array.from(registeredTools.entries());
1051
+ await Promise.all(entries.map(([form]) => unregisterFormTool(form)));
1052
+ }
1062
1053
 
1063
1054
  // src/interceptor.ts
1064
1055
  var pendingExecutions = /* @__PURE__ */ new WeakMap();
@@ -1075,7 +1066,20 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1075
1066
  formFieldElements.set(form, metadata.fieldElements);
1076
1067
  }
1077
1068
  attachSubmitInterceptor(form, toolName);
1078
- return async (params) => {
1069
+ return async (params, client) => {
1070
+ const modelContextClient = client;
1071
+ if (config.autoSubmit && metadata?.annotations?.destructiveHint === true && typeof modelContextClient?.requestUserInteraction === "function") {
1072
+ const approved = await modelContextClient.requestUserInteraction(async () => {
1073
+ return new Promise((resolve) => {
1074
+ const ok = window.confirm(`Agent requested a destructive action via "${toolName}". Continue?`);
1075
+ resolve(ok);
1076
+ });
1077
+ });
1078
+ if (!approved) {
1079
+ window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
1080
+ return { content: [{ type: "text", text: `Cancelled "${toolName}" by user.` }] };
1081
+ }
1082
+ }
1079
1083
  pendingFillWarnings.set(form, []);
1080
1084
  pendingWarnings.delete(form);
1081
1085
  fillFormFields(form, params);
@@ -1212,7 +1216,23 @@ function findInShadowRoots(root, selector) {
1212
1216
  }
1213
1217
  return null;
1214
1218
  }
1219
+ function getAssociatedInputsByName(form, type, name) {
1220
+ return Array.from(form.elements).filter(
1221
+ (el) => el instanceof HTMLInputElement && el.type === type && el.name === name
1222
+ );
1223
+ }
1215
1224
  function findNativeField(form, key) {
1225
+ const named = form.elements.namedItem(key);
1226
+ if (typeof named === "object" && named !== null && (named instanceof HTMLInputElement || named instanceof HTMLTextAreaElement || named instanceof HTMLSelectElement)) {
1227
+ return named;
1228
+ }
1229
+ if (named instanceof RadioNodeList) {
1230
+ const first = named[0];
1231
+ const firstObj = first;
1232
+ if (firstObj instanceof HTMLInputElement || firstObj instanceof HTMLTextAreaElement || firstObj instanceof HTMLSelectElement) {
1233
+ return firstObj;
1234
+ }
1235
+ }
1216
1236
  const esc = CSS.escape(key);
1217
1237
  const light = form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
1218
1238
  `input#${esc}, textarea#${esc}, select#${esc}`
@@ -1232,10 +1252,7 @@ function fillFormFields(form, params) {
1232
1252
  fillInput(input, form, key, value);
1233
1253
  if (input.type === "checkbox") {
1234
1254
  if (Array.isArray(value)) {
1235
- const esc = CSS.escape(key);
1236
- snapshot[key] = Array.from(
1237
- form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`)
1238
- ).filter((b) => b.checked).map((b) => b.value);
1255
+ snapshot[key] = getAssociatedInputsByName(form, "checkbox", key).filter((b) => b.checked).map((b) => b.value);
1239
1256
  } else {
1240
1257
  snapshot[key] = input.checked;
1241
1258
  }
@@ -1284,8 +1301,7 @@ function fillInput(input, form, key, value) {
1284
1301
  const type = input.type.toLowerCase();
1285
1302
  if (type === "checkbox") {
1286
1303
  if (Array.isArray(value)) {
1287
- const esc = CSS.escape(key);
1288
- const allBoxes = form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`);
1304
+ const allBoxes = getAssociatedInputsByName(form, "checkbox", key);
1289
1305
  for (const box of allBoxes) {
1290
1306
  setReactChecked(box, value.map(String).includes(box.value));
1291
1307
  }
@@ -1326,10 +1342,7 @@ function fillInput(input, form, key, value) {
1326
1342
  return;
1327
1343
  }
1328
1344
  if (type === "radio") {
1329
- const esc = CSS.escape(key);
1330
- const radios = form.querySelectorAll(
1331
- `input[type="radio"][name="${esc}"]`
1332
- );
1345
+ const radios = getAssociatedInputsByName(form, "radio", key);
1333
1346
  for (const radio of radios) {
1334
1347
  if (radio.value === String(value)) {
1335
1348
  if (_checkedSetter) {
@@ -1652,9 +1665,35 @@ function isExcluded(form, config) {
1652
1665
  }
1653
1666
  return false;
1654
1667
  }
1668
+ function withNumericSuffix(baseName, n) {
1669
+ const suffix = `_${n}`;
1670
+ return `${baseName.slice(0, Math.max(1, 64 - suffix.length))}${suffix}`;
1671
+ }
1672
+ function getUsedToolNames(excludeForm) {
1673
+ const names = new Set(registeredOrphanToolNames);
1674
+ for (const { form, name } of getAllRegisteredTools()) {
1675
+ if (excludeForm && form === excludeForm)
1676
+ continue;
1677
+ names.add(name);
1678
+ }
1679
+ return names;
1680
+ }
1681
+ function ensureUniqueToolName(baseName, excludeForm) {
1682
+ const used = getUsedToolNames(excludeForm);
1683
+ if (!used.has(baseName))
1684
+ return baseName;
1685
+ let i = 2;
1686
+ let candidate = withNumericSuffix(baseName, i);
1687
+ while (used.has(candidate)) {
1688
+ i++;
1689
+ candidate = withNumericSuffix(baseName, i);
1690
+ }
1691
+ return candidate;
1692
+ }
1655
1693
  async function registerForm(form, config) {
1656
1694
  if (isExcluded(form, config))
1657
1695
  return;
1696
+ const previousName = getRegisteredToolName(form);
1658
1697
  let override;
1659
1698
  for (const [selector, ovr] of Object.entries(config.overrides)) {
1660
1699
  try {
@@ -1666,6 +1705,11 @@ async function registerForm(form, config) {
1666
1705
  }
1667
1706
  }
1668
1707
  const metadata = analyzeForm(form, override);
1708
+ const resolvedName = ensureUniqueToolName(metadata.name, form);
1709
+ if (resolvedName !== metadata.name && config.debug) {
1710
+ console.warn(`[auto-webmcp] tool name collision: "${metadata.name}" renamed to "${resolvedName}"`);
1711
+ }
1712
+ metadata.name = resolvedName;
1669
1713
  if (config.debug) {
1670
1714
  warnToolQuality(metadata.name, metadata.description);
1671
1715
  }
@@ -1677,6 +1721,9 @@ async function registerForm(form, config) {
1677
1721
  '[type="submit"], button[data-variant="primary"], button:not([type])'
1678
1722
  ) ?? null;
1679
1723
  const pendingBtns = window["__pendingSubmitBtns"] ??= {};
1724
+ if (previousName && previousName !== metadata.name) {
1725
+ delete pendingBtns[previousName];
1726
+ }
1680
1727
  pendingBtns[metadata.name] = formSubmitBtn;
1681
1728
  if (config.debug) {
1682
1729
  console.log(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
@@ -1684,12 +1731,14 @@ async function registerForm(form, config) {
1684
1731
  emit("form:registered", form, metadata.name);
1685
1732
  }
1686
1733
  async function unregisterForm(form, config) {
1687
- const { getRegisteredToolName: getRegisteredToolName2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
1688
- const name = getRegisteredToolName2(form);
1734
+ const name = getRegisteredToolName(form);
1689
1735
  if (!name)
1690
1736
  return;
1691
1737
  await unregisterFormTool(form);
1692
1738
  registeredForms.delete(form);
1739
+ const pendingBtns = window["__pendingSubmitBtns"];
1740
+ if (pendingBtns)
1741
+ delete pendingBtns[name];
1693
1742
  if (config.debug) {
1694
1743
  console.log(`[auto-webmcp] Unregistered: ${name}`);
1695
1744
  }
@@ -1738,11 +1787,47 @@ function scheduleReAnalysis(form, config) {
1738
1787
  }, RE_ANALYSIS_DEBOUNCE_MS)
1739
1788
  );
1740
1789
  }
1790
+ function scheduleFormReAnalysisById(formId, config) {
1791
+ const owner = document.getElementById(formId);
1792
+ if (owner instanceof HTMLFormElement && registeredForms.has(owner)) {
1793
+ scheduleReAnalysis(owner, config);
1794
+ }
1795
+ }
1796
+ function resolveOwnerForm(el) {
1797
+ const closest = el.closest("form");
1798
+ if (closest instanceof HTMLFormElement)
1799
+ return closest;
1800
+ const explicitOwner = el.form;
1801
+ if (explicitOwner instanceof HTMLFormElement)
1802
+ return explicitOwner;
1803
+ const formId = el.getAttribute("form");
1804
+ if (formId) {
1805
+ const byId = document.getElementById(formId);
1806
+ if (byId instanceof HTMLFormElement)
1807
+ return byId;
1808
+ }
1809
+ return null;
1810
+ }
1741
1811
  function startObserver(config) {
1742
1812
  if (observer)
1743
1813
  return;
1744
1814
  observer = new MutationObserver((mutations) => {
1745
1815
  for (const mutation of mutations) {
1816
+ if (mutation.type === "attributes" && mutation.target instanceof Element) {
1817
+ const target = mutation.target;
1818
+ const ownerForm = resolveOwnerForm(target);
1819
+ if (ownerForm && registeredForms.has(ownerForm)) {
1820
+ scheduleReAnalysis(ownerForm, config);
1821
+ } else if (target instanceof HTMLFormElement) {
1822
+ void registerForm(target, config);
1823
+ } else if (isInterestingNode(target) && !target.closest("form")) {
1824
+ scheduleOrphanRescan(config);
1825
+ }
1826
+ if (mutation.attributeName === "form" && mutation.oldValue) {
1827
+ scheduleFormReAnalysisById(mutation.oldValue, config);
1828
+ }
1829
+ continue;
1830
+ }
1746
1831
  for (const node of mutation.addedNodes) {
1747
1832
  if (!(node instanceof Element))
1748
1833
  continue;
@@ -1771,7 +1856,12 @@ function startObserver(config) {
1771
1856
  }
1772
1857
  }
1773
1858
  });
1774
- observer.observe(document.body, { childList: true, subtree: true });
1859
+ observer.observe(document.body, {
1860
+ childList: true,
1861
+ subtree: true,
1862
+ attributes: true,
1863
+ attributeOldValue: true
1864
+ });
1775
1865
  }
1776
1866
  function listenForRouteChanges(config) {
1777
1867
  window.addEventListener("hashchange", () => scanForms(config));
@@ -1915,6 +2005,15 @@ async function scanOrphanInputs(config) {
1915
2005
  }
1916
2006
  console.log(`[auto-webmcp] orphan: submit button for group:`, submitBtn ? `"${submitBtn.textContent?.trim()}" disabled=${submitBtn.disabled}` : "none");
1917
2007
  const metadata = analyzeOrphanInputGroup(container, inputs, submitBtn);
2008
+ if (registeredOrphanToolNames.has(metadata.name)) {
2009
+ console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
2010
+ continue;
2011
+ }
2012
+ const orphanName = ensureUniqueToolName(metadata.name);
2013
+ if (orphanName !== metadata.name && config.debug) {
2014
+ console.warn(`[auto-webmcp] orphan tool name collision: "${metadata.name}" renamed to "${orphanName}"`);
2015
+ }
2016
+ metadata.name = orphanName;
1918
2017
  console.log(`[auto-webmcp] orphan: tool="${metadata.name}" schema keys:`, Object.keys(metadata.inputSchema.properties));
1919
2018
  const inputPairs = [];
1920
2019
  const schemaProps = metadata.inputSchema.properties;
@@ -1935,7 +2034,7 @@ async function scanOrphanInputs(config) {
1935
2034
  continue;
1936
2035
  }
1937
2036
  const toolName = metadata.name;
1938
- const execute = async (params) => {
2037
+ const execute = async (params, _client) => {
1939
2038
  console.log(`[auto-webmcp] orphan execute: tool="${toolName}" params=`, params);
1940
2039
  console.log(`[auto-webmcp] orphan execute: inputPairs=`, inputPairs.map((p) => p.key));
1941
2040
  for (const { key, el } of inputPairs) {
@@ -2004,10 +2103,6 @@ async function scanOrphanInputs(config) {
2004
2103
  return { content: [{ type: "text", text: "Fields filled and form submitted." }] };
2005
2104
  };
2006
2105
  try {
2007
- if (registeredOrphanToolNames.has(metadata.name)) {
2008
- console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
2009
- continue;
2010
- }
2011
2106
  const toolDef = {
2012
2107
  name: metadata.name,
2013
2108
  description: metadata.description,
@@ -2058,7 +2153,6 @@ function stopDiscovery() {
2058
2153
  }
2059
2154
 
2060
2155
  // src/index.ts
2061
- init_registry();
2062
2156
  async function autoWebMCP(config) {
2063
2157
  const resolved = resolveConfig(config);
2064
2158
  if (resolved.debug) {