auto-webmcp 0.3.6 → 0.3.8

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.
@@ -611,13 +611,14 @@ function buildSchema(form) {
611
611
  required.push(key);
612
612
  }
613
613
  }
614
- return { schema: { type: "object", properties, required }, fieldElements };
614
+ return { schema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object", properties, required }, fieldElements };
615
615
  }
616
+ var AUTO_GENERATED_ID_RE = /^_r_[0-9a-z]+_$|^:[a-z0-9]+:$/i;
616
617
  function resolveNativeControlFallbackKey(control) {
617
618
  const el = control;
618
619
  if (el.dataset["webmcpName"])
619
620
  return sanitizeName(el.dataset["webmcpName"]);
620
- if (control.id)
621
+ if (control.id && !AUTO_GENERATED_ID_RE.test(control.id))
621
622
  return sanitizeName(control.id);
622
623
  const label = control.getAttribute("aria-label");
623
624
  if (label)
@@ -816,7 +817,24 @@ function analyzeOrphanInputGroup(container, inputs, submitBtn) {
816
817
  const name = inferOrphanToolName(container, submitBtn);
817
818
  const description = inferOrphanToolDescription(container);
818
819
  const { schema: inputSchema, fieldElements } = buildSchemaFromInputs(inputs);
819
- return { name, description, inputSchema, fieldElements };
820
+ const annotations = inferOrphanAnnotations(submitBtn);
821
+ return { name, description, inputSchema, annotations, fieldElements };
822
+ }
823
+ function inferOrphanAnnotations(submitBtn) {
824
+ const annotations = {};
825
+ const submitText = submitBtn instanceof HTMLInputElement ? submitBtn.value.trim() : submitBtn?.textContent?.trim() ?? "";
826
+ if (READONLY_BUTTON_PATTERNS.test(submitText)) {
827
+ annotations.readOnlyHint = true;
828
+ annotations.idempotentHint = true;
829
+ }
830
+ if (DESTRUCTIVE_BUTTON_PATTERNS.test(submitText)) {
831
+ annotations.destructiveHint = true;
832
+ }
833
+ if (annotations.readOnlyHint !== true) {
834
+ annotations.openWorldHint = true;
835
+ }
836
+ const hasNonDefault = annotations.readOnlyHint === true || annotations.destructiveHint === true || annotations.idempotentHint === true || annotations.openWorldHint === false;
837
+ return hasNonDefault ? annotations : {};
820
838
  }
821
839
  function inferOrphanToolName(container, submitBtn) {
822
840
  if (submitBtn) {
@@ -871,8 +889,8 @@ function buildSchemaFromInputs(inputs) {
871
889
  const processedRadioGroups = /* @__PURE__ */ new Set();
872
890
  const processedCheckboxGroups = /* @__PURE__ */ new Set();
873
891
  for (const control of inputs) {
874
- const name = control.name;
875
- const fieldKey = name || resolveNativeControlFallbackKey(control);
892
+ const rawName = control.name;
893
+ const fieldKey = (rawName ? sanitizeName(rawName) : null) || resolveNativeControlFallbackKey(control);
876
894
  if (!fieldKey)
877
895
  continue;
878
896
  if (control instanceof HTMLInputElement && control.type === "radio") {
@@ -911,12 +929,12 @@ function buildSchemaFromInputs(inputs) {
911
929
  }
912
930
  }
913
931
  properties[fieldKey] = schemaProp;
914
- if (!name)
932
+ if (!rawName)
915
933
  fieldElements.set(fieldKey, control);
916
934
  if (control.required)
917
935
  required.push(fieldKey);
918
936
  }
919
- return { schema: { type: "object", properties, required }, fieldElements };
937
+ return { schema: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object", properties, required }, fieldElements };
920
938
  }
921
939
 
922
940
  // src/discovery.ts
@@ -1423,7 +1441,7 @@ async function registerForm(form, config) {
1423
1441
  registeredForms.add(form);
1424
1442
  registeredFormCount++;
1425
1443
  if (config.debug) {
1426
- console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
1444
+ console.log(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
1427
1445
  }
1428
1446
  emit("form:registered", form, metadata.name);
1429
1447
  }
@@ -1435,7 +1453,7 @@ async function unregisterForm(form, config) {
1435
1453
  await unregisterFormTool(form);
1436
1454
  registeredForms.delete(form);
1437
1455
  if (config.debug) {
1438
- console.debug(`[auto-webmcp] Unregistered: ${name}`);
1456
+ console.log(`[auto-webmcp] Unregistered: ${name}`);
1439
1457
  }
1440
1458
  emit("form:unregistered", form, name);
1441
1459
  }
@@ -1536,7 +1554,7 @@ async function scanOrphanInputs(config) {
1536
1554
  if (!isWebMCPSupported())
1537
1555
  return;
1538
1556
  const SUBMIT_BTN_SELECTOR = '[type="submit"]:not([disabled]), button:not([type]):not([disabled])';
1539
- const SUBMIT_BTN_GROUPING_SELECTOR = '[type="submit"], button:not([type])';
1557
+ const SUBMIT_BTN_GROUPING_SELECTOR = '[type="submit"]';
1540
1558
  const SUBMIT_TEXT_RE = /subscribe|submit|sign[\s-]?up|send|join|go|search/i;
1541
1559
  const orphanInputs = Array.from(
1542
1560
  document.querySelectorAll(
@@ -1544,11 +1562,17 @@ async function scanOrphanInputs(config) {
1544
1562
  )
1545
1563
  ).filter((el) => {
1546
1564
  if (el instanceof HTMLInputElement && ORPHAN_EXCLUDED_TYPES.has(el.type.toLowerCase())) {
1565
+ console.log(`[auto-webmcp] orphan: skipping excluded type "${el.type}" (name="${el.name}" id="${el.id}")`);
1547
1566
  return false;
1548
1567
  }
1549
1568
  const rect = el.getBoundingClientRect();
1550
- return rect.width > 0 && rect.height > 0;
1569
+ if (rect.width === 0 || rect.height === 0) {
1570
+ console.log(`[auto-webmcp] orphan: skipping invisible input (name="${el.name}" id="${el.id}")`);
1571
+ return false;
1572
+ }
1573
+ return true;
1551
1574
  });
1575
+ console.log(`[auto-webmcp] orphan: found ${orphanInputs.length} visible orphan input(s)`);
1552
1576
  if (orphanInputs.length === 0)
1553
1577
  return;
1554
1578
  const groups = /* @__PURE__ */ new Map();
@@ -1565,10 +1589,12 @@ async function scanOrphanInputs(config) {
1565
1589
  }
1566
1590
  container = container.parentElement;
1567
1591
  }
1592
+ console.log(`[auto-webmcp] orphan: input (name="${input.name}" id="${input.id}") grouped into container`, foundContainer);
1568
1593
  if (!groups.has(foundContainer))
1569
1594
  groups.set(foundContainer, []);
1570
1595
  groups.get(foundContainer).push(input);
1571
1596
  }
1597
+ console.log(`[auto-webmcp] orphan: ${groups.size} group(s) found`);
1572
1598
  for (const [container, inputs] of groups) {
1573
1599
  const allCandidates = Array.from(
1574
1600
  container.querySelectorAll(SUBMIT_BTN_SELECTOR)
@@ -1577,6 +1603,12 @@ async function scanOrphanInputs(config) {
1577
1603
  return r.width > 0 && r.height > 0;
1578
1604
  });
1579
1605
  let submitBtn = allCandidates[allCandidates.length - 1] ?? null;
1606
+ const disabledCandidates = Array.from(
1607
+ container.querySelectorAll(SUBMIT_BTN_GROUPING_SELECTOR)
1608
+ ).filter((b) => b.disabled);
1609
+ if (!submitBtn && disabledCandidates.length > 0) {
1610
+ console.log(`[auto-webmcp] orphan: no enabled submit button found in container \u2014 ${disabledCandidates.length} disabled button(s) present:`, disabledCandidates.map((b) => b.textContent?.trim()));
1611
+ }
1580
1612
  if (!submitBtn) {
1581
1613
  const pageBtns = Array.from(document.querySelectorAll("button")).filter(
1582
1614
  (b) => {
@@ -1585,36 +1617,82 @@ async function scanOrphanInputs(config) {
1585
1617
  }
1586
1618
  );
1587
1619
  submitBtn = pageBtns[pageBtns.length - 1] ?? null;
1620
+ if (submitBtn)
1621
+ console.log(`[auto-webmcp] orphan: using page-wide fallback submit button: "${submitBtn.textContent?.trim()}"`);
1588
1622
  }
1623
+ console.log(`[auto-webmcp] orphan: submit button for group:`, submitBtn ? `"${submitBtn.textContent?.trim()}" disabled=${submitBtn.disabled}` : "none");
1589
1624
  const metadata = analyzeOrphanInputGroup(container, inputs, submitBtn);
1625
+ console.log(`[auto-webmcp] orphan: tool="${metadata.name}" schema keys:`, Object.keys(metadata.inputSchema.properties));
1590
1626
  const inputPairs = [];
1591
1627
  const schemaProps = metadata.inputSchema.properties;
1628
+ const AUTO_ID_RE = /^_r_[0-9a-z]+_$/i;
1592
1629
  for (const el of inputs) {
1593
- const key = el.name || el.dataset["webmcpName"] || el.id || el.getAttribute("aria-label") || null;
1630
+ const id = el.id && !AUTO_ID_RE.test(el.id) ? el.id : null;
1631
+ const key = el.name || el.dataset["webmcpName"] || id || el.getAttribute("aria-label") || (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el.placeholder || null : null) || null;
1594
1632
  const safeKey = key ? key.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) : null;
1595
- if (safeKey && schemaProps[safeKey]) {
1633
+ const matched = !!(safeKey && schemaProps[safeKey]);
1634
+ console.log(`[auto-webmcp] orphan: field (name="${el.name}" id="${el.id}") rawKey="${key}" safeKey="${safeKey}" matched=${matched}`);
1635
+ if (matched) {
1596
1636
  inputPairs.push({ key: safeKey, el });
1597
1637
  }
1598
1638
  }
1639
+ console.log(`[auto-webmcp] orphan: ${inputPairs.length}/${inputs.length} input(s) mapped to schema keys`);
1599
1640
  const toolName = metadata.name;
1600
1641
  const execute = async (params) => {
1642
+ console.log(`[auto-webmcp] orphan execute: tool="${toolName}" params=`, params);
1643
+ console.log(`[auto-webmcp] orphan execute: inputPairs=`, inputPairs.map((p) => p.key));
1601
1644
  for (const { key, el } of inputPairs) {
1602
1645
  if (params[key] !== void 0) {
1646
+ console.log(`[auto-webmcp] orphan execute: filling key="${key}" value=`, params[key], "element=", el);
1603
1647
  fillElement(el, params[key]);
1648
+ console.log(`[auto-webmcp] orphan execute: after fill, element value=`, el.value);
1649
+ } else {
1650
+ console.log(`[auto-webmcp] orphan execute: key="${key}" not in params, skipping`);
1604
1651
  }
1605
1652
  }
1606
1653
  window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
1607
- return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
1654
+ if (!config.autoSubmit) {
1655
+ console.log(`[auto-webmcp] orphan execute: autoSubmit=false, returning without clicking submit`);
1656
+ return { content: [{ type: "text", text: "Fields filled. Ready to submit." }] };
1657
+ }
1658
+ console.log(`[auto-webmcp] orphan execute: polling for enabled submit button (up to 2s)...`);
1659
+ let btn = null;
1660
+ const deadline = Date.now() + 2e3;
1661
+ while (Date.now() < deadline) {
1662
+ const candidates = Array.from(
1663
+ container.querySelectorAll(SUBMIT_BTN_SELECTOR)
1664
+ ).filter((b) => {
1665
+ const r = b.getBoundingClientRect();
1666
+ return r.width > 0 && r.height > 0;
1667
+ });
1668
+ const last = candidates[candidates.length - 1] ?? null;
1669
+ if (last) {
1670
+ btn = last;
1671
+ break;
1672
+ }
1673
+ await new Promise((r) => setTimeout(r, 100));
1674
+ }
1675
+ if (!btn) {
1676
+ console.warn(`[auto-webmcp] orphan execute: submit button still disabled after 2s`);
1677
+ return { content: [{ type: "text", text: "Fields filled but the submit button is still disabled. The page may require additional input before submitting." }] };
1678
+ }
1679
+ console.log(`[auto-webmcp] orphan execute: clicking submit button "${btn.textContent?.trim()}"`);
1680
+ btn.click();
1681
+ return { content: [{ type: "text", text: "Fields filled and form submitted." }] };
1608
1682
  };
1609
1683
  try {
1610
- await navigator.modelContext.registerTool({
1684
+ const toolDef = {
1611
1685
  name: metadata.name,
1612
1686
  description: metadata.description,
1613
1687
  inputSchema: metadata.inputSchema,
1614
1688
  execute
1615
- });
1689
+ };
1690
+ if (metadata.annotations && Object.keys(metadata.annotations).length > 0) {
1691
+ toolDef.annotations = metadata.annotations;
1692
+ }
1693
+ await navigator.modelContext.registerTool(toolDef);
1616
1694
  if (config.debug) {
1617
- console.debug(`[auto-webmcp] Orphan tool registered: ${metadata.name}`, metadata);
1695
+ console.log(`[auto-webmcp] Orphan tool registered: ${metadata.name}`, metadata);
1618
1696
  }
1619
1697
  } catch {
1620
1698
  }