auto-webmcp 0.2.1 → 0.2.3

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.
@@ -91,6 +91,16 @@ function resolveConfig(userConfig) {
91
91
  }
92
92
 
93
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
+ ];
94
104
  function inputTypeToSchema(input) {
95
105
  if (input instanceof HTMLInputElement) {
96
106
  return mapInputElement(input);
@@ -186,6 +196,49 @@ function collectRadioOneOf(form, name) {
186
196
  return { const: r.value, title: title || r.value };
187
197
  });
188
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
+ }
189
242
  function getRadioLabelText(radio) {
190
243
  const parent = radio.closest("label");
191
244
  if (parent) {
@@ -211,8 +264,8 @@ var formIndex = 0;
211
264
  function analyzeForm(form, override) {
212
265
  const name = override?.name ?? inferToolName(form);
213
266
  const description = override?.description ?? inferToolDescription(form);
214
- const inputSchema = buildSchema(form);
215
- return { name, description, inputSchema };
267
+ const { schema: inputSchema, fieldElements } = buildSchema(form);
268
+ return { name, description, inputSchema, fieldElements };
216
269
  }
217
270
  function inferToolName(form) {
218
271
  const nativeName = form.getAttribute("toolname");
@@ -312,6 +365,7 @@ function inferToolDescription(form) {
312
365
  function buildSchema(form) {
313
366
  const properties = {};
314
367
  const required = [];
368
+ const fieldElements = /* @__PURE__ */ new Map();
315
369
  const processedRadioGroups = /* @__PURE__ */ new Set();
316
370
  const controls = Array.from(
317
371
  form.querySelectorAll(
@@ -320,12 +374,13 @@ function buildSchema(form) {
320
374
  );
321
375
  for (const control of controls) {
322
376
  const name = control.name;
323
- if (!name)
377
+ const fieldKey = name || resolveNativeControlFallbackKey(control);
378
+ if (!fieldKey)
324
379
  continue;
325
380
  if (control instanceof HTMLInputElement && control.type === "radio") {
326
- if (processedRadioGroups.has(name))
381
+ if (processedRadioGroups.has(fieldKey))
327
382
  continue;
328
- processedRadioGroups.add(name);
383
+ processedRadioGroups.add(fieldKey);
329
384
  }
330
385
  const schemaProp = inputTypeToSchema(control);
331
386
  if (!schemaProp)
@@ -335,17 +390,123 @@ function buildSchema(form) {
335
390
  if (desc)
336
391
  schemaProp.description = desc;
337
392
  if (control instanceof HTMLInputElement && control.type === "radio") {
338
- schemaProp.enum = collectRadioEnum(form, name);
339
- const radioOneOf = collectRadioOneOf(form, name);
393
+ schemaProp.enum = collectRadioEnum(form, fieldKey);
394
+ const radioOneOf = collectRadioOneOf(form, fieldKey);
340
395
  if (radioOneOf.length > 0)
341
396
  schemaProp.oneOf = radioOneOf;
342
397
  }
343
- properties[name] = schemaProp;
398
+ properties[fieldKey] = schemaProp;
399
+ if (!name) {
400
+ fieldElements.set(fieldKey, control);
401
+ }
344
402
  if (control.required) {
345
- required.push(name);
403
+ required.push(fieldKey);
404
+ }
405
+ }
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);
346
425
  }
347
426
  }
348
- return { type: "object", properties, required };
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 "";
349
510
  }
350
511
  function inferFieldTitle(control) {
351
512
  if ("dataset" in control && control.dataset["webmcpTitle"]) {
@@ -414,7 +575,15 @@ init_registry();
414
575
 
415
576
  // src/interceptor.ts
416
577
  var pendingExecutions = /* @__PURE__ */ new WeakMap();
417
- 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
+ }
418
587
  attachSubmitInterceptor(form, toolName);
419
588
  return async (params) => {
420
589
  fillFormFields(form, params);
@@ -437,8 +606,8 @@ function attachSubmitInterceptor(form, toolName) {
437
606
  return;
438
607
  const { resolve } = pending;
439
608
  pendingExecutions.delete(form);
440
- const formData = serializeFormData(form);
441
- const text = JSON.stringify(formData);
609
+ const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
610
+ const text = `Form submitted. Fields: ${JSON.stringify(formData)}`;
442
611
  const result = { content: [{ type: "text", text }] };
443
612
  if (e.agentInvoked && typeof e.respondWith === "function") {
444
613
  e.preventDefault();
@@ -450,52 +619,112 @@ function attachSubmitInterceptor(form, toolName) {
450
619
  window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
451
620
  });
452
621
  }
622
+ function setReactValue(el, v) {
623
+ el.focus();
624
+ el.select?.();
625
+ if (document.execCommand("insertText", false, v)) {
626
+ return;
627
+ }
628
+ const setter = el instanceof HTMLTextAreaElement ? _textareaValueSetter : _inputValueSetter;
629
+ if (setter) {
630
+ setter.call(el, v);
631
+ } else {
632
+ el.value = v;
633
+ }
634
+ el.dispatchEvent(new InputEvent("input", { bubbles: true, cancelable: true, inputType: "insertText", data: v }));
635
+ el.dispatchEvent(new Event("change", { bubbles: true }));
636
+ }
637
+ function setReactChecked(el, checked) {
638
+ if (_checkedSetter) {
639
+ _checkedSetter.call(el, checked);
640
+ } else {
641
+ el.checked = checked;
642
+ }
643
+ el.dispatchEvent(new Event("change", { bubbles: true }));
644
+ }
645
+ function findNativeField(form, key) {
646
+ const esc = CSS.escape(key);
647
+ return form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
648
+ `input#${esc}, textarea#${esc}, select#${esc}`
649
+ );
650
+ }
453
651
  function fillFormFields(form, params) {
454
- for (const [name, value] of Object.entries(params)) {
455
- const escapedName = CSS.escape(name);
456
- const input = form.querySelector(
457
- `[name="${escapedName}"]`
458
- );
459
- if (!input)
652
+ lastParams.set(form, params);
653
+ const fieldEls = formFieldElements.get(form);
654
+ for (const [key, value] of Object.entries(params)) {
655
+ const input = findNativeField(form, key);
656
+ if (input) {
657
+ if (input instanceof HTMLInputElement) {
658
+ fillInput(input, form, key, value);
659
+ } else if (input instanceof HTMLTextAreaElement) {
660
+ setReactValue(input, String(value ?? ""));
661
+ } else if (input instanceof HTMLSelectElement) {
662
+ input.value = String(value ?? "");
663
+ input.dispatchEvent(new Event("change", { bubbles: true }));
664
+ }
460
665
  continue;
461
- if (input instanceof HTMLInputElement) {
462
- fillInput(input, form, name, value);
463
- } else if (input instanceof HTMLTextAreaElement) {
464
- input.value = String(value ?? "");
465
- input.dispatchEvent(new Event("input", { bubbles: true }));
466
- input.dispatchEvent(new Event("change", { bubbles: true }));
467
- } else if (input instanceof HTMLSelectElement) {
468
- input.value = String(value ?? "");
469
- input.dispatchEvent(new Event("change", { bubbles: true }));
666
+ }
667
+ const ariaEl = fieldEls?.get(key);
668
+ if (ariaEl) {
669
+ if (ariaEl instanceof HTMLInputElement) {
670
+ fillInput(ariaEl, form, key, value);
671
+ } else if (ariaEl instanceof HTMLTextAreaElement) {
672
+ setReactValue(ariaEl, String(value ?? ""));
673
+ } else if (ariaEl instanceof HTMLSelectElement) {
674
+ ariaEl.value = String(value ?? "");
675
+ ariaEl.dispatchEvent(new Event("change", { bubbles: true }));
676
+ } else {
677
+ fillAriaField(ariaEl, value);
678
+ }
470
679
  }
471
680
  }
472
681
  }
473
- function fillInput(input, form, name, value) {
682
+ function fillInput(input, form, key, value) {
474
683
  const type = input.type.toLowerCase();
475
684
  if (type === "checkbox") {
476
- input.checked = Boolean(value);
477
- input.dispatchEvent(new Event("change", { bubbles: true }));
685
+ setReactChecked(input, Boolean(value));
478
686
  return;
479
687
  }
480
688
  if (type === "radio") {
481
- const escapedName = CSS.escape(name);
689
+ const esc = CSS.escape(key);
482
690
  const radios = form.querySelectorAll(
483
- `input[type="radio"][name="${escapedName}"]`
691
+ `input[type="radio"][name="${esc}"]`
484
692
  );
485
693
  for (const radio of radios) {
486
694
  if (radio.value === String(value)) {
487
- radio.checked = true;
695
+ if (_checkedSetter) {
696
+ _checkedSetter.call(radio, true);
697
+ } else {
698
+ radio.checked = true;
699
+ }
488
700
  radio.dispatchEvent(new Event("change", { bubbles: true }));
489
701
  break;
490
702
  }
491
703
  }
492
704
  return;
493
705
  }
494
- input.value = String(value ?? "");
495
- input.dispatchEvent(new Event("input", { bubbles: true }));
496
- input.dispatchEvent(new Event("change", { bubbles: true }));
706
+ setReactValue(input, String(value ?? ""));
707
+ }
708
+ function fillAriaField(el, value) {
709
+ const role = el.getAttribute("role");
710
+ if (role === "checkbox" || role === "switch") {
711
+ el.setAttribute("aria-checked", String(Boolean(value)));
712
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
713
+ return;
714
+ }
715
+ if (role === "radio") {
716
+ el.setAttribute("aria-checked", "true");
717
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
718
+ return;
719
+ }
720
+ const htmlEl = el;
721
+ if (htmlEl.isContentEditable) {
722
+ htmlEl.textContent = String(value ?? "");
723
+ }
724
+ el.dispatchEvent(new Event("input", { bubbles: true }));
725
+ el.dispatchEvent(new Event("change", { bubbles: true }));
497
726
  }
498
- function serializeFormData(form) {
727
+ function serializeFormData(form, params, fieldEls) {
499
728
  const result = {};
500
729
  const data = new FormData(form);
501
730
  for (const [key, val] of data.entries()) {
@@ -510,6 +739,27 @@ function serializeFormData(form) {
510
739
  result[key] = val;
511
740
  }
512
741
  }
742
+ if (params) {
743
+ for (const key of Object.keys(params)) {
744
+ if (key in result)
745
+ continue;
746
+ const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
747
+ if (!el)
748
+ continue;
749
+ if (el instanceof HTMLInputElement && el.type === "checkbox") {
750
+ result[key] = el.checked;
751
+ } else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
752
+ result[key] = el.value;
753
+ } else {
754
+ const role = el.getAttribute("role");
755
+ if (role === "checkbox" || role === "switch") {
756
+ result[key] = el.getAttribute("aria-checked") === "true";
757
+ } else {
758
+ result[key] = el.textContent?.trim() ?? "";
759
+ }
760
+ }
761
+ }
762
+ }
513
763
  return result;
514
764
  }
515
765
 
@@ -621,8 +871,9 @@ async function registerForm(form, config) {
621
871
  if (config.debug) {
622
872
  warnToolQuality(metadata.name, metadata.description);
623
873
  }
624
- const execute = buildExecuteHandler(form, config, metadata.name);
874
+ const execute = buildExecuteHandler(form, config, metadata.name, metadata);
625
875
  await registerFormTool(form, metadata, execute);
876
+ registeredForms.add(form);
626
877
  if (config.debug) {
627
878
  console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
628
879
  }
@@ -634,12 +885,43 @@ async function unregisterForm(form, config) {
634
885
  if (!name)
635
886
  return;
636
887
  await unregisterFormTool(form);
888
+ registeredForms.delete(form);
637
889
  if (config.debug) {
638
890
  console.debug(`[auto-webmcp] Unregistered: ${name}`);
639
891
  }
640
892
  emit("form:unregistered", form, name);
641
893
  }
642
894
  var observer = null;
895
+ var registeredForms = /* @__PURE__ */ new WeakSet();
896
+ var reAnalysisTimers = /* @__PURE__ */ new Map();
897
+ var RE_ANALYSIS_DEBOUNCE_MS = 300;
898
+ function isInterestingNode(node) {
899
+ const tag = node.tagName.toLowerCase();
900
+ if (tag === "input" || tag === "textarea" || tag === "select")
901
+ return true;
902
+ const role = node.getAttribute("role");
903
+ if (role && ARIA_ROLES_TO_SCAN.includes(role))
904
+ return true;
905
+ if (node.querySelector("input, textarea, select"))
906
+ return true;
907
+ for (const r of ARIA_ROLES_TO_SCAN) {
908
+ if (node.querySelector(`[role="${r}"]`))
909
+ return true;
910
+ }
911
+ return false;
912
+ }
913
+ function scheduleReAnalysis(form, config) {
914
+ const existing = reAnalysisTimers.get(form);
915
+ if (existing)
916
+ clearTimeout(existing);
917
+ reAnalysisTimers.set(
918
+ form,
919
+ setTimeout(() => {
920
+ reAnalysisTimers.delete(form);
921
+ void registerForm(form, config);
922
+ }, RE_ANALYSIS_DEBOUNCE_MS)
923
+ );
924
+ }
643
925
  function startObserver(config) {
644
926
  if (observer)
645
927
  return;
@@ -648,8 +930,15 @@ function startObserver(config) {
648
930
  for (const node of mutation.addedNodes) {
649
931
  if (!(node instanceof Element))
650
932
  continue;
651
- const forms = node instanceof HTMLFormElement ? [node] : Array.from(node.querySelectorAll("form"));
652
- for (const form of forms) {
933
+ if (node instanceof HTMLFormElement) {
934
+ void registerForm(node, config);
935
+ continue;
936
+ }
937
+ const parentForm = node.closest("form");
938
+ if (parentForm instanceof HTMLFormElement && registeredForms.has(parentForm) && isInterestingNode(node)) {
939
+ scheduleReAnalysis(parentForm, config);
940
+ }
941
+ for (const form of Array.from(node.querySelectorAll("form"))) {
653
942
  void registerForm(form, config);
654
943
  }
655
944
  }