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.
@@ -1,9 +1,20 @@
1
1
  // src/config.ts
2
2
  function resolveConfig(userConfig) {
3
+ const strict = userConfig?.paramBinding?.strict ?? false;
4
+ const enableAliasResolution = strict ? false : userConfig?.paramBinding?.enableAliasResolution ?? true;
3
5
  return {
4
6
  exclude: userConfig?.exclude ?? [],
5
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
+ },
6
16
  overrides: userConfig?.overrides ?? {},
17
+ preserveExisting: userConfig?.preserveExisting ?? false,
7
18
  debug: userConfig?.debug ?? false
8
19
  };
9
20
  }
@@ -963,6 +974,53 @@ function buildSchemaFromInputs(inputs) {
963
974
  }
964
975
 
965
976
  // src/registry.ts
977
+ var EXECUTE_OUTPUT_SCHEMA = {
978
+ type: "object",
979
+ properties: {
980
+ status: {
981
+ type: "string",
982
+ enum: ["success", "partial", "error", "awaiting_user_action", "timed_out", "blocked_invalid"],
983
+ description: "Outcome of the form execution."
984
+ },
985
+ filled_fields: {
986
+ type: "object",
987
+ description: "Field name to submitted value map."
988
+ },
989
+ skipped_fields: {
990
+ type: "array",
991
+ items: { type: "string" },
992
+ description: "Fields the agent provided but that could not be filled."
993
+ },
994
+ missing_required: {
995
+ type: "array",
996
+ items: { type: "string" },
997
+ description: "Required fields not supplied by the agent."
998
+ },
999
+ validation_errors: {
1000
+ type: "array",
1001
+ items: {
1002
+ type: "object",
1003
+ properties: {
1004
+ field: { type: "string" },
1005
+ constraint: { type: "string", description: "HTML ValidityState key that failed." },
1006
+ message: { type: "string" }
1007
+ },
1008
+ required: ["field", "constraint", "message"]
1009
+ },
1010
+ description: "Per-field HTML5 validation failures (present when status is blocked_invalid)."
1011
+ },
1012
+ existing_values: {
1013
+ type: "object",
1014
+ description: "Field values present in the form before the agent filled it."
1015
+ },
1016
+ warnings: {
1017
+ type: "array",
1018
+ items: { type: "object" },
1019
+ description: "Non-fatal fill warnings (alias_resolved, clamped, not_filled, etc.)."
1020
+ }
1021
+ },
1022
+ required: ["status", "filled_fields", "skipped_fields", "missing_required", "warnings"]
1023
+ };
966
1024
  var registeredTools = /* @__PURE__ */ new Map();
967
1025
  var registrationControllers = /* @__PURE__ */ new Map();
