@apex-inc/mcp-server 0.15.0 → 0.17.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.
- package/dist/index.js +0 -0
- package/dist/tools.d.ts +301 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +520 -3
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/skills/apex-growth-intelligence/SKILL.md +53 -0
- package/skills/apex-integration-cookbook/SKILL.md +84 -0
- package/skills/apex-spec/SKILL.md +52 -4
package/dist/tools.js
CHANGED
|
@@ -53,6 +53,24 @@ function titleCaseEvent(event) {
|
|
|
53
53
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
54
54
|
.join(" ");
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Query string for the pack report windows: relative (`days` / `months`)
|
|
58
|
+
* or explicit `start` + `end` (YYYY-MM-DD, both required together —
|
|
59
|
+
* explicit bounds win server-side).
|
|
60
|
+
*/
|
|
61
|
+
function windowQuery(opts) {
|
|
62
|
+
const params = new URLSearchParams();
|
|
63
|
+
if (opts.days)
|
|
64
|
+
params.set("days", String(opts.days));
|
|
65
|
+
if (opts.months)
|
|
66
|
+
params.set("months", String(opts.months));
|
|
67
|
+
if (opts.start && opts.end) {
|
|
68
|
+
params.set("start", opts.start);
|
|
69
|
+
params.set("end", opts.end);
|
|
70
|
+
}
|
|
71
|
+
const qs = params.toString();
|
|
72
|
+
return qs ? `?${qs}` : "";
|
|
73
|
+
}
|
|
56
74
|
function errMsg(err) {
|
|
57
75
|
return err instanceof Error ? err.message : String(err);
|
|
58
76
|
}
|
|
@@ -1599,6 +1617,327 @@ export const toolDefinitions = {
|
|
|
1599
1617
|
};
|
|
1600
1618
|
},
|
|
1601
1619
|
},
|
|
1620
|
+
get_channel_economics: {
|
|
1621
|
+
description: `${APEX} — "Where should my next dollar go?" Per acquisition channel (raw first-touch): gross-margin-adjusted LTV:CAC, payback months, CAC, and customers, split into Acquisition (paid) vs Leverage (owned/organic, no spend). Computed from real contacts + spend + revenue — metrics with missing inputs are shown as "—", never fabricated. Free intelligence; the verdict is never gated.`,
|
|
1622
|
+
schema: z.object({
|
|
1623
|
+
days: z
|
|
1624
|
+
.union([z.number(), z.literal("all")])
|
|
1625
|
+
.optional()
|
|
1626
|
+
.describe("Trailing window in days, or 'all' for since-inception (default)."),
|
|
1627
|
+
}),
|
|
1628
|
+
handler: async ({ days }) => {
|
|
1629
|
+
const q = days && days !== "all" ? `?days=${days}` : "?days=all";
|
|
1630
|
+
const res = await apiGet(`/api/channels/economics${q}`);
|
|
1631
|
+
const channels = res.data?.channels ?? [];
|
|
1632
|
+
if (channels.length === 0) {
|
|
1633
|
+
return {
|
|
1634
|
+
content: [
|
|
1635
|
+
{
|
|
1636
|
+
type: "text",
|
|
1637
|
+
text: `${APEX} No channel economics yet — connect attributed traffic (Apex Links auto-tag it) and revenue so acquisition channels can be measured.`,
|
|
1638
|
+
},
|
|
1639
|
+
],
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
const label = (slug) => ({
|
|
1643
|
+
linkedin: "LinkedIn Ads",
|
|
1644
|
+
google: "Google",
|
|
1645
|
+
meta: "Meta",
|
|
1646
|
+
microsoft: "Microsoft Ads",
|
|
1647
|
+
tiktok: "TikTok",
|
|
1648
|
+
twitter: "Twitter / X",
|
|
1649
|
+
direct: "Direct",
|
|
1650
|
+
email: "Email",
|
|
1651
|
+
affiliate: "Partner Network",
|
|
1652
|
+
})[slug] ?? slug.charAt(0).toUpperCase() + slug.slice(1);
|
|
1653
|
+
const money = (n) => n == null ? "—" : `$${Math.round(n).toLocaleString()}`;
|
|
1654
|
+
const ratio = (r) => (r == null ? "—" : `${r.toFixed(2)}x`);
|
|
1655
|
+
const ranked = [...channels].sort((a, b) => (b.ltvCacRatio ?? -1) - (a.ltvCacRatio ?? -1));
|
|
1656
|
+
const acquisition = ranked.filter((c) => c.spend !== null && c.spend > 0);
|
|
1657
|
+
const leverage = ranked.filter((c) => c.spend === null || c.spend === 0);
|
|
1658
|
+
const fmt = (c) => ` • ${label(c.channel)}: ${ratio(c.ltvCacRatio)} LTV:CAC · payback ${c.paybackMonths != null ? `${c.paybackMonths}mo` : "—"} · CAC ${money(c.cac)} · ${c.customers.toLocaleString()} customers`;
|
|
1659
|
+
const lines = [
|
|
1660
|
+
`${APEX} Channel economics — gross-margin LTV:CAC`,
|
|
1661
|
+
"═".repeat(40),
|
|
1662
|
+
];
|
|
1663
|
+
if (res.data?.needsReferee) {
|
|
1664
|
+
lines.push("Revenue figures hidden: two live revenue sources with no referee picked. Choose one in Connect revenue.", "");
|
|
1665
|
+
}
|
|
1666
|
+
if (acquisition.length > 0) {
|
|
1667
|
+
lines.push("Acquisition (paid):", ...acquisition.map(fmt), "");
|
|
1668
|
+
}
|
|
1669
|
+
if (leverage.length > 0) {
|
|
1670
|
+
lines.push("Leverage (owned / organic):", ...leverage.map(fmt), "");
|
|
1671
|
+
}
|
|
1672
|
+
lines.push("3-5x LTV:CAC is healthy; <1x loses margin on every customer; >5x is likely underinvested.");
|
|
1673
|
+
if (res.data?.grossMarginFromDefault) {
|
|
1674
|
+
lines.push(`(Using a default ${res.data.grossMargin != null
|
|
1675
|
+
? `${Math.round(res.data.grossMargin * 100)}%`
|
|
1676
|
+
: ""} gross margin — set your real margin in workspace settings to sharpen this.)`);
|
|
1677
|
+
}
|
|
1678
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1679
|
+
},
|
|
1680
|
+
},
|
|
1681
|
+
// ─── Vertical Widget Packs read tools (2026-07-02) ─────────────────────
|
|
1682
|
+
// One tool per pack read API. All aggregate-only (analytics:read scope);
|
|
1683
|
+
// small-n suppression applies to API responses exactly as to widgets.
|
|
1684
|
+
get_saas_recurring_revenue: {
|
|
1685
|
+
description: `${APEX} — SaaS recurring-revenue economics: MRR, ARR, the monthly waterfall (new / expansion / reactivation / contraction / churn — accounting-identity tested), and NRR/GRR for the last complete month. Computed from materialized monthly snapshots (calendar months, UTC); the current month is flagged in-progress. NRR/GRR read "collecting history" until 13 months of snapshots exist — never fabricated.`,
|
|
1686
|
+
schema: z.object({
|
|
1687
|
+
months: z
|
|
1688
|
+
.number()
|
|
1689
|
+
.optional()
|
|
1690
|
+
.describe("Months of waterfall history to include (2-36, default 13)."),
|
|
1691
|
+
start: z
|
|
1692
|
+
.string()
|
|
1693
|
+
.optional()
|
|
1694
|
+
.describe("Explicit window start, YYYY-MM-DD (mapped to calendar months; use with end — overrides months)."),
|
|
1695
|
+
end: z
|
|
1696
|
+
.string()
|
|
1697
|
+
.optional()
|
|
1698
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1699
|
+
}),
|
|
1700
|
+
handler: async ({ months, start, end }) => {
|
|
1701
|
+
const q = windowQuery({ months, start, end });
|
|
1702
|
+
const res = await apiGet(`/api/saas/recurring-revenue${q}`);
|
|
1703
|
+
const d = res.data;
|
|
1704
|
+
if (!d?.hasData) {
|
|
1705
|
+
return {
|
|
1706
|
+
content: [
|
|
1707
|
+
{
|
|
1708
|
+
type: "text",
|
|
1709
|
+
text: `${APEX} No MRR state yet — connect Stripe or send subscription events (subscription_started / subscription_event with amount + period_type) and monthly snapshots build from there.`,
|
|
1710
|
+
},
|
|
1711
|
+
],
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
const money = (n) => n == null ? "—" : `$${Math.round(n).toLocaleString()}`;
|
|
1715
|
+
const pct = (r) => r == null ? "collecting history" : `${Math.round(r * 1000) / 10}%`;
|
|
1716
|
+
const lines = [
|
|
1717
|
+
`${APEX} Recurring revenue`,
|
|
1718
|
+
"═".repeat(40),
|
|
1719
|
+
`MRR ${money(d.currentMrr)} (month in progress) · ARR ${money(d.currentArr)} · ${d.payingAccounts ?? "—"} paying accounts`,
|
|
1720
|
+
`NRR ${pct(d.retention?.nrr ?? null)} · GRR ${pct(d.retention?.grr ?? null)} (trailing 12mo, ${d.retention?.cohortSize ?? 0}-account cohort)`,
|
|
1721
|
+
"",
|
|
1722
|
+
"Waterfall (last months):",
|
|
1723
|
+
...d.waterfall
|
|
1724
|
+
.slice(-6)
|
|
1725
|
+
.map((m) => ` • ${m.month}${m.inProgress ? " (in progress)" : ""}: ${money(m.startMrr)} → ${money(m.endMrr)} (+${money(m.newMrr)} new, +${money(m.expansionMrr)} expansion, +${money(m.reactivationMrr)} reactivation, −${money(m.contractionMrr)} contraction, −${money(m.churnedMrr)} churn)`),
|
|
1726
|
+
];
|
|
1727
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1728
|
+
},
|
|
1729
|
+
},
|
|
1730
|
+
get_saas_expansion: {
|
|
1731
|
+
description: `${APEX} — SaaS expansion economics: expansion vs contraction MRR for the last complete month, accounts expanding (counts — small-n honest), and seat growth from seat_count. Metrics with missing inputs come back null, never fabricated.`,
|
|
1732
|
+
schema: z.object({
|
|
1733
|
+
months: z
|
|
1734
|
+
.number()
|
|
1735
|
+
.optional()
|
|
1736
|
+
.describe("Months of series history to include (2-36, default 13)."),
|
|
1737
|
+
start: z
|
|
1738
|
+
.string()
|
|
1739
|
+
.optional()
|
|
1740
|
+
.describe("Explicit window start, YYYY-MM-DD (mapped to calendar months; use with end — overrides months)."),
|
|
1741
|
+
end: z
|
|
1742
|
+
.string()
|
|
1743
|
+
.optional()
|
|
1744
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1745
|
+
}),
|
|
1746
|
+
handler: async ({ months, start, end }) => {
|
|
1747
|
+
const q = windowQuery({ months, start, end });
|
|
1748
|
+
const res = await apiGet(`/api/saas/expansion${q}`);
|
|
1749
|
+
const d = res.data;
|
|
1750
|
+
if (!d?.hasData) {
|
|
1751
|
+
return {
|
|
1752
|
+
content: [
|
|
1753
|
+
{
|
|
1754
|
+
type: "text",
|
|
1755
|
+
text: `${APEX} No subscription data yet — expansion metrics unlock once MRR snapshots exist.`,
|
|
1756
|
+
},
|
|
1757
|
+
],
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
const money = (n) => n == null ? "—" : `$${Math.round(n).toLocaleString()}`;
|
|
1761
|
+
const seatsLine = d.seatCount != null
|
|
1762
|
+
? `Seats: ${d.seatCount}${d.seatCountPrevMonth != null ? ` (${d.seatCount - d.seatCountPrevMonth >= 0 ? "+" : ""}${d.seatCount - d.seatCountPrevMonth} vs last month)` : ""}`
|
|
1763
|
+
: "Seats: — (send seat_count on subscription events to unlock)";
|
|
1764
|
+
return {
|
|
1765
|
+
content: [
|
|
1766
|
+
{
|
|
1767
|
+
type: "text",
|
|
1768
|
+
text: [
|
|
1769
|
+
`${APEX} Expansion (last complete month)`,
|
|
1770
|
+
"═".repeat(40),
|
|
1771
|
+
`Expansion MRR ${money(d.expansionMrrLastMonth)} · Contraction MRR ${money(d.contractionMrrLastMonth)}`,
|
|
1772
|
+
`Accounts expanding: ${d.expandedAccounts ?? "—"} of ${d.payingAccountsAtStart ?? "—"} paying at month start`,
|
|
1773
|
+
seatsLine,
|
|
1774
|
+
].join("\n"),
|
|
1775
|
+
},
|
|
1776
|
+
],
|
|
1777
|
+
};
|
|
1778
|
+
},
|
|
1779
|
+
},
|
|
1780
|
+
get_ecommerce_product_sales: {
|
|
1781
|
+
description: `${APEX} — E-commerce product sales: top products by revenue with the per-product view → add-to-cart → purchase funnel and refund counts. Products need a product_id on commerce events; never-wired signals are reported as unwired, not zero.`,
|
|
1782
|
+
schema: z.object({
|
|
1783
|
+
days: z.number().optional().describe("Trailing window in days (default 90, max 180)."),
|
|
1784
|
+
start: z
|
|
1785
|
+
.string()
|
|
1786
|
+
.optional()
|
|
1787
|
+
.describe("Explicit window start, YYYY-MM-DD (use with end — overrides days)."),
|
|
1788
|
+
end: z
|
|
1789
|
+
.string()
|
|
1790
|
+
.optional()
|
|
1791
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1792
|
+
}),
|
|
1793
|
+
handler: async ({ days, start, end }) => {
|
|
1794
|
+
const q = windowQuery({ days, start, end });
|
|
1795
|
+
const res = await apiGet(`/api/ecommerce/product-sales${q}`);
|
|
1796
|
+
const d = res.data;
|
|
1797
|
+
if (!d?.hasData) {
|
|
1798
|
+
return {
|
|
1799
|
+
content: [
|
|
1800
|
+
{
|
|
1801
|
+
type: "text",
|
|
1802
|
+
text: `${APEX} No product-scoped events yet — fire product_view / add_to_cart / purchases with a product_id.`,
|
|
1803
|
+
},
|
|
1804
|
+
],
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
const money = (n) => `$${Math.round(n).toLocaleString()}`;
|
|
1808
|
+
const lines = [
|
|
1809
|
+
`${APEX} Product sales — ${money(d.totals.revenue)} product revenue, ${d.totals.purchases} purchases`,
|
|
1810
|
+
"═".repeat(40),
|
|
1811
|
+
...d.items
|
|
1812
|
+
.slice(0, 10)
|
|
1813
|
+
.map((i) => ` • ${i.label ?? i.itemKey}${i.category ? ` [${i.category}]` : ""}: ${money(i.revenue)} · ${i.views} views → ${i.adds} adds → ${i.purchases} purchases${i.refunds > 0 ? ` · ${i.refunds} refunds` : ""}`),
|
|
1814
|
+
];
|
|
1815
|
+
if (!d.wired.views)
|
|
1816
|
+
lines.push("(product_view not wired — funnel rates unavailable)");
|
|
1817
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1818
|
+
},
|
|
1819
|
+
},
|
|
1820
|
+
get_ecommerce_returns: {
|
|
1821
|
+
description: `${APEX} — E-commerce returns & refunds: refund rate (money back, order basis), return rate (goods back — separate metric, never summed), reason breakdown, refund cycle time, AOV, and repeat purchase. Refund data lands automatically from Stripe; return_requested / return_completed events power the goods-back story.`,
|
|
1822
|
+
schema: z.object({
|
|
1823
|
+
days: z.number().optional().describe("Trailing window in days (default 90, max 180)."),
|
|
1824
|
+
start: z
|
|
1825
|
+
.string()
|
|
1826
|
+
.optional()
|
|
1827
|
+
.describe("Explicit window start, YYYY-MM-DD (use with end — overrides days)."),
|
|
1828
|
+
end: z
|
|
1829
|
+
.string()
|
|
1830
|
+
.optional()
|
|
1831
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1832
|
+
}),
|
|
1833
|
+
handler: async ({ days, start, end }) => {
|
|
1834
|
+
const q = windowQuery({ days, start, end });
|
|
1835
|
+
const res = await apiGet(`/api/ecommerce/returns${q}`);
|
|
1836
|
+
const d = res.data;
|
|
1837
|
+
if (!d?.hasData) {
|
|
1838
|
+
return {
|
|
1839
|
+
content: [
|
|
1840
|
+
{
|
|
1841
|
+
type: "text",
|
|
1842
|
+
text: `${APEX} No order data in the window yet — connect Stripe or send purchase events.`,
|
|
1843
|
+
},
|
|
1844
|
+
],
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const lines = [
|
|
1848
|
+
`${APEX} Returns & refunds`,
|
|
1849
|
+
"═".repeat(40),
|
|
1850
|
+
`Refunded orders: ${d.refundedOrders} of ${d.ordersInWindow} ($${Math.round(d.refundedAmount).toLocaleString()})`,
|
|
1851
|
+
`Returns (goods back): ${d.returnsCompleted ?? "— (wire return_completed to unlock)"}`,
|
|
1852
|
+
`Refund cycle: ${d.refundCycleHours != null ? `${d.refundCycleHours}h median` : "—"} · AOV: ${d.aov != null ? `$${d.aov}` : "—"} · Repeat: ${d.repeatCustomers} of ${d.purchasingCustomers} customers`,
|
|
1853
|
+
];
|
|
1854
|
+
if (d.reasons.length > 0) {
|
|
1855
|
+
lines.push("", "Reasons:", ...d.reasons.map((r) => ` • ${r.reason}: ${r.count}`));
|
|
1856
|
+
}
|
|
1857
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1858
|
+
},
|
|
1859
|
+
},
|
|
1860
|
+
get_marketplace_metrics: {
|
|
1861
|
+
description: `${APEX} — Marketplace health across six pillars: liquidity (search→transaction, match rate, zero-result searches, unfulfilled demand), supply & demand balance, economics (GMV vs the take — fee_amount is the platform's revenue, GMV never inflates LTV), buyer retention, trust rates, and concentration risk. Sliceable by category / geo / price_band (low-volume slices are suppressed for privacy). Metrics whose events aren't wired come back as unwired hints, never fabricated zeros.`,
|
|
1862
|
+
schema: z.object({
|
|
1863
|
+
days: z.number().optional().describe("Trailing window in days (default 90, max 180)."),
|
|
1864
|
+
start: z
|
|
1865
|
+
.string()
|
|
1866
|
+
.optional()
|
|
1867
|
+
.describe("Explicit window start, YYYY-MM-DD (use with end — overrides days)."),
|
|
1868
|
+
end: z
|
|
1869
|
+
.string()
|
|
1870
|
+
.optional()
|
|
1871
|
+
.describe("Explicit window end, YYYY-MM-DD (use with start)."),
|
|
1872
|
+
category: z.string().optional().describe("Slice: listing category."),
|
|
1873
|
+
geo: z.string().optional().describe("Slice: transaction geo."),
|
|
1874
|
+
priceBand: z.string().optional().describe("Slice: price band (e.g. '100-250')."),
|
|
1875
|
+
}),
|
|
1876
|
+
handler: async ({ days, start, end, category, geo, priceBand, }) => {
|
|
1877
|
+
const params = new URLSearchParams();
|
|
1878
|
+
if (days)
|
|
1879
|
+
params.set("days", String(days));
|
|
1880
|
+
if (start && end) {
|
|
1881
|
+
params.set("start", start);
|
|
1882
|
+
params.set("end", end);
|
|
1883
|
+
}
|
|
1884
|
+
if (category)
|
|
1885
|
+
params.set("category", category);
|
|
1886
|
+
if (geo)
|
|
1887
|
+
params.set("geo", geo);
|
|
1888
|
+
if (priceBand)
|
|
1889
|
+
params.set("price_band", priceBand);
|
|
1890
|
+
const qs = params.toString();
|
|
1891
|
+
const res = await apiGet(`/api/marketplace/metrics${qs ? `?${qs}` : ""}`);
|
|
1892
|
+
const d = res.data;
|
|
1893
|
+
if (!d?.hasData) {
|
|
1894
|
+
return {
|
|
1895
|
+
content: [
|
|
1896
|
+
{
|
|
1897
|
+
type: "text",
|
|
1898
|
+
text: `${APEX} No marketplace events yet — wire seller_signup, listing_created, search, offer_submitted, and transaction_completed (with fee_amount, buyer_id, seller_id).`,
|
|
1899
|
+
},
|
|
1900
|
+
],
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
if (d.sliceSuppressed) {
|
|
1904
|
+
return {
|
|
1905
|
+
content: [
|
|
1906
|
+
{
|
|
1907
|
+
type: "text",
|
|
1908
|
+
text: `${APEX} That slice has too few participants to report honestly (privacy floor). Broaden the slice.`,
|
|
1909
|
+
},
|
|
1910
|
+
],
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
const money = (n) => n == null ? "—" : `$${Math.round(n).toLocaleString()}`;
|
|
1914
|
+
const L = d.liquidity;
|
|
1915
|
+
const lines = [
|
|
1916
|
+
`${APEX} Marketplace metrics`,
|
|
1917
|
+
"═".repeat(40),
|
|
1918
|
+
`Liquidity: ${L.searchersWhoTransacted}/${L.searchers} searchers transacted (7d)` +
|
|
1919
|
+
(L.searchesWithResultCount > 0
|
|
1920
|
+
? ` · ${L.zeroResultSearches}/${L.searchesWithResultCount} zero-result searches`
|
|
1921
|
+
: "") +
|
|
1922
|
+
` · ${L.transactions} transactions / ${L.offers} offers` +
|
|
1923
|
+
(L.medianHoursToMatch != null ? ` · ${L.medianHoursToMatch}h median to match` : "") +
|
|
1924
|
+
` · ${L.buyersUnfulfilled}/${L.buyersWithIntent} buyers unfulfilled`,
|
|
1925
|
+
`Supply & demand: ${d.supplyDemand.activeBuyers} buyers · ${d.supplyDemand.activeSellers} sellers (${d.supplyDemand.sellersWithTransaction} transacting) · ${d.supplyDemand.newListings} new listings`,
|
|
1926
|
+
`Economics: GMV ${money(d.economics.gmv)} (refund-adj ${money(d.economics.refundAdjustedGmv)}) · take ${money(d.economics.netRevenue)}${d.economics.takeRate != null ? ` (${Math.round(d.economics.takeRate * 1000) / 10}%)` : ""} · contribution ${money(d.economics.contributionMargin)} · payouts ${money(d.economics.sellerPayouts)} · AOV ${money(d.economics.aov)}`,
|
|
1927
|
+
d.demandMix
|
|
1928
|
+
? `Demand mix: paid ${money(d.demandMix.paidGmv)} · organic ${money(d.demandMix.organicGmv)} · unattributed ${money(d.demandMix.unattributedGmv)} (buyer first-touch)`
|
|
1929
|
+
: "Demand mix: — (needs identified buyers with acquisition attribution)",
|
|
1930
|
+
`Trust: ${d.trust.canceled} cancels · ${d.trust.refunded} refunds · ${d.trust.disputes} disputes over ${d.trust.transactions} transactions` +
|
|
1931
|
+
(d.trust.averageRating != null ? ` · ${d.trust.averageRating}/5 avg rating` : ""),
|
|
1932
|
+
d.concentration.suppressed
|
|
1933
|
+
? `Concentration: needs ≥5 sellers (currently ${d.concentration.sellerCount}) for an honest read`
|
|
1934
|
+
: `Concentration: top 10% of sellers = ${Math.round((d.concentration.topSellerGmvShare?.top10 ?? 0) * 1000) / 10}% of GMV (${d.concentration.sellerCount} sellers)`,
|
|
1935
|
+
];
|
|
1936
|
+
if (!d.wired.searches)
|
|
1937
|
+
lines.push("(search not wired — liquidity is partial)");
|
|
1938
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1939
|
+
},
|
|
1940
|
+
},
|
|
1602
1941
|
suggest_experiment: {
|
|
1603
1942
|
description: `${APEX} — Get smart experiment suggestions based on context. Analyzes your assumptions, past experiments, and confidence gaps to recommend what to test next.`,
|
|
1604
1943
|
schema: z.object({
|
|
@@ -2079,19 +2418,20 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
2079
2418
|
},
|
|
2080
2419
|
},
|
|
2081
2420
|
identify_user: {
|
|
2082
|
-
description: `${APEX} — Identify a user by email for identity stitching. Works identically to the SDK's identify() and the snippet's apex.identify(). Links an anonymous visitor to a known email/lead.`,
|
|
2421
|
+
description: `${APEX} — Identify a user by email for identity stitching. Works identically to the SDK's identify() and the snippet's apex.identify(). Links an anonymous visitor to a known email/lead. Pass avatarUrl (a canonical attribute) to give the Contact a profile photo that renders across Apex — Customers list, detail page, and Live Customers widget.`,
|
|
2083
2422
|
schema: z.object({
|
|
2084
2423
|
email: z.string().describe("User email address"),
|
|
2085
2424
|
name: z.string().optional().describe("User name"),
|
|
2086
2425
|
company: z.string().optional().describe("Company name"),
|
|
2426
|
+
avatarUrl: z.string().optional().describe("Profile photo URL (https). Maps to the canonical avatar_url attribute; renders across Apex. Validated on write — non-https/invalid URLs are dropped and the UI falls back to initials. From OIDC logins this is the `picture` claim (`photoURL` in Firebase)."),
|
|
2087
2427
|
metadata: z.record(z.unknown()).optional().describe("Additional traits"),
|
|
2088
2428
|
}),
|
|
2089
|
-
handler: async ({ email, name, company, metadata }) => {
|
|
2429
|
+
handler: async ({ email, name, company, avatarUrl, metadata }) => {
|
|
2090
2430
|
await apiPost("/api/identity/stitch", {
|
|
2091
2431
|
visitorId: `mcp-${email}`,
|
|
2092
2432
|
email,
|
|
2093
2433
|
workspaceKey: (process.env.APEX_WORKSPACE_KEY || process.env.APEX_PROJECT_KEY) || "default",
|
|
2094
|
-
metadata: { name, company, ...metadata, source: "mcp" },
|
|
2434
|
+
metadata: { name, company, avatar_url: avatarUrl, ...metadata, source: "mcp" },
|
|
2095
2435
|
});
|
|
2096
2436
|
return {
|
|
2097
2437
|
content: [{
|
|
@@ -2150,6 +2490,52 @@ Events fired through this tool carry a stable synthetic visitorId (mcp-agent-*,
|
|
|
2150
2490
|
};
|
|
2151
2491
|
},
|
|
2152
2492
|
},
|
|
2493
|
+
define_event: {
|
|
2494
|
+
description: `${APEX} — Define a governed custom event in the workspace's Schema (the customer-extensible registry). Promotes one of the merchant's own events to a first-class, typed, versioned definition that behaves like a canonical Apex Spec event (selectable as a journey trigger/goal, resolvable as comm tokens, validated at ingest). Names must be snake_case and CANNOT shadow a canonical Apex Spec event — call get_event_spec first and reuse the canonical event when one fits. Propose the schema from the merchant's observed payloads (get_schema) and codebase, then confirm with them before defining.`,
|
|
2495
|
+
schema: z.object({
|
|
2496
|
+
name: z.string().describe("snake_case event name, not already in the Apex Spec"),
|
|
2497
|
+
description: z.string().describe("One sentence: when this event fires"),
|
|
2498
|
+
fields: z
|
|
2499
|
+
.record(z.string())
|
|
2500
|
+
.describe('Field name -> type. Types: "string" | "number" | "boolean" | "object" | "array"; append "?" for optional (e.g. {"widget_id":"string","width":"number?"})'),
|
|
2501
|
+
scope: z
|
|
2502
|
+
.enum(["workspace", "org"])
|
|
2503
|
+
.optional()
|
|
2504
|
+
.describe("workspace (default) or org (shared across the org's workspaces)"),
|
|
2505
|
+
primary_metric: z
|
|
2506
|
+
.boolean()
|
|
2507
|
+
.optional()
|
|
2508
|
+
.describe("True if this is a conversion/outcome that can be a journey goal"),
|
|
2509
|
+
recommended_transport: z
|
|
2510
|
+
.enum(["client", "server", "either"])
|
|
2511
|
+
.optional()
|
|
2512
|
+
.describe("Where to fire it: server for money-truth, client for intent/engagement"),
|
|
2513
|
+
}),
|
|
2514
|
+
handler: async (args) => {
|
|
2515
|
+
const res = await apiPost("/api/schema/events", {
|
|
2516
|
+
name: args.name,
|
|
2517
|
+
description: args.description,
|
|
2518
|
+
fields: args.fields,
|
|
2519
|
+
scope: args.scope,
|
|
2520
|
+
primaryMetric: args.primaryMetric,
|
|
2521
|
+
recommendedTransport: args.recommendedTransport,
|
|
2522
|
+
});
|
|
2523
|
+
if (res.error) {
|
|
2524
|
+
const hint = res.suggestion
|
|
2525
|
+
? ` Use the canonical event "${res.suggestion}" instead.`
|
|
2526
|
+
: "";
|
|
2527
|
+
return { content: [{ type: "text", text: `Could not define "${args.name}": ${res.error}.${hint}` }] };
|
|
2528
|
+
}
|
|
2529
|
+
return {
|
|
2530
|
+
content: [
|
|
2531
|
+
{
|
|
2532
|
+
type: "text",
|
|
2533
|
+
text: `Defined governed event "${res.name}" (SchemaVer ${res.schemaVer ?? "1-0-0"}, draft). It's now selectable as a trigger/goal and resolvable as comm tokens once published.`,
|
|
2534
|
+
},
|
|
2535
|
+
],
|
|
2536
|
+
};
|
|
2537
|
+
},
|
|
2538
|
+
},
|
|
2153
2539
|
log_prediction: {
|
|
2154
2540
|
description: `${APEX} — Log a prediction about a metric change. Feeds the calibration loop. Before calling, present the metric, expected change, and confidence as options the user can pick from or adjust.`,
|
|
2155
2541
|
schema: z.object({
|
|
@@ -3889,5 +4275,136 @@ _Suggest the next step the user should tackle based on what's incomplete in the
|
|
|
3889
4275
|
};
|
|
3890
4276
|
},
|
|
3891
4277
|
},
|
|
4278
|
+
// ─── Conversions & Targets (Goals & Conversions Convergence — PR8) ──────────
|
|
4279
|
+
// A Conversion is the merchant-facing definition of a valued action (the
|
|
4280
|
+
// owned-event matcher managed on /dashboard/conversions). A Target is an
|
|
4281
|
+
// aggregate KPI number to hit by a date. "Goal" is retired as a noun.
|
|
4282
|
+
list_conversions: {
|
|
4283
|
+
description: `${APEX} — List the workspace's Conversions (the owned-event definitions of valued actions, e.g. "Purchase", "Signup"). Call this when the user asks what conversions are defined, or before creating an experiment/journey that needs an objective. The one flagged primary drives default experiment objectives, funnel metrics, and scoring.`,
|
|
4284
|
+
schema: z.object({}),
|
|
4285
|
+
handler: async () => {
|
|
4286
|
+
const conversions = await apiGet("/api/conversion-goals");
|
|
4287
|
+
const active = conversions.filter((c) => !c.archivedAt);
|
|
4288
|
+
if (active.length === 0) {
|
|
4289
|
+
return {
|
|
4290
|
+
content: [
|
|
4291
|
+
{
|
|
4292
|
+
type: "text",
|
|
4293
|
+
text: "No conversions defined yet. Use create_conversion to define one (e.g. a custom_event named 'purchase').",
|
|
4294
|
+
},
|
|
4295
|
+
],
|
|
4296
|
+
};
|
|
4297
|
+
}
|
|
4298
|
+
const lines = active.map((c) => `• ${c.name} (${c.id}) — ${c.type}${c.eventName ? ` [${c.eventName}]` : ""}${c.isPrimary ? " ★ primary" : ""}`);
|
|
4299
|
+
return {
|
|
4300
|
+
content: [
|
|
4301
|
+
{
|
|
4302
|
+
type: "text",
|
|
4303
|
+
text: `${active.length} conversion${active.length === 1 ? "" : "s"}:\n\n${lines.join("\n")}`,
|
|
4304
|
+
},
|
|
4305
|
+
],
|
|
4306
|
+
};
|
|
4307
|
+
},
|
|
4308
|
+
},
|
|
4309
|
+
create_conversion: {
|
|
4310
|
+
description: `${APEX} — Define a new Conversion (an owned-event definition of a valued action). Most conversions are a custom_event with an eventName (e.g. "purchase", "signup") matching an Apex Spec event you send via track. Set isPrimary to make it the default objective for experiments and the headline funnel metric.`,
|
|
4311
|
+
schema: z.object({
|
|
4312
|
+
name: z.string().describe('Human label, e.g. "Purchase" or "Signup"'),
|
|
4313
|
+
type: z
|
|
4314
|
+
.enum([
|
|
4315
|
+
"form_submit",
|
|
4316
|
+
"click",
|
|
4317
|
+
"pageview",
|
|
4318
|
+
"custom_event",
|
|
4319
|
+
"file_download",
|
|
4320
|
+
"engaged_time",
|
|
4321
|
+
])
|
|
4322
|
+
.describe("How the conversion is detected. custom_event is the most common (set eventName)."),
|
|
4323
|
+
eventName: z
|
|
4324
|
+
.string()
|
|
4325
|
+
.optional()
|
|
4326
|
+
.describe("For custom_event: the Apex Spec event name to match, e.g. 'purchase'."),
|
|
4327
|
+
selector: z
|
|
4328
|
+
.string()
|
|
4329
|
+
.optional()
|
|
4330
|
+
.describe("For click/form_submit: a CSS selector to match."),
|
|
4331
|
+
targetUrl: z
|
|
4332
|
+
.string()
|
|
4333
|
+
.optional()
|
|
4334
|
+
.describe("For pageview: the URL (or path) that counts as a completion."),
|
|
4335
|
+
isPrimary: z
|
|
4336
|
+
.boolean()
|
|
4337
|
+
.optional()
|
|
4338
|
+
.describe("Make this the primary conversion (only one can be primary)."),
|
|
4339
|
+
}),
|
|
4340
|
+
handler: async (args) => {
|
|
4341
|
+
const payload = {
|
|
4342
|
+
name: args.name,
|
|
4343
|
+
type: args.type,
|
|
4344
|
+
metricType: "binary",
|
|
4345
|
+
isPrimary: args.isPrimary ?? false,
|
|
4346
|
+
...(args.eventName ? { eventName: args.eventName } : {}),
|
|
4347
|
+
...(args.selector ? { selector: args.selector } : {}),
|
|
4348
|
+
...(args.targetUrl ? { targetUrl: args.targetUrl } : {}),
|
|
4349
|
+
};
|
|
4350
|
+
const data = await apiPost("/api/conversion-goals", payload);
|
|
4351
|
+
return {
|
|
4352
|
+
content: [
|
|
4353
|
+
{
|
|
4354
|
+
type: "text",
|
|
4355
|
+
text: `Conversion created.\n\n${JSON.stringify(data, null, 2)}`,
|
|
4356
|
+
},
|
|
4357
|
+
],
|
|
4358
|
+
};
|
|
4359
|
+
},
|
|
4360
|
+
},
|
|
4361
|
+
set_primary_conversion: {
|
|
4362
|
+
description: `${APEX} — Mark a Conversion as the primary one. Experiments without an explicit objective default to the primary conversion, and the headline funnel + scoring use it. Pass the conversion id from list_conversions.`,
|
|
4363
|
+
schema: z.object({
|
|
4364
|
+
conversionId: z.string().describe("The conversion id to promote (from list_conversions)."),
|
|
4365
|
+
}),
|
|
4366
|
+
handler: async ({ conversionId }) => {
|
|
4367
|
+
const data = await apiPatch("/api/conversion-goals", { id: conversionId, isPrimary: true });
|
|
4368
|
+
return {
|
|
4369
|
+
content: [
|
|
4370
|
+
{
|
|
4371
|
+
type: "text",
|
|
4372
|
+
text: `Primary conversion set.\n\n${JSON.stringify(data, null, 2)}`,
|
|
4373
|
+
},
|
|
4374
|
+
],
|
|
4375
|
+
};
|
|
4376
|
+
},
|
|
4377
|
+
},
|
|
4378
|
+
create_target: {
|
|
4379
|
+
description: `${APEX} — Create a Target: an aggregate KPI number to hit by a date (MRR, pipeline, "1,000 signups by Q3"). Optionally bind it to a Conversion (sourceConversionId from list_conversions) to auto-count progress from that conversion's completions; otherwise it's a manual number you update yourself.`,
|
|
4380
|
+
schema: z.object({
|
|
4381
|
+
metric: z.string().describe('What you\'re tracking, e.g. "MRR", "Signups", "Pipeline".'),
|
|
4382
|
+
target: z.number().describe("The numeric goal to reach."),
|
|
4383
|
+
deadline: z.string().describe('Target date, ISO "YYYY-MM-DD".'),
|
|
4384
|
+
sourceConversionId: z
|
|
4385
|
+
.string()
|
|
4386
|
+
.optional()
|
|
4387
|
+
.describe("Optional: bind to a Conversion (from list_conversions) to auto-track progress."),
|
|
4388
|
+
}),
|
|
4389
|
+
handler: async (args) => {
|
|
4390
|
+
const payload = {
|
|
4391
|
+
metric: args.metric,
|
|
4392
|
+
target: args.target,
|
|
4393
|
+
deadline: args.deadline,
|
|
4394
|
+
...(args.sourceConversionId
|
|
4395
|
+
? { sourceConversionId: args.sourceConversionId }
|
|
4396
|
+
: {}),
|
|
4397
|
+
};
|
|
4398
|
+
const data = await apiPost("/api/targets", payload);
|
|
4399
|
+
return {
|
|
4400
|
+
content: [
|
|
4401
|
+
{
|
|
4402
|
+
type: "text",
|
|
4403
|
+
text: `Target created.\n\n${JSON.stringify(data, null, 2)}`,
|
|
4404
|
+
},
|
|
4405
|
+
],
|
|
4406
|
+
};
|
|
4407
|
+
},
|
|
4408
|
+
},
|
|
3892
4409
|
};
|
|
3893
4410
|
//# sourceMappingURL=tools.js.map
|