auto-webmcp 0.3.15 → 0.3.17
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/README.md +15 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/auto-webmcp.cjs.js +402 -122
- package/dist/auto-webmcp.cjs.js.map +4 -4
- package/dist/auto-webmcp.esm.js +402 -129
- 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/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/interceptor.d.ts +3 -3
- 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,88 +1,18 @@
|
|
|
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) {
|
|
3
|
+
const strict = userConfig?.paramBinding?.strict ?? false;
|
|
4
|
+
const enableAliasResolution = strict ? false : userConfig?.paramBinding?.enableAliasResolution ?? true;
|
|
83
5
|
return {
|
|
84
6
|
exclude: userConfig?.exclude ?? [],
|
|
85
7
|
autoSubmit: userConfig?.autoSubmit ?? false,
|
|
8
|
+
declarativeMode: userConfig?.declarativeMode ?? "skip",
|
|
9
|
+
paramBinding: {
|
|
10
|
+
strict,
|
|
11
|
+
enableAliasResolution
|
|
12
|
+
},
|
|
13
|
+
execution: {
|
|
14
|
+
timeoutMs: Math.max(100, userConfig?.execution?.timeoutMs ?? 15e3)
|
|
15
|
+
},
|
|
86
16
|
overrides: userConfig?.overrides ?? {},
|
|
87
17
|
debug: userConfig?.debug ?? false
|
|
88
18
|
};
|
|
@@ -231,19 +161,19 @@ function mapSelectElement(select) {
|
|
|
231
161
|
return { type: "string", enum: enumValues, oneOf };
|
|
232
162
|
}
|
|
233
163
|
function collectCheckboxEnum(form, name) {
|
|
234
|
-
return Array.from(
|
|
235
|
-
|
|
164
|
+
return Array.from(form.elements).filter(
|
|
165
|
+
(el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === name
|
|
236
166
|
).map((cb) => cb.value).filter((v) => v !== "" && v !== "on");
|
|
237
167
|
}
|
|
238
168
|
function collectRadioEnum(form, name) {
|
|
239
|
-
const radios = Array.from(
|
|
240
|
-
|
|
169
|
+
const radios = Array.from(form.elements).filter(
|
|
170
|
+
(el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === name
|
|
241
171
|
);
|
|
242
172
|
return radios.map((r) => r.value).filter((v) => v !== "");
|
|
243
173
|
}
|
|
244
174
|
function collectRadioOneOf(form, name) {
|
|
245
|
-
const radios = Array.from(
|
|
246
|
-
|
|
175
|
+
const radios = Array.from(form.elements).filter(
|
|
176
|
+
(el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === name
|
|
247
177
|
).filter((r) => r.value !== "");
|
|
248
178
|
return radios.map((r) => {
|
|
249
179
|
const title = getRadioLabelText(r);
|
|
@@ -503,20 +433,26 @@ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
|
|
|
503
433
|
}
|
|
504
434
|
return results;
|
|
505
435
|
}
|
|
436
|
+
function collectFormAssociatedControls(form) {
|
|
437
|
+
const controls = Array.from(form.elements).filter(
|
|
438
|
+
(el) => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement
|
|
439
|
+
);
|
|
440
|
+
const seen = new Set(controls);
|
|
441
|
+
for (const shadowControl of collectShadowControls(form)) {
|
|
442
|
+
if (!seen.has(shadowControl)) {
|
|
443
|
+
controls.push(shadowControl);
|
|
444
|
+
seen.add(shadowControl);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return controls;
|
|
448
|
+
}
|
|
506
449
|
function buildSchema(form) {
|
|
507
450
|
const properties = {};
|
|
508
451
|
const required = [];
|
|
509
452
|
const fieldElements = /* @__PURE__ */ new Map();
|
|
510
453
|
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
511
454
|
const processedCheckboxGroups = /* @__PURE__ */ new Set();
|
|
512
|
-
const controls =
|
|
513
|
-
...Array.from(
|
|
514
|
-
form.querySelectorAll(
|
|
515
|
-
"input, textarea, select"
|
|
516
|
-
)
|
|
517
|
-
),
|
|
518
|
-
...collectShadowControls(form)
|
|
519
|
-
];
|
|
455
|
+
const controls = collectFormAssociatedControls(form);
|
|
520
456
|
for (const control of controls) {
|
|
521
457
|
const name = control.name;
|
|
522
458
|
const fieldKey = name || resolveNativeControlFallbackKey(control);
|
|
@@ -549,8 +485,8 @@ function buildSchema(form) {
|
|
|
549
485
|
const radioOneOf = collectRadioOneOf(form, fieldKey);
|
|
550
486
|
if (radioOneOf.length > 0)
|
|
551
487
|
schemaProp.oneOf = radioOneOf;
|
|
552
|
-
const checkedRadio = form.
|
|
553
|
-
|
|
488
|
+
const checkedRadio = Array.from(form.elements).find(
|
|
489
|
+
(el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === fieldKey && el.checked
|
|
554
490
|
);
|
|
555
491
|
if (checkedRadio?.value)
|
|
556
492
|
schemaProp.default = checkedRadio.value;
|
|
@@ -565,10 +501,8 @@ function buildSchema(form) {
|
|
|
565
501
|
};
|
|
566
502
|
if (schemaProp.description)
|
|
567
503
|
arrayProp.description = schemaProp.description;
|
|
568
|
-
const checkedBoxes = Array.from(
|
|
569
|
-
|
|
570
|
-
`input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
|
|
571
|
-
)
|
|
504
|
+
const checkedBoxes = Array.from(form.elements).filter(
|
|
505
|
+
(el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === fieldKey && el.checked
|
|
572
506
|
).map((b) => b.value);
|
|
573
507
|
if (checkedBoxes.length > 0)
|
|
574
508
|
arrayProp.default = checkedBoxes;
|
|
@@ -1038,8 +972,68 @@ function buildSchemaFromInputs(inputs) {
|
|
|
1038
972
|
return { schema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object", properties, required }, fieldElements };
|
|
1039
973
|
}
|
|
1040
974
|
|
|
1041
|
-
// src/
|
|
1042
|
-
|
|
975
|
+
// src/registry.ts
|
|
976
|
+
var registeredTools = /* @__PURE__ */ new Map();
|
|
977
|
+
var registrationControllers = /* @__PURE__ */ new Map();
|
|
978
|
+
function isWebMCPSupported() {
|
|
979
|
+
return typeof navigator !== "undefined" && typeof navigator.modelContext !== "undefined";
|
|
980
|
+
}
|
|
981
|
+
async function registerFormTool(form, metadata, execute) {
|
|
982
|
+
if (!isWebMCPSupported())
|
|
983
|
+
return;
|
|
984
|
+
const existing = registeredTools.get(form);
|
|
985
|
+
if (existing) {
|
|
986
|
+
await unregisterFormTool(form);
|
|
987
|
+
}
|
|
988
|
+
const toolDef = {
|
|
989
|
+
name: metadata.name,
|
|
990
|
+
description: metadata.description,
|
|
991
|
+
inputSchema: metadata.inputSchema,
|
|
992
|
+
execute
|
|
993
|
+
};
|
|
994
|
+
if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
|
|
995
|
+
toolDef.annotations = metadata.annotations;
|
|
996
|
+
}
|
|
997
|
+
const controller = new AbortController();
|
|
998
|
+
registrationControllers.set(form, controller);
|
|
999
|
+
try {
|
|
1000
|
+
await navigator.modelContext.registerTool(toolDef, { signal: controller.signal });
|
|
1001
|
+
} catch {
|
|
1002
|
+
try {
|
|
1003
|
+
await navigator.modelContext.unregisterTool?.(metadata.name);
|
|
1004
|
+
await navigator.modelContext.registerTool(toolDef, { signal: controller.signal });
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
registeredTools.set(form, metadata.name);
|
|
1009
|
+
}
|
|
1010
|
+
async function unregisterFormTool(form) {
|
|
1011
|
+
if (!isWebMCPSupported())
|
|
1012
|
+
return;
|
|
1013
|
+
const name = registeredTools.get(form);
|
|
1014
|
+
if (!name)
|
|
1015
|
+
return;
|
|
1016
|
+
const controller = registrationControllers.get(form);
|
|
1017
|
+
if (controller) {
|
|
1018
|
+
controller.abort();
|
|
1019
|
+
registrationControllers.delete(form);
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
await navigator.modelContext.unregisterTool?.(name);
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
registeredTools.delete(form);
|
|
1026
|
+
}
|
|
1027
|
+
function getRegisteredToolName(form) {
|
|
1028
|
+
return registeredTools.get(form);
|
|
1029
|
+
}
|
|
1030
|
+
function getAllRegisteredTools() {
|
|
1031
|
+
return Array.from(registeredTools.entries()).map(([form, name]) => ({ form, name }));
|
|
1032
|
+
}
|
|
1033
|
+
async function unregisterAll() {
|
|
1034
|
+
const entries = Array.from(registeredTools.entries());
|
|
1035
|
+
await Promise.all(entries.map(([form]) => unregisterFormTool(form)));
|
|
1036
|
+
}
|
|
1043
1037
|
|
|
1044
1038
|
// src/interceptor.ts
|
|
1045
1039
|
var pendingExecutions = /* @__PURE__ */ new WeakMap();
|
|
@@ -1051,30 +1045,169 @@ var lastFilledSnapshot = /* @__PURE__ */ new WeakMap();
|
|
|
1051
1045
|
var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
1052
1046
|
var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
|
|
1053
1047
|
var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
|
|
1048
|
+
function normalizeAliasKey(raw) {
|
|
1049
|
+
return raw.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
1050
|
+
}
|
|
1051
|
+
function addAlias(index, alias, schemaKey) {
|
|
1052
|
+
if (!alias)
|
|
1053
|
+
return;
|
|
1054
|
+
const normalized = normalizeAliasKey(alias);
|
|
1055
|
+
if (!normalized)
|
|
1056
|
+
return;
|
|
1057
|
+
if (!index.has(normalized))
|
|
1058
|
+
index.set(normalized, /* @__PURE__ */ new Set());
|
|
1059
|
+
index.get(normalized).add(schemaKey);
|
|
1060
|
+
}
|
|
1061
|
+
function buildAliasIndex(form, metadata) {
|
|
1062
|
+
const index = /* @__PURE__ */ new Map();
|
|
1063
|
+
const properties = metadata?.inputSchema?.properties ?? {};
|
|
1064
|
+
for (const [schemaKey, prop] of Object.entries(properties)) {
|
|
1065
|
+
addAlias(index, schemaKey, schemaKey);
|
|
1066
|
+
addAlias(index, schemaKey.replace(/_/g, " "), schemaKey);
|
|
1067
|
+
addAlias(index, prop.title, schemaKey);
|
|
1068
|
+
const nativeEl = findNativeField(form, schemaKey);
|
|
1069
|
+
const mappedEl = metadata?.fieldElements?.get(schemaKey);
|
|
1070
|
+
const el = nativeEl ?? mappedEl ?? null;
|
|
1071
|
+
if (!el)
|
|
1072
|
+
continue;
|
|
1073
|
+
const htmlEl = el;
|
|
1074
|
+
addAlias(index, htmlEl.getAttribute("id"), schemaKey);
|
|
1075
|
+
addAlias(index, htmlEl.getAttribute("name"), schemaKey);
|
|
1076
|
+
addAlias(index, htmlEl.getAttribute("aria-label"), schemaKey);
|
|
1077
|
+
addAlias(index, htmlEl.getAttribute("placeholder"), schemaKey);
|
|
1078
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
1079
|
+
for (const label of Array.from(el.labels ?? [])) {
|
|
1080
|
+
addAlias(index, label.textContent?.trim(), schemaKey);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return index;
|
|
1085
|
+
}
|
|
1086
|
+
function resolveParamsForSchema(form, params, metadata, config) {
|
|
1087
|
+
const resolved = {};
|
|
1088
|
+
const warnings = [];
|
|
1089
|
+
const properties = metadata?.inputSchema?.properties ?? {};
|
|
1090
|
+
const aliasEnabled = config.paramBinding.enableAliasResolution;
|
|
1091
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1092
|
+
if (key in properties)
|
|
1093
|
+
resolved[key] = value;
|
|
1094
|
+
}
|
|
1095
|
+
if (!aliasEnabled)
|
|
1096
|
+
return { resolved, warnings };
|
|
1097
|
+
const aliasIndex = buildAliasIndex(form, metadata);
|
|
1098
|
+
for (const [rawKey, value] of Object.entries(params)) {
|
|
1099
|
+
if (rawKey in properties)
|
|
1100
|
+
continue;
|
|
1101
|
+
const candidates = aliasIndex.get(normalizeAliasKey(rawKey));
|
|
1102
|
+
if (!candidates || candidates.size !== 1)
|
|
1103
|
+
continue;
|
|
1104
|
+
const target = Array.from(candidates)[0];
|
|
1105
|
+
if (!target || target in resolved)
|
|
1106
|
+
continue;
|
|
1107
|
+
resolved[target] = value;
|
|
1108
|
+
warnings.push({
|
|
1109
|
+
field: target,
|
|
1110
|
+
type: "alias_resolved",
|
|
1111
|
+
original: rawKey,
|
|
1112
|
+
message: `resolved "${rawKey}" to schema field "${target}"`
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
return { resolved, warnings };
|
|
1116
|
+
}
|
|
1117
|
+
function collectInvalidFieldWarnings(form) {
|
|
1118
|
+
const warnings = [];
|
|
1119
|
+
const controls = Array.from(form.elements).filter(
|
|
1120
|
+
(el) => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement
|
|
1121
|
+
);
|
|
1122
|
+
for (const control of controls) {
|
|
1123
|
+
if (!control.willValidate)
|
|
1124
|
+
continue;
|
|
1125
|
+
if (control.checkValidity())
|
|
1126
|
+
continue;
|
|
1127
|
+
const field = control.name || control.id || control.getAttribute("aria-label") || "unknown_field";
|
|
1128
|
+
warnings.push({
|
|
1129
|
+
field,
|
|
1130
|
+
type: "blocked_submit",
|
|
1131
|
+
message: control.validationMessage || `field "${field}" failed validation`
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
return warnings;
|
|
1135
|
+
}
|
|
1054
1136
|
function buildExecuteHandler(form, config, toolName, metadata) {
|
|
1055
1137
|
if (metadata?.fieldElements) {
|
|
1056
1138
|
formFieldElements.set(form, metadata.fieldElements);
|
|
1057
1139
|
}
|
|
1058
1140
|
attachSubmitInterceptor(form, toolName);
|
|
1059
|
-
return async (params) => {
|
|
1141
|
+
return async (params, client) => {
|
|
1142
|
+
const modelContextClient = client;
|
|
1143
|
+
if (config.autoSubmit && metadata?.annotations?.destructiveHint === true && typeof modelContextClient?.requestUserInteraction === "function") {
|
|
1144
|
+
const approved = await modelContextClient.requestUserInteraction(async () => {
|
|
1145
|
+
return new Promise((resolve) => {
|
|
1146
|
+
const ok = window.confirm(`Agent requested a destructive action via "${toolName}". Continue?`);
|
|
1147
|
+
resolve(ok);
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
if (!approved) {
|
|
1151
|
+
window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
|
|
1152
|
+
return { content: [{ type: "text", text: `Cancelled "${toolName}" by user.` }] };
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1060
1155
|
pendingFillWarnings.set(form, []);
|
|
1061
1156
|
pendingWarnings.delete(form);
|
|
1062
|
-
|
|
1063
|
-
|
|
1157
|
+
const { resolved: resolvedParams, warnings: aliasWarnings } = resolveParamsForSchema(
|
|
1158
|
+
form,
|
|
1159
|
+
params,
|
|
1160
|
+
metadata,
|
|
1161
|
+
config
|
|
1162
|
+
);
|
|
1163
|
+
if (aliasWarnings.length > 0) {
|
|
1164
|
+
pendingFillWarnings.set(form, [...pendingFillWarnings.get(form) ?? [], ...aliasWarnings]);
|
|
1165
|
+
}
|
|
1166
|
+
fillFormFields(form, resolvedParams);
|
|
1167
|
+
const missingNow = getMissingRequired(metadata, resolvedParams);
|
|
1064
1168
|
if (missingNow.length > 0)
|
|
1065
1169
|
pendingWarnings.set(form, missingNow);
|
|
1066
1170
|
window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
|
|
1067
1171
|
return new Promise((resolve, reject) => {
|
|
1068
|
-
|
|
1172
|
+
const timeoutMs = config.execution.timeoutMs;
|
|
1173
|
+
const timeoutId = setTimeout(() => {
|
|
1174
|
+
const pending = pendingExecutions.get(form);
|
|
1175
|
+
if (!pending)
|
|
1176
|
+
return;
|
|
1177
|
+
pendingExecutions.delete(form);
|
|
1178
|
+
const timedOutState = config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0 ? "timed_out" : "awaiting_user_action";
|
|
1179
|
+
const warn = {
|
|
1180
|
+
field: "__form__",
|
|
1181
|
+
type: "timeout",
|
|
1182
|
+
message: timedOutState === "timed_out" ? `tool execution timed out after ${timeoutMs}ms` : `waiting for user submit (timed out after ${timeoutMs}ms)`
|
|
1183
|
+
};
|
|
1184
|
+
const structured = {
|
|
1185
|
+
status: timedOutState,
|
|
1186
|
+
filled_fields: serializeFormData(form, lastParams.get(form), formFieldElements.get(form)),
|
|
1187
|
+
skipped_fields: [],
|
|
1188
|
+
missing_required: pendingWarnings.get(form) ?? [],
|
|
1189
|
+
warnings: [...pendingFillWarnings.get(form) ?? [], warn]
|
|
1190
|
+
};
|
|
1191
|
+
pendingWarnings.delete(form);
|
|
1192
|
+
pendingFillWarnings.delete(form);
|
|
1193
|
+
lastFilledSnapshot.delete(form);
|
|
1194
|
+
resolve({
|
|
1195
|
+
content: [
|
|
1196
|
+
{ type: "text", text: warn.message },
|
|
1197
|
+
{ type: "text", text: JSON.stringify(structured) }
|
|
1198
|
+
]
|
|
1199
|
+
});
|
|
1200
|
+
}, timeoutMs);
|
|
1201
|
+
pendingExecutions.set(form, { resolve, reject, timeoutId });
|
|
1069
1202
|
if (config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0) {
|
|
1070
1203
|
waitForDomStable(form).then(async () => {
|
|
1071
1204
|
try {
|
|
1072
|
-
fillFormFields(form,
|
|
1205
|
+
fillFormFields(form, resolvedParams);
|
|
1073
1206
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1074
|
-
const reset = getResetFields(form,
|
|
1207
|
+
const reset = getResetFields(form, resolvedParams, formFieldElements.get(form));
|
|
1075
1208
|
if (reset.length === 0)
|
|
1076
1209
|
break;
|
|
1077
|
-
fillFormFields(form,
|
|
1210
|
+
fillFormFields(form, resolvedParams);
|
|
1078
1211
|
await waitForDomStable(form, 400, 100);
|
|
1079
1212
|
}
|
|
1080
1213
|
let submitForm = form;
|
|
@@ -1085,7 +1218,9 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
1085
1218
|
const found = liveBtn?.closest("form");
|
|
1086
1219
|
if (found) {
|
|
1087
1220
|
submitForm = found;
|
|
1088
|
-
pendingExecutions.
|
|
1221
|
+
const pending = pendingExecutions.get(form);
|
|
1222
|
+
const nextPending = pending?.timeoutId ? { resolve, reject, timeoutId: pending.timeoutId } : { resolve, reject };
|
|
1223
|
+
pendingExecutions.set(submitForm, nextPending);
|
|
1089
1224
|
attachSubmitInterceptor(submitForm, toolName);
|
|
1090
1225
|
}
|
|
1091
1226
|
}
|
|
@@ -1093,6 +1228,39 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
1093
1228
|
pendingWarnings.set(submitForm, pendingWarnings.get(form));
|
|
1094
1229
|
pendingWarnings.delete(form);
|
|
1095
1230
|
}
|
|
1231
|
+
if (!submitForm.checkValidity()) {
|
|
1232
|
+
const pending = pendingExecutions.get(submitForm) ?? pendingExecutions.get(form);
|
|
1233
|
+
if (pending) {
|
|
1234
|
+
if (pending.timeoutId)
|
|
1235
|
+
clearTimeout(pending.timeoutId);
|
|
1236
|
+
pendingExecutions.delete(submitForm);
|
|
1237
|
+
pendingExecutions.delete(form);
|
|
1238
|
+
const warnings = [
|
|
1239
|
+
...pendingFillWarnings.get(submitForm) ?? pendingFillWarnings.get(form) ?? [],
|
|
1240
|
+
...collectInvalidFieldWarnings(submitForm)
|
|
1241
|
+
];
|
|
1242
|
+
pendingFillWarnings.delete(submitForm);
|
|
1243
|
+
pendingFillWarnings.delete(form);
|
|
1244
|
+
const structured = {
|
|
1245
|
+
status: "blocked_invalid",
|
|
1246
|
+
filled_fields: serializeFormData(submitForm, lastParams.get(submitForm) ?? lastParams.get(form), formFieldElements.get(submitForm) ?? formFieldElements.get(form)),
|
|
1247
|
+
skipped_fields: [],
|
|
1248
|
+
missing_required: pendingWarnings.get(submitForm) ?? pendingWarnings.get(form) ?? [],
|
|
1249
|
+
warnings
|
|
1250
|
+
};
|
|
1251
|
+
pendingWarnings.delete(submitForm);
|
|
1252
|
+
pendingWarnings.delete(form);
|
|
1253
|
+
lastFilledSnapshot.delete(submitForm);
|
|
1254
|
+
lastFilledSnapshot.delete(form);
|
|
1255
|
+
resolve({
|
|
1256
|
+
content: [
|
|
1257
|
+
{ type: "text", text: "Form submission blocked by native validation." },
|
|
1258
|
+
{ type: "text", text: JSON.stringify(structured) }
|
|
1259
|
+
]
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1096
1264
|
submitForm.requestSubmit();
|
|
1097
1265
|
} catch (err) {
|
|
1098
1266
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1111,6 +1279,8 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
1111
1279
|
if (!pending)
|
|
1112
1280
|
return;
|
|
1113
1281
|
const { resolve } = pending;
|
|
1282
|
+
if (pending.timeoutId)
|
|
1283
|
+
clearTimeout(pending.timeoutId);
|
|
1114
1284
|
pendingExecutions.delete(form);
|
|
1115
1285
|
const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
|
|
1116
1286
|
lastFilledSnapshot.delete(form);
|
|
@@ -1193,7 +1363,23 @@ function findInShadowRoots(root, selector) {
|
|
|
1193
1363
|
}
|
|
1194
1364
|
return null;
|
|
1195
1365
|
}
|
|
1366
|
+
function getAssociatedInputsByName(form, type, name) {
|
|
1367
|
+
return Array.from(form.elements).filter(
|
|
1368
|
+
(el) => el instanceof HTMLInputElement && el.type === type && el.name === name
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1196
1371
|
function findNativeField(form, key) {
|
|
1372
|
+
const named = form.elements.namedItem(key);
|
|
1373
|
+
if (typeof named === "object" && named !== null && (named instanceof HTMLInputElement || named instanceof HTMLTextAreaElement || named instanceof HTMLSelectElement)) {
|
|
1374
|
+
return named;
|
|
1375
|
+
}
|
|
1376
|
+
if (named instanceof RadioNodeList) {
|
|
1377
|
+
const first = named[0];
|
|
1378
|
+
const firstObj = first;
|
|
1379
|
+
if (firstObj instanceof HTMLInputElement || firstObj instanceof HTMLTextAreaElement || firstObj instanceof HTMLSelectElement) {
|
|
1380
|
+
return firstObj;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1197
1383
|
const esc = CSS.escape(key);
|
|
1198
1384
|
const light = form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
|
|
1199
1385
|
`input#${esc}, textarea#${esc}, select#${esc}`
|
|
@@ -1213,10 +1399,7 @@ function fillFormFields(form, params) {
|
|
|
1213
1399
|
fillInput(input, form, key, value);
|
|
1214
1400
|
if (input.type === "checkbox") {
|
|
1215
1401
|
if (Array.isArray(value)) {
|
|
1216
|
-
|
|
1217
|
-
snapshot[key] = Array.from(
|
|
1218
|
-
form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`)
|
|
1219
|
-
).filter((b) => b.checked).map((b) => b.value);
|
|
1402
|
+
snapshot[key] = getAssociatedInputsByName(form, "checkbox", key).filter((b) => b.checked).map((b) => b.value);
|
|
1220
1403
|
} else {
|
|
1221
1404
|
snapshot[key] = input.checked;
|
|
1222
1405
|
}
|
|
@@ -1265,8 +1448,7 @@ function fillInput(input, form, key, value) {
|
|
|
1265
1448
|
const type = input.type.toLowerCase();
|
|
1266
1449
|
if (type === "checkbox") {
|
|
1267
1450
|
if (Array.isArray(value)) {
|
|
1268
|
-
const
|
|
1269
|
-
const allBoxes = form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`);
|
|
1451
|
+
const allBoxes = getAssociatedInputsByName(form, "checkbox", key);
|
|
1270
1452
|
for (const box of allBoxes) {
|
|
1271
1453
|
setReactChecked(box, value.map(String).includes(box.value));
|
|
1272
1454
|
}
|
|
@@ -1307,10 +1489,7 @@ function fillInput(input, form, key, value) {
|
|
|
1307
1489
|
return;
|
|
1308
1490
|
}
|
|
1309
1491
|
if (type === "radio") {
|
|
1310
|
-
const
|
|
1311
|
-
const radios = form.querySelectorAll(
|
|
1312
|
-
`input[type="radio"][name="${esc}"]`
|
|
1313
|
-
);
|
|
1492
|
+
const radios = getAssociatedInputsByName(form, "radio", key);
|
|
1314
1493
|
for (const radio of radios) {
|
|
1315
1494
|
if (radio.value === String(value)) {
|
|
1316
1495
|
if (_checkedSetter) {
|
|
@@ -1633,9 +1812,48 @@ function isExcluded(form, config) {
|
|
|
1633
1812
|
}
|
|
1634
1813
|
return false;
|
|
1635
1814
|
}
|
|
1815
|
+
function withNumericSuffix(baseName, n) {
|
|
1816
|
+
const suffix = `_${n}`;
|
|
1817
|
+
return `${baseName.slice(0, Math.max(1, 64 - suffix.length))}${suffix}`;
|
|
1818
|
+
}
|
|
1819
|
+
function getUsedToolNames(excludeForm) {
|
|
1820
|
+
const names = new Set(registeredOrphanToolNames);
|
|
1821
|
+
for (const { form, name } of getAllRegisteredTools()) {
|
|
1822
|
+
if (excludeForm && form === excludeForm)
|
|
1823
|
+
continue;
|
|
1824
|
+
names.add(name);
|
|
1825
|
+
}
|
|
1826
|
+
return names;
|
|
1827
|
+
}
|
|
1828
|
+
function ensureUniqueToolName(baseName, excludeForm) {
|
|
1829
|
+
const used = getUsedToolNames(excludeForm);
|
|
1830
|
+
if (!used.has(baseName))
|
|
1831
|
+
return baseName;
|
|
1832
|
+
let i = 2;
|
|
1833
|
+
let candidate = withNumericSuffix(baseName, i);
|
|
1834
|
+
while (used.has(candidate)) {
|
|
1835
|
+
i++;
|
|
1836
|
+
candidate = withNumericSuffix(baseName, i);
|
|
1837
|
+
}
|
|
1838
|
+
return candidate;
|
|
1839
|
+
}
|
|
1840
|
+
function hasNativeDeclarativeTool(form) {
|
|
1841
|
+
return form.getAttribute("toolname")?.trim().length ? true : false;
|
|
1842
|
+
}
|
|
1636
1843
|
async function registerForm(form, config) {
|
|
1637
1844
|
if (isExcluded(form, config))
|
|
1638
1845
|
return;
|
|
1846
|
+
const previousName = getRegisteredToolName(form);
|
|
1847
|
+
if (hasNativeDeclarativeTool(form) && config.declarativeMode !== "force") {
|
|
1848
|
+
if (previousName) {
|
|
1849
|
+
await unregisterFormTool(form);
|
|
1850
|
+
}
|
|
1851
|
+
if (config.debug) {
|
|
1852
|
+
const mode = config.declarativeMode;
|
|
1853
|
+
console.log(`[auto-webmcp] Skipping imperative registration for native declarative form (mode=${mode})`);
|
|
1854
|
+
}
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1639
1857
|
let override;
|
|
1640
1858
|
for (const [selector, ovr] of Object.entries(config.overrides)) {
|
|
1641
1859
|
try {
|
|
@@ -1647,6 +1865,11 @@ async function registerForm(form, config) {
|
|
|
1647
1865
|
}
|
|
1648
1866
|
}
|
|
1649
1867
|
const metadata = analyzeForm(form, override);
|
|
1868
|
+
const resolvedName = ensureUniqueToolName(metadata.name, form);
|
|
1869
|
+
if (resolvedName !== metadata.name && config.debug) {
|
|
1870
|
+
console.warn(`[auto-webmcp] tool name collision: "${metadata.name}" renamed to "${resolvedName}"`);
|
|
1871
|
+
}
|
|
1872
|
+
metadata.name = resolvedName;
|
|
1650
1873
|
if (config.debug) {
|
|
1651
1874
|
warnToolQuality(metadata.name, metadata.description);
|
|
1652
1875
|
}
|
|
@@ -1658,6 +1881,9 @@ async function registerForm(form, config) {
|
|
|
1658
1881
|
'[type="submit"], button[data-variant="primary"], button:not([type])'
|
|
1659
1882
|
) ?? null;
|
|
1660
1883
|
const pendingBtns = window["__pendingSubmitBtns"] ??= {};
|
|
1884
|
+
if (previousName && previousName !== metadata.name) {
|
|
1885
|
+
delete pendingBtns[previousName];
|
|
1886
|
+
}
|
|
1661
1887
|
pendingBtns[metadata.name] = formSubmitBtn;
|
|
1662
1888
|
if (config.debug) {
|
|
1663
1889
|
console.log(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
|
|
@@ -1665,12 +1891,14 @@ async function registerForm(form, config) {
|
|
|
1665
1891
|
emit("form:registered", form, metadata.name);
|
|
1666
1892
|
}
|
|
1667
1893
|
async function unregisterForm(form, config) {
|
|
1668
|
-
const
|
|
1669
|
-
const name = getRegisteredToolName2(form);
|
|
1894
|
+
const name = getRegisteredToolName(form);
|
|
1670
1895
|
if (!name)
|
|
1671
1896
|
return;
|
|
1672
1897
|
await unregisterFormTool(form);
|
|
1673
1898
|
registeredForms.delete(form);
|
|
1899
|
+
const pendingBtns = window["__pendingSubmitBtns"];
|
|
1900
|
+
if (pendingBtns)
|
|
1901
|
+
delete pendingBtns[name];
|
|
1674
1902
|
if (config.debug) {
|
|
1675
1903
|
console.log(`[auto-webmcp] Unregistered: ${name}`);
|
|
1676
1904
|
}
|
|
@@ -1719,11 +1947,47 @@ function scheduleReAnalysis(form, config) {
|
|
|
1719
1947
|
}, RE_ANALYSIS_DEBOUNCE_MS)
|
|
1720
1948
|
);
|
|
1721
1949
|
}
|
|
1950
|
+
function scheduleFormReAnalysisById(formId, config) {
|
|
1951
|
+
const owner = document.getElementById(formId);
|
|
1952
|
+
if (owner instanceof HTMLFormElement && registeredForms.has(owner)) {
|
|
1953
|
+
scheduleReAnalysis(owner, config);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function resolveOwnerForm(el) {
|
|
1957
|
+
const closest = el.closest("form");
|
|
1958
|
+
if (closest instanceof HTMLFormElement)
|
|
1959
|
+
return closest;
|
|
1960
|
+
const explicitOwner = el.form;
|
|
1961
|
+
if (explicitOwner instanceof HTMLFormElement)
|
|
1962
|
+
return explicitOwner;
|
|
1963
|
+
const formId = el.getAttribute("form");
|
|
1964
|
+
if (formId) {
|
|
1965
|
+
const byId = document.getElementById(formId);
|
|
1966
|
+
if (byId instanceof HTMLFormElement)
|
|
1967
|
+
return byId;
|
|
1968
|
+
}
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1722
1971
|
function startObserver(config) {
|
|
1723
1972
|
if (observer)
|
|
1724
1973
|
return;
|
|
1725
1974
|
observer = new MutationObserver((mutations) => {
|
|
1726
1975
|
for (const mutation of mutations) {
|
|
1976
|
+
if (mutation.type === "attributes" && mutation.target instanceof Element) {
|
|
1977
|
+
const target = mutation.target;
|
|
1978
|
+
const ownerForm = resolveOwnerForm(target);
|
|
1979
|
+
if (ownerForm && registeredForms.has(ownerForm)) {
|
|
1980
|
+
scheduleReAnalysis(ownerForm, config);
|
|
1981
|
+
} else if (target instanceof HTMLFormElement) {
|
|
1982
|
+
void registerForm(target, config);
|
|
1983
|
+
} else if (isInterestingNode(target) && !target.closest("form")) {
|
|
1984
|
+
scheduleOrphanRescan(config);
|
|
1985
|
+
}
|
|
1986
|
+
if (mutation.attributeName === "form" && mutation.oldValue) {
|
|
1987
|
+
scheduleFormReAnalysisById(mutation.oldValue, config);
|
|
1988
|
+
}
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1727
1991
|
for (const node of mutation.addedNodes) {
|
|
1728
1992
|
if (!(node instanceof Element))
|
|
1729
1993
|
continue;
|
|
@@ -1752,7 +2016,12 @@ function startObserver(config) {
|
|
|
1752
2016
|
}
|
|
1753
2017
|
}
|
|
1754
2018
|
});
|
|
1755
|
-
observer.observe(document.body, {
|
|
2019
|
+
observer.observe(document.body, {
|
|
2020
|
+
childList: true,
|
|
2021
|
+
subtree: true,
|
|
2022
|
+
attributes: true,
|
|
2023
|
+
attributeOldValue: true
|
|
2024
|
+
});
|
|
1756
2025
|
}
|
|
1757
2026
|
function listenForRouteChanges(config) {
|
|
1758
2027
|
window.addEventListener("hashchange", () => scanForms(config));
|
|
@@ -1896,6 +2165,15 @@ async function scanOrphanInputs(config) {
|
|
|
1896
2165
|
}
|
|
1897
2166
|
console.log(`[auto-webmcp] orphan: submit button for group:`, submitBtn ? `"${submitBtn.textContent?.trim()}" disabled=${submitBtn.disabled}` : "none");
|
|
1898
2167
|
const metadata = analyzeOrphanInputGroup(container, inputs, submitBtn);
|
|
2168
|
+
if (registeredOrphanToolNames.has(metadata.name)) {
|
|
2169
|
+
console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
const orphanName = ensureUniqueToolName(metadata.name);
|
|
2173
|
+
if (orphanName !== metadata.name && config.debug) {
|
|
2174
|
+
console.warn(`[auto-webmcp] orphan tool name collision: "${metadata.name}" renamed to "${orphanName}"`);
|
|
2175
|
+
}
|
|
2176
|
+
metadata.name = orphanName;
|
|
1899
2177
|
console.log(`[auto-webmcp] orphan: tool="${metadata.name}" schema keys:`, Object.keys(metadata.inputSchema.properties));
|
|
1900
2178
|
const inputPairs = [];
|
|
1901
2179
|
const schemaProps = metadata.inputSchema.properties;
|
|
@@ -1916,7 +2194,7 @@ async function scanOrphanInputs(config) {
|
|
|
1916
2194
|
continue;
|
|
1917
2195
|
}
|
|
1918
2196
|
const toolName = metadata.name;
|
|
1919
|
-
const execute = async (params) => {
|
|
2197
|
+
const execute = async (params, _client) => {
|
|
1920
2198
|
console.log(`[auto-webmcp] orphan execute: tool="${toolName}" params=`, params);
|
|
1921
2199
|
console.log(`[auto-webmcp] orphan execute: inputPairs=`, inputPairs.map((p) => p.key));
|
|
1922
2200
|
for (const { key, el } of inputPairs) {
|
|
@@ -1985,10 +2263,6 @@ async function scanOrphanInputs(config) {
|
|
|
1985
2263
|
return { content: [{ type: "text", text: "Fields filled and form submitted." }] };
|
|
1986
2264
|
};
|
|
1987
2265
|
try {
|
|
1988
|
-
if (registeredOrphanToolNames.has(metadata.name)) {
|
|
1989
|
-
console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
|
|
1990
|
-
continue;
|
|
1991
|
-
}
|
|
1992
2266
|
const toolDef = {
|
|
1993
2267
|
name: metadata.name,
|
|
1994
2268
|
description: metadata.description,
|
|
@@ -2039,7 +2313,6 @@ function stopDiscovery() {
|
|
|
2039
2313
|
}
|
|
2040
2314
|
|
|
2041
2315
|
// src/index.ts
|
|
2042
|
-
init_registry();
|
|
2043
2316
|
async function autoWebMCP(config) {
|
|
2044
2317
|
const resolved = resolveConfig(config);
|
|
2045
2318
|
if (resolved.debug) {
|