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.
@@ -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
- form.querySelectorAll(`input[type="checkbox"][name="${CSS.escape(name)}"]`)
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
- form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
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
- form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
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.querySelector(
553
- `input[type="radio"][name="${CSS.escape(fieldKey)}"]:checked`
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
- form.querySelectorAll(
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/discovery.ts
1042
- init_registry();
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
- fillFormFields(form, params);
1063
- const missingNow = getMissingRequired(metadata, params);
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
- pendingExecutions.set(form, { resolve, reject });
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, params);
1205
+ fillFormFields(form, resolvedParams);
1073
1206
  for (let attempt = 0; attempt < 2; attempt++) {
1074
- const reset = getResetFields(form, params, formFieldElements.get(form));
1207
+ const reset = getResetFields(form, resolvedParams, formFieldElements.get(form));
1075
1208
  if (reset.length === 0)
1076
1209
  break;
1077
- fillFormFields(form, params);
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.set(submitForm, { resolve, reject });
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
- const esc = CSS.escape(key);
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 esc = CSS.escape(key);
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 esc = CSS.escape(key);
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 { getRegisteredToolName: getRegisteredToolName2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
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, { childList: true, subtree: true });
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) {