auto-webmcp 0.2.4 → 0.2.6

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.
@@ -14,4 +14,9 @@ export interface ToolMetadata {
14
14
  export declare function resetFormIndex(): void;
15
15
  /** Derive ToolMetadata from a <form> element */
16
16
  export declare function analyzeForm(form: HTMLFormElement, override?: FormOverride): ToolMetadata;
17
+ /**
18
+ * Derive ToolMetadata from a group of form controls that are NOT inside a <form>.
19
+ * Used by discovery.ts's orphan-input scanner for pages like newsletter landing pages.
20
+ */
21
+ export declare function analyzeOrphanInputGroup(container: Element, inputs: Array<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>, submitBtn: HTMLButtonElement | HTMLInputElement | null): ToolMetadata;
17
22
  //# sourceMappingURL=analyzer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAA8H,MAAM,aAAa,CAAC;AACrK,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,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,CAMxF"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAA8H,MAAM,aAAa,CAAC;AACrK,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,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,CAMxF;AAkaD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,CAAC,EACzE,SAAS,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,GACrD,YAAY,CAKd"}
@@ -454,6 +454,12 @@ function resolveNativeControlFallbackKey(control) {
454
454
  const label = control.getAttribute("aria-label");
455
455
  if (label)
456
456
  return sanitizeName(label);
457
+ if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.placeholder?.trim()) {
458
+ return sanitizeName(control.placeholder.trim());
459
+ }
460
+ if (control instanceof HTMLInputElement && control.type !== "text") {
461
+ return control.type;
462
+ }
457
463
  return null;
458
464
  }
459
465
  function collectAriaControls(form) {
@@ -538,6 +544,9 @@ function inferFieldTitle(control) {
538
544
  return humanizeName(control.name);
539
545
  if (control.id)
540
546
  return humanizeName(control.id);
547
+ if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.placeholder?.trim()) {
548
+ return control.placeholder.trim();
549
+ }
541
550
  return "";
542
551
  }
