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.
@@ -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
- return { name, description, inputSchema, fieldElements };
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 = Array.from(
428
- form.querySelectorAll(
429
- "input, textarea, select"
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
- const missing = getMissingRequired(metadata, params);
868
- if (missing.length > 0)
869
- pendingWarnings.set(submitForm, missing);
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 missing = pendingWarnings.get(form);
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 allWarnings = [
896
- ...missing?.length ? [`required fields were not filled: ${missing.join(", ")}`] : [],
897
- ...fillWarnings
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 = allWarnings.length ? ` Note: ${allWarnings.join("; ")}.` : "";
1021
+ const warningText = allWarnMessages.length ? ` Note: ${allWarnMessages.join("; ")}.` : "";
900
1022
  const text = `Form submitted. Fields: ${JSON.stringify(formData)}${warningText}`;
901
- const result = { content: [{ type: "text", text }] };
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(`"${key}" expects a number, got: ${JSON.stringify(value)}`);
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
- pendingFillWarnings.get(form)?.push(
1042
- `"${key}" value ${num} is outside allowed range [${input.min || "?"}, ${input.max || "?"}]`
1043
- );
1044
- input.value = String(Math.min(Math.max(num, min), max));
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
  }