968
1026
  function isWebMCPSupported() {
@@ -979,6 +1037,7 @@ async function registerFormTool(form, metadata, execute) {
979
1037
  name: metadata.name,
980
1038
  description: metadata.description,
981
1039
  inputSchema: metadata.inputSchema,
1040
+ outputSchema: EXECUTE_OUTPUT_SCHEMA,
982
1041
  execute
983
1042
  };
984
1043
  if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
@@ -1032,9 +1091,128 @@ var formFieldElements = /* @__PURE__ */ new WeakMap();
1032
1091
  var pendingWarnings = /* @__PURE__ */ new WeakMap();
1033
1092
  var pendingFillWarnings = /* @__PURE__ */ new WeakMap();
1034
1093
  var lastFilledSnapshot = /* @__PURE__ */ new WeakMap();
1094
+ var preFillValues = /* @__PURE__ */ new WeakMap();
1035
1095
  var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
1036
1096
  var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
1037
1097
  var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
1098
+ function normalizeAliasKey(raw) {
1099
+ return raw.toLowerCase().replace(/[^a-z0-9]+/g, "");
1100
+ }
1101
+ function addAlias(index, alias, schemaKey) {
1102
+ if (!alias)
1103
+ return;
1104
+ const normalized = normalizeAliasKey(alias);
1105
+ if (!normalized)
1106
+ return;
1107
+ if (!index.has(normalized))
1108
+ index.set(normalized, /* @__PURE__ */ new Set());
1109
+ index.get(normalized).add(schemaKey);
1110
+ }
1111
+ function buildAliasIndex(form, metadata) {
1112
+ const index = /* @__PURE__ */ new Map();
1113
+ const properties = metadata?.inputSchema?.properties ?? {};
1114
+ for (const [schemaKey, prop] of Object.entries(properties)) {
1115
+ addAlias(index, schemaKey, schemaKey);
1116
+ addAlias(index, schemaKey.replace(/_/g, " "), schemaKey);
1117
+ addAlias(index, prop.title, schemaKey);
1118
+ const nativeEl = findNativeField(form, schemaKey);
1119
+ const mappedEl = metadata?.fieldElements?.get(schemaKey);
1120
+ const el = nativeEl ?? mappedEl ?? null;
1121
+ if (!el)
1122
+ continue;
1123
+ const htmlEl = el;
1124
+ addAlias(index, htmlEl.getAttribute("id"), schemaKey);
1125
+ addAlias(index, htmlEl.getAttribute("name"), schemaKey);
1126
+ addAlias(index, htmlEl.getAttribute("aria-label"), schemaKey);
1127
+ addAlias(index, htmlEl.getAttribute("placeholder"), schemaKey);
1128
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
1129
+ for (const label of Array.from(el.labels ?? [])) {
1130
+ addAlias(index, label.textContent?.trim(), schemaKey);
1131
+ }
1132
+ }
1133
+ }
1134
+ return index;
1135
+ }
1136
+ function resolveParamsForSchema(form, params, metadata, config) {
1137
+ const resolved = {};
1138
+ const warnings = [];
1139
+ const properties = metadata?.inputSchema?.properties ?? {};
1140
+ const aliasEnabled = config.paramBinding.enableAliasResolution;
1141
+ for (const [key, value] of Object.entries(params)) {
1142
+ if (key in properties)
1143
+ resolved[key] = value;
1144
+ }
1145
+ if (!aliasEnabled)
1146
+ return { resolved, warnings };
1147
+ const aliasIndex = buildAliasIndex(form, metadata);
1148
+ for (const [rawKey, value] of Object.entries(params)) {
1149
+ if (rawKey in properties)
1150
+ continue;
1151
+ const candidates = aliasIndex.get(normalizeAliasKey(rawKey));
1152
+ if (!candidates || candidates.size !== 1)
1153
+ continue;
1154
+ const target = Array.from(candidates)[0];
1155
+ if (!target || target in resolved)
1156
+ continue;
1157
+ resolved[target] = value;
1158
+ warnings.push({
1159
+ field: target,
1160
+ type: "alias_resolved",
1161
+ original: rawKey,
1162
+ message: `resolved "${rawKey}" to schema field "${target}"`
1163
+ });
1164
+ }
1165
+ return { resolved, warnings };
1166
+ }
1167
+ function collectInvalidFieldWarnings(form) {
1168
+ const warnings = [];
1169
+ const controls = Array.from(form.elements).filter(
1170
+ (el) => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement
1171
+ );
1172
+ for (const control of controls) {
1173
+ if (!control.willValidate)
1174
+ continue;
1175
+ if (control.checkValidity())
1176
+ continue;
1177
+ const field = control.name || control.id || control.getAttribute("aria-label") || "unknown_field";
1178
+ warnings.push({
1179
+ field,
1180
+ type: "blocked_submit",
1181
+ message: control.validationMessage || `field "${field}" failed validation`
1182
+ });
1183
+ }
1184
+ return warnings;
1185
+ }
1186
+ function captureCurrentValues(form) {
1187
+ const result = {};
1188
+ try {
1189
+ const data = new FormData(form);
1190
+ for (const [key, val] of data.entries()) {
1191
+ if (result[key] !== void 0) {
1192
+ const existing = result[key];
1193
+ result[key] = Array.isArray(existing) ? [...existing, val] : [existing, val];
1194
+ } else {
1195
+ result[key] = val;
1196
+ }
1197
+ }
1198
+ } catch {
1199
+ }
1200
+ return result;
1201
+ }
1202
+ function collectValidationErrors(form) {
1203
+ const errors = [];
1204
+ for (const control of Array.from(form.elements)) {
1205
+ if (!(control instanceof HTMLInputElement) && !(control instanceof HTMLTextAreaElement) && !(control instanceof HTMLSelectElement))
1206
+ continue;
1207
+ if (!control.willValidate || control.checkValidity())
1208
+ continue;
1209
+ const field = control.name || control.id || control.getAttribute("aria-label") || "unknown_field";
1210
+ const v = control.validity;
1211
+ 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";
1212
+ errors.push({ field, constraint, message: control.validationMessage || `field "${field}" failed validation` });
1213
+ }
1214
+ return errors;
1215
+ }
1038
1216
  function buildExecuteHandler(form, config, toolName, metadata) {
1039
1217
  if (metadata?.fieldElements) {
1040
1218
  formFieldElements.set(form, metadata.fieldElements);
@@ -1056,22 +1234,86 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1056
1234
  }
1057
1235
  pendingFillWarnings.set(form, []);
1058
1236
  pendingWarnings.delete(form);
1059
- fillFormFields(form, params);
1060
- const missingNow = getMissingRequired(metadata, params);
1237
+ const existingSnapshot = captureCurrentValues(form);
1238
+ preFillValues.set(form, existingSnapshot);
1239
+ const { resolved: resolvedParams, warnings: aliasWarnings } = resolveParamsForSchema(
1240
+ form,
1241
+ params,
1242
+ metadata,
1243
+ config
1244
+ );
1245
+ if (aliasWarnings.length > 0) {
1246
+ pendingFillWarnings.set(form, [...pendingFillWarnings.get(form) ?? [], ...aliasWarnings]);
1247
+ }
1248
+ let paramsToFill = resolvedParams;
1249
+ if (config.preserveExisting) {
1250
+ const preserved = [];
1251
+ paramsToFill = Object.fromEntries(
1252
+ Object.entries(resolvedParams).filter(([key]) => {
1253
+ const current = existingSnapshot[key];
1254
+ const hasValue = current !== void 0 && current !== "" && current !== null;
1255
+ if (hasValue) {
1256
+ preserved.push({
1257
+ field: key,
1258
+ type: "not_filled",
1259
+ message: `field "${key}" already has a value and preserveExisting is enabled`
1260
+ });
1261
+ }
1262
+ return !hasValue;
1263
+ })
1264
+ );
1265
+ if (preserved.length > 0) {
1266
+ pendingFillWarnings.set(form, [...pendingFillWarnings.get(form) ?? [], ...preserved]);
1267
+ }
1268
+ }
1269
+ fillFormFields(form, paramsToFill);
1270
+ const missingNow = getMissingRequired(metadata, resolvedParams);
1061
1271
  if (missingNow.length > 0)
1062
1272
  pendingWarnings.set(form, missingNow);
1063
1273
  window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
1064
1274
  return new Promise((resolve, reject) => {
1065
- pendingExecutions.set(form, { resolve, reject });
1275
+ const timeoutMs = config.execution.timeoutMs;
1276
+ const timeoutId = setTimeout(() => {
1277
+ const pending = pendingExecutions.get(form);
1278
+ if (!pending)
1279
+ return;
1280
+ pendingExecutions.delete(form);
1281
+ const timedOutState = config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0 ? "timed_out" : "awaiting_user_action";
1282
+ const warn = {
1283
+ field: "__form__",
1284
+ type: "timeout",
1285
+ message: timedOutState === "timed_out" ? `tool execution timed out after ${timeoutMs}ms` : `waiting for user submit (timed out after ${timeoutMs}ms)`
1286
+ };
1287
+ const _existingValsTimeout = preFillValues.get(form);
1288
+ const structured = {
1289
+ status: timedOutState,
1290
+ filled_fields: serializeFormData(form, lastParams.get(form), formFieldElements.get(form)),
1291
+ skipped_fields: [],
1292
+ missing_required: pendingWarnings.get(form) ?? [],
1293
+ warnings: [...pendingFillWarnings.get(form) ?? [], warn],
1294
+ ..._existingValsTimeout !== void 0 && { existing_values: _existingValsTimeout }
1295
+ };
1296
+ pendingWarnings.delete(form);
1297
+ pendingFillWarnings.delete(form);
1298
+ lastFilledSnapshot.delete(form);
1299
+ preFillValues.delete(form);
1300
+ resolve({
1301
+ content: [
1302
+ { type: "text", text: warn.message },
1303
+ { type: "text", text: JSON.stringify(structured) }
1304
+ ]
1305
+ });
1306
+ }, timeoutMs);
1307
+ pendingExecutions.set(form, { resolve, reject, timeoutId });
1066
1308
  if (config.autoSubmit || form.hasAttribute("toolautosubmit") || form.dataset["webmcpAutosubmit"] !== void 0) {
1067
1309
  waitForDomStable(form).then(async () => {
1068
1310
  try {
1069
- fillFormFields(form, params);
1311
+ fillFormFields(form, resolvedParams);
1070
1312
  for (let attempt = 0; attempt < 2; attempt++) {
1071
- const reset = getResetFields(form, params, formFieldElements.get(form));
1313
+ const reset = getResetFields(form, resolvedParams, formFieldElements.get(form));
1072
1314
  if (reset.length === 0)
1073
1315
  break;
1074
- fillFormFields(form, params);
1316
+ fillFormFields(form, resolvedParams);
1075
1317
  await waitForDomStable(form, 400, 100);
1076
1318
  }
1077
1319
  let submitForm = form;
@@ -1082,7 +1324,9 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1082
1324
  const found = liveBtn?.closest("form");
1083
1325
  if (found) {
1084
1326
  submitForm = found;
1085
- pendingExecutions.set(submitForm, { resolve, reject });
1327
+ const pending = pendingExecutions.get(form);
1328
+ const nextPending = pending?.timeoutId ? { resolve, reject, timeoutId: pending.timeoutId } : { resolve, reject };
1329
+ pendingExecutions.set(submitForm, nextPending);
1086
1330
  attachSubmitInterceptor(submitForm, toolName);
1087
1331
  }
1088
1332
  }
@@ -1090,6 +1334,43 @@ function buildExecuteHandler(form, config, toolName, metadata) {
1090
1334
  pendingWarnings.set(submitForm, pendingWarnings.get(form));
1091
1335
  pendingWarnings.delete(form);
1092
1336
  }
1337
+ if (!submitForm.checkValidity()) {
1338
+ const pending = pendingExecutions.get(submitForm) ?? pendingExecutions.get(form);
1339
+ if (pending) {
1340
+ if (pending.timeoutId)
1341
+ clearTimeout(pending.timeoutId);
1342
+ pendingExecutions.delete(submitForm);
1343
+ pendingExecutions.delete(form);
1344
+ const warnings = [
1345
+ ...pendingFillWarnings.get(submitForm) ?? pendingFillWarnings.get(form) ?? [],
1346
+ ...collectInvalidFieldWarnings(submitForm)
1347
+ ];
1348
+ pendingFillWarnings.delete(submitForm);
1349
+ pendingFillWarnings.delete(form);
1350
+ const _existingValsBlocked = preFillValues.get(form);
1351
+ const structured = {
1352
+ status: "blocked_invalid",
1353
+ filled_fields: serializeFormData(submitForm, lastParams.get(submitForm) ?? lastParams.get(form), formFieldElements.get(submitForm) ?? formFieldElements.get(form)),
1354
+ skipped_fields: [],
1355
+ missing_required: pendingWarnings.get(submitForm) ?? pendingWarnings.get(form) ?? [],
1356
+ warnings,
1357
+ validation_errors: collectValidationErrors(submitForm),
1358
+ ..._existingValsBlocked !== void 0 && { existing_values: _existingValsBlocked }
1359
+ };
1360
+ pendingWarnings.delete(submitForm);
1361
+ pendingWarnings.delete(form);
1362
+ lastFilledSnapshot.delete(submitForm);
1363
+ lastFilledSnapshot.delete(form);
1364
+ preFillValues.delete(form);
1365
+ resolve({
1366
+ content: [
1367
+ { type: "text", text: "Form submission blocked by native validation." },
1368
+ { type: "text", text: JSON.stringify(structured) }
1369
+ ]
1370
+ });
1371
+ }
1372
+ return;
1373
+ }
1093
1374
  submitForm.requestSubmit();
1094
1375
  } catch (err) {
1095
1376
  reject(err instanceof Error ? err : new Error(String(err)));
@@ -1108,9 +1389,13 @@ function attachSubmitInterceptor(form, toolName) {
1108
1389
  if (!pending)
1109
1390
  return;
1110
1391
  const { resolve } = pending;
1392
+ if (pending.timeoutId)
1393
+ clearTimeout(pending.timeoutId);
1111
1394
  pendingExecutions.delete(form);
1112
1395
  const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
1396
+ const existingVals = preFillValues.get(form);
1113
1397
  lastFilledSnapshot.delete(form);
1398
+ preFillValues.delete(form);
1114
1399
  const missingRequired = pendingWarnings.get(form) ?? [];
1115
1400
  pendingWarnings.delete(form);
1116
1401
  const fillWarnings = pendingFillWarnings.get(form) ?? [];
@@ -1128,7 +1413,8 @@ function attachSubmitInterceptor(form, toolName) {
1128
1413
  message: `required field "${f}" was not provided`
1129
1414
  })),
1130
1415
  ...fillWarnings
1131
- ]
1416
+ ],
1417
+ ...existingVals !== void 0 && { existing_values: existingVals }
1132
1418
  };
1133
1419
  const allWarnMessages = [
1134
1420
  ...missingRequired.length ? [`required fields were not filled: ${missingRequired.join(", ")}`] : [],
@@ -1150,6 +1436,7 @@ function attachSubmitInterceptor(form, toolName) {
1150
1436
  });
1151
1437
  form.addEventListener("reset", () => {
1152
1438
  lastFilledSnapshot.delete(form);
1439
+ preFillValues.delete(form);
1153
1440
  window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
1154
1441
  });
1155
1442
  }
@@ -1664,10 +1951,23 @@ function ensureUniqueToolName(baseName, excludeForm) {
1664
1951
  }
1665
1952
  return candidate;
1666
1953
  }
1954
+ function hasNativeDeclarativeTool(form) {
1955
+ return form.getAttribute("toolname")?.trim().length ? true : false;
1956
+ }
1667
1957
  async function registerForm(form, config) {
1668
1958
  if (isExcluded(form, config))
1669
1959
  return;
1670
1960
  const previousName = getRegisteredToolName(form);
1961
+ if (hasNativeDeclarativeTool(form) && config.declarativeMode !== "force") {
1962
+ if (previousName) {
1963
+ await unregisterFormTool(form);
1964
+ }
1965
+ if (config.debug) {
1966
+ const mode = config.declarativeMode;
1967
+ console.log(`[auto-webmcp] Skipping imperative registration for native declarative form (mode=${mode})`);
1968
+ }
1969
+ return;
1970
+ }
1671
1971
  let override;
1672
1972
  for (const [selector, ovr] of Object.entries(config.overrides)) {
1673
1973
  try {