543
552
  function inferFieldDescription(control) {
@@ -588,6 +597,88 @@ function labelTextWithoutNested(label) {
588
597
  function humanizeName(raw) {
589
598
  return raw.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().replace(/\b\w/g, (c) => c.toUpperCase());
590
599
  }
600
+ function analyzeOrphanInputGroup(container, inputs, submitBtn) {
601
+ const name = inferOrphanToolName(container, submitBtn);
602
+ const description = inferOrphanToolDescription(container);
603
+ const { schema: inputSchema, fieldElements } = buildSchemaFromInputs(inputs);
604
+ return { name, description, inputSchema, fieldElements };
605
+ }
606
+ function inferOrphanToolName(container, submitBtn) {
607
+ if (submitBtn) {
608
+ const text = submitBtn instanceof HTMLInputElement ? submitBtn.value.trim() : submitBtn.textContent?.trim() ?? "";
609
+ if (text && text.length > 0 && text.length < 80)
610
+ return sanitizeName(text);
611
+ }
612
+ const heading = getNearestHeadingTextFrom(container);
613
+ if (heading)
614
+ return sanitizeName(heading);
615
+ const title = document.title?.trim();
616
+ if (title)
617
+ return sanitizeName(title);
618
+ return `form_${++formIndex}`;
619
+ }
620
+ function inferOrphanToolDescription(container) {
621
+ const heading = getNearestHeadingTextFrom(container);
622
+ const pageTitle = document.title?.trim();
623
+ if (heading && pageTitle && heading !== pageTitle)
624
+ return `${heading} on ${pageTitle}`;
625
+ if (heading)
626
+ return heading;
627
+ if (pageTitle)
628
+ return pageTitle;
629
+ return "Submit form";
630
+ }
631
+ function getNearestHeadingTextFrom(el) {
632
+ const inner = el.querySelector("h1, h2, h3");
633
+ if (inner?.textContent?.trim())
634
+ return inner.textContent.trim();
635
+ let node = el;
636
+ while (node) {
637
+ let sibling = node.previousElementSibling;
638
+ while (sibling) {
639
+ if (/^H[1-3]$/i.test(sibling.tagName)) {
640
+ const text = sibling.textContent?.trim() ?? "";
641
+ if (text)
642
+ return text;
643
+ }
644
+ sibling = sibling.previousElementSibling;
645
+ }
646
+ node = node.parentElement;
647
+ if (!node || node === document.body)
648
+ break;
649
+ }
650
+ return "";
651
+ }
652
+ function buildSchemaFromInputs(inputs) {
653
+ const properties = {};
654
+ const required = [];
655
+ const fieldElements = /* @__PURE__ */ new Map();
656
+ const processedRadioGroups = /* @__PURE__ */ new Set();
657
+ for (const control of inputs) {
658
+ const name = control.name;
659
+ const fieldKey = name || resolveNativeControlFallbackKey(control);
660
+ if (!fieldKey)
661
+ continue;
662
+ if (control instanceof HTMLInputElement && control.type === "radio") {
663
+ if (processedRadioGroups.has(fieldKey))
664
+ continue;
665
+ processedRadioGroups.add(fieldKey);
666
+ }
667
+ const schemaProp = inputTypeToSchema(control);
668
+ if (!schemaProp)
669
+ continue;
670
+ schemaProp.title = inferFieldTitle(control);
671
+ const desc = inferFieldDescription(control);
672
+ if (desc)
673
+ schemaProp.description = desc;
674
+ properties[fieldKey] = schemaProp;
675
+ if (!name)
676
+ fieldElements.set(fieldKey, control);
677
+ if (control.required)
678
+ required.push(fieldKey);
679
+ }
680
+ return { schema: { type: "object", properties, required }, fieldElements };
681
+ }
591
682
 
592
683
  // src/discovery.ts
593
684
  init_registry();
@@ -610,7 +701,22 @@ function buildExecuteHandler(form, config, toolName, metadata) {
610
701
  return new Promise((resolve, reject) => {
611
702
  pendingExecutions.set(form, { resolve, reject });
612
703
  if (config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0) {
613
- form.requestSubmit();
704
+ setTimeout(() => {
705
+ fillFormFields(form, params);
706
+ let submitForm = form;
707
+ if (!form.isConnected) {
708
+ const liveBtn = document.querySelector(
709
+ 'button[type="submit"]:not([disabled]), input[type="submit"]:not([disabled])'
710
+ );
711
+ const found = liveBtn?.closest("form");
712
+ if (found) {
713
+ submitForm = found;
714
+ pendingExecutions.set(submitForm, { resolve, reject });
715
+ attachSubmitInterceptor(submitForm, toolName);
716
+ }
717
+ }
718
+ submitForm.requestSubmit();
719
+ }, 300);
614
720
  }
615
721
  });
616
722
  };
@@ -701,16 +807,25 @@ function fillFormFields(form, params) {
701
807
  continue;
702
808
  }
703
809
  const ariaEl = fieldEls?.get(key);
704
- if (ariaEl && ariaEl.isConnected) {
705
- if (ariaEl instanceof HTMLInputElement) {
706
- fillInput(ariaEl, form, key, value);
707
- } else if (ariaEl instanceof HTMLTextAreaElement) {
708
- setReactValue(ariaEl, String(value ?? ""));
709
- } else if (ariaEl instanceof HTMLSelectElement) {
710
- ariaEl.value = String(value ?? "");
711
- ariaEl.dispatchEvent(new Event("change", { bubbles: true }));
810
+ if (ariaEl) {
811
+ let effectiveEl = ariaEl;
812
+ if (!ariaEl.isConnected) {
813
+ const elId = ariaEl.id;
814
+ if (elId) {
815
+ const fresh = document.getElementById(elId) ?? findInShadowRoots(document, `#${CSS.escape(elId)}`);
816
+ if (fresh)
817
+ effectiveEl = fresh;
818
+ }
819
+ }
820
+ if (effectiveEl instanceof HTMLInputElement) {
821
+ fillInput(effectiveEl, form, key, value);
822
+ } else if (effectiveEl instanceof HTMLTextAreaElement) {
823
+ setReactValue(effectiveEl, String(value ?? ""));
824
+ } else if (effectiveEl instanceof HTMLSelectElement) {
825
+ effectiveEl.value = String(value ?? "");
826
+ effectiveEl.dispatchEvent(new Event("change", { bubbles: true }));
712
827
  } else {
713
- fillAriaField(ariaEl, value);
828
+ fillAriaField(effectiveEl, value);
714
829
  }
715
830
  }
716
831
  }
@@ -779,8 +894,7 @@ function serializeFormData(form, params, fieldEls) {
779
894
  for (const key of Object.keys(params)) {
780
895
  if (key in result)
781
896
  continue;
782
- const storedEl = fieldEls?.get(key);
783
- const el = findNativeField(form, key) ?? (storedEl?.isConnected ? storedEl : null) ?? null;
897
+ const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
784
898
  if (!el)
785
899
  continue;
786
900
  if (el instanceof HTMLInputElement && el.type === "checkbox") {
@@ -799,6 +913,31 @@ function serializeFormData(form, params, fieldEls) {
799
913
  }
800
914
  return result;
801
915
  }
916
+ function fillElement(el, value) {
917
+ if (el instanceof HTMLInputElement) {
918
+ const type = el.type.toLowerCase();
919
+ if (type === "checkbox") {
920
+ setReactChecked(el, Boolean(value));
921
+ } else if (type === "radio") {
922
+ if (el.value === String(value)) {
923
+ if (_checkedSetter)
924
+ _checkedSetter.call(el, true);
925
+ else
926
+ el.checked = true;
927
+ el.dispatchEvent(new Event("change", { bubbles: true }));
928
+ }
929
+ } else {
930
+ setReactValue(el, String(value ?? ""));
931
+ }
932
+ } else if (el instanceof HTMLTextAreaElement) {
933
+ setReactValue(el, String(value ?? ""));
934
+ } else if (el instanceof HTMLSelectElement) {
935
+ el.value = String(value ?? "");
936
+ el.dispatchEvent(new Event("change", { bubbles: true }));
937
+ } else {
938
+ fillAriaField(el, value);
939
+ }
940
+ }
802
941
 
803
942
  // src/enhancer.ts
804
943
  async function enrichMetadata(metadata, enhancer) {
@@ -911,6 +1050,7 @@ async function registerForm(form, config) {
911
1050
  const execute = buildExecuteHandler(form, config, metadata.name, metadata);
912
1051
  await registerFormTool(form, metadata, execute);
913
1052
  registeredForms.add(form);
1053
+ registeredFormCount++;
914
1054
  if (config.debug) {
915
1055
  console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
916
1056
  }
@@ -930,6 +1070,7 @@ async function unregisterForm(form, config) {
930
1070
  }
931
1071
  var observer = null;
932
1072
  var registeredForms = /* @__PURE__ */ new WeakSet();
1073
+ var registeredFormCount = 0;
933
1074
  var reAnalysisTimers = /* @__PURE__ */ new Map();
934
1075
  var RE_ANALYSIS_DEBOUNCE_MS = 300;
935
1076
  function isInterestingNode(node) {
@@ -1011,6 +1152,102 @@ async function scanForms(config) {
1011
1152
  const forms = Array.from(document.querySelectorAll("form"));
1012
1153
  await Promise.allSettled(forms.map((form) => registerForm(form, config)));
1013
1154
  }
1155
+ var ORPHAN_EXCLUDED_TYPES = /* @__PURE__ */ new Set([
1156
+ "password",
1157
+ "hidden",
1158
+ "file",
1159
+ "submit",
1160
+ "reset",
1161
+ "button",
1162
+ "image"
1163
+ ]);
1164
+ async function scanOrphanInputs(config) {
1165
+ if (!isWebMCPSupported())
1166
+ return;
1167
+ const SUBMIT_BTN_SELECTOR = '[type="submit"]:not([disabled]), button:not([type]):not([disabled])';
1168
+ const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search/i;
1169
+ const orphanInputs = Array.from(
1170
+ document.querySelectorAll(
1171
+ "input:not(form input), textarea:not(form textarea), select:not(form select)"
1172
+ )
1173
+ ).filter((el) => {
1174
+ if (el instanceof HTMLInputElement && ORPHAN_EXCLUDED_TYPES.has(el.type.toLowerCase())) {
1175
+ return false;
1176
+ }
1177
+ const rect = el.getBoundingClientRect();
1178
+ return rect.width > 0 && rect.height > 0;
1179
+ });
1180
+ if (orphanInputs.length === 0)
1181
+ return;
1182
+ const groups = /* @__PURE__ */ new Map();
1183
+ for (const input of orphanInputs) {
1184
+ let container = input.parentElement;
1185
+ let foundContainer = input.parentElement ?? document.body;
1186
+ while (container && container !== document.body) {
1187
+ const hasSubmitBtn = container.querySelector(SUBMIT_BTN_SELECTOR) !== null || Array.from(container.querySelectorAll("button")).some(
1188
+ (b) => SUBMIT_TEXT_RE.test(b.textContent ?? "")
1189
+ );
1190
+ if (hasSubmitBtn) {
1191
+ foundContainer = container;
1192
+ break;
1193
+ }
1194
+ container = container.parentElement;
1195
+ }
1196
+ if (!groups.has(foundContainer))
1197
+ groups.set(foundContainer, []);
1198
+ groups.get(foundContainer).push(input);
1199
+ }
1200
+ for (const [container, inputs] of groups) {
1201
+ const allCandidates = Array.from(
1202
+ container.querySelectorAll(SUBMIT_BTN_SELECTOR)
1203
+ ).filter((b) => {
1204
+ const r = b.getBoundingClientRect();
1205
+ return r.width > 0 && r.height > 0;
1206
+ });
1207
+ let submitBtn = allCandidates[allCandidates.length - 1] ?? null;
1208
+ if (!submitBtn) {
1209
+ const pageBtns = Array.from(document.querySelectorAll("button")).filter(
1210
+ (b) => {
1211
+ const r = b.getBoundingClientRect();
1212
+ return r.width > 0 && r.height > 0 && SUBMIT_TEXT_RE.test(b.textContent ?? "");
1213
+ }
1214
+ );
1215
+ submitBtn = pageBtns[pageBtns.length - 1] ?? null;
1216
+ }
1217
+ const metadata = analyzeOrphanInputGroup(container, inputs, submitBtn);
1218
+ const inputPairs = [];
1219
+ const schemaProps = metadata.inputSchema.properties;
1220
+ for (const el of inputs) {
1221
+ const key = el.name || el.dataset["webmcpName"] || el.id || el.getAttribute("aria-label") || null;
1222
+ const safeKey = key ? key.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) : null;
1223
+ if (safeKey && schemaProps[safeKey]) {
1224
+ inputPairs.push({ key: safeKey, el });
1225
+ }
1226
+ }
1227
+ const toolName = metadata.name;
1228
+ const execute = async (params) => {
1229
+ for (const { key, el } of inputPairs) {
1230
+ if (params[key] !== void 0) {
1231
+ fillElement(el, params[key]);
1232
+ }
1233
+ }
1234
+ window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
1235
+ return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
1236
+ };
1237
+ try {
1238
+ await navigator.modelContext.registerTool({
1239
+ name: metadata.name,
1240
+ description: metadata.description,
1241
+ inputSchema: metadata.inputSchema,
1242
+ execute
1243
+ });
1244
+ if (config.debug) {
1245
+ console.debug(`[auto-webmcp] Orphan tool registered: ${metadata.name}`, metadata);
1246
+ }
1247
+ } catch {
1248
+ }
1249
+ }
1250
+ }
1014
1251
  function warnToolQuality(name, description) {
1015
1252
  if (/^form_\d+$|^submit$|^form$/.test(name)) {
1016
1253
  console.warn(`[auto-webmcp] Tool "${name}" has a generic name. Consider adding a toolname or data-webmcp-name attribute.`);
@@ -1028,9 +1265,13 @@ async function startDiscovery(config) {
1028
1265
  (resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true })
1029
1266
  );
1030
1267
  }
1268
+ registeredFormCount = 0;
1031
1269
  startObserver(config);
1032
1270
  listenForRouteChanges(config);
1033
1271
  await scanForms(config);
1272
+ if (registeredFormCount === 0) {
1273
+ await scanOrphanInputs(config);
1274
+ }
1034
1275
  }
1035
1276
  function stopDiscovery() {
1036
1277
  observer?.disconnect();