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.
@@ -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);
@@ -490,32 +410,39 @@ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
490
410
  const results = [];
491
411
  for (const el of Array.from(root.querySelectorAll("*"))) {
492
412
  if (el.shadowRoot) {
493
- results.push(
494
- ...Array.from(
495
- el.shadowRoot.querySelectorAll(
496
- "input, textarea, select"
497
- )
498
- ),
499
- ...collectShadowControls(el.shadowRoot, visited)
413
+ const found = Array.from(
414
+ el.shadowRoot.querySelectorAll(
415
+ "input, textarea, select"
416
+ )
500
417
  );
418
+ if (found.length > 0) {
419
+ 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}"]`));
420
+ }
421
+ results.push(...found, ...collectShadowControls(el.shadowRoot, visited));
501
422
  }
502
423
  }
503
424
  return results;
504
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
+ }
505
439
  function buildSchema(form) {
506
440
  const properties = {};
507
441
  const required = [];
508
442
  const fieldElements = /* @__PURE__ */ new Map();
509
443
  const processedRadioGroups = /* @__PURE__ */ new Set();
510
444
  const processedCheckboxGroups = /* @__PURE__ */ new Set();
511
- const controls = [
512
- ...Array.from(
513
- form.querySelectorAll(
514
- "input, textarea, select"
515
- )
516
- ),
517
- ...collectShadowControls(form)
518
- ];
445
+ const controls = collectFormAssociatedControls(form);
519
446
  for (const control of controls) {
520
447
  const name = control.name;
521
448
  const fieldKey = name || resolveNativeControlFallbackKey(control);
@@ -548,8 +475,8 @@ function buildSchema(form) {
548
475
  const radioOneOf = collectRadioOneOf(form, fieldKey);
549
476
  if (radioOneOf.length > 0)
550
477
  schemaProp.oneOf = radioOneOf;
551
- const checkedRadio = form.querySelector(
552
- `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
553
480
  );
554
481
  if (checkedRadio?.value)
555
482
  schemaProp.default = checkedRadio.value;
@@ -564,10 +491,8 @@ function buildSchema(form) {
564
491
  };
565
492
  if (schemaProp.description)
566
493
  arrayProp.description = schemaProp.description;
567
- const checkedBoxes = Array.from(
568
- form.querySelectorAll(
569
- `input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
570
- )
494
+ const checkedBoxes = Array.from(form.elements).filter(
495
+ (el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === fieldKey && el.checked
571
496
  ).map((b) => b.value);
572
497
  if (checkedBoxes.length > 0)
573
498
  arrayProp.default = checkedBoxes;
@@ -581,9 +506,23 @@ function buildSchema(form) {
581
506
  if (!name) {
582
507
  fieldElements.set(fieldKey, control);
583
508
  }
584
- if (control.required) {
585
- required.push(fieldKey);
509
+ let isRequired = control.required;
510
+ if (!isRequired) {
511
+ let hostNode = control;
512
+ while (true) {
513
+ const root = hostNode.getRootNode();
514
+ if (!(root instanceof ShadowRoot))
515
+ break;
516
+ const host = root.host;
517
+ if (host.hasAttribute("required") || host.getAttribute("aria-required") === "true") {
518
+ isRequired = true;
519
+ break;
520
+ }
521
+ hostNode = host;
522
+ }
586
523
  }
524
+ if (isRequired)
525
+ required.push(fieldKey);
587
526
  }
588
527
  const ariaControls = collectAriaControls(form);
589
528
  const processedAriaRadioGroups = /* @__PURE__ */ new Set();
@@ -626,11 +565,40 @@ function resolveNativeControlFallbackKey(control) {
626
565
  if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.placeholder?.trim()) {
627
566
  return sanitizeName(control.placeholder.trim());
628
567
  }
568
+ const hostKey = resolveShadowHostKey(control);
569
+ if (hostKey)
570
+ return hostKey;
629
571
  if (control instanceof HTMLInputElement && control.type !== "text") {
630
572
  return control.type;
631
573
  }
632
574
  return null;
633
575
  }
576
+ function resolveShadowHostKey(el) {
577
+ let node = el;
578
+ while (true) {
579
+ const root = node.getRootNode();
580
+ if (!(root instanceof ShadowRoot))
581
+ break;
582
+ const host = root.host;
583
+ const fieldName = host.getAttribute("field-name");
584
+ if (fieldName) {
585
+ console.log("[auto-webmcp] shadow host key: field-name=", fieldName);
586
+ return sanitizeName(fieldName);
587
+ }
588
+ const hostLabel = host.getAttribute("label") || host.getAttribute("aria-label");
589
+ if (hostLabel) {
590
+ console.log("[auto-webmcp] shadow host key: label=", hostLabel);
591
+ return sanitizeName(hostLabel);
592
+ }
593
+ const hostName = host.getAttribute("name");
594
+ if (hostName) {
595
+ console.log("[auto-webmcp] shadow host key: name=", hostName);
596
+ return sanitizeName(hostName);
597
+ }
598
+ node = host;
599
+ }
600
+ return null;
601
+ }
634
602
  function resolveAriaElementKey(el) {
635
603
  if (el.dataset["webmcpName"])
636
604
  return sanitizeName(el.dataset["webmcpName"]);
@@ -792,12 +760,40 @@ function getAssociatedLabelText(control) {
792
760
  return text;
793
761
  }
794
762
  }
763
+ const ownRoot = control.getRootNode();
764
+ if (ownRoot instanceof ShadowRoot) {
765
+ if (control.id) {
766
+ const shadowLabel = ownRoot.querySelector(`label[for="${CSS.escape(control.id)}"]`);
767
+ if (shadowLabel) {
768
+ const text = labelTextWithoutNested(shadowLabel);
769
+ if (text)
770
+ return text;
771
+ }
772
+ }
773
+ const anyLabel = ownRoot.querySelector("label");
774
+ if (anyLabel) {
775
+ const text = labelTextWithoutNested(anyLabel);
776
+ if (text)
777
+ return text;
778
+ }
779
+ }
795
780
  const parent = control.closest("label");
796
781
  if (parent) {
797
782
  const text = labelTextWithoutNested(parent);
798
783
  if (text)
799
784
  return text;
800
785
  }
786
+ let node = control;
787
+ while (true) {
788
+ const root = node.getRootNode();
789
+ if (!(root instanceof ShadowRoot))
790
+ break;
791
+ const host = root.host;
792
+ const hostLabel = host.getAttribute("label") || host.getAttribute("aria-label");
793
+ if (hostLabel)
794
+ return hostLabel;
795
+ node = host;
796
+ }
801
797
  return "";
802
798
  }
803
799
  function labelTextWithoutNested(label) {
@@ -966,8 +962,68 @@ function buildSchemaFromInputs(inputs) {
966
962
  return { schema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object", properties, required }, fieldElements };
967
963
  }
968
964
 
969
- // src/discovery.ts
970
- 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
+ }
971
1027
 
972
1028
  // src/interceptor.ts
973
1029
  var pendingExecutions = /* @__PURE__ */ new WeakMap();
@@ -984,7 +1040,20 @@ function buildExecuteHandler(form, config, toolName, metadata) {
984
1040
  formFieldElements.set(form, metadata.fieldElements);
985
1041
  }
986
1042
  attachSubmitInterceptor(form, toolName);
987
- 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
+ }
988
1057
  pendingFillWarnings.set(form, []);
989
1058
  pendingWarnings.delete(form);
990
1059
  fillFormFields(form, params);
@@ -1121,7 +1190,23 @@ function findInShadowRoots(root, selector) {
1121
1190
  }
1122
1191
  return null;
1123
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
+ }
1124
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
+ }
1125
1210
  const esc = CSS.escape(key);
1126
1211
  const light = form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
1127
1212
  `input#${esc}, textarea#${esc}, select#${esc}`
@@ -1141,10 +1226,7 @@ function fillFormFields(form, params) {
1141
1226
  fillInput(input, form, key, value);
1142
1227
  if (input.type === "checkbox") {
1143
1228
  if (Array.isArray(value)) {
1144
- const esc = CSS.escape(key);
1145
- snapshot[key] = Array.from(
1146
- form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`)
1147
- ).filter((b) => b.checked).map((b) => b.value);
1229
+ snapshot[key] = getAssociatedInputsByName(form, "checkbox", key).filter((b) => b.checked).map((b) => b.value);
1148
1230
  } else {
1149
1231
  snapshot[key] = input.checked;
1150
1232
  }
@@ -1193,8 +1275,7 @@ function fillInput(input, form, key, value) {
1193
1275
  const type = input.type.toLowerCase();
1194
1276
  if (type === "checkbox") {
1195
1277
  if (Array.isArray(value)) {
1196
- const esc = CSS.escape(key);
1197
- const allBoxes = form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`);
1278
+ const allBoxes = getAssociatedInputsByName(form, "checkbox", key);
1198
1279
  for (const box of allBoxes) {
1199
1280
  setReactChecked(box, value.map(String).includes(box.value));
1200
1281
  }
@@ -1235,10 +1316,7 @@ function fillInput(input, form, key, value) {
1235
1316
  return;
1236
1317
  }
1237
1318
  if (type === "radio") {
1238
- const esc = CSS.escape(key);
1239
- const radios = form.querySelectorAll(
1240
- `input[type="radio"][name="${esc}"]`
1241
- );
1319
+ const radios = getAssociatedInputsByName(form, "radio", key);
1242
1320
  for (const radio of radios) {
1243
1321
  if (radio.value === String(value)) {
1244
1322
  if (_checkedSetter) {
@@ -1497,6 +1575,51 @@ function getMissingRequired(metadata, params) {
1497
1575
  return [];
1498
1576
  return metadata.inputSchema.required.filter((fieldKey) => !(fieldKey in params));
1499
1577
  }
1578
+ async function fillComboboxButton(el, value) {
1579
+ const text = String(value ?? "").trim();
1580
+ console.log("[auto-webmcp] fillComboboxButton: clicking button, value=", JSON.stringify(text));
1581
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1582
+ const listbox = await new Promise((resolve) => {
1583
+ const deadline = Date.now() + 1e3;
1584
+ const poll = () => {
1585
+ const candidate = document.querySelector('[role="listbox"]') ?? document.querySelector('[role="option"]')?.closest('[role="listbox"]') ?? null;
1586
+ if (candidate) {
1587
+ resolve(candidate);
1588
+ return;
1589
+ }
1590
+ if (Date.now() >= deadline) {
1591
+ resolve(null);
1592
+ return;
1593
+ }
1594
+ setTimeout(poll, 50);
1595
+ };
1596
+ poll();
1597
+ });
1598
+ if (!listbox) {
1599
+ console.warn("[auto-webmcp] fillComboboxButton: listbox did not appear after 1s");
1600
+ return;
1601
+ }
1602
+ const options = Array.from(listbox.querySelectorAll('[role="option"]'));
1603
+ console.log("[auto-webmcp] fillComboboxButton: listbox has", options.length, "options");
1604
+ const lowerValue = text.toLowerCase();
1605
+ const match = options.find((opt) => {
1606
+ const dataValue = (opt.getAttribute("data-value") ?? "").toLowerCase();
1607
+ const ariaLabel = (opt.getAttribute("aria-label") ?? "").toLowerCase();
1608
+ const optText = (opt.textContent ?? "").trim().toLowerCase();
1609
+ return dataValue === lowerValue || ariaLabel === lowerValue || optText === lowerValue;
1610
+ });
1611
+ if (match) {
1612
+ console.log("[auto-webmcp] fillComboboxButton: clicking option", match.textContent?.trim());
1613
+ match.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1614
+ } else {
1615
+ console.warn(
1616
+ "[auto-webmcp] fillComboboxButton: no option matched",
1617
+ JSON.stringify(text),
1618
+ "available:",
1619
+ options.map((o) => o.textContent?.trim())
1620
+ );
1621
+ }
1622
+ }
1500
1623
 
1501
1624
  // src/discovery.ts
1502
1625
  function emit(type, form, toolName) {
@@ -1516,9 +1639,35 @@ function isExcluded(form, config) {
1516
1639
  }
1517
1640
  return false;
1518
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
+ }
1519
1667
  async function registerForm(form, config) {
1520
1668
  if (isExcluded(form, config))
1521
1669
  return;
1670
+ const previousName = getRegisteredToolName(form);
1522
1671
  let override;
1523
1672
  for (const [selector, ovr] of Object.entries(config.overrides)) {
1524
1673
  try {
@@ -1530,6 +1679,11 @@ async function registerForm(form, config) {
1530
1679
  }
1531
1680
  }
1532
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;
1533
1687
  if (config.debug) {
1534
1688
  warnToolQuality(metadata.name, metadata.description);
1535
1689
  }
@@ -1541,6 +1695,9 @@ async function registerForm(form, config) {
1541
1695
  '[type="submit"], button[data-variant="primary"], button:not([type])'
1542
1696
  ) ?? null;
1543
1697
  const pendingBtns = window["__pendingSubmitBtns"] ??= {};
1698
+ if (previousName && previousName !== metadata.name) {
1699
+ delete pendingBtns[previousName];
1700
+ }
1544
1701
  pendingBtns[metadata.name] = formSubmitBtn;
1545
1702
  if (config.debug) {
1546
1703
  console.log(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
@@ -1548,12 +1705,14 @@ async function registerForm(form, config) {
1548
1705
  emit("form:registered", form, metadata.name);
1549
1706
  }
1550
1707
  async function unregisterForm(form, config) {
1551
- const { getRegisteredToolName: getRegisteredToolName2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
1552
- const name = getRegisteredToolName2(form);
1708
+ const name = getRegisteredToolName(form);
1553
1709
  if (!name)
1554
1710
  return;
1555
1711
  await unregisterFormTool(form);
1556
1712
  registeredForms.delete(form);
1713
+ const pendingBtns = window["__pendingSubmitBtns"];
1714
+ if (pendingBtns)
1715
+ delete pendingBtns[name];
1557
1716
  if (config.debug) {
1558
1717
  console.log(`[auto-webmcp] Unregistered: ${name}`);
1559
1718
  }
@@ -1564,6 +1723,17 @@ var registeredForms = /* @__PURE__ */ new WeakSet();
1564
1723
  var registeredFormCount = 0;
1565
1724
  var reAnalysisTimers = /* @__PURE__ */ new Map();
1566
1725
  var RE_ANALYSIS_DEBOUNCE_MS = 300;
1726
+ var orphanRescanTimer = null;
1727
+ var ORPHAN_RESCAN_DEBOUNCE_MS = 500;
1728
+ var registeredOrphanToolNames = /* @__PURE__ */ new Set();
1729
+ function scheduleOrphanRescan(config) {
1730
+ if (orphanRescanTimer)
1731
+ clearTimeout(orphanRescanTimer);
1732
+ orphanRescanTimer = setTimeout(() => {
1733
+ orphanRescanTimer = null;
1734
+ void scanOrphanInputs(config);
1735
+ }, ORPHAN_RESCAN_DEBOUNCE_MS);
1736
+ }
1567
1737
  function isInterestingNode(node) {
1568
1738
  const tag = node.tagName.toLowerCase();
1569
1739
  if (tag === "input" || tag === "textarea" || tag === "select")
@@ -1591,11 +1761,47 @@ function scheduleReAnalysis(form, config) {
1591
1761
  }, RE_ANALYSIS_DEBOUNCE_MS)
1592
1762
  );
1593
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
+ }
1594
1785
  function startObserver(config) {
1595
1786
  if (observer)
1596
1787
  return;
1597
1788
  observer = new MutationObserver((mutations) => {
1598
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
+ }
1599
1805
  for (const node of mutation.addedNodes) {
1600
1806
  if (!(node instanceof Element))
1601
1807
  continue;
@@ -1610,6 +1816,9 @@ function startObserver(config) {
1610
1816
  for (const form of Array.from(node.querySelectorAll("form"))) {
1611
1817
  void registerForm(form, config);
1612
1818
  }
1819
+ if (isInterestingNode(node) && !node.closest("form")) {
1820
+ scheduleOrphanRescan(config);
1821
+ }
1613
1822
  }
1614
1823
  for (const node of mutation.removedNodes) {
1615
1824
  if (!(node instanceof Element))
@@ -1621,7 +1830,12 @@ function startObserver(config) {
1621
1830
  }
1622
1831
  }
1623
1832
  });
1624
- 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
+ });
1625
1839
  }
1626
1840
  function listenForRouteChanges(config) {
1627
1841
  window.addEventListener("hashchange", () => scanForms(config));
@@ -1657,10 +1871,10 @@ async function scanOrphanInputs(config) {
1657
1871
  return;
1658
1872
  const SUBMIT_BTN_SELECTOR = '[type="submit"]:not([disabled]), button[data-variant="primary"]:not([disabled])';
1659
1873
  const SUBMIT_BTN_GROUPING_SELECTOR = '[type="submit"], button[data-variant="primary"]';
1660
- const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search|post|tweet|publish/i;
1874
+ const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search|post|tweet|publish|save/i;
1661
1875
  const orphanInputs = Array.from(
1662
1876
  document.querySelectorAll(
1663
- '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)'
1877
+ '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"])'
1664
1878
  )
1665
1879
  ).filter((el) => {
1666
1880
  if (el instanceof HTMLInputElement && ORPHAN_EXCLUDED_TYPES.has(el.type.toLowerCase())) {
@@ -1765,6 +1979,15 @@ async function scanOrphanInputs(config) {
1765
1979
  }
1766
1980
  console.log(`[auto-webmcp] orphan: submit button for group:`, submitBtn ? `"${submitBtn.textContent?.trim()}" disabled=${submitBtn.disabled}` : "none");
1767
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;
1768
1991
  console.log(`[auto-webmcp] orphan: tool="${metadata.name}" schema keys:`, Object.keys(metadata.inputSchema.properties));
1769
1992
  const inputPairs = [];
1770
1993
  const schemaProps = metadata.inputSchema.properties;
@@ -1785,13 +2008,17 @@ async function scanOrphanInputs(config) {
1785
2008
  continue;
1786
2009
  }
1787
2010
  const toolName = metadata.name;
1788
- const execute = async (params) => {
2011
+ const execute = async (params, _client) => {
1789
2012
  console.log(`[auto-webmcp] orphan execute: tool="${toolName}" params=`, params);
1790
2013
  console.log(`[auto-webmcp] orphan execute: inputPairs=`, inputPairs.map((p) => p.key));
1791
2014
  for (const { key, el } of inputPairs) {
1792
2015
  if (params[key] !== void 0) {
1793
2016
  console.log(`[auto-webmcp] orphan execute: filling key="${key}" value=`, params[key], "element=", el);
1794
- fillElement(el, params[key]);
2017
+ if (el.getAttribute("role") === "combobox" && el.tagName.toLowerCase() === "button") {
2018
+ await fillComboboxButton(el, params[key]);
2019
+ } else {
2020
+ fillElement(el, params[key]);
2021
+ }
1795
2022
  console.log(`[auto-webmcp] orphan execute: after fill, element value=`, el.value);
1796
2023
  } else {
1797
2024
  console.log(`[auto-webmcp] orphan execute: key="${key}" not in params, skipping`);
@@ -1803,22 +2030,43 @@ async function scanOrphanInputs(config) {
1803
2030
  console.log(`[auto-webmcp] orphan execute: autoSubmit=false, returning without clicking submit`);
1804
2031
  return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
1805
2032
  }
1806
- console.log(`[auto-webmcp] orphan execute: polling for enabled submit button (up to 2s)...`);
2033
+ console.log(`[auto-webmcp] orphan execute: resolving submit button (up to 2s)...`);
1807
2034
  let btn = null;
1808
- const deadline = Date.now() + 2e3;
1809
- while (Date.now() < deadline) {
1810
- const candidates = Array.from(
1811
- container.querySelectorAll(SUBMIT_BTN_SELECTOR)
2035
+ if (submitBtn && document.contains(submitBtn)) {
2036
+ const isEnabled = !submitBtn.disabled && submitBtn.getAttribute("aria-disabled") !== "true";
2037
+ const r = submitBtn.getBoundingClientRect();
2038
+ if (isEnabled && r.width > 0 && r.height > 0) {
2039
+ btn = submitBtn;
2040
+ console.log(`[auto-webmcp] orphan execute: using captured submit button "${btn.textContent?.trim()}"`);
2041
+ }
2042
+ }
2043
+ if (!btn) {
2044
+ const deadline = Date.now() + 2e3;
2045
+ while (Date.now() < deadline) {
2046
+ const candidates = Array.from(
2047
+ container.querySelectorAll(SUBMIT_BTN_SELECTOR)
2048
+ ).filter((b) => {
2049
+ const r = b.getBoundingClientRect();
2050
+ return r.width > 0 && r.height > 0;
2051
+ });
2052
+ const last = candidates[candidates.length - 1] ?? null;
2053
+ if (last) {
2054
+ btn = last;
2055
+ break;
2056
+ }
2057
+ await new Promise((r) => setTimeout(r, 100));
2058
+ }
2059
+ }
2060
+ if (!btn) {
2061
+ const textBtns = Array.from(
2062
+ (container !== document.body ? container : document).querySelectorAll('button, [role="button"]')
1812
2063
  ).filter((b) => {
1813
2064
  const r = b.getBoundingClientRect();
1814
- return r.width > 0 && r.height > 0;
2065
+ return r.width > 0 && r.height > 0 && !b.disabled && b.getAttribute("aria-disabled") !== "true" && SUBMIT_TEXT_RE.test(b.textContent ?? "");
1815
2066
  });
1816
- const last = candidates[candidates.length - 1] ?? null;
1817
- if (last) {
1818
- btn = last;
1819
- break;
1820
- }
1821
- await new Promise((r) => setTimeout(r, 100));
2067
+ btn = textBtns[textBtns.length - 1] ?? null;
2068
+ if (btn)
2069
+ console.log(`[auto-webmcp] orphan execute: using text-matched fallback button "${btn.textContent?.trim()}"`);
1822
2070
  }
1823
2071
  if (!btn) {
1824
2072
  console.warn(`[auto-webmcp] orphan execute: submit button still disabled after 2s`);
@@ -1839,6 +2087,7 @@ async function scanOrphanInputs(config) {
1839
2087
  toolDef.annotations = metadata.annotations;
1840
2088
  }
1841
2089
  await navigator.modelContext.registerTool(toolDef);
2090
+ registeredOrphanToolNames.add(metadata.name);
1842
2091
  const pendingBtns = window["__pendingSubmitBtns"] ??= {};
1843
2092
  pendingBtns[metadata.name] = submitBtn;
1844
2093
  if (config.debug) {
@@ -1866,6 +2115,7 @@ async function startDiscovery(config) {
1866
2115
  );
1867
2116
  }
1868
2117
  registeredFormCount = 0;
2118
+ registeredOrphanToolNames.clear();
1869
2119
  startObserver(config);
1870
2120
  listenForRouteChanges(config);
1871
2121
  await scanForms(config);
@@ -1877,7 +2127,6 @@ function stopDiscovery() {
1877
2127
  }
1878
2128
 
1879
2129
  // src/index.ts
1880
- init_registry();
1881
2130
  async function autoWebMCP(config) {
1882
2131
  const resolved = resolveConfig(config);
1883
2132
  if (resolved.debug) {