auto-webmcp 0.2.4 → 0.2.5

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;AA8YD;;;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"}
@@ -588,6 +588,88 @@ function labelTextWithoutNested(label) {
588
588
  function humanizeName(raw) {
589
589
  return raw.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().replace(/\b\w/g, (c) => c.toUpperCase());
590
590
  }
591
+ function analyzeOrphanInputGroup(container, inputs, submitBtn) {
592
+ const name = inferOrphanToolName(container, submitBtn);
593
+ const description = inferOrphanToolDescription(container);
594
+ const { schema: inputSchema, fieldElements } = buildSchemaFromInputs(inputs);
595
+ return { name, description, inputSchema, fieldElements };
596
+ }
597
+ function inferOrphanToolName(container, submitBtn) {
598
+ if (submitBtn) {
599
+ const text = submitBtn instanceof HTMLInputElement ? submitBtn.value.trim() : submitBtn.textContent?.trim() ?? "";
600
+ if (text && text.length > 0 && text.length < 80)
601
+ return sanitizeName(text);
602
+ }
603
+ const heading = getNearestHeadingTextFrom(container);
604
+ if (heading)
605
+ return sanitizeName(heading);
606
+ const title = document.title?.trim();
607
+ if (title)
608
+ return sanitizeName(title);
609
+ return `form_${++formIndex}`;
610
+ }
611
+ function inferOrphanToolDescription(container) {
612
+ const heading = getNearestHeadingTextFrom(container);
613
+ const pageTitle = document.title?.trim();
614
+ if (heading && pageTitle && heading !== pageTitle)
615
+ return `${heading} on ${pageTitle}`;
616
+ if (heading)
617
+ return heading;
618
+ if (pageTitle)
619
+ return pageTitle;
620
+ return "Submit form";
621
+ }
622
+ function getNearestHeadingTextFrom(el) {
623
+ const inner = el.querySelector("h1, h2, h3");
624
+ if (inner?.textContent?.trim())
625
+ return inner.textContent.trim();
626
+ let node = el;
627
+ while (node) {
628
+ let sibling = node.previousElementSibling;
629
+ while (sibling) {
630
+ if (/^H[1-3]$/i.test(sibling.tagName)) {
631
+ const text = sibling.textContent?.trim() ?? "";
632
+ if (text)
633
+ return text;
634
+ }
635
+ sibling = sibling.previousElementSibling;
636
+ }
637
+ node = node.parentElement;
638
+ if (!node || node === document.body)
639
+ break;
640
+ }
641
+ return "";
642
+ }
643
+ function buildSchemaFromInputs(inputs) {
644
+ const properties = {};
645
+ const required = [];
646
+ const fieldElements = /* @__PURE__ */ new Map();
647
+ const processedRadioGroups = /* @__PURE__ */ new Set();
648
+ for (const control of inputs) {
649
+ const name = control.name;
650
+ const fieldKey = name || resolveNativeControlFallbackKey(control);
651
+ if (!fieldKey)
652
+ continue;
653
+ if (control instanceof HTMLInputElement && control.type === "radio") {
654
+ if (processedRadioGroups.has(fieldKey))
655
+ continue;
656
+ processedRadioGroups.add(fieldKey);
657
+ }
658
+ const schemaProp = inputTypeToSchema(control);
659
+ if (!schemaProp)
660
+ continue;
661
+ schemaProp.title = inferFieldTitle(control);
662
+ const desc = inferFieldDescription(control);
663
+ if (desc)
664
+ schemaProp.description = desc;
665
+ properties[fieldKey] = schemaProp;
666
+ if (!name)
667
+ fieldElements.set(fieldKey, control);
668
+ if (control.required)
669
+ required.push(fieldKey);
670
+ }
671
+ return { schema: { type: "object", properties, required }, fieldElements };
672
+ }
591
673
 
592
674
  // src/discovery.ts
593
675
  init_registry();
@@ -610,7 +692,22 @@ function buildExecuteHandler(form, config, toolName, metadata) {
610
692
  return new Promise((resolve, reject) => {
611
693
  pendingExecutions.set(form, { resolve, reject });
612
694
  if (config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0) {
613
- form.requestSubmit();
695
+ setTimeout(() => {
696
+ fillFormFields(form, params);
697
+ let submitForm = form;
698
+ if (!form.isConnected) {
699
+ const liveBtn = document.querySelector(
700
+ 'button[type="submit"]:not([disabled]), input[type="submit"]:not([disabled])'
701
+ );
702
+ const found = liveBtn?.closest("form");
703
+ if (found) {
704
+ submitForm = found;
705
+ pendingExecutions.set(submitForm, { resolve, reject });
706
+ attachSubmitInterceptor(submitForm, toolName);
707
+ }
708
+ }
709
+ submitForm.requestSubmit();
710
+ }, 300);
614
711
  }
615
712
  });
616
713
  };
@@ -701,16 +798,25 @@ function fillFormFields(form, params) {
701
798
  continue;
702
799
  }
703
800
  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 }));
