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.
- package/README.md +2 -0
- package/dist/analyzer.d.ts +2 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/auto-webmcp.cjs.js +330 -41
- package/dist/auto-webmcp.cjs.js.map +2 -2
- package/dist/auto-webmcp.esm.js +330 -41
- package/dist/auto-webmcp.esm.js.map +2 -2
- package/dist/auto-webmcp.iife.js +2 -2
- package/dist/auto-webmcp.iife.js.map +3 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/interceptor.d.ts +2 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Automatically make any HTML form WebMCP-ready — zero explicit coding required.**
|
|
4
4
|
|
|
5
|
+
[Read the article on dev.to](https://dev.to/prasannagyde/every-web-form-should-be-callable-by-ai-agents-and-yours-can-be-today-228) · [Live demo](https://autowebmcp.dev/demo) · [Platform guides](https://autowebmcp.dev/platforms)
|
|
6
|
+
|
|
5
7
|
Drop in one script tag (or one `import`) and every `<form>` on your page is
|
|
6
8
|
instantly registered as a structured tool that in-browser AI agents can
|
|
7
9
|
discover and use via Chrome's
|
package/dist/analyzer.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface ToolMetadata {
|
|
|
7
7
|
name: string;
|
|
8
8
|
description: string;
|
|
9
9
|
inputSchema: JsonSchema;
|
|
10
|
+
/** Key → DOM element for fields not addressable by name (id-keyed or ARIA-role controls). */
|
|
11
|
+
fieldElements?: Map<string, Element>;
|
|
10
12
|
}
|
|
11
13
|
/** Reset form index counter (useful in tests) */
|
|
12
14
|
export declare function resetFormIndex(): void;
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAA8H,MAAM,aAAa,CAAC;AACrK,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,6FAA6F;IAC7F,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAKD,iDAAiD;AACjD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAMxF"}
|
package/dist/auto-webmcp.cjs.js
CHANGED
|
@@ -110,6 +110,16 @@ function resolveConfig(userConfig) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// src/schema.ts
|
|
113
|
+
var ARIA_ROLES_TO_SCAN = [
|
|
114
|
+
"textbox",
|
|
115
|
+
"combobox",
|
|
116
|
+
"checkbox",
|
|
117
|
+
"radio",
|
|
118
|
+
"switch",
|
|
119
|
+
"spinbutton",
|
|
120
|
+
"searchbox",
|
|
121
|
+
"slider"
|
|
122
|
+
];
|
|
113
123
|
function inputTypeToSchema(input) {
|
|
114
124
|
if (input instanceof HTMLInputElement) {
|
|
115
125
|
return mapInputElement(input);
|
|
@@ -205,6 +215,49 @@ function collectRadioOneOf(form, name) {
|
|
|
205
215
|
return { const: r.value, title: title || r.value };
|
|
206
216
|
});
|
|
207
217
|
}
|
|
218
|
+
function ariaRoleToSchema(el, role) {
|
|
219
|
+
switch (role) {
|
|
220
|
+
case "checkbox":
|
|
221
|
+
case "switch":
|
|
222
|
+
return { type: "boolean" };
|
|
223
|
+
case "spinbutton":
|
|
224
|
+
case "slider": {
|
|
225
|
+
const prop = { type: "number" };
|
|
226
|
+
const min = el.getAttribute("aria-valuemin");
|
|
227
|
+
const max = el.getAttribute("aria-valuemax");
|
|
228
|
+
if (min !== null)
|
|
229
|
+
prop.minimum = parseFloat(min);
|
|
230
|
+
if (max !== null)
|
|
231
|
+
prop.maximum = parseFloat(max);
|
|
232
|
+
return prop;
|
|
233
|
+
}
|
|
234
|
+
case "combobox": {
|
|
235
|
+
const ownedId = el.getAttribute("aria-owns") ?? el.getAttribute("aria-controls");
|
|
236
|
+
if (ownedId) {
|
|
237
|
+
const listbox = document.getElementById(ownedId);
|
|
238
|
+
if (listbox) {
|
|
239
|
+
const options = Array.from(listbox.querySelectorAll('[role="option"]')).filter(
|
|
240
|
+
(o) => o.getAttribute("aria-disabled") !== "true"
|
|
241
|
+
);
|
|
242
|
+
if (options.length > 0) {
|
|
243
|
+
const enumValues = options.map((o) => (o.getAttribute("data-value") ?? o.textContent ?? "").trim()).filter(Boolean);
|
|
244
|
+
const oneOf = options.map((o) => ({
|
|
245
|
+
const: (o.getAttribute("data-value") ?? o.textContent ?? "").trim(),
|
|
246
|
+
title: (o.textContent ?? "").trim()
|
|
247
|
+
}));
|
|
248
|
+
return { type: "string", enum: enumValues, oneOf };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { type: "string" };
|
|
253
|
+
}
|
|
254
|
+
case "textbox":
|
|
255
|
+
case "searchbox":
|
|
256
|
+
case "radio":
|
|
257
|
+
default:
|
|
258
|
+
return { type: "string" };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
208
261
|
function getRadioLabelText(radio) {
|
|
209
262
|
const parent = radio.closest("label");
|
|
210
263
|
if (parent) {
|
|
@@ -230,8 +283,8 @@ var formIndex = 0;
|
|
|
230
283
|
function analyzeForm(form, override) {
|
|
231
284
|
const name = override?.name ?? inferToolName(form);
|
|
232
285
|
const description = override?.description ?? inferToolDescription(form);
|
|
233
|
-
const inputSchema = buildSchema(form);
|
|
234
|
-
return { name, description, inputSchema };
|
|
286
|
+
const { schema: inputSchema, fieldElements } = buildSchema(form);
|
|
287
|
+
return { name, description, inputSchema, fieldElements };
|
|
235
288
|
}
|
|
236
289
|
function inferToolName(form) {
|
|
237
290
|
const nativeName = form.getAttribute("toolname");
|
|
@@ -331,6 +384,7 @@ function inferToolDescription(form) {
|
|
|
331
384
|
function buildSchema(form) {
|
|
332
385
|
const properties = {};
|
|
333
386
|
const required = [];
|
|
387
|
+
const fieldElements = /* @__PURE__ */ new Map();
|
|
334
388
|
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
335
389
|
const controls = Array.from(
|
|
336
390
|
form.querySelectorAll(
|
|
@@ -339,12 +393,13 @@ function buildSchema(form) {
|
|
|
339
393
|
);
|
|
340
394
|
for (const control of controls) {
|
|
341
395
|
const name = control.name;
|
|
342
|
-
|
|
396
|
+
const fieldKey = name || resolveNativeControlFallbackKey(control);
|
|
397
|
+
if (!fieldKey)
|
|
343
398
|
continue;
|
|
344
399
|
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
345
|
-
if (processedRadioGroups.has(
|
|
400
|
+
if (processedRadioGroups.has(fieldKey))
|
|
346
401
|
continue;
|
|
347
|
-
processedRadioGroups.add(
|
|
402
|
+
processedRadioGroups.add(fieldKey);
|
|
348
403
|
}
|
|
349
404
|
const schemaProp = inputTypeToSchema(control);
|
|
350
405
|
if (!schemaProp)
|
|
@@ -354,17 +409,123 @@ function buildSchema(form) {
|
|
|
354
409
|
if (desc)
|
|
355
410
|
schemaProp.description = desc;
|
|
356
411
|
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
357
|
-
schemaProp.enum = collectRadioEnum(form,
|
|
358
|
-
const radioOneOf = collectRadioOneOf(form,
|
|
412
|
+
schemaProp.enum = collectRadioEnum(form, fieldKey);
|
|
413
|
+
const radioOneOf = collectRadioOneOf(form, fieldKey);
|
|
359
414
|
if (radioOneOf.length > 0)
|
|
360
415
|
schemaProp.oneOf = radioOneOf;
|
|
361
416
|
}
|
|
362
|
-
properties[
|
|
417
|
+
properties[fieldKey] = schemaProp;
|
|
418
|
+
if (!name) {
|
|
419
|
+
fieldElements.set(fieldKey, control);
|
|
420
|
+
}
|
|
363
421
|
if (control.required) {
|
|
364
|
-
required.push(
|
|
422
|
+
required.push(fieldKey);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const ariaControls = collectAriaControls(form);
|
|
426
|
+
const processedAriaRadioGroups = /* @__PURE__ */ new Set();
|
|
427
|
+
for (const { el, role, key } of ariaControls) {
|
|
428
|
+
if (properties[key])
|
|
429
|
+
continue;
|
|
430
|
+
if (role === "radio") {
|
|
431
|
+
if (processedAriaRadioGroups.has(key))
|
|
432
|
+
continue;
|
|
433
|
+
processedAriaRadioGroups.add(key);
|
|
434
|
+
}
|
|
435
|
+
const schemaProp = ariaRoleToSchema(el, role);
|
|
436
|
+
schemaProp.title = inferAriaFieldTitle(el);
|
|
437
|
+
const desc = inferAriaFieldDescription(el);
|
|
438
|
+
if (desc)
|
|
439
|
+
schemaProp.description = desc;
|
|
440
|
+
properties[key] = schemaProp;
|
|
441
|
+
fieldElements.set(key, el);
|
|
442
|
+
if (el.getAttribute("aria-required") === "true") {
|
|
443
|
+
required.push(key);
|
|
365
444
|
}
|
|
366
445
|
}
|
|
367
|
-
return { type: "object", properties, required };
|
|
446
|
+
return { schema: { type: "object", properties, required }, fieldElements };
|
|
447
|
+
}
|
|
448
|
+
function resolveNativeControlFallbackKey(control) {
|
|
449
|
+
const el = control;
|
|
450
|
+
if (el.dataset["webmcpName"])
|
|
451
|
+
return sanitizeName(el.dataset["webmcpName"]);
|
|
452
|
+
if (control.id)
|
|
453
|
+
return sanitizeName(control.id);
|
|
454
|
+
const label = control.getAttribute("aria-label");
|
|
455
|
+
if (label)
|
|
456
|
+
return sanitizeName(label);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
function collectAriaControls(form) {
|
|
460
|
+
const selector = ARIA_ROLES_TO_SCAN.map((r) => `[role="${r}"]`).join(", ");
|
|
461
|
+
const results = [];
|
|
462
|
+
for (const el of Array.from(form.querySelectorAll(selector))) {
|
|
463
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)
|
|
464
|
+
continue;
|
|
465
|
+
if (el.getAttribute("aria-hidden") === "true" || el.hidden)
|
|
466
|
+
continue;
|
|
467
|
+
const role = el.getAttribute("role");
|
|
468
|
+
const key = resolveAriaFieldKey(el);
|
|
469
|
+
if (!key)
|
|
470
|
+
continue;
|
|
471
|
+
results.push({ el, role, key });
|
|
472
|
+
}
|
|
473
|
+
return results;
|
|
474
|
+
}
|
|
475
|
+
function resolveAriaFieldKey(el) {
|
|
476
|
+
const htmlEl = el;
|
|
477
|
+
if (htmlEl.dataset?.["webmcpName"])
|
|
478
|
+
return sanitizeName(htmlEl.dataset["webmcpName"]);
|
|
479
|
+
if (el.id)
|
|
480
|
+
return sanitizeName(el.id);
|
|
481
|
+
const label = el.getAttribute("aria-label");
|
|
482
|
+
if (label)
|
|
483
|
+
return sanitizeName(label);
|
|
484
|
+
const labelledById = el.getAttribute("aria-labelledby");
|
|
485
|
+
if (labelledById) {
|
|
486
|
+
const text = document.getElementById(labelledById)?.textContent?.trim();
|
|
487
|
+
if (text)
|
|
488
|
+
return sanitizeName(text);
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
function inferAriaFieldTitle(el) {
|
|
493
|
+
const htmlEl = el;
|
|
494
|
+
if (htmlEl.dataset?.["webmcpTitle"])
|
|
495
|
+
return htmlEl.dataset["webmcpTitle"];
|
|
496
|
+
const label = el.getAttribute("aria-label");
|
|
497
|
+
if (label)
|
|
498
|
+
return label.trim();
|
|
499
|
+
const labelledById = el.getAttribute("aria-labelledby");
|
|
500
|
+
if (labelledById) {
|
|
501
|
+
const text = document.getElementById(labelledById)?.textContent?.trim();
|
|
502
|
+
if (text)
|
|
503
|
+
return text;
|
|
504
|
+
}
|
|
505
|
+
if (el.id)
|
|
506
|
+
return humanizeName(el.id);
|
|
507
|
+
return "";
|
|
508
|
+
}
|
|
509
|
+
function inferAriaFieldDescription(el) {
|
|
510
|
+
const nativeParamDesc = el.getAttribute("toolparamdescription");
|
|
511
|
+
if (nativeParamDesc)
|
|
512
|
+
return nativeParamDesc.trim();
|
|
513
|
+
const htmlEl = el;
|
|
514
|
+
if (htmlEl.dataset?.["webmcpDescription"])
|
|
515
|
+
return htmlEl.dataset["webmcpDescription"];
|
|
516
|
+
const ariaDesc = el.getAttribute("aria-description");
|
|
517
|
+
if (ariaDesc)
|
|
518
|
+
return ariaDesc;
|
|
519
|
+
const describedById = el.getAttribute("aria-describedby");
|
|
520
|
+
if (describedById) {
|
|
521
|
+
const text = document.getElementById(describedById)?.textContent?.trim();
|
|
522
|
+
if (text)
|
|
523
|
+
return text;
|
|
524
|
+
}
|
|
525
|
+
const placeholder = el.getAttribute("placeholder") ?? el.dataset?.["placeholder"];
|
|
526
|
+
if (placeholder)
|
|
527
|
+
return placeholder.trim();
|
|
528
|
+
return "";
|
|
368
529
|
}
|
|
369
530
|
function inferFieldTitle(control) {
|
|
370
531
|
if ("dataset" in control && control.dataset["webmcpTitle"]) {
|
|
@@ -433,7 +594,15 @@ init_registry();
|
|
|
433
594
|
|
|
434
595
|
// src/interceptor.ts
|
|
435
596
|
var pendingExecutions = /* @__PURE__ */ new WeakMap();
|
|
436
|
-
|
|
597
|
+
var lastParams = /* @__PURE__ */ new WeakMap();
|
|
598
|
+
var formFieldElements = /* @__PURE__ */ new WeakMap();
|
|
599
|
+
var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
600
|
+
var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
|
|
601
|
+
var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
|
|
602
|
+
function buildExecuteHandler(form, config, toolName, metadata) {
|
|
603
|
+
if (metadata?.fieldElements) {
|
|
604
|
+
formFieldElements.set(form, metadata.fieldElements);
|
|
605
|
+
}
|
|
437
606
|
attachSubmitInterceptor(form, toolName);
|
|
438
607
|
return async (params) => {
|
|
439
608
|
fillFormFields(form, params);
|
|
@@ -456,8 +625,8 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
456
625
|
return;
|
|
457
626
|
const { resolve } = pending;
|
|
458
627
|
pendingExecutions.delete(form);
|
|
459
|
-
const formData = serializeFormData(form);
|
|
460
|
-
const text = JSON.stringify(formData)
|
|
628
|
+
const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
|
|
629
|
+
const text = `Form submitted. Fields: ${JSON.stringify(formData)}`;
|
|
461
630
|
const result = { content: [{ type: "text", text }] };
|
|
462
631
|
if (e.agentInvoked && typeof e.respondWith === "function") {
|
|
463
632
|
e.preventDefault();
|
|
@@ -469,52 +638,112 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
469
638
|
window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
|
|
470
639
|
});
|
|
471
640
|
}
|
|
641
|
+
function setReactValue(el, v) {
|
|
642
|
+
el.focus();
|
|
643
|
+
el.select?.();
|
|
644
|
+
if (document.execCommand("insertText", false, v)) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const setter = el instanceof HTMLTextAreaElement ? _textareaValueSetter : _inputValueSetter;
|
|
648
|
+
if (setter) {
|
|
649
|
+
setter.call(el, v);
|
|
650
|
+
} else {
|
|
651
|
+
el.value = v;
|
|
652
|
+
}
|
|
653
|
+
el.dispatchEvent(new InputEvent("input", { bubbles: true, cancelable: true, inputType: "insertText", data: v }));
|
|
654
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
655
|
+
}
|
|
656
|
+
function setReactChecked(el, checked) {
|
|
657
|
+
if (_checkedSetter) {
|
|
658
|
+
_checkedSetter.call(el, checked);
|
|
659
|
+
} else {
|
|
660
|
+
el.checked = checked;
|
|
661
|
+
}
|
|
662
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
663
|
+
}
|
|
664
|
+
function findNativeField(form, key) {
|
|
665
|
+
const esc = CSS.escape(key);
|
|
666
|
+
return form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
|
|
667
|
+
`input#${esc}, textarea#${esc}, select#${esc}`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
472
670
|
function fillFormFields(form, params) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
)
|
|
478
|
-
|
|
671
|
+
lastParams.set(form, params);
|
|
672
|
+
const fieldEls = formFieldElements.get(form);
|
|
673
|
+
for (const [key, value] of Object.entries(params)) {
|
|
674
|
+
const input = findNativeField(form, key);
|
|
675
|
+
if (input) {
|
|
676
|
+
if (input instanceof HTMLInputElement) {
|
|
677
|
+
fillInput(input, form, key, value);
|
|
678
|
+
} else if (input instanceof HTMLTextAreaElement) {
|
|
679
|
+
setReactValue(input, String(value ?? ""));
|
|
680
|
+
} else if (input instanceof HTMLSelectElement) {
|
|
681
|
+
input.value = String(value ?? "");
|
|
682
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
683
|
+
}
|
|
479
684
|
continue;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
685
|
+
}
|
|
686
|
+
const ariaEl = fieldEls?.get(key);
|
|
687
|
+
if (ariaEl) {
|
|
688
|
+
if (ariaEl instanceof HTMLInputElement) {
|
|
689
|
+
fillInput(ariaEl, form, key, value);
|
|
690
|
+
} else if (ariaEl instanceof HTMLTextAreaElement) {
|
|
691
|
+
setReactValue(ariaEl, String(value ?? ""));
|
|
692
|
+
} else if (ariaEl instanceof HTMLSelectElement) {
|
|
693
|
+
ariaEl.value = String(value ?? "");
|
|
694
|
+
ariaEl.dispatchEvent(new Event("change", { bubbles: true }));
|
|
695
|
+
} else {
|
|
696
|
+
fillAriaField(ariaEl, value);
|
|
697
|
+
}
|
|
489
698
|
}
|
|
490
699
|
}
|
|
491
700
|
}
|
|
492
|
-
function fillInput(input, form,
|
|
701
|
+
function fillInput(input, form, key, value) {
|
|
493
702
|
const type = input.type.toLowerCase();
|
|
494
703
|
if (type === "checkbox") {
|
|
495
|
-
input
|
|
496
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
704
|
+
setReactChecked(input, Boolean(value));
|
|
497
705
|
return;
|
|
498
706
|
}
|
|
499
707
|
if (type === "radio") {
|
|
500
|
-
const
|
|
708
|
+
const esc = CSS.escape(key);
|
|
501
709
|
const radios = form.querySelectorAll(
|
|
502
|
-
`input[type="radio"][name="${
|
|
710
|
+
`input[type="radio"][name="${esc}"]`
|
|
503
711
|
);
|
|
504
712
|
for (const radio of radios) {
|
|
505
713
|
if (radio.value === String(value)) {
|
|
506
|
-
|
|
714
|
+
if (_checkedSetter) {
|
|
715
|
+
_checkedSetter.call(radio, true);
|
|
716
|
+
} else {
|
|
717
|
+
radio.checked = true;
|
|
718
|
+
}
|
|
507
719
|
radio.dispatchEvent(new Event("change", { bubbles: true }));
|
|
508
720
|
break;
|
|
509
721
|
}
|
|
510
722
|
}
|
|
511
723
|
return;
|
|
512
724
|
}
|
|
513
|
-
input
|
|
514
|
-
|
|
515
|
-
|
|
725
|
+
setReactValue(input, String(value ?? ""));
|
|
726
|
+
}
|
|
727
|
+
function fillAriaField(el, value) {
|
|
728
|
+
const role = el.getAttribute("role");
|
|
729
|
+
if (role === "checkbox" || role === "switch") {
|
|
730
|
+
el.setAttribute("aria-checked", String(Boolean(value)));
|
|
731
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (role === "radio") {
|
|
735
|
+
el.setAttribute("aria-checked", "true");
|
|
736
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const htmlEl = el;
|
|
740
|
+
if (htmlEl.isContentEditable) {
|
|
741
|
+
htmlEl.textContent = String(value ?? "");
|
|
742
|
+
}
|
|
743
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
744
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
516
745
|
}
|
|
517
|
-
function serializeFormData(form) {
|
|
746
|
+
function serializeFormData(form, params, fieldEls) {
|
|
518
747
|
const result = {};
|
|
519
748
|
const data = new FormData(form);
|
|
520
749
|
for (const [key, val] of data.entries()) {
|
|
@@ -529,6 +758,27 @@ function serializeFormData(form) {
|
|
|
529
758
|
result[key] = val;
|
|
530
759
|
}
|
|
531
760
|
}
|
|
761
|
+
if (params) {
|
|
762
|
+
for (const key of Object.keys(params)) {
|
|
763
|
+
if (key in result)
|
|
764
|
+
continue;
|
|
765
|
+
const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
|
|
766
|
+
if (!el)
|
|
767
|
+
continue;
|
|
768
|
+
if (el instanceof HTMLInputElement && el.type === "checkbox") {
|
|
769
|
+
result[key] = el.checked;
|
|
770
|
+
} else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
771
|
+
result[key] = el.value;
|
|
772
|
+
} else {
|
|
773
|
+
const role = el.getAttribute("role");
|
|
774
|
+
if (role === "checkbox" || role === "switch") {
|
|
775
|
+
result[key] = el.getAttribute("aria-checked") === "true";
|
|
776
|
+
} else {
|
|
777
|
+
result[key] = el.textContent?.trim() ?? "";
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
532
782
|
return result;
|
|
533
783
|
}
|
|
534
784
|
|
|
@@ -640,8 +890,9 @@ async function registerForm(form, config) {
|
|
|
640
890
|
if (config.debug) {
|
|
641
891
|
warnToolQuality(metadata.name, metadata.description);
|
|
642
892
|
}
|
|
643
|
-
const execute = buildExecuteHandler(form, config, metadata.name);
|
|
893
|
+
const execute = buildExecuteHandler(form, config, metadata.name, metadata);
|
|
644
894
|
await registerFormTool(form, metadata, execute);
|
|
895
|
+
registeredForms.add(form);
|
|
645
896
|
if (config.debug) {
|
|
646
897
|
console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
|
|
647
898
|
}
|
|
@@ -653,12 +904,43 @@ async function unregisterForm(form, config) {
|
|
|
653
904
|
if (!name)
|
|
654
905
|
return;
|
|
655
906
|
await unregisterFormTool(form);
|
|
907
|
+
registeredForms.delete(form);
|
|
656
908
|
if (config.debug) {
|
|
657
909
|
console.debug(`[auto-webmcp] Unregistered: ${name}`);
|
|
658
910
|
}
|
|
659
911
|
emit("form:unregistered", form, name);
|
|
660
912
|
}
|
|
661
913
|
var observer = null;
|
|
914
|
+
var registeredForms = /* @__PURE__ */ new WeakSet();
|
|
915
|
+
var reAnalysisTimers = /* @__PURE__ */ new Map();
|
|
916
|
+
var RE_ANALYSIS_DEBOUNCE_MS = 300;
|
|
917
|
+
function isInterestingNode(node) {
|
|
918
|
+
const tag = node.tagName.toLowerCase();
|
|
919
|
+
if (tag === "input" || tag === "textarea" || tag === "select")
|
|
920
|
+
return true;
|
|
921
|
+
const role = node.getAttribute("role");
|
|
922
|
+
if (role && ARIA_ROLES_TO_SCAN.includes(role))
|
|
923
|
+
return true;
|
|
924
|
+
if (node.querySelector("input, textarea, select"))
|
|
925
|
+
return true;
|
|
926
|
+
for (const r of ARIA_ROLES_TO_SCAN) {
|
|
927
|
+
if (node.querySelector(`[role="${r}"]`))
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
function scheduleReAnalysis(form, config) {
|
|
933
|
+
const existing = reAnalysisTimers.get(form);
|
|
934
|
+
if (existing)
|
|
935
|
+
clearTimeout(existing);
|
|
936
|
+
reAnalysisTimers.set(
|
|
937
|
+
form,
|
|
938
|
+
setTimeout(() => {
|
|
939
|
+
reAnalysisTimers.delete(form);
|
|
940
|
+
void registerForm(form, config);
|
|
941
|
+
}, RE_ANALYSIS_DEBOUNCE_MS)
|
|
942
|
+
);
|
|
943
|
+
}
|
|
662
944
|
function startObserver(config) {
|
|
663
945
|
if (observer)
|
|
664
946
|
return;
|
|
@@ -667,8 +949,15 @@ function startObserver(config) {
|
|
|
667
949
|
for (const node of mutation.addedNodes) {
|
|
668
950
|
if (!(node instanceof Element))
|
|
669
951
|
continue;
|
|
670
|
-
|
|
671
|
-
|
|
952
|
+
if (node instanceof HTMLFormElement) {
|
|
953
|
+
void registerForm(node, config);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const parentForm = node.closest("form");
|
|
957
|
+
if (parentForm instanceof HTMLFormElement && registeredForms.has(parentForm) && isInterestingNode(node)) {
|
|
958
|
+
scheduleReAnalysis(parentForm, config);
|
|
959
|
+
}
|
|
960
|
+
for (const form of Array.from(node.querySelectorAll("form"))) {
|
|
672
961
|
void registerForm(form, config);
|
|
673
962
|
}
|
|
674
963
|
}
|