auto-webmcp 0.2.1 → 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.
- 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 +315 -40
- package/dist/auto-webmcp.cjs.js.map +2 -2
- package/dist/auto-webmcp.esm.js +315 -40
- 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);
|
|
365
423
|
}
|
|
366
424
|
}
|
|
367
|
-
|
|
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);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
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,7 +625,7 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
456
625
|
return;
|
|
457
626
|
const { resolve } = pending;
|
|
458
627
|
pendingExecutions.delete(form);
|
|
459
|
-
const formData = serializeFormData(form);
|
|
628
|
+
const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
|
|
460
629
|
const text = JSON.stringify(formData);
|
|
461
630
|
const result = { content: [{ type: "text", text }] };
|
|
462
631
|
if (e.agentInvoked && typeof e.respondWith === "function") {
|
|
@@ -469,52 +638,98 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
469
638
|
window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
|
|
470
639
|
});
|
|
471
640
|
}
|
|
641
|
+
function setReactValue(el, v) {
|
|
642
|
+
const setter = el instanceof HTMLTextAreaElement ? _textareaValueSetter : _inputValueSetter;
|
|
643
|
+
if (setter) {
|
|
644
|
+
setter.call(el, v);
|
|
645
|
+
} else {
|
|
646
|
+
el.value = v;
|
|
647
|
+
}
|
|
648
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
649
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
650
|
+
}
|
|
651
|
+
function setReactChecked(el, checked) {
|
|
652
|
+
if (_checkedSetter) {
|
|
653
|
+
_checkedSetter.call(el, checked);
|
|
654
|
+
} else {
|
|
655
|
+
el.checked = checked;
|
|
656
|
+
}
|
|
657
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
658
|
+
}
|
|
659
|
+
function findNativeField(form, key) {
|
|
660
|
+
const esc = CSS.escape(key);
|
|
661
|
+
return form.querySelector(`[name="${esc}"]`) ?? form.querySelector(
|
|
662
|
+
`input#${esc}, textarea#${esc}, select#${esc}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
472
665
|
function fillFormFields(form, params) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
)
|
|
478
|
-
|
|
666
|
+
lastParams.set(form, params);
|
|
667
|
+
const fieldEls = formFieldElements.get(form);
|
|
668
|
+
for (const [key, value] of Object.entries(params)) {
|
|
669
|
+
const input = findNativeField(form, key);
|
|
670
|
+
if (input) {
|
|
671
|
+
if (input instanceof HTMLInputElement) {
|
|
672
|
+
fillInput(input, form, key, value);
|
|
673
|
+
} else if (input instanceof HTMLTextAreaElement) {
|
|
674
|
+
setReactValue(input, String(value ?? ""));
|
|
675
|
+
} else if (input instanceof HTMLSelectElement) {
|
|
676
|
+
input.value = String(value ?? "");
|
|
677
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
678
|
+
}
|
|
479
679
|
continue;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
485
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
486
|
-
} else if (input instanceof HTMLSelectElement) {
|
|
487
|
-
input.value = String(value ?? "");
|
|
488
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
680
|
+
}
|
|
681
|
+
const ariaEl = fieldEls?.get(key);
|
|
682
|
+
if (ariaEl) {
|
|
683
|
+
fillAriaField(ariaEl, value);
|
|
489
684
|
}
|
|
490
685
|
}
|
|
491
686
|
}
|
|
492
|
-
function fillInput(input, form,
|
|
687
|
+
function fillInput(input, form, key, value) {
|
|
493
688
|
const type = input.type.toLowerCase();
|
|
494
689
|
if (type === "checkbox") {
|
|
495
|
-
input
|
|
496
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
690
|
+
setReactChecked(input, Boolean(value));
|
|
497
691
|
return;
|
|
498
692
|
}
|
|
499
693
|
if (type === "radio") {
|
|
500
|
-
const
|
|
694
|
+
const esc = CSS.escape(key);
|
|
501
695
|
const radios = form.querySelectorAll(
|
|
502
|
-
`input[type="radio"][name="${
|
|
696
|
+
`input[type="radio"][name="${esc}"]`
|
|
503
697
|
);
|
|
504
698
|
for (const radio of radios) {
|
|
505
699
|
if (radio.value === String(value)) {
|
|
506
|
-
|
|
700
|
+
if (_checkedSetter) {
|
|
701
|
+
_checkedSetter.call(radio, true);
|
|
702
|
+
} else {
|
|
703
|
+
radio.checked = true;
|
|
704
|
+
}
|
|
507
705
|
radio.dispatchEvent(new Event("change", { bubbles: true }));
|
|
508
706
|
break;
|
|
509
707
|
}
|
|
510
708
|
}
|
|
511
709
|
return;
|
|
512
710
|
}
|
|
513
|
-
input
|
|
514
|
-
|
|
515
|
-
|
|
711
|
+
setReactValue(input, String(value ?? ""));
|
|
712
|
+
}
|
|
713
|
+
function fillAriaField(el, value) {
|
|
714
|
+
const role = el.getAttribute("role");
|
|
715
|
+
if (role === "checkbox" || role === "switch") {
|
|
716
|
+
el.setAttribute("aria-checked", String(Boolean(value)));
|
|
717
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (role === "radio") {
|
|
721
|
+
el.setAttribute("aria-checked", "true");
|
|
722
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const htmlEl = el;
|
|
726
|
+
if (htmlEl.isContentEditable) {
|
|
727
|
+
htmlEl.textContent = String(value ?? "");
|
|
728
|
+
}
|
|
729
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
730
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
516
731
|
}
|
|
517
|
-
function serializeFormData(form) {
|
|
732
|
+
function serializeFormData(form, params, fieldEls) {
|
|
518
733
|
const result = {};
|
|
519
734
|
const data = new FormData(form);
|
|
520
735
|
for (const [key, val] of data.entries()) {
|
|
@@ -529,6 +744,27 @@ function serializeFormData(form) {
|
|
|
529
744
|
result[key] = val;
|
|
530
745
|
}
|
|
531
746
|
}
|
|
747
|
+
if (params) {
|
|
748
|
+
for (const key of Object.keys(params)) {
|
|
749
|
+
if (key in result)
|
|
750
|
+
continue;
|
|
751
|
+
const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
|
|
752
|
+
if (!el)
|
|
753
|
+
continue;
|
|
754
|
+
if (el instanceof HTMLInputElement && el.type === "checkbox") {
|
|
755
|
+
result[key] = el.checked;
|
|
756
|
+
} else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
757
|
+
result[key] = el.value;
|
|
758
|
+
} else {
|
|
759
|
+
const role = el.getAttribute("role");
|
|
760
|
+
if (role === "checkbox" || role === "switch") {
|
|
761
|
+
result[key] = el.getAttribute("aria-checked") === "true";
|
|
762
|
+
} else {
|
|
763
|
+
result[key] = el.textContent?.trim() ?? "";
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
532
768
|
return result;
|
|
533
769
|
}
|
|
534
770
|
|
|
@@ -640,8 +876,9 @@ async function registerForm(form, config) {
|
|
|
640
876
|
if (config.debug) {
|
|
641
877
|
warnToolQuality(metadata.name, metadata.description);
|
|
642
878
|
}
|
|
643
|
-
const execute = buildExecuteHandler(form, config, metadata.name);
|
|
879
|
+
const execute = buildExecuteHandler(form, config, metadata.name, metadata);
|
|
644
880
|
await registerFormTool(form, metadata, execute);
|
|
881
|
+
registeredForms.add(form);
|
|
645
882
|
if (config.debug) {
|
|
646
883
|
console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
|
|
647
884
|
}
|
|
@@ -653,12 +890,43 @@ async function unregisterForm(form, config) {
|
|
|
653
890
|
if (!name)
|
|
654
891
|
return;
|
|
655
892
|
await unregisterFormTool(form);
|
|
893
|
+
registeredForms.delete(form);
|
|
656
894
|
if (config.debug) {
|
|
657
895
|
console.debug(`[auto-webmcp] Unregistered: ${name}`);
|
|
658
896
|
}
|
|
659
897
|
emit("form:unregistered", form, name);
|
|
660
898
|
}
|
|
661
899
|
var observer = null;
|
|
900
|
+
var registeredForms = /* @__PURE__ */ new WeakSet();
|
|
901
|
+
var reAnalysisTimers = /* @__PURE__ */ new Map();
|
|
902
|
+
var RE_ANALYSIS_DEBOUNCE_MS = 300;
|
|
903
|
+
function isInterestingNode(node) {
|
|
904
|
+
const tag = node.tagName.toLowerCase();
|
|
905
|
+
if (tag === "input" || tag === "textarea" || tag === "select")
|
|
906
|
+
return true;
|
|
907
|
+
const role = node.getAttribute("role");
|
|
908
|
+
if (role && ARIA_ROLES_TO_SCAN.includes(role))
|
|
909
|
+
return true;
|
|
910
|
+
if (node.querySelector("input, textarea, select"))
|
|
911
|
+
return true;
|
|
912
|
+
for (const r of ARIA_ROLES_TO_SCAN) {
|
|
913
|
+
if (node.querySelector(`[role="${r}"]`))
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
function scheduleReAnalysis(form, config) {
|
|
919
|
+
const existing = reAnalysisTimers.get(form);
|
|
920
|
+
if (existing)
|
|
921
|
+
clearTimeout(existing);
|
|
922
|
+
reAnalysisTimers.set(
|
|
923
|
+
form,
|
|
924
|
+
setTimeout(() => {
|
|
925
|
+
reAnalysisTimers.delete(form);
|
|
926
|
+
void registerForm(form, config);
|
|
927
|
+
}, RE_ANALYSIS_DEBOUNCE_MS)
|
|
928
|
+
);
|
|
929
|
+
}
|
|
662
930
|
function startObserver(config) {
|
|
663
931
|
if (observer)
|
|
664
932
|
return;
|
|
@@ -667,8 +935,15 @@ function startObserver(config) {
|
|
|
667
935
|
for (const node of mutation.addedNodes) {
|
|
668
936
|
if (!(node instanceof Element))
|
|
669
937
|
continue;
|
|
670
|
-
|
|
671
|
-
|
|
938
|
+
if (node instanceof HTMLFormElement) {
|
|
939
|
+
void registerForm(node, config);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const parentForm = node.closest("form");
|
|
943
|
+
if (parentForm instanceof HTMLFormElement && registeredForms.has(parentForm) && isInterestingNode(node)) {
|
|
944
|
+
scheduleReAnalysis(parentForm, config);
|
|
945
|
+
}
|
|
946
|
+
for (const form of Array.from(node.querySelectorAll("form"))) {
|
|
672
947
|
void registerForm(form, config);
|
|
673
948
|
}
|
|
674
949
|
}
|