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.
@@ -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
- form.querySelectorAll(`input[type="checkbox"][name="${CSS.escape(name)}"]`)
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
- form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
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
- form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
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.querySelector(
572
- `input[type="radio"][name="${CSS.escape(fieldKey)}"]:checked`
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
- form.querySelectorAll(
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/discovery.ts
1061
- init_registry();
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
- fillFormFields(form, params);
1082
- 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);
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
- 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 });
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, params);
1231
+ fillFormFields(form, resolvedParams);
1092
1232
  for (let attempt = 0; attempt < 2; attempt++) {
1093
- const reset = getResetFields(form, params, formFieldElements.get(form));
1233
+ const reset = getResetFields(form, resolvedParams, formFieldElements.get(form));
1094
1234
  if (reset.length === 0)
1095
1235
  break;
1096
- fillFormFields(form, params);
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.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);
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
- const esc = CSS.escape(key);
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 esc = CSS.escape(key);
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 esc = CSS.escape(key);
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 { getRegisteredToolName: getRegisteredToolName2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
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, { childList: true, subtree: true });
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) {