auto-webmcp 0.3.16 → 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 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,9 +26,19 @@ 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 ?? {},
33
43
  debug: userConfig?.debug ?? false
34
44
  };
@@ -1061,6 +1071,94 @@ var lastFilledSnapshot = /* @__PURE__ */ new WeakMap();
1061
1071
  var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
1062
1072
  var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
1063
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
+ }
1064
1162
  function buildExecuteHandler(form, config, toolName, metadata) {
1065
1163
  if (metadata?.fieldElements) {
1066
1164
  formFieldElements.set(form, metadata.fieldElements);
@@ -1082,22 +1180,60 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1082
1180
  }
1083
1181
  pendingFillWarnings.set(form, []);
1084
1182
  pendingWarnings.delete(form);
1085
- fillFormFields(form, params);
1086
- const missingNow = getMissingRequired(metadata, params);
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);
1087
1194
  if (missingNow.length > 0)
1088
1195
  pendingWarnings.set(form, missingNow);
1089
1196
  window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
1090
1197
  return new Promise((resolve, reject) => {
1091
- pendingExecutions.set(form, { resolve, reject });
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 });
1092
1228
  if (config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0) {
1093
1229
  waitForDomStable(form).then(async () => {
1094
1230
  try {
1095
- fillFormFields(form, params);
1231
+ fillFormFields(form, resolvedParams);
1096
1232
  for (let attempt = 0; attempt < 2; attempt++) {
1097
- const reset = getResetFields(form, params, formFieldElements.get(form));
1233
+ const reset = getResetFields(form, resolvedParams, formFieldElements.get(form));
1098
1234
  if (reset.length === 0)
1099
1235
  break;
1100
- fillFormFields(form, params);
1236
+ fillFormFields(form, resolvedParams);
1101
1237
  await waitForDomStable(form, 400, 100);
1102
1238
  }
1103
1239
  let submitForm = form;
@@ -1108,7 +1244,9 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1108
1244
  const found = liveBtn?.closest("form");
1109
1245
  if (found) {
1110
1246
  submitForm = found;
1111
- pendingExecutions.set(submitForm, { resolve, reject });
1247
+ const pending = pendingExecutions.get(form);
1248
+ const nextPending = pending?.timeoutId ? { resolve, reject, timeoutId: pending.timeoutId } : { resolve, reject };
1249
+ pendingExecutions.set(submitForm, nextPending);
1112
1250
  attachSubmitInterceptor(submitForm, toolName);
1113
1251
  }
1114
1252
  }
@@ -1116,6 +1254,39 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1116
1254
  pendingWarnings.set(submitForm, pendingWarnings.get(form));
1117
1255
  pendingWarnings.delete(form);
1118
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
+ }
1119
1290
  submitForm.requestSubmit();
1120
1291
  } catch (err) {
1121
1292
  reject(err instanceof Error ? err : new Error(String(err)));
@@ -1134,6 +1305,8 @@ function attachSubmitInterceptor(form, toolName) {
1134
1305
  if (!pending)
1135
1306
  return;
1136
1307
  const { resolve } = pending;
1308
+ if (pending.timeoutId)
1309
+ clearTimeout(pending.timeoutId);
1137
1310
  pendingExecutions.delete(form);
1138
1311
  const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
1139
1312
  lastFilledSnapshot.delete(form);
@@ -1690,10 +1863,23 @@ function ensureUniqueToolName(baseName, excludeForm) {
1690
1863
  }
1691
1864
  return candidate;
1692
1865
  }
1866
+ function hasNativeDeclarativeTool(form) {
1867
+ return form.getAttribute("toolname")?.trim().length ? true : false;
1868
+ }
1693
1869
  async function registerForm(form, config) {
1694
1870
  if (isExcluded(form, config))
1695
1871
  return;
1696
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
+ }
1697
1883
  let override;
1698
1884
  for (const [selector, ovr] of Object.entries(config.overrides)) {
1699
1885
  try {