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.
@@ -3,10 +3,17 @@
3
3
  */
4
4
  import { JsonSchema } from './schema.js';
5
5
  import { FormOverride } from './config.js';
6
+ export interface ToolAnnotations {
7
+ readOnlyHint?: boolean;
8
+ destructiveHint?: boolean;
9
+ idempotentHint?: boolean;
10
+ openWorldHint?: boolean;
11
+ }
6
12
  export interface ToolMetadata {
7
13
  name: string;
8
14
  description: string;
9
15
  inputSchema: JsonSchema;
16
+ annotations?: ToolAnnotations;
10
17
  /** Key → DOM element for fields not addressable by name (id-keyed or ARIA-role controls). */
11
18
  fieldElements?: Map<string, Element>;
12
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAmJ,MAAM,aAAa,CAAC;AAC1L,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;AAsgBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,CAAC,EACzE,SAAS,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,GACrD,YAAY,CAKd"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAmJ,MAAM,aAAa,CAAC;AAC1L,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,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,CAOxF;AAipBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,CAAC,EACzE,SAAS,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,GACrD,YAAY,CAKd"}
@@ -40,22 +40,21 @@ async function registerFormTool(form, metadata, execute) {
40
40
  if (existing) {
41
41
  await unregisterFormTool(form);
42
42
  }
43
+ const toolDef = {
44
+ name: metadata.name,
45
+ description: metadata.description,
46
+ inputSchema: metadata.inputSchema,
47
+ execute
48
+ };
49
+ if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
50
+ toolDef.annotations = metadata.annotations;
51
+ }
43
52
  try {
44
- await navigator.modelContext.registerTool({
45
- name: metadata.name,
46
- description: metadata.description,
47
- inputSchema: metadata.inputSchema,
48
- execute
49
- });
53
+ await navigator.modelContext.registerTool(toolDef);
50
54
  } catch {
51
55
  try {
52
56
  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
- });
57
+ await navigator.modelContext.registerTool(toolDef);
59
58
  } catch {
60
59
  }
61
60
  }
@@ -340,7 +339,8 @@ function analyzeForm(form, override) {
340
339
  const name = override?.name ?? inferToolName(form);
341
340
  const description = override?.description ?? inferToolDescription(form);
342
341
  const { schema: inputSchema, fieldElements } = buildSchema(form);
343
- return { name, description, inputSchema, fieldElements };
342
+ const annotations = inferAnnotations(form);
343
+ return { name, description, inputSchema, annotations, fieldElements };
344
344
  }
