auto-webmcp 0.3.14 → 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.
@@ -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);
@@ -509,32 +436,39 @@ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
509
436
  const results = [];
510
437
  for (const el of Array.from(root.querySelectorAll("*"))) {
511
438
  if (el.shadowRoot) {
512
- results.push(
513
- ...Array.from(
514
- el.shadowRoot.querySelectorAll(
515
- "input, textarea, select"
516
- )
517
- ),
518
- ...collectShadowControls(el.shadowRoot, visited)
439
+ const found = Array.from(
440
+ el.shadowRoot.querySelectorAll(
441
+ "input, textarea, select"
442
+ )
519
443
  );
444
+ if (found.length > 0) {
445
+ console.log(`[auto-webmcp] shadow: found ${found.length} control(s) in ${el.tagName.toLowerCase()} shadow root:`, found.map((f) => `${f.tagName.toLowerCase()}[type=${f.type ?? "?"}][name="${f.name}"][id="${f.id}"]`));
446
+ }
447
+ results.push(...found, ...collectShadowControls(el.shadowRoot, visited));
520
448
  }
521
449
  }
522
450
  return results;
523
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
+ }
524
465
  function buildSchema(form) {
525
466
  const properties = {};
526
467
  const required = [];
527
468
  const fieldElements = /* @__PURE__ */ new Map();
528
469
  const processedRadioGroups = /* @__PURE__ */ new Set();
529
470
  const processedCheckboxGroups = /* @__PURE__ */ new Set();
530
- const controls = [
531
- ...Array.from(
532
- form.querySelectorAll(
533
- "input, textarea, select"
534
- )
535
- ),
536
- ...collectShadowControls(form)
537
- ];
471
+ const controls = collectFormAssociatedControls(form);
538
472
  for (const control of controls) {
539
473
  const name = control.name;
540
474
  const fieldKey = name || resolveNativeControlFallbackKey(control);
@@ -567,8 +501,8 @@ function buildSchema(form) {
567
501
  const radioOneOf = collectRadioOneOf(form, fieldKey);
568
502
  if (radioOneOf.length > 0)
569
503
  schemaProp.oneOf = radioOneOf;
570
- const checkedRadio = form.querySelector(
571
- `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
572
506
  );
573
507
  if (checkedRadio?.value)
574
508
  schemaProp.default = checkedRadio.value;
@@ -583,10 +517,8 @@ function buildSchema(form) {
583
517
  };
584
518
  if (schemaProp.description)
585
519
  arrayProp.description = schemaProp.description;
586
- const checkedBoxes = Array.from(
587
- form.querySelectorAll(
588
- `input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
589
- )
520
+ const checkedBoxes = Array.from(form.elements).filter(
521
+ (el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === fieldKey && el.checked
590
522
  ).map((b) => b.value);
591
523
  if (checkedBoxes.length > 0)
592
524
  arrayProp.default = checkedBoxes;
@@ -600,9 +532,23 @@ function buildSchema(form) {
600
532
  if (!name) {
601
533
  fieldElements.set(fieldKey, control);
602
534
  }
603
- if (control.required) {
604
- required.push(fieldKey);
535
+ let isRequired = control.required;
536
+ if (!isRequired) {
537
+ let hostNode = control;
538
+ while (true) {
539
+ const root = hostNode.getRootNode();
540
+ if (!(root instanceof ShadowRoot))
541
+ break;
542
+ const host = root.host;
543
+ if (host.hasAttribute("required") || host.getAttribute("aria-required") === "true") {
544
+ isRequired = true;
545
+ break;
546
+ }
547
+ hostNode = host;
548
+ }
605
549
  }
550
+ if (isRequired)
551
+ required.push(fieldKey);
606
552
  }
607
553
  const ariaControls = collectAriaControls(form);
608
554
  const processedAriaRadioGroups = /* @__PURE__ */ new Set();
@@ -645,11 +591,40 @@ function resolveNativeControlFallbackKey(control) {
645
591
  if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.placeholder?.trim()) {
646
592
  return sanitizeName(control.placeholder.trim());
647
593
  }
594
+ const hostKey = resolveShadowHostKey(control);
595
+ if (hostKey)
596
+ return hostKey;
648
597
  if (control instanceof HTMLInputElement && control.type !== "text") {
649
598
  return control.type;
650
599
  }
651
600
  return null;
652
601
  }
602
+ function resolveShadowHostKey(el) {
603
+ let node = el;
604
+ while (true) {
605
+ const root = node.getRootNode();
606
+ if (!(root instanceof ShadowRoot))
607
+ break;
608
+ const host = root.host;
609
+ const fieldName = host.getAttribute("field-name");
610
+ if (fieldName) {
611
+ console.log("[auto-webmcp] shadow host key: field-name=", fieldName);
612
+ return sanitizeName(fieldName);
613
+ }
614
+ const hostLabel = host.getAttribute("label") || host.getAttribute("aria-label");
615
+ if (hostLabel) {
616
+ console.log("[auto-webmcp] shadow host key: label=", hostLabel);
617
+ return sanitizeName(hostLabel);
618
+ }
619
+ const hostName = host.getAttribute("name");
620
+ if (hostName) {
621
+ console.log("[auto-webmcp] shadow host key: name=", hostName);
622
+ return sanitizeName(hostName);
623
+ }
624
+ node = host;
625
+ }
626
+ return null;
627
+ }
653
628
  function resolveAriaElementKey(el) {
654
629
  if (el.dataset["webmcpName"])
655
630
  return sanitizeName(el.dataset["webmcpName"]);
@@ -811,12 +786,40 @@ function getAssociatedLabelText(control) {
811
786
  return text;
812
787
  }
813
788
  }
789
+ const ownRoot = control.getRootNode();
790
+ if (ownRoot instanceof ShadowRoot) {
791
+ if (control.id) {
792
+ const shadowLabel = ownRoot.querySelector(`label[for="${CSS.escape(control.id)}"]`);
793
+ if (shadowLabel) {
794
+ const text = labelTextWithoutNested(shadowLabel);
795
+ if (text)
796
+ return text;
797
+ }
798
+ }
799
+ const anyLabel = ownRoot.querySelector("label");
800
+ if (anyLabel) {
801
+ const text = labelTextWithoutNested(anyLabel);
802
+ if (text)
803
+ return text;
804
+ }
805
+ }
814
806
  const parent = control.closest("label");
815
807
  if (parent) {
816
808
  const text = labelTextWithoutNested(parent);
817
809
  if (text)
818
810
  return text;
819
811
  }
812
+ let node = control;
813
+ while (true) {
814
+ const root = node.getRootNode();
815
+ if (!(root instanceof ShadowRoot))
816
+ break;
817
+ const host = root.host;
818
+ const hostLabel = host.getAttribute("label") || host.getAttribute("aria-label");
819
+ if (hostLabel)
820
+ return hostLabel;
821
+ node = host;
822
+ }
820
823
  return "";
821
824
  }
822
825
  function labelTextWithoutNested(label) {
@@ -985,8 +988,68 @@ function buildSchemaFromInputs(inputs) {
985
988
  return { schema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object", properties, required }, fieldElements };
986
989
  }
987
990
 
988
- // src/discovery.ts
989
- 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
+ }
990
1053
 
991
1054
  // src/interceptor.ts
992
1055
  var pendingExecutions = /* @__PURE__ */ new WeakMap();
@@ -1003,7 +1066,20 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1003
1066
  formFieldElements.set(form, metadata.fieldElements);
1004
1067
  }
1005
1068
  attachSubmitInterceptor(form, toolName);
1006
- 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
+ }
1007
1083
  pendingFillWarnings.set(form, []);
1008
1084
  pendingWarnings.delete(form);
1009
1085
  fillFormFields(form, params);
@@ -1140,7 +1216,23 @@ function findInShadowRoots(root, selector) {
1140
1216
  }
1141
1217
  return null;
1142
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
+ }
1143
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
+ }
1144
1236
  const esc = CSS.escape(key);
