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.cjs.js
CHANGED
|
@@ -3,9 +3,6 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __esm = (fn, res) => function __init() {
|
|
7
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
-
};
|
|
9
6
|
var __export = (target, all) => {
|
|
10
7
|
for (var name in all)
|
|
11
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -20,76 +17,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
17
|
};
|
|
21
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
19
|
|
|
23
|
-
// src/registry.ts
|
|
24
|
-
var registry_exports = {};
|
|
25
|
-
__export(registry_exports, {
|
|
26
|
-
getAllRegisteredTools: () => getAllRegisteredTools,
|
|
27
|
-
getRegisteredToolName: () => getRegisteredToolName,
|
|
28
|
-
isWebMCPSupported: () => isWebMCPSupported,
|
|
29
|
-
registerFormTool: () => registerFormTool,
|
|
30
|
-
unregisterAll: () => unregisterAll,
|
|
31
|
-
unregisterFormTool: () => unregisterFormTool
|
|
32
|
-
});
|
|
33
|
-
function isWebMCPSupported() {
|
|
34
|
-
return typeof navigator !== "undefined" && typeof navigator.modelContext !== "undefined";
|
|
35
|
-
}
|
|
36
|
-
async function registerFormTool(form, metadata, execute) {
|
|
37
|
-
if (!isWebMCPSupported())
|
|
38
|
-
return;
|
|
39
|
-
const existing = registeredTools.get(form);
|
|
40
|
-
if (existing) {
|
|
41
|
-
await unregisterFormTool(form);
|
|
42
|
-
}
|
|
43
|
-
const toolDef = {
|
|
44
|
-
name: metadata.name,
|
|
45
|
-
description: metadata.description,
|
|
46
|
-
inputSchema: metadata.inputSchema,
|
|
47
|
-
execute
|
|
48
|
-
};
|
|
49
|
-
if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
|
|
50
|
-
toolDef.annotations = metadata.annotations;
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
await navigator.modelContext.registerTool(toolDef);
|
|
54
|
-
} catch {
|
|
55
|
-
try {
|
|
56
|
-
await navigator.modelContext.unregisterTool(metadata.name);
|
|
57
|
-
await navigator.modelContext.registerTool(toolDef);
|
|
58
|
-
} catch {
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
registeredTools.set(form, metadata.name);
|
|
62
|
-
}
|
|
63
|
-
async function unregisterFormTool(form) {
|
|
64
|
-
if (!isWebMCPSupported())
|
|
65
|
-
return;
|
|
66
|
-
const name = registeredTools.get(form);
|
|
67
|
-
if (!name)
|
|
68
|
-
return;
|
|
69
|
-
try {
|
|
70
|
-
await navigator.modelContext.unregisterTool(name);
|
|
71
|
-
} catch {
|
|
72
|
-
}
|
|
73
|
-
registeredTools.delete(form);
|
|
74
|
-
}
|
|
75
|
-
function getRegisteredToolName(form) {
|
|
76
|
-
return registeredTools.get(form);
|
|
77
|
-
}
|
|
78
|
-
function getAllRegisteredTools() {
|
|
79
|
-
return Array.from(registeredTools.entries()).map(([form, name]) => ({ form, name }));
|
|
80
|
-
}
|
|
81
|
-
async function unregisterAll() {
|
|
82
|
-
const entries = Array.from(registeredTools.entries());
|
|
83
|
-
await Promise.all(entries.map(([form]) => unregisterFormTool(form)));
|
|
84
|
-
}
|
|
85
|
-
var registeredTools;
|
|
86
|
-
var init_registry = __esm({
|
|
87
|
-
"src/registry.ts"() {
|
|
88
|
-
"use strict";
|
|
89
|
-
registeredTools = /* @__PURE__ */ new Map();
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
20
|
// src/index.ts
|
|
94
21
|
var src_exports = {};
|
|
95
22
|
__export(src_exports, {
|
|
@@ -99,9 +26,19 @@ module.exports = __toCommonJS(src_exports);
|
|
|
99
26
|
|
|
100
27
|
// src/config.ts
|
|
101
28
|
function resolveConfig(userConfig) {
|
|
29
|
+
const strict = userConfig?.paramBinding?.strict ?? false;
|
|
30
|
+
const enableAliasResolution = strict ? false : userConfig?.paramBinding?.enableAliasResolution ?? true;
|
|
102
31
|
return {
|
|
103
32
|
exclude: userConfig?.exclude ?? [],
|
|
104
33
|
autoSubmit: userConfig?.autoSubmit ?? false,
|
|
34
|
+
declarativeMode: userConfig?.declarativeMode ?? "skip",
|
|
35
|
+
paramBinding: {
|
|
36
|
+
strict,
|
|
37
|
+
enableAliasResolution
|
|
38
|
+
},
|
|
39
|
+
execution: {
|
|
40
|
+
timeoutMs: Math.max(100, userConfig?.execution?.timeoutMs ?? 15e3)
|
|
41
|
+
},
|
|
105
42
|
overrides: userConfig?.overrides ?? {},
|
|
106
43
|
debug: userConfig?.debug ?? false
|
|
107
44
|
};
|
|
@@ -250,19 +187,19 @@ function mapSelectElement(select) {
|
|
|
250
187
|
return { type: "string", enum: enumValues, oneOf };
|
|
251
188
|
}
|
|
252
189
|
function collectCheckboxEnum(form, name) {
|
|
253
|
-
return Array.from(
|
|
254
|
-
|
|
190
|
+
return Array.from(form.elements).filter(
|
|
191
|
+
(el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === name
|
|
255
192
|
).map((cb) => cb.value).filter((v) => v !== "" && v !== "on");
|
|
256
193
|
}
|
|
257
194
|
function collectRadioEnum(form, name) {
|
|
258
|
-
const radios = Array.from(
|
|
259
|
-
|
|
195
|
+
const radios = Array.from(form.elements).filter(
|
|
196
|
+
(el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === name
|
|
260
197
|
);
|
|
261
198
|
return radios.map((r) => r.value).filter((v) => v !== "");
|
|
262
199
|
}
|
|
263
200
|
function collectRadioOneOf(form, name) {
|
|
264
|
-
const radios = Array.from(
|
|
265
|
-
|
|
201
|
+
const radios = Array.from(form.elements).filter(
|
|
202
|
+
(el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === name
|
|
266
203
|
).filter((r) => r.value !== "");
|
|
267
204
|
return radios.map((r) => {
|
|
268
205
|
const title = getRadioLabelText(r);
|
|
@@ -522,20 +459,26 @@ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
|
|
|
522
459
|
}
|
|
523
460
|
return results;
|
|
524
461
|
}
|
|
462
|
+
function collectFormAssociatedControls(form) {
|
|
463
|
+
const controls = Array.from(form.elements).filter(
|
|
464
|
+
(el) => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement
|
|
465
|
+
);
|
|
466
|
+
const seen = new Set(controls);
|
|
467
|
+
for (const shadowControl of collectShadowControls(form)) {
|
|
468
|
+
if (!seen.has(shadowControl)) {
|
|
469
|
+
controls.push(shadowControl);
|
|
470
|
+
seen.add(shadowControl);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return controls;
|
|
474
|
+
}
|
|
525
475
|
function buildSchema(form) {
|
|
526
476
|
const properties = {};
|
|
527
477
|
const required = [];
|
|
528
478
|
const fieldElements = /* @__PURE__ */ new Map();
|
|
529
479
|
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
530
480
|
const processedCheckboxGroups = /* @__PURE__ */ new Set();
|
|
531
|
-
const controls =
|
|
532
|
-
...Array.from(
|
|
533
|
-
form.querySelectorAll(
|
|
534
|
-
"input, textarea, select"
|
|
535
|
-
)
|
|
536
|
-
),
|
|
537
|
-
...collectShadowControls(form)
|
|
538
|
-
];
|
|
481
|
+
const controls = collectFormAssociatedControls(form);
|
|
539
482
|
for (const control of controls) {
|
|
540
483
|
const name = control.name;
|
|
541
484
|
const fieldKey = name || resolveNativeControlFallbackKey(control);
|
|
@@ -568,8 +511,8 @@ function buildSchema(form) {
|
|
|
568
511
|
const radioOneOf = collectRadioOneOf(form, fieldKey);
|
|
569
512
|
if (radioOneOf.length > 0)
|
|
570
513
|
schemaProp.oneOf = radioOneOf;
|
|
571
|
-
const checkedRadio = form.
|
|
572
|
-
|
|
514
|
+
const checkedRadio = Array.from(form.elements).find(
|
|
515
|
+
(el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === fieldKey && el.checked
|
|
573
516
|
);
|
|
574
517
|
if (checkedRadio?.value)
|
|
575
518
|
schemaProp.default = checkedRadio.value;
|
|
@@ -584,10 +527,8 @@ function buildSchema(form) {
|
|
|
584
527
|
};
|
|
585
528
|
if (schemaProp.description)
|
|
586
529
|
arrayProp.description = schemaProp.description;
|
|
587
|
-
const checkedBoxes = Array.from(
|
|
588
|
-
|
|
589
|
-
`input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
|
|
590
|
-
)
|
|
530
|
+
const checkedBoxes = Array.from(form.elements).filter(
|
|
531
|
+
(el) => el instanceof HTMLInputElement && el.type === "checkbox" && el.name === fieldKey && el.checked
|
|
591
532
|
).map((b) => b.value);
|
|
592
533
|
if (checkedBoxes.length > 0)
|
|
593
534
|
arrayProp.default = checkedBoxes;
|
|
@@ -1057,8 +998,68 @@ function buildSchemaFromInputs(inputs) {
|
|
|
1057
998
|
return { schema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object", properties, required }, fieldElements };
|
|
1058
999
|
}
|
|
1059
1000
|
|
|
1060
|
-
// src/
|
|
1061
|
-
|
|
1001
|
+
// src/registry.ts
|
|
1002
|
+
var registeredTools = /* @__PURE__ */ new Map();
|
|
1003
|
+
var registrationControllers = /* @__PURE__ */ new Map();
|
|
1004
|
+
function isWebMCPSupported() {
|
|
1005
|
+
return typeof navigator !== "undefined" && typeof navigator.modelContext !== "undefined";
|
|
1006
|
+
}
|
|
1007
|
+
async function registerFormTool(form, metadata, execute) {
|
|
1008
|
+
if (!isWebMCPSupported())
|
|
1009
|
+
return;
|
|
1010
|
+
const existing = registeredTools.get(form);
|
|
1011
|
+
if (existing) {
|
|
1012
|
+
await unregisterFormTool(form);
|
|
1013
|
+
}
|
|
1014
|
+
const toolDef = {
|
|
1015
|
+
name: metadata.name,
|
|
1016
|
+
description: metadata.description,
|
|
1017
|
+
inputSchema: metadata.inputSchema,
|
|
1018
|
+
execute
|
|
1019
|
+
};
|
|
1020
|
+
if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
|
|
1021
|
+
toolDef.annotations = metadata.annotations;
|
|
1022
|
+
}
|
|
1023
|
+
const controller = new AbortController();
|
|
1024
|
+
registrationControllers.set(form, controller);
|
|
1025
|
+
try {
|
|
1026
|
+
await navigator.modelContext.registerTool(toolDef, { signal: controller.signal });
|
|
1027
|
+
} catch {
|
|
1028
|
+
try {
|
|
1029
|
+
await navigator.modelContext.unregisterTool?.(metadata.name);
|
|
1030
|
+
await navigator.modelContext.registerTool(toolDef, { signal: controller.signal });
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
registeredTools.set(form, metadata.name);
|
|
1035
|
+
}
|
|
1036
|
+
async function unregisterFormTool(form) {
|
|
1037
|
+
if (!isWebMCPSupported())
|
|
1038
|
+
return;
|
|
1039
|
+
const name = registeredTools.get(form);
|
|
1040
|
+
if (!name)
|
|
1041
|
+
return;
|
|
1042
|
+
const controller = registrationControllers.get(form);
|
|
1043
|
+
if (controller) {
|
|
1044
|
+
controller.abort();
|
|
1045
|
+
registrationControllers.delete(form);
|
|
1046
|
+
}
|
|
1047
|
+
try {
|
|
1048
|
+
await navigator.modelContext.unregisterTool?.(name);
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
registeredTools.delete(form);
|
|
1052
|
+
}
|
|
1053
|
+
function getRegisteredToolName(form) {
|
|
1054
|
+
return registeredTools.get(form);
|
|
1055
|
+
}
|
|
1056
|
+
function getAllRegisteredTools() {
|
|
1057
|
+
return Array.from(registeredTools.entries()).map(([form, name]) => ({ form, name }));
|
|
1058
|
+
}
|
|
1059
|
+
async function unregisterAll() {
|
|
1060
|
+
const entries = Array.from(registeredTools.entries());
|
|
1061
|
+
await Promise.all(entries.map(([form]) => unregisterFormTool(form)));
|
|
1062
|
+
}
|
|
1062
1063
|
|
|
1063
1064
|
// src/interceptor.ts
|
|
1064
1065
|
var pendingExecutions = /* @__PURE__ */ new WeakMap();
|
|
@@ -1070,30 +1071,169 @@ var lastFilledSnapshot = /* @__PURE__ */ new WeakMap();
|
|
|
1070
1071
|
var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
1071
1072
|
var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
|
|
1072
1073
|
var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
|
|
1074
|
+
function normalizeAliasKey(raw) {
|
|
1075
|
+
return raw.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
1076
|
+
}
|
|
1077
|
+
function addAlias(index, alias, schemaKey) {
|
|
1078
|
+
if (!alias)
|
|
1079
|
+
return;
|
|
1080
|
+
const normalized = normalizeAliasKey(alias);
|
|
1081
|
+
if (!normalized)
|
|
1082
|
+
return;
|
|
1083
|
+
if (!index.has(normalized))
|
|
1084
|
+
index.set(normalized, /* @__PURE__ */ new Set());
|
|
1085
|
+
index.get(normalized).add(schemaKey);
|
|
1086
|
+
}
|
|
1087
|
+
function buildAliasIndex(form, metadata) {
|
|
1088
|
+
const index = /* @__PURE__ */ new Map();
|
|
1089
|
+
const properties = metadata?.inputSchema?.properties ?? {};
|
|
1090
|
+
for (const [schemaKey, prop] of Object.entries(properties)) {
|
|
1091
|
+
addAlias(index, schemaKey, schemaKey);
|
|
1092
|
+
addAlias(index, schemaKey.replace(/_/g, " "), schemaKey);
|
|
1093
|
+
addAlias(index, prop.title, schemaKey);
|
|
1094
|
+
const nativeEl = findNativeField(form, schemaKey);
|
|
1095
|
+
const mappedEl = metadata?.fieldElements?.get(schemaKey);
|
|
1096
|
+
const el = nativeEl ?? mappedEl ?? null;
|
|
1097
|
+
if (!el)
|
|
1098
|
+
continue;
|
|
1099
|
+
const htmlEl = el;
|
|
1100
|
+
addAlias(index, htmlEl.getAttribute("id"), schemaKey);
|
|
1101
|
+
addAlias(index, htmlEl.getAttribute("name"), schemaKey);
|
|
1102
|
+
addAlias(index, htmlEl.getAttribute("aria-label"), schemaKey);
|
|
1103
|
+
addAlias(index, htmlEl.getAttribute("placeholder"), schemaKey);
|
|
1104
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
1105
|
+
for (const label of Array.from(el.labels ?? [])) {
|
|
1106
|
+
addAlias(index, label.textContent?.trim(), schemaKey);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return index;
|
|
1111
|
+
}
|
|
1112
|
+
function resolveParamsForSchema(form, params, metadata, config) {
|
|
1113
|
+
const resolved = {};
|
|
1114
|
+
const warnings = [];
|
|
1115
|
+
const properties = metadata?.inputSchema?.properties ?? {};
|
|
1116
|
+
const aliasEnabled = config.paramBinding.enableAliasResolution;
|
|
1117
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1118
|
+
if (key in properties)
|
|
1119
|
+
resolved[key] = value;
|
|
1120
|
+
}
|
|
1121
|
+
if (!aliasEnabled)
|
|
1122
|
+
return { resolved, warnings };
|
|
1123
|
+
const aliasIndex = buildAliasIndex(form, metadata);
|
|
1124
|
+
for (const [rawKey, value] of Object.entries(params)) {
|
|
1125
|
+
if (rawKey in properties)
|
|
1126
|
+
continue;
|
|
1127
|
+
const candidates = aliasIndex.get(normalizeAliasKey(rawKey));
|
|
1128
|
+
if (!candidates || candidates.size !== 1)
|
|
1129
|
+
continue;
|
|
1130
|
+
const target = Array.from(candidates)[0];
|
|
1131
|
+
if (!target || target in resolved)
|
|
1132
|
+
continue;
|
|
1133
|
+
resolved[target] = value;
|
|
1134
|
+
warnings.push({
|
|
1135
|
+
field: target,
|
|
1136
|
+
type: "alias_resolved",
|
|
1137
|
+
original: rawKey,
|
|
1138
|
+
message: `resolved "${rawKey}" to schema field "${target}"`
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
return { resolved, warnings };
|
|
1142
|
+
}
|
|
1143
|
+
function collectInvalidFieldWarnings(form) {
|
|
1144
|
+
const warnings = [];
|
|
1145
|
+
const controls = Array.from(form.elements).filter(
|
|
1146
|
+
(el) => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement
|
|
1147
|
+
);
|
|
1148
|
+
for (const control of controls) {
|
|
1149
|
+
if (!control.willValidate)
|
|
1150
|
+
continue;
|
|
1151
|
+
if (control.checkValidity())
|
|
1152
|
+
continue;
|
|
1153
|
+
const field = control.name || control.id || control.getAttribute("aria-label") || "unknown_field";
|
|
1154
|
+
warnings.push({
|
|
1155
|
+
field,
|
|
1156
|
+
type: "blocked_submit",
|
|
1157
|
+
message: control.validationMessage || `field "${field}" failed validation`
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return warnings;
|
|
1161
|
+
}
|
|
1073
1162
|
function buildExecuteHandler(form, config, toolName, metadata) {
|
|
1074
1163
|
if (metadata?.fieldElements) {
|
|
1075
1164
|
formFieldElements.set(form, metadata.fieldElements);
|
|
1076
1165
|
}
|
|
1077
1166
|
attachSubmitInterceptor(form, toolName);
|
|
1078
|
-
return async (params) => {
|
|
1167
|
+
return async (params, client) => {
|
|
1168
|
+
const modelContextClient = client;
|
|
1169
|
+
if (config.autoSubmit && metadata?.annotations?.destructiveHint === true && typeof modelContextClient?.requestUserInteraction === "function") {
|
|
1170
|
+
const approved = await modelContextClient.requestUserInteraction(async () => {
|
|
1171
|
+
return new Promise((resolve) => {
|
|
1172
|
+
const ok = window.confirm(`Agent requested a destructive action via "${toolName}". Continue?`);
|
|
1173
|
+
resolve(ok);
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
if (!approved) {
|
|
1177
|
+
window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
|
|
1178
|
+
return { content: [{ type: "text", text: `Cancelled "${toolName}" by user.` }] };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1079
1181
|
pendingFillWarnings.set(form, []);
|
|
1080
1182
|
pendingWarnings.delete(form);
|
|
1081
|
-
|
|
1082
|
-
|
|
1183
|
+
const { resolved: resolvedParams, warnings: aliasWarnings } = resolveParamsForSchema(
|
|
1184
|
+
form,
|
|
1185
|
+
params,
|
|
1186
|
+
metadata,
|
|
1187
|
+
config
|
|
1188
|
+
);
|
|
1189
|
+
if (aliasWarnings.length > 0) {
|
|
1190
|
+
pendingFillWarnings.set(form, [...pendingFillWarnings.get(form) ?? [], ...aliasWarnings]);
|
|
1191
|
+
}
|
|
1192
|
+
fillFormFields(form, resolvedParams);
|
|
1193
|
+
const missingNow = getMissingRequired(metadata, resolvedParams);
|
|
1083
1194
|
if (missingNow.length > 0)
|
|
1084
1195
|
pendingWarnings.set(form, missingNow);
|
|
1085
1196
|
window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
|
|
1086
1197
|
return new Promise((resolve, reject) => {
|
|
1087
|
-
|
|
1198
|
+
const timeoutMs = config.execution.timeoutMs;
|
|
1199
|
+
const timeoutId = setTimeout(() => {
|
|
1200
|
+
const pending = pendingExecutions.get(form);
|
|
1201
|
+
if (!pending)
|
|
1202
|
+
return;
|
|
1203
|
+
pendingExecutions.delete(form);
|
|
1204
|
+
const timedOutState = config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0 ? "timed_out" : "awaiting_user_action";
|
|
1205
|
+
const warn = {
|
|
1206
|
+
field: "__form__",
|
|
1207
|
+
type: "timeout",
|
|
1208
|
+
message: timedOutState === "timed_out" ? `tool execution timed out after ${timeoutMs}ms` : `waiting for user submit (timed out after ${timeoutMs}ms)`
|
|
1209
|
+
};
|
|
1210
|
+
const structured = {
|
|
1211
|
+
status: timedOutState,
|
|
1212
|
+
filled_fields: serializeFormData(form, lastParams.get(form), formFieldElements.get(form)),
|
|
1213
|
+
skipped_fields: [],
|
|
1214
|
+
missing_required: pendingWarnings.get(form) ?? [],
|
|
1215
|
+
warnings: [...pendingFillWarnings.get(form) ?? [], warn]
|
|
1216
|
+
};
|
|
1217
|
+
pendingWarnings.delete(form);
|
|
1218
|
+
pendingFillWarnings.delete(form);
|
|
1219
|
+
lastFilledSnapshot.delete(form);
|
|
1220
|
+
resolve({
|
|
1221
|
+
content: [
|
|
1222
|
+
{ type: "text", text: warn.message },
|
|
1223
|
+
{ type: "text", text: JSON.stringify(structured) }
|
|
1224
|
+
]
|
|
1225
|
+
});
|
|
1226
|
+
}, timeoutMs);
|
|
1227
|
+
pendingExecutions.set(form, { resolve, reject, timeoutId });
|
|
1088
1228
|
if (config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0) {
|
|
1089
1229
|
waitForDomStable(form).then(async () => {
|
|
1090
1230
|
try {
|
|
1091
|
-
fillFormFields(form,
|
|
1231
|
+
fillFormFields(form, resolvedParams);
|
|
1092
1232
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1093
|
-
const reset = getResetFields(form,
|
|
1233
|
+
const reset = getResetFields(form, resolvedParams, formFieldElements.get(form));
|
|
1094
1234
|
if (reset.length === 0)
|
|
1095
1235
|
break;
|
|
1096
|
-
fillFormFields(form,
|
|
1236
|
+
fillFormFields(form, resolvedParams);
|
|
1097
1237
|
await waitForDomStable(form, 400, 100);
|
|
1098
1238
|
}
|
|
1099
1239
|
let submitForm = form;
|
|
@@ -1104,7 +1244,9 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
1104
1244
|
const found = liveBtn?.closest("form");
|
|
1105
1245
|
if (found) {
|
|
1106
1246
|
submitForm = found;
|
|
1107
|
-
pendingExecutions.
|
|
1247
|
+
const pending = pendingExecutions.get(form);
|
|
1248
|
+
const nextPending = pending?.timeoutId ? { resolve, reject, timeoutId: pending.timeoutId } : { resolve, reject };
|
|
1249
|
+
pendingExecutions.set(submitForm, nextPending);
|
|
1108
1250
|
attachSubmitInterceptor(submitForm, toolName);
|
|
1109
1251
|
}
|
|
1110
1252
|
}
|
|
@@ -1112,6 +1254,39 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
1112
1254
|
pendingWarnings.set(submitForm, pendingWarnings.get(form));
|
|
1113
1255
|
pendingWarnings.delete(form);
|
|
1114
1256
|
}
|
|
1257
|
+
if (!submitForm.checkValidity()) {
|
|
1258
|
+
const pending = pendingExecutions.get(submitForm) ?? pendingExecutions.get(form);
|
|
1259
|
+
if (pending) {
|
|
1260
|
+
if (pending.timeoutId)
|
|
1261
|
+
clearTimeout(pending.timeoutId);
|
|
1262
|
+
pendingExecutions.delete(submitForm);
|
|
1263
|
+
pendingExecutions.delete(form);
|
|
1264
|
+
const warnings = [
|
|
1265
|
+
...pendingFillWarnings.get(submitForm) ?? pendingFillWarnings.get(form) ?? [],
|
|
1266
|
+
...collectInvalidFieldWarnings(submitForm)
|
|
1267
|
+
];
|
|
1268
|
+
pendingFillWarnings.delete(submitForm);
|
|
1269
|
+
pendingFillWarnings.delete(form);
|
|
1270
|
+
const structured = {
|
|
1271
|
+
status: "blocked_invalid",
|
|
1272
|
+
filled_fields: serializeFormData(submitForm, lastParams.get(submitForm) ?? lastParams.get(form), formFieldElements.get(submitForm) ?? formFieldElements.get(form)),
|
|
1273
|
+
skipped_fields: [],
|
|
1274
|
+
missing_required: pendingWarnings.get(submitForm) ?? pendingWarnings.get(form) ?? [],
|
|
1275
|
+
warnings
|
|
1276
|
+
};
|
|
1277
|
+
pendingWarnings.delete(submitForm);
|
|
1278
|
+
pendingWarnings.delete(form);
|
|
1279
|
+
lastFilledSnapshot.delete(submitForm);
|
|
1280
|
+
lastFilledSnapshot.delete(form);
|
|
1281
|
+
resolve({
|
|
1282
|
+
content: [
|
|
1283
|
+
{ type: "text", text: "Form submission blocked by native validation." },
|
|
1284
|
+
{ type: "text", text: JSON.stringify(structured) }
|
|
1285
|
+
]
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1115
1290
|
submitForm.requestSubmit();
|
|
1116
1291
|
} catch (err) {
|
|
1117
1292
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1130,6 +1305,8 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
1130
1305
|
if (!pending)
|
|
1131
1306
|
return;
|
|
1132
1307
|
const { resolve } = pending;
|
|
1308
|
+
if (pending.timeoutId)
|
|
1309
|
+
clearTimeout(pending.timeoutId);
|
|
1133
1310
|
pendingExecutions.delete(form);
|
|
1134
1311
|
const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
|
|
1135
1312
|
lastFilledSnapshot.delete(form);
|
|
@@ -1212,7 +1389,23 @@ function findInShadowRoots(root, selector) {
|
|
|
1212
1389
|
}
|
|
1213
1390
|
return null;
|
|
1214
1391
|
}
|
|
1392
|
+
function getAssociatedInputsByName(form, type, name) {
|
|
1393
|
+
return Array.from(form.elements).filter(
|
|
1394
|
+
(el) => el instanceof HTMLInputElement && el.type === type && el.name === name
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1215
1397
|
function findNativeField(form, key) {
|
|
1398
|
+
const named = form.elements.namedItem(key);
|
|
1399
|
+
if (typeof named === "object" && named !== null && (named instanceof HTMLInputElement || named instanceof HTMLTextAreaElement || named instanceof HTMLSelectElement)) {
|
|
1400
|
+
return named;
|
|
1401
|
+
}
|
|
1402
|
+
if (named instanceof RadioNodeList) {
|
|
1403
|
+
const first = named[0];
|
|
1404
|
+
const firstObj = first;
|
|
1405
|
+
if (firstObj instanceof HTMLInputElement || firstObj instanceof HTMLTextAreaElement || firstObj instanceof HTMLSelectElement) {
|
|
1406
|
+
return firstObj;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1216
1409
|
const esc = CSS.escape(key);
|
|
1217
1410
|
const light = form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
|
|
1218
1411
|
`input#${esc}, textarea#${esc}, select#${esc}`
|
|
@@ -1232,10 +1425,7 @@ function fillFormFields(form, params) {
|
|
|
1232
1425
|
fillInput(input, form, key, value);
|
|
1233
1426
|
if (input.type === "checkbox") {
|
|
1234
1427
|
if (Array.isArray(value)) {
|
|
1235
|
-
|
|
1236
|
-
snapshot[key] = Array.from(
|
|
1237
|
-
form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`)
|
|
1238
|
-
).filter((b) => b.checked).map((b) => b.value);
|
|
1428
|
+
snapshot[key] = getAssociatedInputsByName(form, "checkbox", key).filter((b) => b.checked).map((b) => b.value);
|
|
1239
1429
|
} else {
|
|
1240
1430
|
snapshot[key] = input.checked;
|
|
1241
1431
|
}
|
|
@@ -1284,8 +1474,7 @@ function fillInput(input, form, key, value) {
|
|
|
1284
1474
|
const type = input.type.toLowerCase();
|
|
1285
1475
|
if (type === "checkbox") {
|
|
1286
1476
|
if (Array.isArray(value)) {
|
|
1287
|
-
const
|
|
1288
|
-
const allBoxes = form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`);
|
|
1477
|
+
const allBoxes = getAssociatedInputsByName(form, "checkbox", key);
|
|
1289
1478
|
for (const box of allBoxes) {
|
|
1290
1479
|
setReactChecked(box, value.map(String).includes(box.value));
|
|
1291
1480
|
}
|
|
@@ -1326,10 +1515,7 @@ function fillInput(input, form, key, value) {
|
|
|
1326
1515
|
return;
|
|
1327
1516
|
}
|
|
1328
1517
|
if (type === "radio") {
|
|
1329
|
-
const
|
|
1330
|
-
const radios = form.querySelectorAll(
|
|
1331
|
-
`input[type="radio"][name="${esc}"]`
|
|
1332
|
-
);
|
|
1518
|
+
const radios = getAssociatedInputsByName(form, "radio", key);
|
|
1333
1519
|
for (const radio of radios) {
|
|
1334
1520
|
if (radio.value === String(value)) {
|
|
1335
1521
|
if (_checkedSetter) {
|
|
@@ -1652,9 +1838,48 @@ function isExcluded(form, config) {
|
|
|
1652
1838
|
}
|
|
1653
1839
|
return false;
|
|
1654
1840
|
}
|
|
1841
|
+
function withNumericSuffix(baseName, n) {
|
|
1842
|
+
const suffix = `_${n}`;
|
|
1843
|
+
return `${baseName.slice(0, Math.max(1, 64 - suffix.length))}${suffix}`;
|
|
1844
|
+
}
|
|
1845
|
+
function getUsedToolNames(excludeForm) {
|
|
1846
|
+
const names = new Set(registeredOrphanToolNames);
|
|
1847
|
+
for (const { form, name } of getAllRegisteredTools()) {
|
|
1848
|
+
if (excludeForm && form === excludeForm)
|
|
1849
|
+
continue;
|
|
1850
|
+
names.add(name);
|
|
1851
|
+
}
|
|
1852
|
+
return names;
|
|
1853
|
+
}
|
|
1854
|
+
function ensureUniqueToolName(baseName, excludeForm) {
|
|
1855
|
+
const used = getUsedToolNames(excludeForm);
|
|
1856
|
+
if (!used.has(baseName))
|
|
1857
|
+
return baseName;
|
|
1858
|
+
let i = 2;
|
|
1859
|
+
let candidate = withNumericSuffix(baseName, i);
|
|
1860
|
+
while (used.has(candidate)) {
|
|
1861
|
+
i++;
|
|
1862
|
+
candidate = withNumericSuffix(baseName, i);
|
|
1863
|
+
}
|
|
1864
|
+
return candidate;
|
|
1865
|
+
}
|
|
1866
|
+
function hasNativeDeclarativeTool(form) {
|
|
1867
|
+
return form.getAttribute("toolname")?.trim().length ? true : false;
|
|
1868
|
+
}
|
|
1655
1869
|
async function registerForm(form, config) {
|
|
1656
1870
|
if (isExcluded(form, config))
|
|
1657
1871
|
return;
|
|
1872
|
+
const previousName = getRegisteredToolName(form);
|
|
1873
|
+
if (hasNativeDeclarativeTool(form) && config.declarativeMode !== "force") {
|
|
1874
|
+
if (previousName) {
|
|
1875
|
+
await unregisterFormTool(form);
|
|
1876
|
+
}
|
|
1877
|
+
if (config.debug) {
|
|
1878
|
+
const mode = config.declarativeMode;
|
|
1879
|
+
console.log(`[auto-webmcp] Skipping imperative registration for native declarative form (mode=${mode})`);
|
|
1880
|
+
}
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1658
1883
|
let override;
|
|
1659
1884
|
for (const [selector, ovr] of Object.entries(config.overrides)) {
|
|
1660
1885
|
try {
|
|
@@ -1666,6 +1891,11 @@ async function registerForm(form, config) {
|
|
|
1666
1891
|
}
|
|
1667
1892
|
}
|
|
1668
1893
|
const metadata = analyzeForm(form, override);
|
|
1894
|
+
const resolvedName = ensureUniqueToolName(metadata.name, form);
|
|
1895
|
+
if (resolvedName !== metadata.name && config.debug) {
|
|
1896
|
+
console.warn(`[auto-webmcp] tool name collision: "${metadata.name}" renamed to "${resolvedName}"`);
|
|
1897
|
+
}
|
|
1898
|
+
metadata.name = resolvedName;
|
|
1669
1899
|
if (config.debug) {
|
|
1670
1900
|
warnToolQuality(metadata.name, metadata.description);
|
|
1671
1901
|
}
|
|
@@ -1677,6 +1907,9 @@ async function registerForm(form, config) {
|
|
|
1677
1907
|
'[type="submit"], button[data-variant="primary"], button:not([type])'
|
|
1678
1908
|
) ?? null;
|
|
1679
1909
|
const pendingBtns = window["__pendingSubmitBtns"] ??= {};
|
|
1910
|
+
if (previousName && previousName !== metadata.name) {
|
|
1911
|
+
delete pendingBtns[previousName];
|
|
1912
|
+
}
|
|
1680
1913
|
pendingBtns[metadata.name] = formSubmitBtn;
|
|
1681
1914
|
if (config.debug) {
|
|
1682
1915
|
console.log(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
|
|
@@ -1684,12 +1917,14 @@ async function registerForm(form, config) {
|
|
|
1684
1917
|
emit("form:registered", form, metadata.name);
|
|
1685
1918
|
}
|
|
1686
1919
|
async function unregisterForm(form, config) {
|
|
1687
|
-
const
|
|
1688
|
-
const name = getRegisteredToolName2(form);
|
|
1920
|
+
const name = getRegisteredToolName(form);
|
|
1689
1921
|
if (!name)
|
|
1690
1922
|
return;
|
|
1691
1923
|
await unregisterFormTool(form);
|
|
1692
1924
|
registeredForms.delete(form);
|
|
1925
|
+
const pendingBtns = window["__pendingSubmitBtns"];
|
|
1926
|
+
if (pendingBtns)
|
|
1927
|
+
delete pendingBtns[name];
|
|
1693
1928
|
if (config.debug) {
|
|
1694
1929
|
console.log(`[auto-webmcp] Unregistered: ${name}`);
|
|
1695
1930
|
}
|
|
@@ -1738,11 +1973,47 @@ function scheduleReAnalysis(form, config) {
|
|
|
1738
1973
|
}, RE_ANALYSIS_DEBOUNCE_MS)
|
|
1739
1974
|
);
|
|
1740
1975
|
}
|
|
1976
|
+
function scheduleFormReAnalysisById(formId, config) {
|
|
1977
|
+
const owner = document.getElementById(formId);
|
|
1978
|
+
if (owner instanceof HTMLFormElement && registeredForms.has(owner)) {
|
|
1979
|
+
scheduleReAnalysis(owner, config);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
function resolveOwnerForm(el) {
|
|
1983
|
+
const closest = el.closest("form");
|
|
1984
|
+
if (closest instanceof HTMLFormElement)
|
|
1985
|
+
return closest;
|
|
1986
|
+
const explicitOwner = el.form;
|
|
1987
|
+
if (explicitOwner instanceof HTMLFormElement)
|
|
1988
|
+
return explicitOwner;
|
|
1989
|
+
const formId = el.getAttribute("form");
|
|
1990
|
+
if (formId) {
|
|
1991
|
+
const byId = document.getElementById(formId);
|
|
1992
|
+
if (byId instanceof HTMLFormElement)
|
|
1993
|
+
return byId;
|
|
1994
|
+
}
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1741
1997
|
function startObserver(config) {
|
|
1742
1998
|
if (observer)
|
|
1743
1999
|
return;
|
|
1744
2000
|
observer = new MutationObserver((mutations) => {
|
|
1745
2001
|
for (const mutation of mutations) {
|
|
2002
|
+
if (mutation.type === "attributes" && mutation.target instanceof Element) {
|
|
2003
|
+
const target = mutation.target;
|
|
2004
|
+
const ownerForm = resolveOwnerForm(target);
|
|
2005
|
+
if (ownerForm && registeredForms.has(ownerForm)) {
|
|
2006
|
+
scheduleReAnalysis(ownerForm, config);
|
|
2007
|
+
} else if (target instanceof HTMLFormElement) {
|
|
2008
|
+
void registerForm(target, config);
|
|
2009
|
+
} else if (isInterestingNode(target) && !target.closest("form")) {
|
|
2010
|
+
scheduleOrphanRescan(config);
|
|
2011
|
+
}
|
|
2012
|
+
if (mutation.attributeName === "form" && mutation.oldValue) {
|
|
2013
|
+
scheduleFormReAnalysisById(mutation.oldValue, config);
|
|
2014
|
+
}
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
1746
2017
|
for (const node of mutation.addedNodes) {
|
|
1747
2018
|
if (!(node instanceof Element))
|
|
1748
2019
|
continue;
|
|
@@ -1771,7 +2042,12 @@ function startObserver(config) {
|
|
|
1771
2042
|
}
|
|
1772
2043
|
}
|
|
1773
2044
|
});
|
|
1774
|
-
observer.observe(document.body, {
|
|
2045
|
+
observer.observe(document.body, {
|
|
2046
|
+
childList: true,
|
|
2047
|
+
subtree: true,
|
|
2048
|
+
attributes: true,
|
|
2049
|
+
attributeOldValue: true
|
|
2050
|
+
});
|
|
1775
2051
|
}
|
|
1776
2052
|
function listenForRouteChanges(config) {
|
|
1777
2053
|
window.addEventListener("hashchange", () => scanForms(config));
|
|
@@ -1915,6 +2191,15 @@ async function scanOrphanInputs(config) {
|
|
|
1915
2191
|
}
|
|
1916
2192
|
console.log(`[auto-webmcp] orphan: submit button for group:`, submitBtn ? `"${submitBtn.textContent?.trim()}" disabled=${submitBtn.disabled}` : "none");
|
|
1917
2193
|
const metadata = analyzeOrphanInputGroup(container, inputs, submitBtn);
|
|
2194
|
+
if (registeredOrphanToolNames.has(metadata.name)) {
|
|
2195
|
+
console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
const orphanName = ensureUniqueToolName(metadata.name);
|
|
2199
|
+
if (orphanName !== metadata.name && config.debug) {
|
|
2200
|
+
console.warn(`[auto-webmcp] orphan tool name collision: "${metadata.name}" renamed to "${orphanName}"`);
|
|
2201
|
+
}
|
|
2202
|
+
metadata.name = orphanName;
|
|
1918
2203
|
console.log(`[auto-webmcp] orphan: tool="${metadata.name}" schema keys:`, Object.keys(metadata.inputSchema.properties));
|
|
1919
2204
|
const inputPairs = [];
|
|
1920
2205
|
const schemaProps = metadata.inputSchema.properties;
|
|
@@ -1935,7 +2220,7 @@ async function scanOrphanInputs(config) {
|
|
|
1935
2220
|
continue;
|
|
1936
2221
|
}
|
|
1937
2222
|
const toolName = metadata.name;
|
|
1938
|
-
const execute = async (params) => {
|
|
2223
|
+
const execute = async (params, _client) => {
|
|
1939
2224
|
console.log(`[auto-webmcp] orphan execute: tool="${toolName}" params=`, params);
|
|
1940
2225
|
console.log(`[auto-webmcp] orphan execute: inputPairs=`, inputPairs.map((p) => p.key));
|
|
1941
2226
|
for (const { key, el } of inputPairs) {
|
|
@@ -2004,10 +2289,6 @@ async function scanOrphanInputs(config) {
|
|
|
2004
2289
|
return { content: [{ type: "text", text: "Fields filled and form submitted." }] };
|
|
2005
2290
|
};
|
|
2006
2291
|
try {
|
|
2007
|
-
if (registeredOrphanToolNames.has(metadata.name)) {
|
|
2008
|
-
console.log(`[auto-webmcp] orphan: "${metadata.name}" already registered, skipping`);
|
|
2009
|
-
continue;
|
|
2010
|
-
}
|
|
2011
2292
|
const toolDef = {
|
|
2012
2293
|
name: metadata.name,
|
|
2013
2294
|
description: metadata.description,
|
|
@@ -2058,7 +2339,6 @@ function stopDiscovery() {
|
|
|
2058
2339
|
}
|
|
2059
2340
|
|
|
2060
2341
|
// src/index.ts
|
|
2061
|
-
init_registry();
|
|
2062
2342
|
async function autoWebMCP(config) {
|
|
2063
2343
|
const resolved = resolveConfig(config);
|
|
2064
2344
|
if (resolved.debug) {
|