345
345
  function inferToolName(form) {
346
346
  const nativeName = form.getAttribute("toolname");
@@ -437,17 +437,105 @@ function inferToolDescription(form) {
437
437
  return pageTitle;
438
438
  return "Submit form";
439
439
  }
440
+ var READONLY_BUTTON_PATTERNS = /^(search|find|look|filter|browse|view|show|check|preview|get|fetch|retrieve|load)\b/i;
441
+ var DESTRUCTIVE_BUTTON_PATTERNS = /^(delete|remove|cancel|terminate|destroy|purge|revoke|unsubscribe|deactivate)\b/i;
442
+ var DESTRUCTIVE_URL_PATTERNS = /\/(delete|remove|cancel|destroy)\b/i;
443
+ function inferAnnotations(form) {
444
+ const annotations = {};
445
+ if (form.dataset["webmcpReadonly"] !== void 0) {
446
+ annotations.readOnlyHint = form.dataset["webmcpReadonly"] !== "false";
447
+ }
448
+ if (form.dataset["webmcpDestructive"] !== void 0) {
449
+ annotations.destructiveHint = form.dataset["webmcpDestructive"] !== "false";
450
+ }
451
+ if (form.dataset["webmcpIdempotent"] !== void 0) {
452
+ annotations.idempotentHint = form.dataset["webmcpIdempotent"] !== "false";
453
+ }
454
+ if (form.dataset["webmcpOpenworld"] !== void 0) {
455
+ annotations.openWorldHint = form.dataset["webmcpOpenworld"] !== "false";
456
+ }
457
+ if (annotations.readOnlyHint === void 0) {
458
+ const isGet = form.method.toLowerCase() === "get";
459
+ const submitText = getSubmitButtonText(form);
460
+ const isReadLabel = submitText ? READONLY_BUTTON_PATTERNS.test(submitText.trim()) : false;
461
+ if (isGet || isReadLabel)
462
+ annotations.readOnlyHint = true;
463
+ }
464
+ if (annotations.destructiveHint === void 0) {
465
+ const submitText = getSubmitButtonText(form);
466
+ const isDestructiveLabel = submitText ? DESTRUCTIVE_BUTTON_PATTERNS.test(submitText.trim()) : false;
467
+ const isDestructiveUrl = form.action ? DESTRUCTIVE_URL_PATTERNS.test(form.action) : false;
468
+ if (isDestructiveLabel || isDestructiveUrl)
469
+ annotations.destructiveHint = true;
470
+ }
471
+ if (annotations.idempotentHint === void 0) {
472
+ if (annotations.readOnlyHint === true || form.method.toLowerCase() === "get") {
473
+ annotations.idempotentHint = true;
474
+ }
475
+ }
476
+ if (annotations.openWorldHint === void 0) {
477
+ annotations.openWorldHint = annotations.readOnlyHint !== true;
478
+ }
479
+ const hasNonDefault = annotations.readOnlyHint === true || annotations.destructiveHint === true || annotations.idempotentHint === true || annotations.openWorldHint === false;
480
+ return hasNonDefault ? annotations : {};
481
+ }
482
+ function extractDefaultValue(control) {
483
+ if (control instanceof HTMLInputElement) {
484
+ const type = control.type.toLowerCase();
485
+ if (type === "checkbox")
486
+ return control.checked ? true : void 0;
487
+ if (type === "radio")
488
+ return void 0;
489
+ if (type === "number" || type === "range") {
490
+ return control.value !== "" ? parseFloat(control.value) : void 0;
491
+ }
492
+ return control.value !== "" ? control.value : void 0;
493
+ }
494
+ if (control instanceof HTMLTextAreaElement) {
495
+ return control.value !== "" ? control.value : void 0;
496
+ }
497
+ if (control instanceof HTMLSelectElement) {
498
+ if (control.multiple) {
499
+ const selected = Array.from(control.options).filter((o) => o.selected).map((o) => o.value);
500
+ return selected.length > 0 ? selected : void 0;
501
+ }
502
+ return control.value !== "" ? control.value : void 0;
503
+ }
504
+ return void 0;
505
+ }
506
+ function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
507
+ if (visited.has(root))
508
+ return [];
509
+ visited.add(root);
510
+ const results = [];
511
+ for (const el of Array.from(root.querySelectorAll("*"))) {
512
+ if (el.shadowRoot) {
513
+ results.push(
514
+ ...Array.from(
515
+ el.shadowRoot.querySelectorAll(
516
+ "input, textarea, select"
517
+ )
518
+ ),
519
+ ...collectShadowControls(el.shadowRoot, visited)
520
+ );
521
+ }
522
+ }
523
+ return results;
524
+ }
440
525
  function buildSchema(form) {
441
526
  const properties = {};
442
527
  const required = [];
443
528
  const fieldElements = /* @__PURE__ */ new Map();
444
529
  const processedRadioGroups = /* @__PURE__ */ new Set();
445
530
  const processedCheckboxGroups = /* @__PURE__ */ new Set();
446
- const controls = Array.from(
447
- form.querySelectorAll(
448
- "input, textarea, select"
449
- )
450
- );
531
+ const controls = [
532
+ ...Array.from(
533
+ form.querySelectorAll(
534
+ "input, textarea, select"
535
+ )
536
+ ),
537
+ ...collectShadowControls(form)
538
+ ];
451
539
  for (const control of controls) {
452
540
  const name = control.name;
453
541
  const fieldKey = name || resolveNativeControlFallbackKey(control);
@@ -472,11 +560,19 @@ function buildSchema(form) {
472
560
  const desc = inferFieldDescription(control);
473
561
  if (desc)
474
562
  schemaProp.description = desc;
563
+ const defaultVal = extractDefaultValue(control);
564
+ if (defaultVal !== void 0)
565
+ schemaProp.default = defaultVal;
475
566
  if (control instanceof HTMLInputElement && control.type === "radio") {
476
567
  schemaProp.enum = collectRadioEnum(form, fieldKey);
477
568
  const radioOneOf = collectRadioOneOf(form, fieldKey);
478
569
  if (radioOneOf.length > 0)
479
570
  schemaProp.oneOf = radioOneOf;
571
+ const checkedRadio = form.querySelector(
572
+ `input[type="radio"][name="${CSS.escape(fieldKey)}"]:checked`
573
+ );
574
+ if (checkedRadio?.value)
575
+ schemaProp.default = checkedRadio.value;
480
576
  }
481
577
  if (control instanceof HTMLInputElement && control.type === "checkbox") {
482
578
  const checkboxValues = collectCheckboxEnum(form, fieldKey);
@@ -488,6 +584,13 @@ function buildSchema(form) {
488
584
  };
489
585
  if (schemaProp.description)
490
586
  arrayProp.description = schemaProp.description;
587
+ const checkedBoxes = Array.from(
588
+ form.querySelectorAll(
589
+ `input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
590
+ )
591
+ ).map((b) => b.value);
592
+ if (checkedBoxes.length > 0)
593
+ arrayProp.default = checkedBoxes;
491
594
  properties[fieldKey] = arrayProp;
492
595
  if (control.required)
493
596
  required.push(fieldKey);
@@ -857,6 +960,9 @@ function buildExecuteHandler(form, config, toolName, metadata) {
857
960
  return async (params) => {
858
961
  pendingFillWarnings.set(form, []);
859
962
  fillFormFields(form, params);
963
+ const missingNow = getMissingRequired(metadata, params);
964
+ if (missingNow.length > 0)
965
+ pendingWarnings.set(form, missingNow);
860
966
  window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
861
967
  return new Promise((resolve, reject) => {
862
968
  pendingExecutions.set(form, { resolve, reject });
@@ -883,9 +989,10 @@ function buildExecuteHandler(form, config, toolName, metadata) {
883
989
  attachSubmitInterceptor(submitForm, toolName);
884
990
  }
885
991
  }
886
- const missing = getMissingRequired(metadata, params);
887
- if (missing.length > 0)
888
- pendingWarnings.set(submitForm, missing);
992
+ if (submitForm !== form && pendingWarnings.has(form)) {
993
+ pendingWarnings.set(submitForm, pendingWarnings.get(form));
994
+ pendingWarnings.delete(form);
995
+ }
889
996
  submitForm.requestSubmit();
890
997
  } catch (err) {
891
998
  reject(err instanceof Error ? err : new Error(String(err)));
@@ -907,17 +1014,37 @@ function attachSubmitInterceptor(form, toolName) {
907
1014
  pendingExecutions.delete(form);
908
1015
  const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
909
1016
  lastFilledSnapshot.delete(form);
910
- const missing = pendingWarnings.get(form);
1017
+ const missingRequired = pendingWarnings.get(form) ?? [];
911
1018
  pendingWarnings.delete(form);
912
1019
  const fillWarnings = pendingFillWarnings.get(form) ?? [];
913
1020
  pendingFillWarnings.delete(form);
914
- const allWarnings = [
915
- ...missing?.length ? [`required fields were not filled: ${missing.join(", ")}`] : [],
916
- ...fillWarnings
1021
+ const skippedFields = fillWarnings.filter((w) => w.type === "not_filled").map((w) => w.field);
1022
+ const structured = {
1023
+ status: missingRequired.length > 0 || skippedFields.length > 0 ? "partial" : "success",
1024
+ filled_fields: formData,
1025
+ skipped_fields: skippedFields,
1026
+ missing_required: missingRequired,
1027
+ warnings: [
1028
+ ...missingRequired.map((f) => ({
1029
+ field: f,
1030
+ type: "missing_required",
1031
+ message: `required field "${f}" was not provided`
1032
+ })),
1033
+ ...fillWarnings
1034
+ ]
1035
+ };
1036
+ const allWarnMessages = [
1037
+ ...missingRequired.length ? [`required fields were not filled: ${missingRequired.join(", ")}`] : [],
1038
+ ...fillWarnings.map((w) => w.message)
917
1039
  ];
918
- const warningText = allWarnings.length ? ` Note: ${allWarnings.join("; ")}.` : "";
1040
+ const warningText = allWarnMessages.length ? ` Note: ${allWarnMessages.join("; ")}.` : "";
919
1041
  const text = `Form submitted. Fields: ${JSON.stringify(formData)}${warningText}`;
920
- const result = { content: [{ type: "text", text }] };
1042
+ const result = {
1043
+ content: [
1044
+ { type: "text", text },
1045
+ { type: "text", text: JSON.stringify(structured) }
1046
+ ]
1047
+ };
921
1048
  if (e.agentInvoked && typeof e.respondWith === "function") {
922
1049
  e.preventDefault();
923
1050
  e.respondWith(Promise.resolve(result));
@@ -1051,16 +1178,26 @@ function fillInput(input, form, key, value) {
1051
1178
  const raw = String(value ?? "");
1052
1179
  const num = Number(raw);
1053
1180
  if (raw === "" || isNaN(num)) {
1054
- pendingFillWarnings.get(form)?.push(`"${key}" expects a number, got: ${JSON.stringify(value)}`);
1181
+ pendingFillWarnings.get(form)?.push({
1182
+ field: key,
1183
+ type: "type_mismatch",
1184
+ message: `"${key}" expects a number, got: ${JSON.stringify(value)}`,
1185
+ original: value
1186
+ });
1055
1187
  return;
1056
1188
  }
1057
1189
  const min = input.min !== "" ? parseFloat(input.min) : -Infinity;
1058
1190
  const max = input.max !== "" ? parseFloat(input.max) : Infinity;
1059
1191
  if (num < min || num > max) {
1060
- pendingFillWarnings.get(form)?.push(
1061
- `"${key}" value ${num} is outside allowed range [${input.min || "?"}, ${input.max || "?"}]`
1062
- );
1063
- input.value = String(Math.min(Math.max(num, min), max));
1192
+ const clamped = Math.min(Math.max(num, min), max);
1193
+ pendingFillWarnings.get(form)?.push({
1194
+ field: key,
1195
+ type: "clamped",
1196
+ message: `"${key}" value ${num} is outside allowed range [${input.min || "?"}, ${input.max || "?"}], clamped to ${clamped}`,
1197
+ original: num,
1198
+ actual: clamped
1199
+ });
1200
+ input.value = String(clamped);
1064
1201
  } else {
1065
1202
  input.value = String(num);
1066
1203
  }