auto-webmcp 0.2.9 → 0.3.0

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;AAynBD;;;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
  }
@@ -207,6 +206,14 @@ function buildStringSchema(input) {
207
206
  }
208
207
  return prop;
209
208
  }
209
+ var PLACEHOLDER_PATTERNS = /^(select|choose|pick)\b|^--+|---/i;
210
+ function isPlaceholderOption(opt) {
211
+ if (opt.disabled)
212
+ return true;
213
+ if (opt.value !== "")
214
+ return false;
215
+ return PLACEHOLDER_PATTERNS.test(opt.text.trim());
216
+ }
210
217
  function mapSelectElement(select) {
211
218
  const enumValues = [];
212
219
  const oneOf = [];
@@ -218,7 +225,7 @@ function mapSelectElement(select) {
218
225
  for (const opt of Array.from(child.children)) {
219
226
  if (!(opt instanceof HTMLOptionElement))
220
227
  continue;
221
- if (opt.disabled || opt.value === "")
228
+ if (isPlaceholderOption(opt))
222
229
  continue;
223
230
  enumValues.push(opt.value);
224
231
  const entry = {
@@ -230,7 +237,7 @@ function mapSelectElement(select) {
230
237
  oneOf.push(entry);
231
238
  }
232
239
  } else if (child instanceof HTMLOptionElement) {
233
- if (child.disabled || child.value === "")
240
+ if (isPlaceholderOption(child))
234
241
  continue;
235
242
  enumValues.push(child.value);
236
243
  oneOf.push({ const: child.value, title: child.text.trim() || child.value });
@@ -238,6 +245,9 @@ function mapSelectElement(select) {
238
245
  }
239
246
  if (enumValues.length === 0)
240
247
  return { type: "string" };
248
+ if (select.multiple) {
249
+ return { type: "array", items: { type: "string", enum: enumValues } };
250
+ }
241
251
  return { type: "string", enum: enumValues, oneOf };
242
252
  }
243
253
  function collectCheckboxEnum(form, name) {
@@ -329,7 +339,8 @@ function analyzeForm(form, override) {
329
339
  const name = override?.name ?? inferToolName(form);
330
340
  const description = override?.description ?? inferToolDescription(form);
331
341
  const { schema: inputSchema, fieldElements } = buildSchema(form);
332
- return { name, description, inputSchema, fieldElements };
342
+ const annotations = inferAnnotations(form);
343
+ return { name, description, inputSchema, annotations, fieldElements };
333
344
  }
334
345
  function inferToolName(form) {
335
346
  const nativeName = form.getAttribute("toolname");
@@ -426,6 +437,72 @@ function inferToolDescription(form) {
426
437
  return pageTitle;
427
438
  return "Submit form";
428
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
+ }
429
506
  function buildSchema(form) {
430
507
  const properties = {};
431
508
  const required = [];
@@ -461,11 +538,19 @@ function buildSchema(form) {
461
538
  const desc = inferFieldDescription(control);
462
539
  if (desc)
463
540
  schemaProp.description = desc;
541
+ const defaultVal = extractDefaultValue(control);
542
+ if (defaultVal !== void 0)
543
+ schemaProp.default = defaultVal;
464
544
  if (control instanceof HTMLInputElement && control.type === "radio") {
465
545
  schemaProp.enum = collectRadioEnum(form, fieldKey);
466
546
  const radioOneOf = collectRadioOneOf(form, fieldKey);
467
547
  if (radioOneOf.length > 0)
468
548
  schemaProp.oneOf = radioOneOf;
549
+ const checkedRadio = form.querySelector(
550
+ `input[type="radio"][name="${CSS.escape(fieldKey)}"]:checked`
551
+ );
552
+ if (checkedRadio?.value)
553
+ schemaProp.default = checkedRadio.value;
469
554
  }
470
555
  if (control instanceof HTMLInputElement && control.type === "checkbox") {
471
556
  const checkboxValues = collectCheckboxEnum(form, fieldKey);
@@ -477,6 +562,13 @@ function buildSchema(form) {
477
562
  };
478
563
  if (schemaProp.description)
479
564
  arrayProp.description = schemaProp.description;
565
+ const checkedBoxes = Array.from(
566
+ form.querySelectorAll(
567
+ `input[type="checkbox"][name="${CSS.escape(fieldKey)}"]:checked`
568
+ )
569
+ ).map((b) => b.value);
570
+ if (checkedBoxes.length > 0)
571
+ arrayProp.default = checkedBoxes;
480
572
  properties[fieldKey] = arrayProp;
481
573
  if (control.required)
482
574
  required.push(fieldKey);
@@ -599,6 +691,9 @@ function resolveAriaFieldKey(el) {
599
691
  return null;
600
692
  }
601
693
  function inferAriaFieldTitle(el) {
694
+ const nativeTitle = el.getAttribute("toolparamtitle");
695
+ if (nativeTitle?.trim())
696
+ return nativeTitle.trim();
602
697
  const htmlEl = el;
603
698
  if (htmlEl.dataset?.["webmcpTitle"])
604
699
  return htmlEl.dataset["webmcpTitle"];
@@ -637,6 +732,9 @@ function inferAriaFieldDescription(el) {
637
732
  return "";
638
733
  }
639
734
  function inferFieldTitle(control) {
735
+ const nativeTitle = control.getAttribute("toolparamtitle");
736
+ if (nativeTitle?.trim())
737
+ return nativeTitle.trim();
640
738
  if ("dataset" in control && control.dataset["webmcpTitle"]) {
641
739
  return control.dataset["webmcpTitle"];
642
740
  }
@@ -834,6 +932,7 @@ var lastParams = /* @__PURE__ */ new WeakMap();
834
932
  var formFieldElements = /* @__PURE__ */ new WeakMap();
835
933
  var pendingWarnings = /* @__PURE__ */ new WeakMap();
836
934
  var pendingFillWarnings = /* @__PURE__ */ new WeakMap();
935
+ var lastFilledSnapshot = /* @__PURE__ */ new WeakMap();
837
936
  var _inputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
838
937
  var _textareaValueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
839
938
  var _checkedSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "checked")?.set;
@@ -894,6 +993,7 @@ function attachSubmitInterceptor(form, toolName) {
894
993
  const { resolve } = pending;
895
994
  pendingExecutions.delete(form);
896
995
  const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
996
+ lastFilledSnapshot.delete(form);
897
997
  const missing = pendingWarnings.get(form);
898
998
  pendingWarnings.delete(form);
899
999
  const fillWarnings = pendingFillWarnings.get(form) ?? [];
@@ -912,6 +1012,7 @@ function attachSubmitInterceptor(form, toolName) {
912
1012
  resolve(result);
913
1013
  });
914
1014
  form.addEventListener("reset", () => {
1015
+ lastFilledSnapshot.delete(form);
915
1016
  window.dispatchEvent(new CustomEvent("toolcancel", { detail: { toolName } }));
916
1017
  });
917
1018
  }
@@ -964,16 +1065,30 @@ function findNativeField(form, key) {
964
1065
  function fillFormFields(form, params) {
965
1066
  lastParams.set(form, params);
966
1067
  const fieldEls = formFieldElements.get(form);
1068
+ const snapshot = {};
967
1069
  for (const [key, value] of Object.entries(params)) {
968
1070
  const input = findNativeField(form, key);
969
1071
  if (input) {
970
1072
  if (input instanceof HTMLInputElement) {
971
1073
  fillInput(input, form, key, value);
1074
+ if (input.type === "checkbox") {
1075
+ if (Array.isArray(value)) {
1076
+ const esc = CSS.escape(key);
1077
+ snapshot[key] = Array.from(
1078
+ form.querySelectorAll(`input[type="checkbox"][name="${esc}"]`)
1079
+ ).filter((b) => b.checked).map((b) => b.value);
1080
+ } else {
1081
+ snapshot[key] = input.checked;
1082
+ }
1083
+ } else {
1084
+ snapshot[key] = input.value;
1085
+ }
972
1086
  } else if (input instanceof HTMLTextAreaElement) {
973
1087
  setReactValue(input, String(value ?? ""));
1088
+ snapshot[key] = input.value;
974
1089
  } else if (input instanceof HTMLSelectElement) {
975
- input.value = String(value ?? "");
976
- input.dispatchEvent(new Event("change", { bubbles: true }));
1090
+ fillSelectElement(input, value);
1091
+ snapshot[key] = input.multiple ? Array.from(input.options).filter((o) => o.selected).map((o) => o.value) : input.value;
977
1092
  }
978
1093
  continue;
979
1094
  }
@@ -990,16 +1105,20 @@ function fillFormFields(form, params) {
990
1105
  }
991
1106
  if (effectiveEl instanceof HTMLInputElement) {
992
1107
  fillInput(effectiveEl, form, key, value);
1108
+ snapshot[key] = effectiveEl.type === "checkbox" ? effectiveEl.checked : effectiveEl.value;
993
1109
  } else if (effectiveEl instanceof HTMLTextAreaElement) {
994
1110
  setReactValue(effectiveEl, String(value ?? ""));
1111
+ snapshot[key] = effectiveEl.value;
995
1112
  } else if (effectiveEl instanceof HTMLSelectElement) {
996
- effectiveEl.value = String(value ?? "");
997
- effectiveEl.dispatchEvent(new Event("change", { bubbles: true }));
1113
+ fillSelectElement(effectiveEl, value);
1114
+ snapshot[key] = effectiveEl.multiple ? Array.from(effectiveEl.options).filter((o) => o.selected).map((o) => o.value) : effectiveEl.value;
998
1115
  } else {
999
1116
  fillAriaField(effectiveEl, value);
1117
+ snapshot[key] = value;
1000
1118
  }
1001
1119
  }
1002
1120
  }
1121
+ lastFilledSnapshot.set(form, snapshot);
1003
1122
  }
1004
1123
  function fillInput(input, form, key, value) {
1005
1124
  const type = input.type.toLowerCase();
@@ -1056,6 +1175,18 @@ function fillInput(input, form, key, value) {
1056
1175
  }
1057
1176
  setReactValue(input, String(value ?? ""));
1058
1177
  }
1178
+ function fillSelectElement(select, value) {
1179
+ if (select.multiple) {
1180
+ const vals = Array.isArray(value) ? value.map(String) : [String(value ?? "")];
1181
+ for (const opt of Array.from(select.options)) {
1182
+ opt.selected = vals.includes(opt.value);
1183
+ }
1184
+ select.dispatchEvent(new Event("change", { bubbles: true }));
1185
+ return;
1186
+ }
1187
+ select.value = String(value ?? "");
1188
+ select.dispatchEvent(new Event("change", { bubbles: true }));
1189
+ }
1059
1190
  function fillAriaField(el, value) {
1060
1191
  const role = el.getAttribute("role");
1061
1192
  if (role === "checkbox" || role === "switch") {
@@ -1094,6 +1225,7 @@ function fillAriaField(el, value) {
1094
1225
  function serializeFormData(form, params, fieldEls) {
1095
1226
  const result = {};
1096
1227
  const data = new FormData(form);
1228
+ const snapshot = lastFilledSnapshot.get(form);
1097
1229
  for (const [key, val] of data.entries()) {
1098
1230
  if (result[key] !== void 0) {
1099
1231
  const existing = result[key];
@@ -1110,6 +1242,10 @@ function serializeFormData(form, params, fieldEls) {
1110
1242
  for (const key of Object.keys(params)) {
1111
1243
  if (key in result)
1112
1244
  continue;
1245
+ if (snapshot && key in snapshot) {
1246
+ result[key] = snapshot[key];
1247
+ continue;
1248
+ }
1113
1249
  const el = findNativeField(form, key) ?? fieldEls?.get(key) ?? null;
1114
1250
  if (!el)
1115
1251
  continue;
@@ -1148,8 +1284,7 @@ function fillElement(el, value) {
1148
1284
  } else if (el instanceof HTMLTextAreaElement) {
1149
1285
  setReactValue(el, String(value ?? ""));
1150
1286
  } else if (el instanceof HTMLSelectElement) {
1151
- el.value = String(value ?? "");
1152
- el.dispatchEvent(new Event("change", { bubbles: true }));
1287
+ fillSelectElement(el, value);
1153
1288
  } else {
1154
1289
  fillAriaField(el, value);
1155
1290
  }