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.
- 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 +336 -48
- package/dist/auto-webmcp.cjs.js.map +2 -2
- package/dist/auto-webmcp.esm.js +336 -48
- 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/registry.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
|
@@ -40,12 +40,25 @@ async function registerFormTool(form, metadata, execute) {
|
|
|
40
40
|
if (existing) {
|
|
41
41
|
await unregisterFormTool(form);
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
try {
|
|
44
|
+
await navigator.modelContext.registerTool({
|
|
45
|
+
name: metadata.name,
|
|
46
|
+
description: metadata.description,
|
|
47
|
+
inputSchema: metadata.inputSchema,
|
|
48
|
+
execute
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
try {
|
|
52
|
+
await navigator.modelContext.unregisterTool(metadata.name);
|
|
53
|
+
await navigator.modelContext.registerTool({
|
|
54
|
+
name: metadata.name,
|
|
55
|
+
description: metadata.description,
|
|
56
|
+
inputSchema: metadata.inputSchema,
|
|
57
|
+
execute
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
49
62
|
registeredTools.set(form, metadata.name);
|
|
50
63
|
}
|
|
51
64
|
async function unregisterFormTool(form) {
|
|
@@ -97,6 +110,16 @@ function resolveConfig(userConfig) {
|
|
|
97
110
|
}
|
|
98
111
|
|
|
99
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
|
+
];
|
|
100
123
|
function inputTypeToSchema(input) {
|
|
101
124
|
if (input instanceof HTMLInputElement) {
|
|
102
125
|
return mapInputElement(input);
|
|
@@ -192,6 +215,49 @@ function collectRadioOneOf(form, name) {
|
|
|
192
215
|
return { const: r.value, title: title || r.value };
|
|
193
216
|
});
|
|
194
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
|
+
}
|
|
195
261
|
function getRadioLabelText(radio) {
|
|
196
262
|
const parent = radio.closest("label");
|
|
197
263
|
if (parent) {
|
|
@@ -217,8 +283,8 @@ var formIndex = 0;
|
|
|
217
283
|
function analyzeForm(form, override) {
|
|
218
284
|
const name = override?.name ?? inferToolName(form);
|
|
219
285
|
const description = override?.description ?? inferToolDescription(form);
|
|
220
|
-
const inputSchema = buildSchema(form);
|
|
221
|
-
return { name, description, inputSchema };
|
|
286
|
+
const { schema: inputSchema, fieldElements } = buildSchema(form);
|
|
287
|
+
return { name, description, inputSchema, fieldElements };
|
|
222
288
|
}
|
|
223
289
|
function inferToolName(form) {
|
|
224
290
|
const nativeName = form.getAttribute("toolname");
|
|
@@ -318,6 +384,7 @@ function inferToolDescription(form) {
|
|
|
318
384
|
function buildSchema(form) {
|
|
319
385
|
const properties = {};
|
|
320
386
|
const required = [];
|
|
387
|
+
const fieldElements = /* @__PURE__ */ new Map();
|
|
321
388
|
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
322
389
|
const controls = Array.from(
|
|
323
390
|
form.querySelectorAll(
|
|
@@ -326,12 +393,13 @@ function buildSchema(form) {
|
|
|
326
393
|
);
|
|
327
394
|
for (const control of controls) {
|
|
328
395
|
const name = control.name;
|
|
329
|
-
|
|
396
|
+
const fieldKey = name || resolveNativeControlFallbackKey(control);
|
|
397
|
+
if (!fieldKey)
|
|
330
398
|
continue;
|
|
331
399
|
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
332
|
-
if (processedRadioGroups.has(
|
|
400
|
+
if (processedRadioGroups.has(fieldKey))
|
|
333
401
|
continue;
|
|
334
|
-
processedRadioGroups.add(
|
|
402
|
+
processedRadioGroups.add(fieldKey);
|
|
335
403
|
}
|
|
336
404
|
const schemaProp = inputTypeToSchema(control);
|
|
337
405
|
if (!schemaProp)
|
|
@@ -341,17 +409,123 @@ function buildSchema(form) {
|
|
|
341
409
|
if (desc)
|
|
342
410
|
schemaProp.description = desc;
|
|
343
411
|
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
344
|
-
schemaProp.enum = collectRadioEnum(form,
|
|
345
|
-
const radioOneOf = collectRadioOneOf(form,
|
|
412
|
+
schemaProp.enum = collectRadioEnum(form, fieldKey);
|
|
413
|
+
const radioOneOf = collectRadioOneOf(form, fieldKey);
|
|
346
414
|
if (radioOneOf.length > 0)
|
|
347
415
|
schemaProp.oneOf = radioOneOf;
|
|
348
416
|
}
|
|
349
|
-
properties[
|
|
417
|
+
properties[fieldKey] = schemaProp;
|
|
418
|
+
if (!name) {
|
|
419
|
+
fieldElements.set(fieldKey, control);
|
|
420
|
+
}
|
|
350
421
|
if (control.required) {
|
|
351
|
-
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);
|
|
352
444
|
}
|
|
353
445
|
}
|
|
354
|
-
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 "";
|
|
355
529
|
}
|
|
356
530
|
function inferFieldTitle(control) {
|
|
357
531
|
if ("dataset" in control && control.dataset["webmcpTitle"]) {
|
|
@@ -420,7 +594,15 @@ init_registry();
|
|
|
420
594
|
|
|
421
595
|
// src/interceptor.ts
|
|
422
596
|
var pendingExecutions = /* @__PURE__ */ new WeakMap();
|
|
423
|
-
|
|
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
|
+
}
|
|
424
606
|
attachSubmitInterceptor(form, toolName);
|
|
425
607
|
return async (params) => {
|
|
426
608
|
fillFormFields(form, params);
|
|
@@ -443,7 +625,7 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
443
625
|
return;
|
|
444
626
|
const { resolve } = pending;
|
|
445
627
|
pendingExecutions.delete(form);
|
|
446
|
-
const formData = serializeFormData(form);
|
|
628
|
+
const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
|
|
447
629
|
const text = JSON.stringify(formData);
|
|
448
630
|
const result = { content: [{ type: "text", text }] };
|
|
449
631
|
if (e.agentInvoked && typeof e.respondWith === "function") {
|
|
@@ -456,52 +638,98 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
456
638
|
window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
|
|
457
639
|
});
|
|
458
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
|
+
}
|
|
459
665
|
function fillFormFields(form, params) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
)
|
|
465
|
-
|
|
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
|
+
}
|
|
466
679
|
continue;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
472
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
473
|
-
} else if (input instanceof HTMLSelectElement) {
|
|
474
|
-
input.value = String(value ?? "");
|
|
475
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
680
|
+
}
|
|
681
|
+
const ariaEl = fieldEls?.get(key);
|
|
682
|
+
if (ariaEl) {
|
|
683
|
+
fillAriaField(ariaEl, value);
|
|
476
684
|
}
|
|
477
685
|
}
|
|
478
686
|
}
|
|
479
|
-
function fillInput(input, form,
|
|
687
|
+
function fillInput(input, form, key, value) {
|
|
480
688
|
const type = input.type.toLowerCase();
|
|
481
689
|
if (type === "checkbox") {
|
|
482
|
-
input
|
|
483
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
690
|
+
setReactChecked(input, Boolean(value));
|
|
484
691
|
return;
|
|
485
692
|
}
|
|
486
693
|
if (type === "radio") {
|
|
487
|
-
const
|
|
694
|
+
const esc = CSS.escape(key);
|
|
488
695
|
const radios = form.querySelectorAll(
|
|
489
|
-
`input[type="radio"][name="${
|
|
696
|
+
`input[type="radio"][name="${esc}"]`
|
|
490
697
|
);
|
|
491
698
|
for (const radio of radios) {
|
|
492
699
|
if (radio.value === String(value)) {
|
|
493
|
-
|
|
700
|
+
if (_checkedSetter) {
|
|
701
|
+
_checkedSetter.call(radio, true);
|
|
702
|
+
} else {
|
|
703
|
+
radio.checked = true;
|
|
704
|
+
}
|
|
494
705
|
radio.dispatchEvent(new Event("change", { bubbles: true }));
|
|
495
706
|
break;
|
|
496
707
|
}
|
|
497
708
|
}
|
|
498
709
|
return;
|
|
499
710
|
}
|
|
500
|
-
input
|
|
501
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
502
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
711
|
+
setReactValue(input, String(value ?? ""));
|
|
503
712
|
}
|
|
504
|
-
function
|
|
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 }));
|
|
731
|
+
}
|
|
732
|
+
function serializeFormData(form, params, fieldEls) {
|
|
505
733
|
const result = {};
|
|
506
734
|
const data = new FormData(form);
|
|
507
735
|
for (const [key, val] of data.entries()) {
|
|
@@ -516,6 +744,27 @@ function serializeFormData(form) {
|
|
|
516
744
|
result[key] = val;
|
|
517
745
|
}
|
|
518
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
|
+
}
|
|
519
768
|
return result;
|
|
520
769
|
}
|
|
521
770
|
|
|
@@ -627,8 +876,9 @@ async function registerForm(form, config) {
|
|
|
627
876
|
if (config.debug) {
|
|
628
877
|
warnToolQuality(metadata.name, metadata.description);
|
|
629
878
|
}
|
|
630
|
-
const execute = buildExecuteHandler(form, config, metadata.name);
|
|
879
|
+
const execute = buildExecuteHandler(form, config, metadata.name, metadata);
|
|
631
880
|
await registerFormTool(form, metadata, execute);
|
|
881
|
+
registeredForms.add(form);
|
|
632
882
|
if (config.debug) {
|
|
633
883
|
console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
|
|
634
884
|
}
|
|
@@ -640,12 +890,43 @@ async function unregisterForm(form, config) {
|
|
|
640
890
|
if (!name)
|
|
641
891
|
return;
|
|
642
892
|
await unregisterFormTool(form);
|
|
893
|
+
registeredForms.delete(form);
|
|
643
894
|
if (config.debug) {
|
|
644
895
|
console.debug(`[auto-webmcp] Unregistered: ${name}`);
|
|
645
896
|
}
|
|
646
897
|
emit("form:unregistered", form, name);
|
|
647
898
|
}
|
|
648
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
|
+
}
|
|
649
930
|
function startObserver(config) {
|
|
650
931
|
if (observer)
|
|
651
932
|
return;
|
|
@@ -654,8 +935,15 @@ function startObserver(config) {
|
|
|
654
935
|
for (const node of mutation.addedNodes) {
|
|
655
936
|
if (!(node instanceof Element))
|
|
656
937
|
continue;
|
|
657
|
-
|
|
658
|
-
|
|
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"))) {
|
|
659
947
|
void registerForm(form, config);
|
|
660
948
|
}
|
|
661
949
|
}
|
|
@@ -689,7 +977,7 @@ function listenForRouteChanges(config) {
|
|
|
689
977
|
}
|
|
690
978
|
async function scanForms(config) {
|
|
691
979
|
const forms = Array.from(document.querySelectorAll("form"));
|
|
692
|
-
await Promise.
|
|
980
|
+
await Promise.allSettled(forms.map((form) => registerForm(form, config)));
|
|
693
981
|
}
|
|
694
982
|
function warnToolQuality(name, description) {
|
|
695
983
|
if (/^form_\d+$|^submit$|^form$/.test(name)) {
|
|
@@ -708,9 +996,9 @@ async function startDiscovery(config) {
|
|
|
708
996
|
(resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true })
|
|
709
997
|
);
|
|
710
998
|
}
|
|
711
|
-
await scanForms(config);
|
|
712
999
|
startObserver(config);
|
|
713
1000
|
listenForRouteChanges(config);
|
|
1001
|
+
await scanForms(config);
|
|
714
1002
|
}
|
|
715
1003
|
function stopDiscovery() {
|
|
716
1004
|
observer?.disconnect();
|