auto-webmcp 0.2.0 → 0.2.2

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.
@@ -28,12 +28,25 @@ async function registerFormTool(form, metadata, execute) {
28
28
  if (existing) {
29
29
  await unregisterFormTool(form);
30
30
  }
31
- await navigator.modelContext.registerTool({
32
- name: metadata.name,
33
- description: metadata.description,
34
- inputSchema: metadata.inputSchema,
35
- execute
36
- });
31
+ try {
32
+ await navigator.modelContext.registerTool({
33
+ name: metadata.name,
34
+ description: metadata.description,
35
+ inputSchema: metadata.inputSchema,
36
+ execute
37
+ });
38
+ } catch {
39
+ try {
40
+ await navigator.modelContext.unregisterTool(metadata.name);
41
+ await navigator.modelContext.registerTool({
42
+ name: metadata.name,
43
+ description: metadata.description,
44
+ inputSchema: metadata.inputSchema,
45
+ execute
46
+ });
47
+ } catch {
48
+ }
49
+ }
37
50
  registeredTools.set(form, metadata.name);
38
51
  }
39
52
  async function unregisterFormTool(form) {
@@ -78,6 +91,16 @@ function resolveConfig(userConfig) {
78
91
  }
79
92
 
80
93
  // src/schema.ts
94
+ var ARIA_ROLES_TO_SCAN = [
95
+ "textbox",
96
+ "combobox",
97
+ "checkbox",
98
+ "radio",
99
+ "switch",
100
+ "spinbutton",
101
+ "searchbox",
102
+ "slider"
103
+ ];
81
104
  function inputTypeToSchema(input) {
82
105
  if (input instanceof HTMLInputElement) {
83
106
  return mapInputElement(input);
@@ -173,6 +196,49 @@ function collectRadioOneOf(form, name) {
173
196
  return { const: r.value, title: title || r.value };
174
197
  });
175
198
  }
199
+ function ariaRoleToSchema(el, role) {
200
+ switch (role) {
201
+ case "checkbox":
202
+ case "switch":
203
+ return { type: "boolean" };
204
+ case "spinbutton":
205
+ case "slider": {
206
+ const prop = { type: "number" };
207
+ const min = el.getAttribute("aria-valuemin");
208
+ const max = el.getAttribute("aria-valuemax");
209
+ if (min !== null)
210
+ prop.minimum = parseFloat(min);
211
+ if (max !== null)
212
+ prop.maximum = parseFloat(max);
213
+ return prop;
214
+ }
215
+ case "combobox": {
216
+ const ownedId = el.getAttribute("aria-owns") ?? el.getAttribute("aria-controls");
217
+ if (ownedId) {
218
+ const listbox = document.getElementById(ownedId);
219
+ if (listbox) {
220
+ const options = Array.from(listbox.querySelectorAll('[role="option"]')).filter(
221
+ (o) => o.getAttribute("aria-disabled") !== "true"
222
+ );
223
+ if (options.length > 0) {
224
+ const enumValues = options.map((o) => (o.getAttribute("data-value") ?? o.textContent ?? "").trim()).filter(Boolean);
225
+ const oneOf = options.map((o) => ({
226
+ const: (o.getAttribute("data-value") ?? o.textContent ?? "").trim(),
227
+ title: (o.textContent ?? "").trim()
228
+ }));
229
+ return { type: "string", enum: enumValues, oneOf };
230
+ }
231
+ }
232
+ }
233
+ return { type: "string" };
234
+ }
235
+ case "textbox":
236
+ case "searchbox":
237
+ case "radio":
238
+ default:
239
+ return { type: "string" };
240
+ }
241
+ }
176
242
  function getRadioLabelText(radio) {
177
243
  const parent = radio.closest("label");
178
244
  if (parent) {
@@ -198,8 +264,8 @@ var formIndex = 0;
198
264
  function analyzeForm(form, override) {
199
265
  const name = override?.name ?? inferToolName(form);
200
266
  const description = override?.description ?? inferToolDescription(form);
201
- const inputSchema = buildSchema(form);
202
- return { name, description, inputSchema };
267
+ const { schema: inputSchema, fieldElements } = buildSchema(form);
268
+ return { name, description, inputSchema, fieldElements };
203
269
  }
204
270
  function inferToolName(form) {
205
271
  const nativeName = form.getAttribute("toolname");
@@ -299,6 +365,7 @@ function inferToolDescription(form) {
299
365
  function buildSchema(form) {
300
366
  const properties = {};
301
367
  const required = [];
368
+ const fieldElements = /* @__PURE__ */ new Map();
302
369
  const processedRadioGroups = /* @__PURE__ */ new Set();
303
370
  const controls = Array.from(
304
371
  form.querySelectorAll(
@@ -307,12 +374,13 @@ function buildSchema(form) {
307
374
  );
308
375
  for (const control of controls) {
309
376
  const name = control.name;
310
- if (!name)
377
+ const fieldKey = name || resolveNativeControlFallbackKey(control);
378
+ if (!fieldKey)
311
379
  continue;
312
380
  if (control instanceof HTMLInputElement && control.type === "radio") {
313
- if (processedRadioGroups.has(name))
381
+ if (processedRadioGroups.has(fieldKey))
314
382
  continue;
315
- processedRadioGroups.add(name);
383
+ processedRadioGroups.add(fieldKey);
316
384
  }
317
385
  const schemaProp = inputTypeToSchema(control);
318
386
  if (!schemaProp)
@@ -322,17 +390,123 @@ function buildSchema(form) {
322
390
  if (desc)
323
391
  schemaProp.description = desc;
324
392
  if (control instanceof HTMLInputElement && control.type === "radio") {
325
- schemaProp.enum = collectRadioEnum(form, name);
326
- const radioOneOf = collectRadioOneOf(form, name);
393
+ schemaProp.enum = collectRadioEnum(form, fieldKey);
394
+ const radioOneOf = collectRadioOneOf(form, fieldKey);
327
395
  if (radioOneOf.length > 0)
328
396
  schemaProp.oneOf = radioOneOf;
329
397
  }
330
- properties[name] = schemaProp;
398
+ properties[fieldKey] = schemaProp;
399
+ if (!name) {
400
+ fieldElements.set(fieldKey, control);
401
+ }
331
402
  if (control.required) {
332
- required.push(name);
403
+ required.push(fieldKey);
333
404
  }
334
405
  }
335
- return { type: "object", properties, required };
406
+ const ariaControls = collectAriaControls(form);
407
+ const processedAriaRadioGroups = /* @__PURE__ */ new Set();
408
+ for (const { el, role, key } of ariaControls) {
409
+ if (properties[key])
410
+ continue;
411
+ if (role === "radio") {
412
+ if (processedAriaRadioGroups.has(key))
413
+ continue;
414
+ processedAriaRadioGroups.add(key);
415
+ }
416
+ const schemaProp = ariaRoleToSchema(el, role);
417
+ schemaProp.title = inferAriaFieldTitle(el);
418
+ const desc = inferAriaFieldDescription(el);
419
+ if (desc)
420
+ schemaProp.description = desc;
421
+ properties[key] = schemaProp;
422
+ fieldElements.set(key, el);
423
+ if (el.getAttribute("aria-required") === "true") {
424
+ required.push(key);
425
+ }
426
+ }
427
+ return { schema: { type: "object", properties, required }, fieldElements };
428
+ }
429
+ function resolveNativeControlFallbackKey(control) {
430
+ const el = control;
431
+ if (el.dataset["webmcpName"])
432
+ return sanitizeName(el.dataset["webmcpName"]);
433
+ if (control.id)
434
+ return sanitizeName(control.id);
435
+ const label = control.getAttribute("aria-label");
436
+ if (label)
437
+ return sanitizeName(label);
438
+ return null;
439
+ }
440
+ function collectAriaControls(form) {
441
+ const selector = ARIA_ROLES_TO_SCAN.map((r) => `[role="${r}"]`).join(", ");
442
+ const results = [];
443
+ for (const el of Array.from(form.querySelectorAll(selector))) {
444
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)
445
+ continue;
446
+ if (el.getAttribute("aria-hidden") === "true" || el.hidden)
447
+ continue;
448
+ const role = el.getAttribute("role");
449
+ const key = resolveAriaFieldKey(el);
450
+ if (!key)
451
+ continue;
452
+ results.push({ el, role, key });
453
+ }
454
+ return results;
455
+ }
456
+ function resolveAriaFieldKey(el) {
457
+ const htmlEl = el;
458
+ if (htmlEl.dataset?.["webmcpName"])
459
+ return sanitizeName(htmlEl.dataset["webmcpName"]);
460
+ if (el.id)
461
+ return sanitizeName(el.id);
462
+ const label = el.getAttribute("aria-label");
463
+ if (label)
464
+ return sanitizeName(label);
465
+ const labelledById = el.getAttribute("aria-labelledby");
466
+ if (labelledById) {
467
+ const text = document.getElementById(labelledById)?.textContent?.trim();
468
+ if (text)
469
+ return sanitizeName(text);
470
+ }
471
+ return null;
472
+ }
473
+ function inferAriaFieldTitle(el) {
474
+ const htmlEl = el;
475
+ if (htmlEl.dataset?.["webmcpTitle"])
476
+ return htmlEl.dataset["webmcpTitle"];
477
+ const label = el.getAttribute("aria-label");
478
+ if (label)
479
+ return label.trim();
480
+ const labelledById = el.getAttribute("aria-labelledby");
481
+ if (labelledById) {
482
+ const text = document.getElementById(labelledById)?.textContent?.trim();
483
+ if (text)
484
+ return text;
485
+ }
486
+ if (el.id)
487
+ return humanizeName(el.id);
488
+ return "";
489
+ }
490
+ function inferAriaFieldDescription(el) {
491
+ const nativeParamDesc = el.getAttribute("toolparamdescription");
492
+ if (nativeParamDesc)
493
+ return nativeParamDesc.trim();
494
+ const htmlEl = el;
495
+ if (htmlEl.dataset?.["webmcpDescription"])
496
+ return htmlEl.dataset["webmcpDescription"];
497
+ const ariaDesc = el.getAttribute("aria-description");
498
+ if (ariaDesc)
499
+ return ariaDesc;
500
+ const describedById = el.getAttribute("aria-describedby");
501
+ if (describedById) {
502
+ const text = document.getElementById(describedById)?.textContent?.trim();
503
+ if (text)
504
+ return text;
505
+ }
506
+ const placeholder = el.getAttribute("placeholder") ?? el.dataset?.["placeholder"];
507
+ if (placeholder)
508
+ return placeholder.trim();
509
+ return "";
336
510
  }
337
511
  function inferFieldTitle(control) {
338
512
  if ("dataset" in control && control.dataset["webmcpTitle"]) {
@@ -401,7 +575,15 @@ init_registry();
401
575
 
402
576
  // src/interceptor.ts
403
577
  var pendingExecutions = /* @__PURE__ */ new WeakMap();
404
- function buildExecuteHandler(form, config, toolName) {
578
+ var lastParams = /* @__PURE__ */ new WeakMap();
579
+ var formFieldElements = /* @__PURE__ */ new WeakMap();
580
+ var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
581
+ var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
582
+ var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
583
+ function buildExecuteHandler(form, config, toolName, metadata) {
584
+ if (metadata?.fieldElements) {
585
+ formFieldElements.set(form, metadata.fieldElements);
586
+ }
405
587
  attachSubmitInterceptor(form, toolName);
406
588
  return async (params) => {
407
589
  fillFormFields(form, params);
@@ -424,7 +606,7 @@ function attachSubmitInterceptor(form, toolName) {
424
606
  return;
425
607
  const { resolve } = pending;
426
608
  pendingExecutions.delete(form);
427
- const formData = serializeFormData(form);
609
+ const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
428
610
  const text = JSON.stringify(formData);
429
611
  const result = { content: [{ type: "text", text }] };
430
612
  if (e.agentInvoked && typeof e.respondWith === "function") {
@@ -437,52 +619,98 @@ function attachSubmitInterceptor(form, toolName) {
437
619
  window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
438
620
  });
439
621
  }
622
+ function setReactValue(el, v) {
623
+ const setter = el instanceof HTMLTextAreaElement ? _textareaValueSetter : _inputValueSetter;
624
+ if (setter) {
625
+ setter.call(el, v);
626
+ } else {
627
+ el.value = v;
628
+ }
629
+ el.dispatchEvent(new Event("input", { bubbles: true }));
630
+ el.dispatchEvent(new Event("change", { bubbles: true }));
631
+ }
632
+ function setReactChecked(el, checked) {
633
+ if (_checkedSetter) {
634
+ _checkedSetter.call(el, checked);
635
+ } else {
636
+ el.checked = checked;
637
+ }
638
+ el.dispatchEvent(new Event("change", { bubbles: true }));
639
+ }
640
+ function findNativeField(form, key) {
641
+ const esc = CSS.escape(key);
642
+ return form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
643
+ `input#${esc}, textarea#${esc}, select#${esc}`
644
+ );
645
+ }
440
646
  function fillFormFields(form, params) {
441
- for (const [name, value] of Object.entries(params)) {
442
- const escapedName = CSS.escape(name);
443
- const input = form.querySelector(
444
- `[name="${escapedName}"]`
445
- );
446
- if (!input)
647
+ lastParams.set(form, params);
648
+ const fieldEls = formFieldElements.get(form);
649
+ for (const [key, value] of Object.entries(params)) {
650
+ const input = findNativeField(form, key);
651
+ if (input) {
652
+ if (input instanceof HTMLInputElement) {
653
+ fillInput(input, form, key, value);
654
+ } else if (input instanceof HTMLTextAreaElement) {
655
+ setReactValue(input, String(value ?? ""));
656
+ } else if (input instanceof HTMLSelectElement) {
657
+ input.value = String(value ?? "");
658
+ input.dispatchEvent(new Event("change", { bubbles: true }));
659
+ }
447
660
  continue;
448
- if (input instanceof HTMLInputElement) {
449
- fillInput(input, form, name, value);
450
- } else if (input instanceof HTMLTextAreaElement) {
451
- input.value = String(value ?? "");
452
- input.dispatchEvent(new Event("input", { bubbles: true }));
453
- input.dispatchEvent(new Event("change", { bubbles: true }));
454
- } else if (input instanceof HTMLSelectElement) {
455
- input.value = String(value ?? "");
456
- input.dispatchEvent(new Event("change", { bubbles: true }));
661
+ }
662
+ const ariaEl = fieldEls?.get(key);
663
+ if (ariaEl) {
664
+ fillAriaField(ariaEl, value);
457
665
  }
458
666
  }
459
667
  }
460
- function fillInput(input, form, name, value) {
668
+ function fillInput(input, form, key, value) {
461
669
  const type = input.type.toLowerCase();
462
670
  if (type === "checkbox") {
463
- input.checked = Boolean(value);
464
- input.dispatchEvent(new Event("change", { bubbles: true }));
671
+ setReactChecked(input, Boolean(value));
465
672
  return;
466
673
  }
467
674
  if (type === "radio") {
468
- const escapedName = CSS.escape(name);
675
+ const esc = CSS.escape(key);
469
676
  const radios = form.querySelectorAll(
470
- `input[type="radio"][name="${escapedName}"]`
677
+ `input[type="radio"][name="${esc}"]`
471
678
  );
472
679
  for (const radio of radios) {
473
680
  if (radio.value === String(value)) {
474
- radio.checked = true;
681
+ if (_checkedSetter) {
682
+ _checkedSetter.call(radio, true);
683
+ } else {
684
+ radio.checked = true;
685
+ }
475
686
  radio.dispatchEvent(new Event("change", { bubbles: true }));
476
687
  break;
477
688
  }
478
689
  }
479
690
  return;
480
691
  }
481
- input.value = String(value ?? "");
482
- input.dispatchEvent(new Event("input", { bubbles: true }));
483
- input.dispatchEvent(new Event("change", { bubbles: true }));
692
+ setReactValue(input, String(value ?? ""));
693
+ }
694
+ function fillAriaField(el, value) {
695
+ const role = el.getAttribute("role");
696
+ if (role === "checkbox" || role === "switch") {
697
+ el.setAttribute("aria-checked", String(Boolean(value)));
698
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
699
+ return;
700
+ }
701
+ if (role === "radio") {
702
+ el.setAttribute("aria-checked", "true");
703
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
704
+ return;
705
+ }
706
+ const htmlEl = el;
707
+ if (htmlEl.isContentEditable) {
708
+ htmlEl.textContent = String(value ?? "");
709
+ }
710
+ el.dispatchEvent(new Event("input", { bubbles: true }));
711
+ el.dispatchEvent(new Event("change", { bubbles: true }));
484
712
  }
485
- function serializeFormData(form) {
713
+ function serializeFormData(form, params, fieldEls) {
486
714
  const result = {};
487
715
  const data = new FormData(form);
488
716
  for (const [key, val] of data.entries()) {
@@ -497,6 +725,27 @@ function serializeFormData(form) {
497
725
  result[key] = val;
498
726
  }
499
727
  }
728
+ if (params) {
729
+ for (const key of Object.keys(params)) {
730
+ if (key in result)
731
+ continue;
732
+ const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
733
+ if (!el)
734
+ continue;
735
+ if (el instanceof HTMLInputElement && el.type === "checkbox") {
736
+ result[key] = el.checked;
737
+ } else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
738
+ result[key] = el.value;
739
+ } else {
740
+ const role = el.getAttribute("role");
741
+ if (role === "checkbox" || role === "switch") {
742
+ result[key] = el.getAttribute("aria-checked") === "true";
743
+ } else {
744
+ result[key] = el.textContent?.trim() ?? "";
745
+ }
746
+ }
747
+ }
748
+ }
500
749
  return result;
501
750
  }
502
751
 
@@ -608,8 +857,9 @@ async function registerForm(form, config) {
608
857
  if (config.debug) {
609
858
  warnToolQuality(metadata.name, metadata.description);
610
859
  }
611
- const execute = buildExecuteHandler(form, config, metadata.name);
860
+ const execute = buildExecuteHandler(form, config, metadata.name, metadata);
612
861
  await registerFormTool(form, metadata, execute);
862
+ registeredForms.add(form);
613
863
  if (config.debug) {
614
864
  console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
615
865
  }
@@ -621,12 +871,43 @@ async function unregisterForm(form, config) {
621
871
  if (!name)
622
872
  return;
623
873
  await unregisterFormTool(form);
874
+ registeredForms.delete(form);
624
875
  if (config.debug) {
625
876
  console.debug(`[auto-webmcp] Unregistered: ${name}`);
626
877
  }
627
878
  emit("form:unregistered", form, name);
628
879
  }
629
880
  var observer = null;
881
+ var registeredForms = /* @__PURE__ */ new WeakSet();
882
+ var reAnalysisTimers = /* @__PURE__ */ new Map();
883
+ var RE_ANALYSIS_DEBOUNCE_MS = 300;
884
+ function isInterestingNode(node) {
885
+ const tag = node.tagName.toLowerCase();
886
+ if (tag === "input" || tag === "textarea" || tag === "select")
887
+ return true;
888
+ const role = node.getAttribute("role");
889
+ if (role && ARIA_ROLES_TO_SCAN.includes(role))
890
+ return true;
891
+ if (node.querySelector("input, textarea, select"))
892
+ return true;
893
+ for (const r of ARIA_ROLES_TO_SCAN) {
894
+ if (node.querySelector(`[role="${r}"]`))
895
+ return true;
896
+ }
897
+ return false;
898
+ }
899
+ function scheduleReAnalysis(form, config) {
900
+ const existing = reAnalysisTimers.get(form);
901
+ if (existing)
902
+ clearTimeout(existing);
903
+ reAnalysisTimers.set(
904
+ form,
905
+ setTimeout(() => {
906
+ reAnalysisTimers.delete(form);
907
+ void registerForm(form, config);
908
+ }, RE_ANALYSIS_DEBOUNCE_MS)
909
+ );
910
+ }
630
911
  function startObserver(config) {
631
912
  if (observer)
632
913
  return;
@@ -635,8 +916,15 @@ function startObserver(config) {
635
916
  for (const node of mutation.addedNodes) {
636
917
  if (!(node instanceof Element))
637
918
  continue;
638
- const forms = node instanceof HTMLFormElement ? [node] : Array.from(node.querySelectorAll("form"));
639
- for (const form of forms) {
919
+ if (node instanceof HTMLFormElement) {
920
+ void registerForm(node, config);
921
+ continue;
922
+ }
923
+ const parentForm = node.closest("form");
924
+ if (parentForm instanceof HTMLFormElement && registeredForms.has(parentForm) && isInterestingNode(node)) {
925
+ scheduleReAnalysis(parentForm, config);
926
+ }
927
+ for (const form of Array.from(node.querySelectorAll("form"))) {
640
928
  void registerForm(form, config);
641
929
  }
642
930
  }
@@ -670,7 +958,7 @@ function listenForRouteChanges(config) {
670
958
  }
671
959
  async function scanForms(config) {
672
960
  const forms = Array.from(document.querySelectorAll("form"));
673
- await Promise.all(forms.map((form) => registerForm(form, config)));
961
+ await Promise.allSettled(forms.map((form) => registerForm(form, config)));
674
962
  }
675
963
  function warnToolQuality(name, description) {
676
964
  if (/^form_\d+$|^submit$|^form$/.test(name)) {
@@ -689,9 +977,9 @@ async function startDiscovery(config) {
689
977
  (resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true })
690
978
  );
691
979
  }
692
- await scanForms(config);
693
980
  startObserver(config);
694
981
  listenForRouteChanges(config);
982
+ await scanForms(config);
695
983
  }
696
984
  function stopDiscovery() {
697
985
  observer?.disconnect();