1145
1237
  const light = form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
1146
1238
  `input#${esc}, textarea#${esc}, select#${esc}`
@@ -1160,10 +1252,7 @@ function fillFormFields(form, params) {
1160
1252
  fillInput(input, form, key, value);
1161
1253
  if (input.type === "checkbox") {
1162
1254
  if (Array.isArray(value)) {
1163
- const esc = CSS.escape(key);
1164
- snapshot[key] = Array.from(
1165
- form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`)
1166
- ).filter((b) => b.checked).map((b) => b.value);
1255
+ snapshot[key] = getAssociatedInputsByName(form, "checkbox", key).filter((b) => b.checked).map((b) => b.value);
1167
1256
  } else {
1168
1257
  snapshot[key] = input.checked;
1169
1258
  }
@@ -1212,8 +1301,7 @@ function fillInput(input, form, key, value) {
1212
1301
  const type = input.type.toLowerCase();
1213
1302
  if (type === "checkbox") {
1214
1303
  if (Array.isArray(value)) {
1215
- const esc = CSS.escape(key);
1216
- const allBoxes = form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`);
1304
+ const allBoxes = getAssociatedInputsByName(form, "checkbox", key);
1217
1305
  for (const box of allBoxes) {
1218
1306
  setReactChecked(box, value.map(String).includes(box.value));
1219
1307
  }
@@ -1254,10 +1342,7 @@ function fillInput(input, form, key, value) {
1254
1342
  return;
1255
1343
  }
1256
1344
  if (type === "radio") {
1257
- const esc = CSS.escape(key);
1258
- const radios = form.querySelectorAll(
1259
- `input[type="radio"][name="${esc}"]`
1260
- );
1345
+ const radios = getAssociatedInputsByName(form, "radio", key);
1261
1346
  for (const radio of radios) {
1262
1347
  if (radio.value === String(value)) {
1263
1348
  if (_checkedSetter) {
@@ -1516,6 +1601,51 @@ function getMissingRequired(metadata, params) {
1516
1601
  return [];
1517
1602
  return metadata.inputSchema.required.filter((fieldKey) => !(fieldKey in params));
1518
1603
  }
1604
+ async function fillComboboxButton(el, value) {
1605
+ const text = String(value ?? "").trim();
1606
+ console.log("[auto-webmcp] fillComboboxButton: clicking button, value=", JSON.stringify(text));
1607
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1608
+ const listbox = await new Promise((resolve) => {
1609
+ const deadline = Date.now() + 1e3;
1610
+ const poll = () => {
1611
+ const candidate = document.querySelector('[role="listbox"]') ?? document.querySelector('[role="option"]')?.closest('[role="listbox"]') ?? null;
1612
+ if (candidate) {
1613
+ resolve(candidate);
1614
+ return;
1615
+ }
1616
+ if (Date.now() >= deadline) {
1617
+ resolve(null);
1618
+ return;
1619
+ }
1620
+ setTimeout(poll, 50);
1621
+ };
1622
+ poll();
1623
+ });
1624
+ if (!listbox) {
1625
+ console.warn("[auto-webmcp] fillComboboxButton: listbox did not appear after 1s");
1626
+ return;
1627
+ }
1628
+ const options = Array.from(listbox.querySelectorAll('[role="option"]'));
1629
+ console.log("[auto-webmcp] fillComboboxButton: listbox has", options.length, "options");
1630
+ const lowerValue = text.toLowerCase();
1631
+ const match = options.find((opt) => {
1632
+ const dataValue = (opt.getAttribute("data-value") ?? "").toLowerCase();
1633
+ const ariaLabel = (opt.getAttribute("aria-label") ?? "").toLowerCase();
1634
+ const optText = (opt.textContent ?? "").trim().toLowerCase();
1635
+ return dataValue === lowerValue || ariaLabel === lowerValue || optText === lowerValue;
1636
+ });
1637
+ if (match) {
1638
+ console.log("[auto-webmcp] fillComboboxButton: clicking option", match.textContent?.trim());
1639
+ match.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1640
+ } else {
1641
+ console.warn(
1642
+ "[auto-webmcp] fillComboboxButton: no option matched",
1643
+ JSON.stringify(text),
1644
+ "available:",
1645
+ options.map((o) => o.textContent?.trim())
1646
+ );
1647
+ }
1648
+ }
1519
1649
 
1520
1650
  // src/discovery.ts
1521
1651
  function emit(type, form, toolName) {
@@ -1535,9 +1665,35 @@ function isExcluded(form, config) {
1535
1665
  }
1536
1666
  return false;
1537
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
+ }
1538
1693
  async function registerForm(form, config) {
1539
1694
  if (isExcluded(form, config))
1540
1695
  return;
1696
+ const previousName = getRegisteredToolName(form);
1541
1697
  let override;
1542
1698
  for (const [selector, ovr] of Object.entries(config.overrides)) {
1543
1699
  try {
@@ -1549,6 +1705,11 @@ async function registerForm(form, config) {
1549
1705
  }
1550
1706
  }
1551
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;
1552
1713
  if (config.debug) {
1553
1714
  warnToolQuality(metadata.name, metadata.description);
1554
1715
  }
@@ -1560,6 +1721,9 @@ async function registerForm(form, config) {
1560
1721
  '[type="submit"], button[data-variant="primary"], button:not([type])'
1561
1722
  ) ?? null;
1562
1723
  const pendingBtns = window["__pendingSubmitBtns"] ??= {};
1724
+ if (previousName && previousName !== metadata.name) {
1725
+ delete pendingBtns[previousName];
1726
+ }
1563
1727
  pendingBtns[metadata.name] = formSubmitBtn;
1564
1728
  if (config.debug) {
1565
1729
  console.log(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
@@ -1567,12 +1731,14 @@ async function registerForm(form, config) {
1567
1731
  emit("form:registered", form, metadata.name);
1568
1732
  }
1569
1733
  async function unregisterForm(form, config) {
1570
- const { getRegisteredToolName: getRegisteredToolName2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
1571
- const name = getRegisteredToolName2(form);
1734
+ const name = getRegisteredToolName(form);
1572
1735
  if (!name)
1573
1736
  return;
1574
1737
  await unregisterFormTool(form);
1575
1738
  registeredForms.delete(form);
1739
+ const pendingBtns = window["__pendingSubmitBtns"];
1740
+ if (pendingBtns)
1741
+ delete pendingBtns[name];
1576
1742
  if (config.debug) {
1577
1743
  console.log(`[auto-webmcp] Unregistered: ${name}`);
1578
1744
  }
@@ -1583,6 +1749,17 @@ var registeredForms = /* @__PURE__ */ new WeakSet();
1583
1749
  var registeredFormCount = 0;
1584
1750
  var reAnalysisTimers = /* @__PURE__ */ new Map();
1585
1751
  var RE_ANALYSIS_DEBOUNCE_MS = 300;
1752
+ var orphanRescanTimer = null;
1753
+ var ORPHAN_RESCAN_DEBOUNCE_MS = 500;
1754
+ var registeredOrphanToolNames = /* @__PURE__ */ new Set();
1755
+ function scheduleOrphanRescan(config) {
1756
+ if (orphanRescanTimer)
1757
+ clearTimeout(orphanRescanTimer);
1758
+ orphanRescanTimer = setTimeout(() => {
1759
+ orphanRescanTimer = null;
1760
+ void scanOrphanInputs(config);
1761
+ }, ORPHAN_RESCAN_DEBOUNCE_MS);
1762
+ }
1586
1763
  function isInterestingNode(node) {
1587
1764
  const tag = node.tagName.toLowerCase();
1588
1765
  if (tag === "input" || tag === "textarea" || tag === "select")
@@ -1610,11 +1787,47 @@ function scheduleReAnalysis(form, config) {
1610
1787
  }, RE_ANALYSIS_DEBOUNCE_MS)
1611
1788
  );
1612
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
+ }
1613
1811
  function startObserver(config) {
1614
1812
  if (observer)
1615
1813
  return;
1616
1814
  observer = new MutationObserver((mutations) => {
1617
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
+ }
1618
1831
  for (const node of mutation.addedNodes) {
1619
1832
  if (!(node instanceof Element))
1620
1833
  continue;
@@ -1629,6 +1842,9 @@ function startObserver(config) {
1629
1842
  for (const form of Array.from(node.querySelectorAll("form"))) {
1630
1843
  void registerForm(form, config);
1631
1844
  }
1845
+ if (isInterestingNode(node) && !node.closest("form")) {
1846
+ scheduleOrphanRescan(config);
1847
+ }
1632
1848
  }
1633
1849
  for (const node of mutation.removedNodes) {
1634
1850
  if (!(node instanceof Element))
@@ -1640,7 +1856,12 @@ function startObserver(config) {
1640
1856
  }
1641
1857
  }
1642
1858
  });
1643
- 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
+ });
1644
1865
  }
1645
1866
  function listenForRouteChanges(config) {
1646
1867
  window.addEventListener("hashchange", () => scanForms(config));
@@ -1676,10 +1897,10 @@ async function scanOrphanInputs(config) {
1676
1897
  return;
1677
1898
  const SUBMIT_BTN_SELECTOR = '[type="submit"]:not([disabled]), button[data-variant="primary"]:not([disabled])';
1678
1899
  const SUBMIT_BTN_GROUPING_SELECTOR = '[type="submit"], button[data-variant="primary"]';
1679
- const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search|post|tweet|publish/i;
1900
+ const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search|post|tweet|publish|save/i;
1680
1901
  const orphanInputs = Array.from(
1681
1902
  document.querySelectorAll(
1682
- 'input:not(form input), textarea:not(form textarea), select:not(form select), [role="textbox"]:not(form [role="textbox"]):not(input):not(textarea), [role="searchbox"]:not(form [role="searchbox"]):not(input):not(textarea), [contenteditable="true"]:not(form [contenteditable="true"]):not(input):not(textarea)'
1903
+ 'input:not(form input), textarea:not(form textarea), select:not(form select), [role="textbox"]:not(form [role="textbox"]):not(input):not(textarea), [role="searchbox"]:not(form [role="searchbox"]):not(input):not(textarea), [contenteditable="true"]:not(form [contenteditable="true"]):not(input):not(textarea), button[role="combobox"]:not(form button[role="combobox"])'
1683
1904
  )
1684
1905
  ).filter((el) => {
1685
1906
  if (el instanceof HTMLInputElement && ORPHAN_EXCLUDED_TYPES.has(el.type.toLowerCase())) {
@@ -1784,6 +2005,15 @@ async function scanOrphanInputs(config) {
1784
2005
  }
1785
2006
  console.log(`[auto-webmcp] orphan: submit button for group:`, submitBtn ? `"${submitBtn.textContent?.trim()}" disabled=${submitBtn.disabled}` : "none");
1786
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;
1787
2017
  console.log(`[auto-webmcp] orphan: tool="${metadata.name}" schema keys:`, Object.keys(metadata.inputSchema.properties));
1788
2018
  const inputPairs = [];
1789
2019
  const schemaProps = metadata.inputSchema.properties;
@@ -1804,13 +2034,17 @@ async function scanOrphanInputs(config) {
1804
2034
  continue;
1805
2035
  }
1806
2036
  const toolName = metadata.name;
1807
- const execute = async (params) => {
2037
+ const execute = async (params, _client) => {
1808
2038
  console.log(`[auto-webmcp] orphan execute: tool="${toolName}" params=`, params);
1809
2039
  console.log(`[auto-webmcp] orphan execute: inputPairs=`, inputPairs.map((p) => p.key));
1810
2040
  for (const { key, el } of inputPairs) {
1811
2041
  if (params[key] !== void 0) {
1812
2042
  console.log(`[auto-webmcp] orphan execute: filling key="${key}" value=`, params[key], "element=", el);
1813
- fillElement(el, params[key]);
2043
+ if (el.getAttribute("role") === "combobox" && el.tagName.toLowerCase() === "button") {
2044
+ await fillComboboxButton(el, params[key]);
2045
+ } else {
2046
+ fillElement(el, params[key]);
2047
+ }
1814
2048
  console.log(`[auto-webmcp] orphan execute: after fill, element value=`, el.value);
1815
2049
  } else {
1816
2050
  console.log(`[auto-webmcp] orphan execute: key="${key}" not in params, skipping`);
@@ -1822,22 +2056,43 @@ async function scanOrphanInputs(config) {
1822
2056
  console.log(`[auto-webmcp] orphan execute: autoSubmit=false, returning without clicking submit`);
1823
2057
  return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
1824
2058
  }
1825
- console.log(`[auto-webmcp] orphan execute: polling for enabled submit button (up to 2s)...`);
2059
+ console.log(`[auto-webmcp] orphan execute: resolving submit button (up to 2s)...`);
1826
2060
  let btn = null;
1827
- const deadline = Date.now() + 2e3;
1828
- while (Date.now() < deadline) {
1829
- const candidates = Array.from(
1830
- container.querySelectorAll(SUBMIT_BTN_SELECTOR)
2061
+ if (submitBtn && document.contains(submitBtn)) {
2062
+ const isEnabled = !submitBtn.disabled && submitBtn.getAttribute("aria-disabled") !== "true";
2063
+ const r = submitBtn.getBoundingClientRect();
2064
+ if (isEnabled && r.width > 0 && r.height > 0) {
2065
+ btn = submitBtn;
2066
+ console.log(`[auto-webmcp] orphan execute: using captured submit button "${btn.textContent?.trim()}"`);
2067
+ }
2068
+ }
2069
+ if (!btn) {
2070
+ const deadline = Date.now() + 2e3;
2071
+ while (Date.now() < deadline) {
2072
+ const candidates = Array.from(
2073
+ container.querySelectorAll(SUBMIT_BTN_SELECTOR)
2074
+ ).filter((b) => {
2075
+ const r = b.getBoundingClientRect();
2076
+ return r.width > 0 && r.height > 0;
2077
+ });
2078
+ const last = candidates[candidates.length - 1] ?? null;
2079
+ if (last) {
2080
+ btn = last;
2081
+ break;
2082
+ }
2083
+ await new Promise((r) => setTimeout(r, 100));
2084
+ }
2085
+ }
2086
+ if (!btn) {
2087
+ const textBtns = Array.from(
2088
+ (container !== document.body ? container : document).querySelectorAll('button, [role="button"]')
1831
2089
  ).filter((b) => {
1832
2090
  const r = b.getBoundingClientRect();
1833
- return r.width > 0 && r.height > 0;
2091
+ return r.width > 0 && r.height > 0 && !b.disabled && b.getAttribute("aria-disabled") !== "true" && SUBMIT_TEXT_RE.test(b.textContent ?? "");
1834
2092
  });
1835
- const last = candidates[candidates.length - 1] ?? null;
1836
- if (last) {
1837
- btn = last;
1838
- break;
1839
- }
1840
- await new Promise((r) => setTimeout(r, 100));
2093
+ btn = textBtns[textBtns.length - 1] ?? null;
2094
+ if (btn)
2095
+ console.log(`[auto-webmcp] orphan execute: using text-matched fallback button "${btn.textContent?.trim()}"`);
1841
2096
  }
1842
2097
  if (!btn) {
1843
2098
  console.warn(`[auto-webmcp] orphan execute: submit button still disabled after 2s`);
@@ -1858,6 +2113,7 @@ async function scanOrphanInputs(config) {
1858
2113
  toolDef.annotations = metadata.annotations;
1859
2114
  }
1860
2115
  await navigator.modelContext.registerTool(toolDef);
2116
+ registeredOrphanToolNames.add(metadata.name);
1861
2117
  const pendingBtns = window["__pendingSubmitBtns"] ??= {};
1862
2118
  pendingBtns[metadata.name] = submitBtn;
1863
2119
  if (config.debug) {
@@ -1885,6 +2141,7 @@ async function startDiscovery(config) {
1885
2141
  );
1886
2142
  }
1887
2143
  registeredFormCount = 0;
2144
+ registeredOrphanToolNames.clear();
1888
2145
  startObserver(config);
1889
2146
  listenForRouteChanges(config);
1890
2147
  await scanForms(config);
@@ -1896,7 +2153,6 @@ function stopDiscovery() {
1896
2153
  }
1897
2154
 
1898
2155
  // src/index.ts
1899
- init_registry();
1900
2156
  async function autoWebMCP(config) {
1901
2157
  const resolved = resolveConfig(config);
1902
2158
  if (resolved.debug) {