auto-webmcp 0.2.10 → 0.3.1
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/dist/analyzer.d.ts +7 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/auto-webmcp.cjs.js +169 -32
- package/dist/auto-webmcp.cjs.js.map +2 -2
- package/dist/auto-webmcp.esm.js +169 -32
- 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/interceptor.d.ts +14 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/registry.d.ts +7 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/auto-webmcp.esm.js
CHANGED
|
@@ -28,22 +28,21 @@ async function registerFormTool(form, metadata, execute) {
|
|
|
28
28
|
if (existing) {
|
|
29
29
|
await unregisterFormTool(form);
|
|
30
30
|
}
|
|
31
|
+
const toolDef = {
|
|
32
|
+
name: metadata.name,
|
|
33
|
+
description: metadata.description,
|
|
34
|
+
inputSchema: metadata.inputSchema,
|
|
35
|
+
execute
|
|
36
|
+
};
|
|
37
|
+
if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
|
|
38
|
+
toolDef.annotations = metadata.annotations;
|
|
39
|
+
}
|
|
31
40
|
try {
|
|
32
|
-
await navigator.modelContext.registerTool(
|
|
33
|
-
name: metadata.name,
|
|
34
|
-
description: metadata.description,
|
|
35
|
-
inputSchema: metadata.inputSchema,
|
|
36
|
-
execute
|
|
37
|
-
});
|
|
41
|
+
await navigator.modelContext.registerTool(toolDef);
|
|
38
42
|
} catch {
|
|
39
43
|
try {
|
|
40
44
|
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
|
-
});
|
|
45
|
+
await navigator.modelContext.registerTool(toolDef);
|
|
47
46
|
} catch {
|
|
48
47
|
}
|
|
49
48
|
}
|
|
@@ -321,7 +320,8 @@ function analyzeForm(form, override) {
|
|
|
321
320
|
const name = override?.name ?? inferToolName(form);
|
|
322
321
|
const description = override?.description ?? inferToolDescription(form);
|
|
323
322
|
const { schema: inputSchema, fieldElements } = buildSchema(form);
|
|
324
|
-
|
|
323
|
+
const annotations = inferAnnotations(form);
|
|
324
|
+
return { name, description, inputSchema, annotations, fieldElements };
|
|
325
325
|
}
|
|
326
326
|
function inferToolName(form) {
|
|
327
327
|
const nativeName = form.getAttribute("toolname");
|
|
@@ -418,17 +418,105 @@ function inferToolDescription(form) {
|
|
|
418
418
|
return pageTitle;
|
|
419
419
|
return "Submit form";
|
|
420
420
|
}
|
|
421
|
+
var READONLY_BUTTON_PATTERNS = /^(search|find|look|filter|browse|view|show|check|preview|get|fetch|retrieve|load)\b/i;
|
|
422
|
+
var DESTRUCTIVE_BUTTON_PATTERNS = /^(delete|remove|cancel|terminate|destroy|purge|revoke|unsubscribe|deactivate)\b/i;
|
|
423
|
+
var DESTRUCTIVE_URL_PATTERNS = /\/(delete|remove|cancel|destroy)\b/i;
|
|
424
|
+
function inferAnnotations(form) {
|
|
425
|
+
const annotations = {};
|
|
426
|
+
if (form.dataset["webmcpReadonly"] !== void 0) {
|
|
427
|
+
annotations.readOnlyHint = form.dataset["webmcpReadonly"] !== "false";
|
|
428
|
+
}
|
|
429
|
+
if (form.dataset["webmcpDestructive"] !== void 0) {
|
|
430
|
+
annotations.destructiveHint = form.dataset["webmcpDestructive"] !== "false";
|
|
431
|
+
}
|
|
432
|
+
if (form.dataset["webmcpIdempotent"] !== void 0) {
|
|
433
|
+
annotations.idempotentHint = form.dataset["webmcpIdempotent"] !== "false";
|
|
434
|
+
}
|
|
435
|
+
if (form.dataset["webmcpOpenworld"] !== void 0) {
|
|
436
|
+
annotations.openWorldHint = form.dataset["webmcpOpenworld"] !== "false";
|
|
437
|
+
}
|
|
438
|
+
if (annotations.readOnlyHint === void 0) {
|
|
439
|
+
const isGet = form.method.toLowerCase() === "get";
|
|
440
|
+
const submitText = getSubmitButtonText(form);
|
|
441
|
+
const isReadLabel = submitText ? READONLY_BUTTON_PATTERNS.test(submitText.trim()) : false;
|
|
442
|
+
if (isGet || isReadLabel)
|
|
443
|
+
annotations.readOnlyHint = true;
|
|
444
|
+
}
|
|
445
|
+
if (annotations.destructiveHint === void 0) {
|
|
446
|
+
const submitText = getSubmitButtonText(form);
|
|
447
|
+
const isDestructiveLabel = submitText ? DESTRUCTIVE_BUTTON_PATTERNS.test(submitText.trim()) : false;
|
|
448
|
+
const isDestructiveUrl = form.action ? DESTRUCTIVE_URL_PATTERNS.test(form.action) : false;
|
|
449
|
+
if (isDestructiveLabel || isDestructiveUrl)
|
|
450
|
+
annotations.destructiveHint = true;
|
|
451
|
+
}
|
|
452
|
+
if (annotations.idempotentHint === void 0) {
|
|
453
|
+
if (annotations.readOnlyHint === true || form.method.toLowerCase() === "get") {
|
|
454
|
+
annotations.idempotentHint = true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (annotations.openWorldHint === void 0) {
|
|
458
|
+
annotations.openWorldHint = annotations.readOnlyHint !== true;
|
|
459
|
+
}
|
|
460
|
+
const hasNonDefault = annotations.readOnlyHint === true || annotations.destructiveHint === true || annotations.idempotentHint === true || annotations.openWorldHint === false;
|
|
461
|
+
return hasNonDefault ? annotations : {};
|
|
462
|
+
}
|
|
463
|
+
function extractDefaultValue(control) {
|
|
464
|
+
if (control instanceof HTMLInputElement) {
|
|
465
|
+
const type = control.type.toLowerCase();
|
|
466
|
+
if (type === "checkbox")
|
|
467
|
+
return control.checked ? true : void 0;
|
|
468
|
+
if (type === "radio")
|
|
469
|
+
return void 0;
|
|
470
|
+
if (type === "number" || type === "range") {
|
|
471
|
+
return control.value !== "" ? parseFloat(control.value) : void 0;
|
|
472
|
+
}
|
|
473
|
+
return control.value !== "" ? control.value : void 0;
|
|
474
|
+
}
|
|
475
|
+
if (control instanceof HTMLTextAreaElement) {
|
|
476
|
+
return control.value !== "" ? control.value : void 0;
|
|
477
|
+
}
|
|
478
|
+
if (control instanceof HTMLSelectElement) {
|
|
479
|
+
if (control.multiple) {
|
|
480
|
+
const selected = Array.from(control.options).filter((o) => o.selected).map((o) => o.value);
|
|
481
|
+
return selected.length > 0 ? selected : void 0;
|
|
482
|
+
}
|
|
483
|
+
return control.value !== "" ? control.value : void 0;
|
|
484
|
+
}
|
|
485
|
+
return void 0;
|
|
486
|
+
}
|
|
487
|
+
function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
|
|
488
|
+
if (visited.has(root))
|
|
489
|
+
return [];
|
|
490
|
+
visited.add(root);
|
|
491
|
+
const results = [];
|
|
492
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
493
|
+
if (el.shadowRoot) {
|
|
494
|
+
results.push(
|
|
495
|
+
...Array.from(
|
|
496
|
+
el.shadowRoot.querySelectorAll(
|
|
497
|
+
"input, textarea, select"
|
|
498
|
+
)
|
|
499
|
+
),
|
|
500
|
+
...collectShadowControls(el.shadowRoot, visited)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return results;
|
|
505
|
+
}
|
|
421
506
|
function buildSchema(form) {
|
|
422
507
|
const properties = {};
|
|
423
508
|
const required = [];
|
|
424
509
|
const fieldElements = /* @__PURE__ */ new Map();
|
|
425
510
|
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
426
511
|
const processedCheckboxGroups = /* @__PURE__ */ new Set();
|
|
427
|
-
const controls =
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
512
|
+
const controls = [
|
|
513
|
+
...Array.from(
|
|
514
|
+
form.querySelectorAll(
|
|
515
|
+
"input, textarea, select"
|
|
516
|
+
)
|
|
517
|
+
),
|
|
518
|
+
...collectShadowControls(form)
|
|
519
|
+
];
|
|
432
520
|
for (const control of controls) {
|
|
433
521
|
const name = control.name;
|
|
434
522
|
const fieldKey = name || resolveNativeControlFallbackKey(control);
|
|
@@ -453,11 +541,19 @@ function buildSchema(form) {
|
|
|
453
541
|
const desc = inferFieldDescription(control);
|
|
454
542
|
if (desc)
|
|
455
543
|
schemaProp.description = desc;
|
|
544
|
+
const defaultVal = extractDefaultValue(control);
|
|
545
|
+
if (defaultVal !== void 0)
|
|
546
|
+
schemaProp.default = defaultVal;
|
|
456
547
|
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
457
548
|
schemaProp.enum = collectRadioEnum(form, fieldKey);
|
|
458
549
|
const radioOneOf = collectRadioOneOf(form, fieldKey);
|
|
459
550
|
if (radioOneOf.length > 0)
|
|
460
551
|
schemaProp.oneOf = radioOneOf;
|
|
552
|
+
const checkedRadio = form.querySelector(
|
|
553
|
+
`input[type="radio"][name="${CSS.escape(fieldKey)}"]:checked`
|
|
554
|
+
);
|
|
555
|
+
if (checkedRadio?.value)
|
|
556
|
+
schemaProp.default = checkedRadio.value;
|
|
461
557
|
}
|
|
462
558
|
if (control instanceof HTMLInputElement && control.type === "checkbox") {
|
|
463
559
|
const checkboxValues = collectCheckboxEnum(form, fieldKey);
|
|
@@ -469,6 +565,13 @@ function buildSchema(form) {
|
|
|
469
565
|
};
|
|
470
566
|
if (schemaProp.description)
|
|
471
567
|
arrayProp.description = schemaProp.description;
|
|
568
|
+
const checkedBoxes = Array.from(
|
|
569
|
+
form.querySelectorAll(
|
|
570
|
+
`input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
|
|
571
|
+
)
|
|
572
|
+
).map((b) => b.value);
|
|
573
|
+
if (checkedBoxes.length > 0)
|
|
574
|
+
arrayProp.default = checkedBoxes;
|
|
472
575
|
properties[fieldKey] = arrayProp;
|
|
473
576
|
if (control.required)
|
|
474
577
|
required.push(fieldKey);
|
|
@@ -838,6 +941,9 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
838
941
|
return async (params) => {
|
|
839
942
|
pendingFillWarnings.set(form, []);
|
|
840
943
|
fillFormFields(form, params);
|
|
944
|
+
const missingNow = getMissingRequired(metadata, params);
|
|
945
|
+
if (missingNow.length > 0)
|
|
946
|
+
pendingWarnings.set(form, missingNow);
|
|
841
947
|
window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
|
|
842
948
|
return new Promise((resolve, reject) => {
|
|
843
949
|
pendingExecutions.set(form, { resolve, reject });
|
|
@@ -864,9 +970,10 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
864
970
|
attachSubmitInterceptor(submitForm, toolName);
|
|
865
971
|
}
|
|
866
972
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
pendingWarnings.
|
|
973
|
+
if (submitForm !== form && pendingWarnings.has(form)) {
|
|
974
|
+
pendingWarnings.set(submitForm, pendingWarnings.get(form));
|
|
975
|
+
pendingWarnings.delete(form);
|
|
976
|
+
}
|
|
870
977
|
submitForm.requestSubmit();
|
|
871
978
|
} catch (err) {
|
|
872
979
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -888,17 +995,37 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
888
995
|
pendingExecutions.delete(form);
|
|
889
996
|
const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
|
|
890
997
|
lastFilledSnapshot.delete(form);
|
|
891
|
-
const
|
|
998
|
+
const missingRequired = pendingWarnings.get(form) ?? [];
|
|
892
999
|
pendingWarnings.delete(form);
|
|
893
1000
|
const fillWarnings = pendingFillWarnings.get(form) ?? [];
|
|
894
1001
|
pendingFillWarnings.delete(form);
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
1002
|
+
const skippedFields = fillWarnings.filter((w) => w.type === "not_filled").map((w) => w.field);
|
|
1003
|
+
const structured = {
|
|
1004
|
+
status: missingRequired.length > 0 || skippedFields.length > 0 ? "partial" : "success",
|
|
1005
|
+
filled_fields: formData,
|
|
1006
|
+
skipped_fields: skippedFields,
|
|
1007
|
+
missing_required: missingRequired,
|
|
1008
|
+
warnings: [
|
|
1009
|
+
...missingRequired.map((f) => ({
|
|
1010
|
+
field: f,
|
|
1011
|
+
type: "missing_required",
|
|
1012
|
+
message: `required field "${f}" was not provided`
|
|
1013
|
+
})),
|
|
1014
|
+
...fillWarnings
|
|
1015
|
+
]
|
|
1016
|
+
};
|
|
1017
|
+
const allWarnMessages = [
|
|
1018
|
+
...missingRequired.length ? [`required fields were not filled: ${missingRequired.join(", ")}`] : [],
|
|
1019
|
+
...fillWarnings.map((w) => w.message)
|
|
898
1020
|
];
|
|
899
|
-
const warningText =
|
|
1021
|
+
const warningText = allWarnMessages.length ? ` Note: ${allWarnMessages.join("; ")}.` : "";
|
|
900
1022
|
const text = `Form submitted. Fields: ${JSON.stringify(formData)}${warningText}`;
|
|
901
|
-
const result = {
|
|
1023
|
+
const result = {
|
|
1024
|
+
content: [
|
|
1025
|
+
{ type: "text", text },
|
|
1026
|
+
{ type: "text", text: JSON.stringify(structured) }
|
|
1027
|
+
]
|
|
1028
|
+
};
|
|
902
1029
|
if (e.agentInvoked && typeof e.respondWith === "function") {
|
|
903
1030
|
e.preventDefault();
|
|
904
1031
|
e.respondWith(Promise.resolve(result));
|
|
@@ -1032,16 +1159,26 @@ function fillInput(input, form, key, value) {
|
|
|
1032
1159
|
const raw = String(value ?? "");
|
|
1033
1160
|
const num = Number(raw);
|
|
1034
1161
|
if (raw === "" || isNaN(num)) {
|
|
1035
|
-
pendingFillWarnings.get(form)?.push(
|
|
1162
|
+
pendingFillWarnings.get(form)?.push({
|
|
1163
|
+
field: key,
|
|
1164
|
+
type: "type_mismatch",
|
|
1165
|
+
message: `"${key}" expects a number, got: ${JSON.stringify(value)}`,
|
|
1166
|
+
original: value
|
|
1167
|
+
});
|
|
1036
1168
|
return;
|
|
1037
1169
|
}
|
|
1038
1170
|
const min = input.min !== "" ? parseFloat(input.min) : -Infinity;
|
|
1039
1171
|
const max = input.max !== "" ? parseFloat(input.max) : Infinity;
|
|
1040
1172
|
if (num < min || num > max) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1173
|
+
const clamped = Math.min(Math.max(num, min), max);
|
|
1174
|
+
pendingFillWarnings.get(form)?.push({
|
|
1175
|
+
field: key,
|
|
1176
|
+
type: "clamped",
|
|
1177
|
+
message: `"${key}" value ${num} is outside allowed range [${input.min || "?"}, ${input.max || "?"}], clamped to ${clamped}`,
|
|
1178
|
+
original: num,
|
|
1179
|
+
actual: clamped
|
|
1180
|
+
});
|
|
1181
|
+
input.value = String(clamped);
|
|
1045
1182
|
} else {
|
|
1046
1183
|
input.value = String(num);
|
|
1047
1184
|
}
|