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.
- package/dist/analyzer.d.ts +5 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/auto-webmcp.cjs.js +253 -12
- package/dist/auto-webmcp.cjs.js.map +2 -2
- package/dist/auto-webmcp.esm.js +253 -12
- package/dist/auto-webmcp.esm.js.map +2 -2
- package/dist/auto-webmcp.iife.js +2 -2
- package/dist/auto-webmcp.iife.js.map +3 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/interceptor.d.ts +5 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/auto-webmcp.esm.js
CHANGED
|
@@ -435,6 +435,12 @@ function resolveNativeControlFallbackKey(control) {
|
|
|
435
435
|
const label = control.getAttribute("aria-label");
|
|
436
436
|
if (label)
|
|
437
437
|
return sanitizeName(label);
|
|
438
|
+
if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.placeholder?.trim()) {
|
|
439
|
+
return sanitizeName(control.placeholder.trim());
|
|
440
|
+
}
|
|
441
|
+
if (control instanceof HTMLInputElement && control.type !== "text") {
|
|
442
|
+
return control.type;
|
|
443
|
+
}
|
|
438
444
|
return null;
|
|
439
445
|
}
|
|
440
446
|
function collectAriaControls(form) {
|
|
@@ -519,6 +525,9 @@ function inferFieldTitle(control) {
|
|
|
519
525
|
return humanizeName(control.name);
|
|
520
526
|
if (control.id)
|
|
521
527
|
return humanizeName(control.id);
|
|
528
|
+
if ((control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) && control.placeholder?.trim()) {
|
|
529
|
+
return control.placeholder.trim();
|
|
530
|
+
}
|
|
522
531
|
return "";
|
|
523
532
|
}
|
|
524
533
|
function inferFieldDescription(control) {
|
|
@@ -569,6 +578,88 @@ function labelTextWithoutNested(label) {
|
|
|
569
578
|
function humanizeName(raw) {
|
|
570
579
|
return raw.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().replace(/\b\w/g, (c) => c.toUpperCase());
|
|
571
580
|
}
|
|
581
|
+
function analyzeOrphanInputGroup(container, inputs, submitBtn) {
|
|
582
|
+
const name = inferOrphanToolName(container, submitBtn);
|
|
583
|
+
const description = inferOrphanToolDescription(container);
|
|
584
|
+
const { schema: inputSchema, fieldElements } = buildSchemaFromInputs(inputs);
|
|
585
|
+
return { name, description, inputSchema, fieldElements };
|
|
586
|
+
}
|
|
587
|
+
function inferOrphanToolName(container, submitBtn) {
|
|
588
|
+
if (submitBtn) {
|
|
589
|
+
const text = submitBtn instanceof HTMLInputElement ? submitBtn.value.trim() : submitBtn.textContent?.trim() ?? "";
|
|
590
|
+
if (text && text.length > 0 && text.length < 80)
|
|
591
|
+
return sanitizeName(text);
|
|
592
|
+
}
|
|
593
|
+
const heading = getNearestHeadingTextFrom(container);
|
|
594
|
+
if (heading)
|
|
595
|
+
return sanitizeName(heading);
|
|
596
|
+
const title = document.title?.trim();
|
|
597
|
+
if (title)
|
|
598
|
+
return sanitizeName(title);
|
|
599
|
+
return `form_${++formIndex}`;
|
|
600
|
+
}
|
|
601
|
+
function inferOrphanToolDescription(container) {
|
|
602
|
+
const heading = getNearestHeadingTextFrom(container);
|
|
603
|
+
const pageTitle = document.title?.trim();
|
|
604
|
+
if (heading && pageTitle && heading !== pageTitle)
|
|
605
|
+
return `${heading} on ${pageTitle}`;
|
|
606
|
+
if (heading)
|
|
607
|
+
return heading;
|
|
608
|
+
if (pageTitle)
|
|
609
|
+
return pageTitle;
|
|
610
|
+
return "Submit form";
|
|
611
|
+
}
|
|
612
|
+
function getNearestHeadingTextFrom(el) {
|
|
613
|
+
const inner = el.querySelector("h1, h2, h3");
|
|
614
|
+
if (inner?.textContent?.trim())
|
|
615
|
+
return inner.textContent.trim();
|
|
616
|
+
let node = el;
|
|
617
|
+
while (node) {
|
|
618
|
+
let sibling = node.previousElementSibling;
|
|
619
|
+
while (sibling) {
|
|
620
|
+
if (/^H[1-3]$/i.test(sibling.tagName)) {
|
|
621
|
+
const text = sibling.textContent?.trim() ?? "";
|
|
622
|
+
if (text)
|
|
623
|
+
return text;
|
|
624
|
+
}
|
|
625
|
+
sibling = sibling.previousElementSibling;
|
|
626
|
+
}
|
|
627
|
+
node = node.parentElement;
|
|
628
|
+
if (!node || node === document.body)
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
return "";
|
|
632
|
+
}
|
|
633
|
+
function buildSchemaFromInputs(inputs) {
|
|
634
|
+
const properties = {};
|
|
635
|
+
const required = [];
|
|
636
|
+
const fieldElements = /* @__PURE__ */ new Map();
|
|
637
|
+
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
638
|
+
for (const control of inputs) {
|
|
639
|
+
const name = control.name;
|
|
640
|
+
const fieldKey = name || resolveNativeControlFallbackKey(control);
|
|
641
|
+
if (!fieldKey)
|
|
642
|
+
continue;
|
|
643
|
+
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
644
|
+
if (processedRadioGroups.has(fieldKey))
|
|
645
|
+
continue;
|
|
646
|
+
processedRadioGroups.add(fieldKey);
|
|
647
|
+
}
|
|
648
|
+
const schemaProp = inputTypeToSchema(control);
|
|
649
|
+
if (!schemaProp)
|
|
650
|
+
continue;
|
|
651
|
+
schemaProp.title = inferFieldTitle(control);
|
|
652
|
+
const desc = inferFieldDescription(control);
|
|
653
|
+
if (desc)
|
|
654
|
+
schemaProp.description = desc;
|
|
655
|
+
properties[fieldKey] = schemaProp;
|
|
656
|
+
if (!name)
|
|
657
|
+
fieldElements.set(fieldKey, control);
|
|
658
|
+
if (control.required)
|
|
659
|
+
required.push(fieldKey);
|
|
660
|
+
}
|
|
661
|
+
return { schema: { type: "object", properties, required }, fieldElements };
|
|
662
|
+
}
|
|
572
663
|
|
|
573
664
|
// src/discovery.ts
|
|
574
665
|
init_registry();
|
|
@@ -591,7 +682,22 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
591
682
|
return new Promise((resolve, reject) => {
|
|
592
683
|
pendingExecutions.set(form, { resolve, reject });
|
|
593
684
|
if (config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0) {
|
|
594
|
-
|
|
685
|
+
setTimeout(() => {
|
|
686
|
+
fillFormFields(form, params);
|
|
687
|
+
let submitForm = form;
|
|
688
|
+
if (!form.isConnected) {
|
|
689
|
+
const liveBtn = document.querySelector(
|
|
690
|
+
'button[type="submit"]:not([disabled]), input[type="submit"]:not([disabled])'
|
|
691
|
+
);
|
|
692
|
+
const found = liveBtn?.closest("form");
|
|
693
|
+
if (found) {
|
|
694
|
+
submitForm = found;
|
|
695
|
+
pendingExecutions.set(submitForm, { resolve, reject });
|
|
696
|
+
attachSubmitInterceptor(submitForm, toolName);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
submitForm.requestSubmit();
|
|
700
|
+
}, 300);
|
|
595
701
|
}
|
|
596
702
|
});
|
|
597
703
|
};
|
|
@@ -682,16 +788,25 @@ function fillFormFields(form, params) {
|
|
|
682
788
|
continue;
|
|
683
789
|
}
|
|
684
790
|
const ariaEl = fieldEls?.get(key);
|
|
685
|
-
if (ariaEl
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
791
|
+
if (ariaEl) {
|
|
792
|
+
let effectiveEl = ariaEl;
|
|
793
|
+
if (!ariaEl.isConnected) {
|
|
794
|
+
const elId = ariaEl.id;
|
|
795
|
+
if (elId) {
|
|
796
|
+
const fresh = document.getElementById(elId) ?? findInShadowRoots(document, `#${CSS.escape(elId)}`);
|
|
797
|
+
if (fresh)
|
|
798
|
+
effectiveEl = fresh;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (effectiveEl instanceof HTMLInputElement) {
|
|
802
|
+
fillInput(effectiveEl, form, key, value);
|
|
803
|
+
} else if (effectiveEl instanceof HTMLTextAreaElement) {
|
|
804
|
+
setReactValue(effectiveEl, String(value ?? ""));
|
|
805
|
+
} else if (effectiveEl instanceof HTMLSelectElement) {
|
|
806
|
+
effectiveEl.value = String(value ?? "");
|
|
807
|
+
effectiveEl.dispatchEvent(new Event("change", { bubbles: true }));
|
|
693
808
|
} else {
|
|
694
|
-
fillAriaField(
|
|
809
|
+
fillAriaField(effectiveEl, value);
|
|
695
810
|
}
|
|
696
811
|
}
|
|
697
812
|
}
|
|
@@ -760,8 +875,7 @@ function serializeFormData(form, params, fieldEls) {
|
|
|
760
875
|
for (const key of Object.keys(params)) {
|
|
761
876
|
if (key in result)
|
|
762
877
|
continue;
|
|
763
|
-
const
|
|
764
|
-
const el = findNativeField(form, key) ?? (storedEl?.isConnected ? storedEl : null) ?? null;
|
|
878
|
+
const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
|
|
765
879
|
if (!el)
|
|
766
880
|
continue;
|
|
767
881
|
if (el instanceof HTMLInputElement && el.type === "checkbox") {
|
|
@@ -780,6 +894,31 @@ function serializeFormData(form, params, fieldEls) {
|
|
|
780
894
|
}
|
|
781
895
|
return result;
|
|
782
896
|
}
|
|
897
|
+
function fillElement(el, value) {
|
|
898
|
+
if (el instanceof HTMLInputElement) {
|
|
899
|
+
const type = el.type.toLowerCase();
|
|
900
|
+
if (type === "checkbox") {
|
|
901
|
+
setReactChecked(el, Boolean(value));
|
|
902
|
+
} else if (type === "radio") {
|
|
903
|
+
if (el.value === String(value)) {
|
|
904
|
+
if (_checkedSetter)
|
|
905
|
+
_checkedSetter.call(el, true);
|
|
906
|
+
else
|
|
907
|
+
el.checked = true;
|
|
908
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
setReactValue(el, String(value ?? ""));
|
|
912
|
+
}
|
|
913
|
+
} else if (el instanceof HTMLTextAreaElement) {
|
|
914
|
+
setReactValue(el, String(value ?? ""));
|
|
915
|
+
} else if (el instanceof HTMLSelectElement) {
|
|
916
|
+
el.value = String(value ?? "");
|
|
917
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
918
|
+
} else {
|
|
919
|
+
fillAriaField(el, value);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
783
922
|
|
|
784
923
|
// src/enhancer.ts
|
|
785
924
|
async function enrichMetadata(metadata, enhancer) {
|
|
@@ -892,6 +1031,7 @@ async function registerForm(form, config) {
|
|
|
892
1031
|
const execute = buildExecuteHandler(form, config, metadata.name, metadata);
|
|
893
1032
|
await registerFormTool(form, metadata, execute);
|
|
894
1033
|
registeredForms.add(form);
|
|
1034
|
+
registeredFormCount++;
|
|
895
1035
|
if (config.debug) {
|
|
896
1036
|
console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
|
|
897
1037
|
}
|
|
@@ -911,6 +1051,7 @@ async function unregisterForm(form, config) {
|
|
|
911
1051
|
}
|
|
912
1052
|
var observer = null;
|
|
913
1053
|
var registeredForms = /* @__PURE__ */ new WeakSet();
|
|
1054
|
+
var registeredFormCount = 0;
|
|
914
1055
|
var reAnalysisTimers = /* @__PURE__ */ new Map();
|
|
915
1056
|
var RE_ANALYSIS_DEBOUNCE_MS = 300;
|
|
916
1057
|
function isInterestingNode(node) {
|
|
@@ -992,6 +1133,102 @@ async function scanForms(config) {
|
|
|
992
1133
|
const forms = Array.from(document.querySelectorAll("form"));
|
|
993
1134
|
await Promise.allSettled(forms.map((form) => registerForm(form, config)));
|
|
994
1135
|
}
|
|
1136
|
+
var ORPHAN_EXCLUDED_TYPES = /* @__PURE__ */ new Set([
|
|
1137
|
+
"password",
|
|
1138
|
+
"hidden",
|
|
1139
|
+
"file",
|
|
1140
|
+
"submit",
|
|
1141
|
+
"reset",
|
|
1142
|
+
"button",
|
|
1143
|
+
"image"
|
|
1144
|
+
]);
|
|
1145
|
+
async function scanOrphanInputs(config) {
|
|
1146
|
+
if (!isWebMCPSupported())
|
|
1147
|
+
return;
|
|
1148
|
+
const SUBMIT_BTN_SELECTOR = '[type="submit"]:not([disabled]), button:not([type]):not([disabled])';
|
|
1149
|
+
const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search/i;
|
|
1150
|
+
const orphanInputs = Array.from(
|
|
1151
|
+
document.querySelectorAll(
|
|
1152
|
+
"input:not(form input), textarea:not(form textarea), select:not(form select)"
|
|
1153
|
+
)
|
|
1154
|
+
).filter((el) => {
|
|
1155
|
+
if (el instanceof HTMLInputElement && ORPHAN_EXCLUDED_TYPES.has(el.type.toLowerCase())) {
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
const rect = el.getBoundingClientRect();
|
|
1159
|
+
return rect.width > 0 && rect.height > 0;
|
|
1160
|
+
});
|
|
1161
|
+
if (orphanInputs.length === 0)
|
|
1162
|
+
return;
|
|
1163
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1164
|
+
for (const input of orphanInputs) {
|
|
1165
|
+
let container = input.parentElement;
|
|
1166
|
+
let foundContainer = input.parentElement ?? document.body;
|
|
1167
|
+
while (container && container !== document.body) {
|
|
1168
|
+
const hasSubmitBtn = container.querySelector(SUBMIT_BTN_SELECTOR) !== null || Array.from(container.querySelectorAll("button")).some(
|
|
1169
|
+
(b) => SUBMIT_TEXT_RE.test(b.textContent ?? "")
|
|
1170
|
+
);
|
|
1171
|
+
if (hasSubmitBtn) {
|
|
1172
|
+
foundContainer = container;
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
container = container.parentElement;
|
|
1176
|
+
}
|
|
1177
|
+
if (!groups.has(foundContainer))
|
|
1178
|
+
groups.set(foundContainer, []);
|
|
1179
|
+
groups.get(foundContainer).push(input);
|
|
1180
|
+
}
|
|
1181
|
+
for (const [container, inputs] of groups) {
|
|
1182
|
+
const allCandidates = Array.from(
|
|
1183
|
+
container.querySelectorAll(SUBMIT_BTN_SELECTOR)
|
|
1184
|
+
).filter((b) => {
|
|
1185
|
+
const r = b.getBoundingClientRect();
|
|
1186
|
+
return r.width > 0 && r.height > 0;
|
|
1187
|
+
});
|
|
1188
|
+
let submitBtn = allCandidates[allCandidates.length - 1] ?? null;
|
|
1189
|
+
if (!submitBtn) {
|
|
1190
|
+
const pageBtns = Array.from(document.querySelectorAll("button")).filter(
|
|
1191
|
+
(b) => {
|
|
1192
|
+
const r = b.getBoundingClientRect();
|
|
1193
|
+
return r.width > 0 && r.height > 0 && SUBMIT_TEXT_RE.test(b.textContent ?? "");
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
submitBtn = pageBtns[pageBtns.length - 1] ?? null;
|
|
1197
|
+
}
|
|
1198
|
+
const metadata = analyzeOrphanInputGroup(container, inputs, submitBtn);
|
|
1199
|
+
const inputPairs = [];
|
|
1200
|
+
const schemaProps = metadata.inputSchema.properties;
|
|
1201
|
+
for (const el of inputs) {
|
|
1202
|
+
const key = el.name || el.dataset["webmcpName"] || el.id || el.getAttribute("aria-label") || null;
|
|
1203
|
+
const safeKey = key ? key.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) : null;
|
|
1204
|
+
if (safeKey && schemaProps[safeKey]) {
|
|
1205
|
+
inputPairs.push({ key: safeKey, el });
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const toolName = metadata.name;
|
|
1209
|
+
const execute = async (params) => {
|
|
1210
|
+
for (const { key, el } of inputPairs) {
|
|
1211
|
+
if (params[key] !== void 0) {
|
|
1212
|
+
fillElement(el, params[key]);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
|
|
1216
|
+
return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
|
|
1217
|
+
};
|
|
1218
|
+
try {
|
|
1219
|
+
await navigator.modelContext.registerTool({
|
|
1220
|
+
name: metadata.name,
|
|
1221
|
+
description: metadata.description,
|
|
1222
|
+
inputSchema: metadata.inputSchema,
|
|
1223
|
+
execute
|
|
1224
|
+
});
|
|
1225
|
+
if (config.debug) {
|
|
1226
|
+
console.debug(`[auto-webmcp] Orphan tool registered: ${metadata.name}`, metadata);
|
|
1227
|
+
}
|
|
1228
|
+
} catch {
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
995
1232
|
function warnToolQuality(name, description) {
|
|
996
1233
|
if (/^form_\d+$|^submit$|^form$/.test(name)) {
|
|
997
1234
|
console.warn(`[auto-webmcp] Tool "${name}" has a generic name. Consider adding a toolname or data-webmcp-name attribute.`);
|
|
@@ -1009,9 +1246,13 @@ async function startDiscovery(config) {
|
|
|
1009
1246
|
(resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true })
|
|
1010
1247
|
);
|
|
1011
1248
|
}
|
|
1249
|
+
registeredFormCount = 0;
|
|
1012
1250
|
startObserver(config);
|
|
1013
1251
|
listenForRouteChanges(config);
|
|
1014
1252
|
await scanForms(config);
|
|
1253
|
+
if (registeredFormCount === 0) {
|
|
1254
|
+
await scanOrphanInputs(config);
|
|
1255
|
+
}
|
|
1015
1256
|
}
|
|
1016
1257
|
function stopDiscovery() {
|
|
1017
1258
|
observer?.disconnect();
|