801
+ if (ariaEl) {
802
+ let effectiveEl = ariaEl;
803
+ if (!ariaEl.isConnected) {
804
+ const elId = ariaEl.id;
805
+ if (elId) {
806
+ const fresh = document.getElementById(elId) ?? findInShadowRoots(document, `#${CSS.escape(elId)}`);
807
+ if (fresh)
808
+ effectiveEl = fresh;
809
+ }
810
+ }
811
+ if (effectiveEl instanceof HTMLInputElement) {
812
+ fillInput(effectiveEl, form, key, value);
813
+ } else if (effectiveEl instanceof HTMLTextAreaElement) {
814
+ setReactValue(effectiveEl, String(value ?? ""));
815
+ } else if (effectiveEl instanceof HTMLSelectElement) {
816
+ effectiveEl.value = String(value ?? "");
817
+ effectiveEl.dispatchEvent(new Event("change", { bubbles: true }));
712
818
  } else {
713
- fillAriaField(ariaEl, value);
819
+ fillAriaField(effectiveEl, value);
714
820
  }
715
821
  }
716
822
  }
@@ -779,8 +885,7 @@ function serializeFormData(form, params, fieldEls) {
779
885
  for (const key of Object.keys(params)) {
780
886
  if (key in result)
781
887
  continue;
782
- const storedEl = fieldEls?.get(key);
783
- const el = findNativeField(form, key) ?? (storedEl?.isConnected ? storedEl : null) ?? null;
888
+ const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
784
889
  if (!el)
785
890
  continue;
786
891
  if (el instanceof HTMLInputElement && el.type === "checkbox") {
@@ -799,6 +904,31 @@ function serializeFormData(form, params, fieldEls) {
799
904
  }
800
905
  return result;
801
906
  }
907
+ function fillElement(el, value) {
908
+ if (el instanceof HTMLInputElement) {
909
+ const type = el.type.toLowerCase();
910
+ if (type === "checkbox") {
911
+ setReactChecked(el, Boolean(value));
912
+ } else if (type === "radio") {
913
+ if (el.value === String(value)) {
914
+ if (_checkedSetter)
915
+ _checkedSetter.call(el, true);
916
+ else
917
+ el.checked = true;
918
+ el.dispatchEvent(new Event("change", { bubbles: true }));
919
+ }
920
+ } else {
921
+ setReactValue(el, String(value ?? ""));
922
+ }
923
+ } else if (el instanceof HTMLTextAreaElement) {
924
+ setReactValue(el, String(value ?? ""));
925
+ } else if (el instanceof HTMLSelectElement) {
926
+ el.value = String(value ?? "");
927
+ el.dispatchEvent(new Event("change", { bubbles: true }));
928
+ } else {
929
+ fillAriaField(el, value);
930
+ }
931
+ }
802
932
 
803
933
  // src/enhancer.ts
804
934
  async function enrichMetadata(metadata, enhancer) {
@@ -911,6 +1041,7 @@ async function registerForm(form, config) {
911
1041
  const execute = buildExecuteHandler(form, config, metadata.name, metadata);
912
1042
  await registerFormTool(form, metadata, execute);
913
1043
  registeredForms.add(form);
1044
+ registeredFormCount++;
914
1045
  if (config.debug) {
915
1046
  console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
916
1047
  }
@@ -930,6 +1061,7 @@ async function unregisterForm(form, config) {
930
1061
  }
931
1062
  var observer = null;
932
1063
  var registeredForms = /* @__PURE__ */ new WeakSet();
1064
+ var registeredFormCount = 0;
933
1065
  var reAnalysisTimers = /* @__PURE__ */ new Map();
934
1066
  var RE_ANALYSIS_DEBOUNCE_MS = 300;
935
1067
  function isInterestingNode(node) {
@@ -1011,6 +1143,102 @@ async function scanForms(config) {
1011
1143
  const forms = Array.from(document.querySelectorAll("form"));
1012
1144
  await Promise.allSettled(forms.map((form) => registerForm(form, config)));
1013
1145
  }
1146
+ var ORPHAN_EXCLUDED_TYPES = /* @__PURE__ */ new Set([
1147
+ "password",
1148
+ "hidden",
1149
+ "file",
1150
+ "submit",
1151
+ "reset",
1152
+ "button",
1153
+ "image"
1154
+ ]);
1155
+ async function scanOrphanInputs(config) {
1156
+ if (!isWebMCPSupported())
1157
+ return;
1158
+ const SUBMIT_BTN_SELECTOR = '[type="submit"]:not([disabled]), button:not([type]):not([disabled])';
1159
+ const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search/i;
1160
+ const orphanInputs = Array.from(
1161
+ document.querySelectorAll(
1162
+ "input:not(form input), textarea:not(form textarea), select:not(form select)"
1163
+ )
1164
+ ).filter((el) => {
1165
+ if (el instanceof HTMLInputElement && ORPHAN_EXCLUDED_TYPES.has(el.type.toLowerCase())) {
1166
+ return false;
1167
+ }
1168
+ const rect = el.getBoundingClientRect();
1169
+ return rect.width > 0 && rect.height > 0;
1170
+ });
1171
+ if (orphanInputs.length === 0)
1172
+ return;
1173
+ const groups = /* @__PURE__ */ new Map();
1174
+ for (const input of orphanInputs) {
1175
+ let container = input.parentElement;
1176
+ let foundContainer = input.parentElement ?? document.body;
1177
+ while (container && container !== document.body) {
1178
+ const hasSubmitBtn = container.querySelector(SUBMIT_BTN_SELECTOR) !== null || Array.from(container.querySelectorAll("button")).some(
1179
+ (b) => SUBMIT_TEXT_RE.test(b.textContent ?? "")
1180
+ );
1181
+ if (hasSubmitBtn) {
1182
+ foundContainer = container;
1183
+ break;
1184
+ }
1185
+ container = container.parentElement;
1186
+ }
1187
+ if (!groups.has(foundContainer))
1188
+ groups.set(foundContainer, []);
1189
+ groups.get(foundContainer).push(input);
1190
+ }
1191
+ for (const [container, inputs] of groups) {
1192
+ const allCandidates = Array.from(
1193
+ container.querySelectorAll(SUBMIT_BTN_SELECTOR)
1194
+ ).filter((b) => {
1195
+ const r = b.getBoundingClientRect();
1196
+ return r.width > 0 && r.height > 0;
1197
+ });
1198
+ let submitBtn = allCandidates[allCandidates.length - 1] ?? null;
1199
+ if (!submitBtn) {
1200
+ const pageBtns = Array.from(document.querySelectorAll("button")).filter(
1201
+ (b) => {
1202
+ const r = b.getBoundingClientRect();
1203
+ return r.width > 0 && r.height > 0 && SUBMIT_TEXT_RE.test(b.textContent ?? "");
1204
+ }
1205
+ );
1206
+ submitBtn = pageBtns[pageBtns.length - 1] ?? null;
1207
+ }
1208
+ const metadata = analyzeOrphanInputGroup(container, inputs, submitBtn);
1209
+ const inputPairs = [];
1210
+ const schemaProps = metadata.inputSchema.properties;
1211
+ for (const el of inputs) {
1212
+ const key = el.name || el.dataset["webmcpName"] || el.id || el.getAttribute("aria-label") || null;
1213
+ const safeKey = key ? key.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) : null;
1214
+ if (safeKey && schemaProps[safeKey]) {
1215
+ inputPairs.push({ key: safeKey, el });
1216
+ }
1217
+ }
1218
+ const toolName = metadata.name;
1219
+ const execute = async (params) => {
1220
+ for (const { key, el } of inputPairs) {
1221
+ if (params[key] !== void 0) {
1222
+ fillElement(el, params[key]);
1223
+ }
1224
+ }
1225
+ window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
1226
+ return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
1227
+ };
1228
+ try {
1229
+ await navigator.modelContext.registerTool({
1230
+ name: metadata.name,
1231
+ description: metadata.description,
1232
+ inputSchema: metadata.inputSchema,
1233
+ execute
1234
+ });
1235
+ if (config.debug) {
1236
+ console.debug(`[auto-webmcp] Orphan tool registered: ${metadata.name}`, metadata);
1237
+ }
1238
+ } catch {
1239
+ }
1240
+ }
1241
+ }
1014
1242
  function warnToolQuality(name, description) {
1015
1243
  if (/^form_\d+$|^submit$|^form$/.test(name)) {
1016
1244
  console.warn(`[auto-webmcp] Tool "${name}" has a generic name. Consider adding a toolname or data-webmcp-name attribute.`);
@@ -1028,9 +1256,13 @@ async function startDiscovery(config) {
1028
1256
  (resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true })
1029
1257
  );
1030
1258
  }
1259
+ registeredFormCount = 0;
1031
1260
  startObserver(config);
1032
1261
  listenForRouteChanges(config);
1033
1262
  await scanForms(config);
1263
+ if (registeredFormCount === 0) {
1264
+ await scanOrphanInputs(config);
1265
+ }
1034
1266
  }
1035
1267
  function stopDiscovery() {
1036
1268
  observer?.disconnect();