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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/auto-webmcp.cjs.js +391 -135
- package/dist/auto-webmcp.cjs.js.map +4 -4
- package/dist/auto-webmcp.esm.js +391 -142
- package/dist/auto-webmcp.esm.js.map +4 -4
- package/dist/auto-webmcp.iife.js +1 -1
- package/dist/auto-webmcp.iife.js.map +4 -4
- package/dist/discovery.d.ts.map +1 -1
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/registry.d.ts +7 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/auto-webmcp.esm.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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.
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
585
|
-
|
|
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/
|
|
970
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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:
|
|
2033
|
+
console.log(`[auto-webmcp] orphan execute: resolving submit button (up to 2s)...`);
|
|
1807
2034
|
let btn = null;
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
const
|
|
1811
|
-
|
|
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
|
-
|
|
1817
|
-
if (
|
|
1818
|
-
|
|
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) {
|