auto-webmcp 0.3.16 → 0.3.18

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 CHANGED
@@ -57,6 +57,21 @@ await autoWebMCP({
57
57
  // Auto-submit when agent invokes (default: false — human must click submit)
58
58
  autoSubmit: false,
59
59
 
60
+ // Handling for forms already using native declarative WebMCP attributes:
61
+ // 'skip' (default), 'augment' (currently same as skip), or 'force'
62
+ declarativeMode: 'skip',
63
+
64
+ // Parameter binding behavior for execute payload keys
65
+ paramBinding: {
66
+ enableAliasResolution: true, // default
67
+ strict: false, // if true, exact schema keys only
68
+ },
69
+
70
+ // Deterministic execute timeout state
71
+ execution: {
72
+ timeoutMs: 15000, // default
73
+ },
74
+
60
75
  // Per-form name / description overrides
61
76
  overrides: {
62
77
  '#checkout-form': {
@@ -26,10 +26,21 @@ module.exports = __toCommonJS(src_exports);
26
26
 
27
27
  // src/config.ts
28
28
  function resolveConfig(userConfig) {
29
+ const strict = userConfig?.paramBinding?.strict ?? false;
30
+ const enableAliasResolution = strict ? false : userConfig?.paramBinding?.enableAliasResolution ?? true;
29
31
  return {
30
32
  exclude: userConfig?.exclude ?? [],
31
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
+ },
32
42
  overrides: userConfig?.overrides ?? {},
43
+ preserveExisting: userConfig?.preserveExisting ?? false,
33
44
  debug: userConfig?.debug ?? false
34
45
  };
35
46
  }
@@ -989,6 +1000,53 @@ function buildSchemaFromInputs(inputs) {
989
1000
  }
990
1001
 
991
1002
  // src/registry.ts
1003
+ var EXECUTE_OUTPUT_SCHEMA = {
1004
+ type: "object",
1005
+ properties: {
1006
+ status: {
1007
+ type: "string",
1008
+ enum: ["success", "partial", "error", "awaiting_user_action", "timed_out", "blocked_invalid"],
1009
+ description: "Outcome of the form execution."
1010
+ },
1011
+ filled_fields: {
1012
+ type: "object",
1013
+ description: "Field name to submitted value map."
1014
+ },
1015
+ skipped_fields: {
1016
+ type: "array",
1017
+ items: { type: "string" },
1018
+ description: "Fields the agent provided but that could not be filled."
1019
+ },
1020
+ missing_required: {
1021
+ type: "array",
1022
+ items: { type: "string" },
1023
+ description: "Required fields not supplied by the agent."
1024
+ },
1025
+ validation_errors: {
1026
+ type: "array",
1027
+ items: {
1028
+ type: "object",
1029
+ properties: {
1030
+ field: { type: "string" },
1031
+ constraint: { type: "string", description: "HTML ValidityState key that failed." },
1032
+ message: { type: "string" }
1033
+ },
1034
+ required: ["field", "constraint", "message"]
1035
+ },
1036
+ description: "Per-field HTML5 validation failures (present when status is blocked_invalid)."
1037
+ },
1038
+ existing_values: {
1039
+ type: "object",
1040
+ description: "Field values present in the form before the agent filled it."
1041
+ },
1042
+ warnings: {
1043
+ type: "array",
1044
+ items: { type: "object" },
1045
+ description: "Non-fatal fill warnings (alias_resolved, clamped, not_filled, etc.)."
1046
+ }
1047
+ },
1048
+ required: ["status", "filled_fields", "skipped_fields", "missing_required", "warnings"]
1049
+ };
992
1050
  var registeredTools = /* @__PURE__ */ new Map();
993
1051
  var registrationControllers = /* @__PURE__ */ new Map();
994
1052
  function isWebMCPSupported() {
@@ -1005,6 +1063,7 @@ async function registerFormTool(form, metadata, execute) {
1005
1063
  name: metadata.name,
1006
1064
  description: metadata.description,
1007
1065
  inputSchema: metadata.inputSchema,
1066
+ outputSchema: EXECUTE_OUTPUT_SCHEMA,
1008
1067
  execute
1009
1068
  };
1010
1069
  if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
@@ -1058,9 +1117,128 @@ var formFieldElements = /* @__PURE__ */ new WeakMap();
1058
1117
  var pendingWarnings = /* @__PURE__ */ new WeakMap();
1059
1118
  var pendingFillWarnings = /* @__PURE__ */ new WeakMap();
1060
1119
  var lastFilledSnapshot = /* @__PURE__ */ new WeakMap();
1120
+ var preFillValues = /* @__PURE__ */ new WeakMap();
1061
1121
  var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
1062
1122
  var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
1063
1123
  var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
1124
+ function normalizeAliasKey(raw) {
1125
+ return raw.toLowerCase().replace(/[^a-z0-9]+/g, "");
1126
+ }
1127
+ function addAlias(index, alias, schemaKey) {
1128
+ if (!alias)
1129
+ return;
1130
+ const normalized = normalizeAliasKey(alias);
1131
+ if (!normalized)
1132
+ return;
1133
+ if (!index.has(normalized))
1134
+ index.set(normalized, /* @__PURE__ */ new Set());
1135
+ index.get(normalized).add(schemaKey);
1136
+ }
1137
+ function buildAliasIndex(form, metadata) {
1138
+ const index = /* @__PURE__ */ new Map();
1139
+ const properties = metadata?.inputSchema?.properties ?? {};
1140
+ for (const [schemaKey, prop] of Object.entries(properties)) {
1141
+ addAlias(index, schemaKey, schemaKey);
1142
+ addAlias(index, schemaKey.replace(/_/g, " "), schemaKey);
1143
+ addAlias(index, prop.title, schemaKey);
1144
+ const nativeEl = findNativeField(form, schemaKey);
1145
+ const mappedEl = metadata?.fieldElements?.get(schemaKey);
1146
+ const el = nativeEl ?? mappedEl ?? null;
1147
+ if (!el)
1148
+ continue;
1149
+ const htmlEl = el;
1150
+ addAlias(index, htmlEl.getAttribute("id"), schemaKey);
1151
+ addAlias(index, htmlEl.getAttribute("name"), schemaKey);
1152
+ addAlias(index, htmlEl.getAttribute("aria-label"), schemaKey);
1153
+ addAlias(index, htmlEl.getAttribute("placeholder"), schemaKey);
1154
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
1155
+ for (const label of Array.from(el.labels ?? [])) {
1156
+ addAlias(index, label.textContent?.trim(), schemaKey);
1157
+ }
1158
+ }
1159
+ }
1160
+ return index;
1161
+ }
1162
+ function resolveParamsForSchema(form, params, metadata, config) {
1163
+ const resolved = {};
1164
+ const warnings = [];
1165
+ const properties = metadata?.inputSchema?.properties ?? {};
1166
+ const aliasEnabled = config.paramBinding.enableAliasResolution;
1167
+ for (const [key, value] of Object.entries(params)) {
1168
+ if (key in properties)
1169
+ resolved[key] = value;
1170
+ }
1171
+ if (!aliasEnabled)
1172
+ return { resolved, warnings };
1173
+ const aliasIndex = buildAliasIndex(form, metadata);
1174
+ for (const [rawKey, value] of Object.entries(params)) {
1175
+ if (rawKey in properties)
1176
+ continue;
1177
+ const candidates = aliasIndex.get(normalizeAliasKey(rawKey));
1178
+ if (!candidates || candidates.size !== 1)
1179
+ continue;
1180
+ const target = Array.from(candidates)[0];
1181
+ if (!target || target in resolved)
1182
+ continue;
1183
+ resolved[target] = value;
1184
+ warnings.push({
1185
+ field: target,
1186
+ type: "alias_resolved",
1187
+ original: rawKey,
1188
+ message: `resolved "${rawKey}" to schema field "${target}"`
1189
+ });
1190
+ }
1191
+ return { resolved, warnings };
1192
+ }
1193
+ function collectInvalidFieldWarnings(form) {
1194
+ const warnings = [];
1195
+ const controls = Array.from(form.elements).filter(
1196
+ (el) => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement
1197
+ );
1198
+ for (const control of controls) {
1199
+ if (!control.willValidate)
1200
+ continue;
1201
+ if (control.checkValidity())
1202
+ continue;
1203
+ const field = control.name || control.id || control.getAttribute("aria-label") || "unknown_field";
1204
+ warnings.push({
1205
+ field,
1206
+ type: "blocked_submit",
1207
+ message: control.validationMessage || `field "${field}" failed validation`
1208
+ });
1209
+ }
1210
+ return warnings;
1211
+ }
1212
+ function captureCurrentValues(form) {
1213
+ const result = {};
1214
+ try {
1215
+ const data = new FormData(form);
1216
+ for (const [key, val] of data.entries()) {
1217
+ if (result[key] !== void 0) {
1218
+ const existing = result[key];
1219
+ result[key] = Array.isArray(existing) ? [...existing, val] : [existing, val];
1220
+ } else {
1221
+ result[key] = val;
1222
+ }
1223
+ }
1224
+ } catch {
1225
+ }
1226
+ return result;
1227
+ }
1228
+ function collectValidationErrors(form) {
1229
+ const errors = [];
1230
+ for (const control of Array.from(form.elements)) {
1231
+ if (!(control instanceof HTMLInputElement) && !(control instanceof HTMLTextAreaElement) && !(control instanceof HTMLSelectElement))
1232
+ continue;
1233
+ if (!control.willValidate || control.checkValidity())
1234
+ continue;
1235
+ const field = control.name || control.id || control.getAttribute("aria-label") || "unknown_field";
1236
+ const v = control.validity;
1237
+ const constraint = v.valueMissing ? "valueMissing" : v.typeMismatch ? "typeMismatch" : v.patternMismatch ? "patternMismatch" : v.tooLong ? "tooLong" : v.tooShort ? "tooShort" : v.rangeUnderflow ? "rangeUnderflow" : v.rangeOverflow ? "rangeOverflow" : v.stepMismatch ? "stepMismatch" : v.customError ? "customError" : "badInput";
1238
+ errors.push({ field, constraint, message: control.validationMessage || `field "${field}" failed validation` });
1239
+ }
1240
+ return errors;
1241
+ }
1064
1242
  function buildExecuteHandler(form, config, toolName, metadata) {
1065
1243
  if (metadata?.fieldElements) {
1066
1244
  formFieldElements.set(form, metadata.fieldElements);
@@ -1082,22 +1260,86 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1082
1260
  }
1083
1261
  pendingFillWarnings.set(form, []);
1084
1262
  pendingWarnings.delete(form);
1085
- fillFormFields(form, params);
1086
- const missingNow = getMissingRequired(metadata, params);
1263
+ const existingSnapshot = captureCurrentValues(form);
1264
+ preFillValues.set(form, existingSnapshot);
1265
+ const { resolved: resolvedParams, warnings: aliasWarnings } = resolveParamsForSchema(
1266
+ form,
1267
+ params,
1268
+ metadata,
1269
+ config
1270
+ );
1271
+ if (aliasWarnings.length > 0) {
1272
+ pendingFillWarnings.set(form, [...pendingFillWarnings.get(form) ?? [], ...aliasWarnings]);
1273
+ }
1274
+ let paramsToFill = resolvedParams;
1275
+ if (config.preserveExisting) {
1276
+ const preserved = [];
1277
+ paramsToFill = Object.fromEntries(
1278
+ Object.entries(resolvedParams).filter(([key]) => {
1279
+ const current = existingSnapshot[key];
1280
+ const hasValue = current !== void 0 && current !== "" && current !== null;
1281
+ if (hasValue) {
1282
+ preserved.push({
1283
+ field: key,
1284
+ type: "not_filled",
1285
+ message: `field "${key}" already has a value and preserveExisting is enabled`
1286
+ });
1287
+ }
1288
+ return !hasValue;
1289
+ })
1290
+ );
1291
+ if (preserved.length > 0) {
1292
+ pendingFillWarnings.set(form, [...pendingFillWarnings.get(form) ?? [], ...preserved]);
1293
+ }
1294
+ }
1295
+ fillFormFields(form, paramsToFill);
1296
+ const missingNow = getMissingRequired(metadata, resolvedParams);
1087
1297
  if (missingNow.length > 0)
1088
1298
  pendingWarnings.set(form, missingNow);
1089
1299
  window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
1090
1300
  return new Promise((resolve, reject) => {
1091
- pendingExecutions.set(form, { resolve, reject });
1301
+ const timeoutMs = config.execution.timeoutMs;
1302
+ const timeoutId = setTimeout(() => {
1303
+ const pending = pendingExecutions.get(form);
1304
+ if (!pending)
1305
+ return;
1306
+ pendingExecutions.delete(form);
1307
+ const timedOutState = config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0 ? "timed_out" : "awaiting_user_action";
1308
+ const warn = {
1309
+ field: "__form__",
1310
+ type: "timeout",
1311
+ message: timedOutState === "timed_out" ? `tool execution timed out after ${timeoutMs}ms` : `waiting for user submit (timed out after ${timeoutMs}ms)`
1312
+ };
1313
+ const _existingValsTimeout = preFillValues.get(form);
1314
+ const structured = {
1315
+ status: timedOutState,
1316
+ filled_fields: serializeFormData(form, lastParams.get(form), formFieldElements.get(form)),
1317
+ skipped_fields: [],
1318
+ missing_required: pendingWarnings.get(form) ?? [],
1319
+ warnings: [...pendingFillWarnings.get(form) ?? [], warn],
1320
+ ..._existingValsTimeout !== void 0 && { existing_values: _existingValsTimeout }
1321
+ };
1322
+ pendingWarnings.delete(form);
1323
+ pendingFillWarnings.delete(form);
1324
+ lastFilledSnapshot.delete(form);
1325
+ preFillValues.delete(form);
1326
+ resolve({
1327
+ content: [
1328
+ { type: "text", text: warn.message },
1329
+ { type: "text", text: JSON.stringify(structured) }
1330
+ ]
1331
+ });
1332
+ }, timeoutMs);
1333
+ pendingExecutions.set(form, { resolve, reject, timeoutId });
1092
1334
  if (config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0) {
1093
1335
  waitForDomStable(form).then(async () => {
1094
1336
  try {
1095
- fillFormFields(form, params);
1337
+ fillFormFields(form, resolvedParams);
1096
1338
  for (let attempt = 0; attempt < 2; attempt++) {
1097
- const reset = getResetFields(form, params, formFieldElements.get(form));
1339
+ const reset = getResetFields(form, resolvedParams, formFieldElements.get(form));
1098
1340
  if (reset.length === 0)
1099
1341
  break;
1100
- fillFormFields(form, params);
1342
+ fillFormFields(form, resolvedParams);
1101
1343
  await waitForDomStable(form, 400, 100);
1102
1344
  }
1103
1345
  let submitForm = form;
@@ -1108,7 +1350,9 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1108
1350
  const found = liveBtn?.closest("form");
1109
1351
  if (found) {
1110
1352
  submitForm = found;
1111
- pendingExecutions.set(submitForm, { resolve, reject });
1353
+ const pending = pendingExecutions.get(form);
1354
+ const nextPending = pending?.timeoutId ? { resolve, reject, timeoutId: pending.timeoutId } : { resolve, reject };
1355
+ pendingExecutions.set(submitForm, nextPending);
1112
1356
  attachSubmitInterceptor(submitForm, toolName);
1113
1357
  }
1114
1358
  }
@@ -1116,6 +1360,43 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1116
1360
  pendingWarnings.set(submitForm, pendingWarnings.get(form));
1117
1361
  pendingWarnings.delete(form);
1118
1362
  }
1363
+ if (!submitForm.checkValidity()) {
1364
+ const pending = pendingExecutions.get(submitForm) ?? pendingExecutions.get(form);
1365
+ if (pending) {
1366
+ if (pending.timeoutId)
1367
+ clearTimeout(pending.timeoutId);
1368
+ pendingExecutions.delete(submitForm);
1369
+ pendingExecutions.delete(form);
1370
+ const warnings = [
1371
+ ...pendingFillWarnings.get(submitForm) ?? pendingFillWarnings.get(form) ?? [],
1372
+ ...collectInvalidFieldWarnings(submitForm)
1373
+ ];
1374
+ pendingFillWarnings.delete(submitForm);
1375
+ pendingFillWarnings.delete(form);
1376
+ const _existingValsBlocked = preFillValues.get(form);
1377
+ const structured = {
1378
+ status: "blocked_invalid",
1379
+ filled_fields: serializeFormData(submitForm, lastParams.get(submitForm) ?? lastParams.get(form), formFieldElements.get(submitForm) ?? formFieldElements.get(form)),
1380
+ skipped_fields: [],
1381
+ missing_required: pendingWarnings.get(submitForm) ?? pendingWarnings.get(form) ?? [],
1382
+ warnings,
1383
+ validation_errors: collectValidationErrors(submitForm),
1384
+ ..._existingValsBlocked !== void 0 && { existing_values: _existingValsBlocked }
1385
+ };
1386
+ pendingWarnings.delete(submitForm);
1387
+ pendingWarnings.delete(form);
1388
+ lastFilledSnapshot.delete(submitForm);
1389
+ lastFilledSnapshot.delete(form);
1390
+ preFillValues.delete(form);
1391
+ resolve({
1392
+ content: [
1393
+ { type: "text", text: "Form submission blocked by native validation." },
1394
+ { type: "text", text: JSON.stringify(structured) }
1395
+ ]
1396
+ });
1397
+ }
1398
+ return;
1399
+ }
1119
1400
  submitForm.requestSubmit();
1120
1401
  } catch (err) {
1121
1402
  reject(err instanceof Error ? err : new Error(String(err)));
@@ -1134,9 +1415,13 @@ function attachSubmitInterceptor(form, toolName) {
1134
1415
  if (!pending)
1135
1416
  return;
1136
1417
  const { resolve } = pending;
1418
+ if (pending.timeoutId)
1419
+ clearTimeout(pending.timeoutId);
1137
1420
  pendingExecutions.delete(form);
1138
1421
  const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
1422
+ const existingVals = preFillValues.get(form);
1139
1423
  lastFilledSnapshot.delete(form);
1424
+ preFillValues.delete(form);
1140
1425
  const missingRequired = pendingWarnings.get(form) ?? [];
1141
1426
  pendingWarnings.delete(form);
1142
1427
  const fillWarnings = pendingFillWarnings.get(form) ?? [];
@@ -1154,7 +1439,8 @@ function attachSubmitInterceptor(form, toolName) {
1154
1439
  message: `required field "${f}" was not provided`
1155
1440
  })),
1156
1441
  ...fillWarnings
1157
- ]
1442
+ ],
1443
+ ...existingVals !== void 0 && { existing_values: existingVals }
1158
1444
  };
1159
1445
  const allWarnMessages = [
1160
1446
  ...missingRequired.length ? [`required fields were not filled: ${missingRequired.join(", ")}`] : [],
@@ -1176,6 +1462,7 @@ function attachSubmitInterceptor(form, toolName) {
1176
1462
  });
1177
1463
  form.addEventListener("reset", () => {
1178
1464
  lastFilledSnapshot.delete(form);
1465
+ preFillValues.delete(form);
1179
1466
  window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
1180
1467
  });
1181
1468
  }
@@ -1690,10 +1977,23 @@ function ensureUniqueToolName(baseName, excludeForm) {
1690
1977
  }
1691
1978
  return candidate;
1692
1979
  }
1980
+ function hasNativeDeclarativeTool(form) {
1981
+ return form.getAttribute("toolname")?.trim().length ? true : false;
1982
+ }
1693
1983
  async function registerForm(form, config) {
1694
1984
  if (isExcluded(form, config))
1695
1985
  return;
1696
1986
  const previousName = getRegisteredToolName(form);
1987
+ if (hasNativeDeclarativeTool(form) && config.declarativeMode !== "force") {
1988
+ if (previousName) {
1989
+ await unregisterFormTool(form);
1990
+ }
1991
+ if (config.debug) {
1992
+ const mode = config.declarativeMode;
1993
+ console.log(`[auto-webmcp] Skipping imperative registration for native declarative form (mode=${mode})`);
1994
+ }
1995
+ return;
1996
+ }
1697
1997
  let override;
1698
1998
  for (const [selector, ovr] of Object.entries(config.overrides)) {
1699
1999
  try {