@sentientui/mcp 0.19.0 → 0.20.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/lib.cjs CHANGED
@@ -41,6 +41,8 @@ __export(lib_exports, {
41
41
  ApiClient: () => ApiClient,
42
42
  ApiError: () => ApiError,
43
43
  PUBLIC_UI_RESOURCE_URIS: () => PUBLIC_UI_RESOURCE_URIS,
44
+ TOOL_CATALOG: () => TOOL_CATALOG,
45
+ TOOL_NAMES: () => TOOL_NAMES,
44
46
  createMcpServer: () => createMcpServer
45
47
  });
46
48
  module.exports = __toCommonJS(lib_exports);
@@ -56,9 +58,10 @@ var import_zod = require("zod");
56
58
 
57
59
  // src/api-client.ts
58
60
  var ApiError = class extends Error {
59
- constructor(status, message) {
61
+ constructor(status, message, detail) {
60
62
  super(message);
61
63
  this.status = status;
64
+ this.detail = detail;
62
65
  this.name = "ApiError";
63
66
  }
64
67
  };
@@ -75,8 +78,14 @@ var ApiClient = class {
75
78
  async post(path, body) {
76
79
  return this.request("POST", path, body);
77
80
  }
81
+ async patch(path, body) {
82
+ return this.request("PATCH", path, body);
83
+ }
84
+ async delete(path, body) {
85
+ return this.request("DELETE", path, body);
86
+ }
78
87
  async request(method, path, body, isRetry = false) {
79
- var _a, _b;
88
+ var _a, _b, _c;
80
89
  const res = await fetch(`${this.baseUrl}/v1/mgmt${path}`, {
81
90
  method,
82
91
  headers: __spreadValues({
@@ -93,7 +102,9 @@ var ApiClient = class {
93
102
  return this.request(method, path, body, true);
94
103
  }
95
104
  }
96
- throw new ApiError(res.status, String((_b = (_a = errBody.error) != null ? _a : errBody.message) != null ? _b : res.statusText));
105
+ const code = String((_b = (_a = errBody.error) != null ? _a : errBody.message) != null ? _b : res.statusText);
106
+ const human = (_c = errBody.message) != null ? _c : errBody.reason;
107
+ throw new ApiError(res.status, code, typeof human === "string" && human !== code ? human : void 0);
97
108
  }
98
109
  };
99
110
 
@@ -165,7 +176,7 @@ function localDate(iso, timeZone) {
165
176
  }
166
177
  function apiErrorGuidance(err, extra) {
167
178
  if (extra && Object.prototype.hasOwnProperty.call(extra, err.message)) {
168
- return extra[err.message];
179
+ return extra[err.message] + detailSuffix(err);
169
180
  }
170
181
  switch (err.message) {
171
182
  case "insufficient_scope":
@@ -174,6 +185,15 @@ function apiErrorGuidance(err, extra) {
174
185
  return "Demo mode is read-only. Create a SentientUI account and sign in (or use a project server key) to make changes.";
175
186
  case "insufficient_role":
176
187
  return "Your account role does not permit this action \u2014 it needs the account owner or an admin.";
188
+ // The mgmt write routes answer a viewer with `role_required` (and origins
189
+ // with `not_authorized`), not `insufficient_role` — so every role refusal
190
+ // fell through to the generic 403 "check your key has access" advice and
191
+ // sent the agent re-authenticating for a permission it already had wrong.
192
+ case "role_required":
193
+ case "not_authorized":
194
+ return `Your role on this project cannot make this change \u2014 it needs an editor or admin.${detailSuffix(err)}`;
195
+ case "ai_unavailable":
196
+ return "AI generation is unavailable on the SentientUI server right now (no model is configured). Nothing was queued \u2014 retry later.";
177
197
  default:
178
198
  break;
179
199
  }
@@ -205,10 +225,25 @@ function apiErrorGuidance(err, extra) {
205
225
  return `This feature requires a higher plan (${err.message}). Upgrade your SentientUI plan, then try again.`;
206
226
  }
207
227
  if (err.status === 403) {
208
- return `Access denied (${err.message}). Check that your key or login has access to this project.`;
228
+ return `Access denied (${err.message}). Check that your key or login has access to this project.${detailSuffix(err)}`;
229
+ }
230
+ if (err.status === 404) {
231
+ return `Not found (${err.message}). The project, component, variant, goal or cell does not exist, or this credential cannot see it (an sk_ server key sees only its own project). Re-read the ids with list_projects, list_components, list_goals or get_cell_matrix and retry with an exact id.` + detailSuffix(err);
232
+ }
233
+ if (err.status === 400) {
234
+ return `The API rejected the arguments (${err.message}).${detailSuffix(err)} Correct the input and retry \u2014 repeating the same call will fail the same way.`;
235
+ }
236
+ if (err.status === 409) {
237
+ return `Conflict (${err.message}): the target's current state does not allow this.${detailSuffix(err)} Read its current state first, then retry the step that makes this valid.`;
238
+ }
239
+ if (err.status === 502 || err.status === 503 || err.status === 504) {
240
+ return `SentientUI is temporarily unavailable (${err.status} ${err.message}). Nothing changed \u2014 wait a moment and retry.`;
209
241
  }
210
242
  return null;
211
243
  }
244
+ function detailSuffix(err) {
245
+ return err.detail ? ` Server says: ${untrusted(err.detail, 300)}` : "";
246
+ }
212
247
  async function settled(p) {
213
248
  try {
214
249
  return { ok: true, value: await p };
@@ -321,6 +356,37 @@ function registerProjectTools(server, client) {
321
356
  };
322
357
  }, CREATE_PROJECT_GUIDANCE)
323
358
  );
359
+ server.registerTool(
360
+ "add_origin",
361
+ {
362
+ title: "Add allowed origin",
363
+ description: "Allow-list a site origin (scheme + host, e.g. https://www.example.com) so the SDK's events from it are accepted. Idempotent: an origin already on the list is left as is. Removing an origin is dashboard-only. Needs an editor/admin login or the project's sk_ server key. Confirm the exact origin with the user first \u2014 an allowed origin can send events into this project.",
364
+ inputSchema: {
365
+ projectId: projectIdSchema,
366
+ origin: import_zod2.z.string().min(1).max(500).describe("The site origin, e.g. https://www.example.com (a pasted page URL is reduced to its origin)")
367
+ },
368
+ outputSchema: {
369
+ origins: import_zod2.z.array(import_zod2.z.string()).describe("The full allow-list after the change")
370
+ },
371
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
372
+ },
373
+ withApiErrorGuidance(async ({ projectId, origin }) => {
374
+ var _a;
375
+ const id = encodeURIComponent(projectId);
376
+ const res = await client.post(`/projects/${id}/origins`, { origin });
377
+ const origins = (_a = res.origins) != null ? _a : [];
378
+ return {
379
+ content: [{
380
+ type: "text",
381
+ text: `Allowed origins: ${origins.map((o) => untrusted(o)).join(", ") || "(none)"}. Events from these origins are accepted (other API instances pick the change up within ~30 seconds, when their key cache expires).`
382
+ }],
383
+ structuredContent: { origins }
384
+ };
385
+ }, {
386
+ invalid_origin: "That is not a usable origin. Pass a scheme and host, e.g. https://www.example.com.",
387
+ origin_required: "An origin is required, e.g. https://www.example.com."
388
+ })
389
+ );
324
390
  server.registerTool(
325
391
  "list_projects",
326
392
  {
@@ -365,13 +431,16 @@ function registerProjectTools(server, client) {
365
431
  "get_project_stats",
366
432
  {
367
433
  title: "Project health stats",
368
- description: "Get health stats for a project: event volume, session count, agent calls, and status.",
434
+ description: "Get ingestion health for a project: status, events and sessions in the last 24 hours, the time of the last event, and the all-time count of calls to the SentientUI agent API (/v1/agent) \u2014 NOT MCP tool calls.",
369
435
  inputSchema: { projectId: projectIdSchema },
370
436
  outputSchema: {
371
437
  status: import_zod2.z.string().describe("Overall project health status"),
372
438
  events24h: import_zod2.z.number().describe("Events in the last 24 hours"),
373
439
  sessions24h: import_zod2.z.number().describe("Sessions in the last 24 hours"),
374
- agentCalls: import_zod2.z.number().describe("Total agent (MCP/API) calls"),
440
+ // Was "Total agent (MCP/API) calls" — but the count comes from
441
+ // agent_events, which only the /v1/agent endpoint writes. MCP tool calls
442
+ // are never in it, so an agent read its own activity as missing.
443
+ agentCalls: import_zod2.z.number().describe("All-time calls to the /v1/agent agent API (not MCP tool calls)"),
375
444
  lastEventAt: import_zod2.z.string().nullable().describe("ISO timestamp of the last event, or null")
376
445
  },
377
446
  annotations: {
@@ -388,7 +457,7 @@ function registerProjectTools(server, client) {
388
457
  `Status: ${stats.status}`,
389
458
  `Events (24h): ${stats.events24h}`,
390
459
  `Sessions (24h): ${stats.sessions24h}`,
391
- `Agent calls (total): ${stats.agentCalls}`,
460
+ `Agent API calls (/v1/agent, all-time): ${stats.agentCalls}`,
392
461
  `Last event: ${(_a = stats.lastEventAt) != null ? _a : "never"}`
393
462
  ].join("\n");
394
463
  return {
@@ -1135,7 +1204,10 @@ function registerPersonaTools(server, client) {
1135
1204
  "get_persona_breakdown",
1136
1205
  {
1137
1206
  title: "Persona breakdown",
1138
- description: "Get the distribution of visitor persona clusters with session counts and reliability scores." + UNTRUSTED_FIELDS_NOTE,
1207
+ // Takes no range/from/to on purpose: the API computes it over every
1208
+ // retained session and has no window. Saying so stops an agent from
1209
+ // reading it beside a 7d get_variant_performance as the same period.
1210
+ description: "Get the distribution of visitor persona clusters with session counts and reliability scores, over ALL retained human sessions (not windowed \u2014 this has no range input, unlike the conversion tools)." + UNTRUSTED_FIELDS_NOTE,
1139
1211
  inputSchema: { projectId: projectIdSchema },
1140
1212
  _meta: uiMeta("persona-breakdown"),
1141
1213
  outputSchema: {
@@ -1196,8 +1268,13 @@ function registerGoalTools(server, client) {
1196
1268
  server.registerTool(
1197
1269
  "get_goal_funnel",
1198
1270
  {
1199
- title: "Goal funnel",
1200
- description: "Get goal hit counts and unique-session conversion rates for a window (default: last 30 calendar days), plus a per-variant breakdown. NOTE: the per-variant completionRate values are ALL-TIME (bounded only by plan retention), NOT windowed \u2014 a variant completion rate needs the full assignment history as its denominator, so it does not change when you narrow `range`, and retired components keep appearing in it. Only hits/uniqueSessions/conversionRate/revenue respect the window. This is a flat per-goal list \u2014 for multi-step funnel drop-off, use get_funnel_report." + UNTRUSTED_FIELDS_NOTE,
1271
+ // Named get_goal_funnel for compatibility (renaming a published tool
1272
+ // breaks every saved prompt and host allow-list), but it is NOT a funnel:
1273
+ // agents asked it for step drop-off and reported per-goal rates as funnel
1274
+ // stages. The title and the first words of the description now say what
1275
+ // it is, and point to the real funnel tool.
1276
+ title: "Goal conversions (per goal \u2014 not a step funnel)",
1277
+ description: "Per-goal conversion summary \u2014 NOT a step funnel (for step-by-step drop-off use list_funnels + get_funnel_report). Gets goal hit counts and unique-session conversion rates for a window (default: last 30 calendar days), plus a per-variant breakdown. NOTE: the per-variant completionRate values are ALL-TIME (bounded only by plan retention), NOT windowed \u2014 a variant completion rate needs the full assignment history as its denominator, so it does not change when you narrow `range`, and retired components keep appearing in it. Only hits/uniqueSessions/conversionRate/revenue respect the window. Per-variant rates are descriptive, not joined to the evidence verdicts \u2014 for whether an arm is ahead, use get_variant_performance." + UNTRUSTED_FIELDS_NOTE,
1201
1278
  inputSchema: __spreadValues({ projectId: projectIdSchema }, rangeInputSchema("30d")),
1202
1279
  _meta: uiMeta("goal-funnel"),
1203
1280
  outputSchema: {
@@ -1587,7 +1664,9 @@ function registerLayoutTools(server, client) {
1587
1664
  "get_layout_stats",
1588
1665
  {
1589
1666
  title: "Layout stats",
1590
- description: "Get per-persona section layout rankings and bandit reward weights." + UNTRUSTED_FIELDS_NOTE,
1667
+ // No window by design: these are the bandit's cumulative pulls and reward,
1668
+ // the state serving reads — not a report over a period.
1669
+ description: "Get per-persona section layout rankings with bandit pulls and average reward \u2014 the cumulative learning state serving uses (not windowed; there is no range input). avgReward is a reward signal, not a conversion rate." + UNTRUSTED_FIELDS_NOTE,
1591
1670
  inputSchema: { projectId: projectIdSchema },
1592
1671
  _meta: uiMeta("layout-stats"),
1593
1672
  outputSchema: {
@@ -1639,7 +1718,7 @@ function registerVariantWriteTools(server, client) {
1639
1718
  "create_variant",
1640
1719
  {
1641
1720
  title: "Create managed variant",
1642
- description: "Create a NO-CODE managed text variant for a component (content stored in SentientUI, rendered by <AdaptiveText>). Use this ONLY for text-only variants the user wants without a code change. For variants that will live in the codebase (full components \u2014 copy, markup, styling), use get_variant_brief and write the variant in code instead; those auto-register on deploy and do not need create_variant. Requires a paid plan (server keys are Starter+; anonymous demo tokens are read-only).",
1721
+ description: "Create a NO-CODE managed text variant for a component (content stored in SentientUI, rendered by <AdaptiveText>). Use this ONLY for text-only variants the user wants without a code change. For variants that will live in the codebase (full components \u2014 copy, markup, styling), use get_variant_brief and write the variant in code instead; those auto-register on deploy and do not need create_variant. Works on every plan with an editor/admin login or the project's sk_ server key (anonymous demo tokens are read-only). The variant is created as a DRAFT \u2014 it serves no traffic until activated with update_variant (status 'active'). Confirm the text with the user before calling.",
1643
1722
  inputSchema: {
1644
1723
  projectId: projectIdSchema,
1645
1724
  componentId: import_zod10.z.string().min(1).max(200).describe("The component ID to add a variant to"),
@@ -1673,7 +1752,7 @@ function registerVariantWriteTools(server, client) {
1673
1752
  return {
1674
1753
  content: [{
1675
1754
  type: "text",
1676
- text: `Managed text variant created: ${result.variantId} ("${result.displayName}") for component ${componentId}${contentNote}. It is in draft state \u2014 activate it from the dashboard. Reminder: this is a no-code managed variant; for code-native variants, edit the code instead (see get_variant_brief).`
1755
+ text: `Managed text variant created: ${result.variantId} ("${result.displayName}") for component ${componentId}${contentNote}. It is a draft and serves no traffic yet \u2014 after the user confirms, activate it with update_variant (status 'active'), or delete it with delete_variant_draft. Reminder: this is a no-code managed variant; for code-native variants, edit the code instead (see get_variant_brief).`
1677
1756
  }],
1678
1757
  structuredContent: {
1679
1758
  variantId: result.variantId,
@@ -1689,20 +1768,24 @@ function registerVariantWriteTools(server, client) {
1689
1768
  "pause_variant",
1690
1769
  {
1691
1770
  title: "Pause variant",
1692
- description: "Pause a variant, stopping traffic from being assigned to it.",
1771
+ description: "Pause a variant: no new traffic is assigned to it until it is resumed. This changes what live visitors see, so CONFIRM with the user before calling. Undo with resume_variant.",
1693
1772
  inputSchema: {
1694
1773
  projectId: projectIdSchema,
1695
- componentId: import_zod10.z.string().describe("The component ID"),
1696
- variantId: import_zod10.z.string().describe("The variant ID to pause")
1774
+ componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
1775
+ variantId: import_zod10.z.string().min(1).max(200).describe("The variant ID to pause")
1697
1776
  },
1698
1777
  outputSchema: {
1699
1778
  variantId: import_zod10.z.string(),
1700
1779
  componentId: import_zod10.z.string(),
1701
1780
  paused: import_zod10.z.literal(true).describe("The variant is now paused")
1702
1781
  },
1782
+ // destructiveHint was false, so hosts that gate destructive tools behind a
1783
+ // confirmation ran this without asking — yet it pulls a live arm out of
1784
+ // serving for every visitor. It is reversible (resume_variant), but the
1785
+ // hint is about the change to the environment, not about undo.
1703
1786
  annotations: {
1704
1787
  readOnlyHint: false,
1705
- destructiveHint: false,
1788
+ destructiveHint: true,
1706
1789
  idempotentHint: true,
1707
1790
  openWorldHint: false
1708
1791
  }
@@ -1713,17 +1796,194 @@ function registerVariantWriteTools(server, client) {
1713
1796
  return {
1714
1797
  content: [{
1715
1798
  type: "text",
1716
- text: `Variant ${variantId} in component ${componentId} has been paused. No new traffic will be assigned to it.`
1799
+ text: `Variant ${untrusted(variantId)} in component ${untrusted(componentId)} is paused. No new traffic will be assigned to it. Undo with resume_variant.`
1717
1800
  }],
1718
1801
  structuredContent: { variantId, componentId, paused: true }
1719
1802
  };
1720
1803
  })
1721
1804
  );
1805
+ server.registerTool(
1806
+ "resume_variant",
1807
+ {
1808
+ title: "Resume variant",
1809
+ description: "Resume a paused variant so it is assigned traffic again \u2014 works for a manual pause (pause_variant) and for a guardrail auto-pause (see list_guardrail_events). Resuming a guardrail-paused arm puts a variant the guardrail judged harmful back in front of visitors, so CONFIRM with the user first.",
1810
+ inputSchema: {
1811
+ projectId: projectIdSchema,
1812
+ componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
1813
+ variantId: import_zod10.z.string().min(1).max(200).describe("The paused variant ID to resume")
1814
+ },
1815
+ outputSchema: {
1816
+ variantId: import_zod10.z.string(),
1817
+ componentId: import_zod10.z.string(),
1818
+ paused: import_zod10.z.literal(false).describe("The variant is no longer paused")
1819
+ },
1820
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1821
+ },
1822
+ withApiErrorGuidance(async ({ projectId, componentId, variantId }) => {
1823
+ const id = encodeURIComponent(projectId);
1824
+ await client.post(`/projects/${id}/guardrails/${encodeURIComponent(componentId)}/unpause`, {
1825
+ variantIds: [variantId]
1826
+ });
1827
+ return {
1828
+ content: [{
1829
+ type: "text",
1830
+ text: `Variant ${untrusted(variantId)} in component ${untrusted(componentId)} is resumed and will be assigned traffic again.`
1831
+ }],
1832
+ structuredContent: { variantId, componentId, paused: false }
1833
+ };
1834
+ })
1835
+ );
1836
+ server.registerTool(
1837
+ "update_variant",
1838
+ {
1839
+ title: "Update managed variant",
1840
+ description: "Edit a NO-CODE managed variant (one created with create_variant or in the dashboard): its display name, its text, or its status \u2014 'active' starts serving it to visitors, 'draft' takes it back out of rotation. Code-native variants cannot be edited here (change the code). Overwriting text or activating changes what live visitors see, so CONFIRM with the user first. Omit a field to leave it unchanged.",
1841
+ inputSchema: {
1842
+ projectId: projectIdSchema,
1843
+ componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
1844
+ variantId: import_zod10.z.string().min(1).max(200).describe("The managed variant ID (dm_\u2026)"),
1845
+ displayName: import_zod10.z.string().min(1).max(200).optional().describe("New display name"),
1846
+ content: import_zod10.z.string().max(1e4).optional().describe("New text content (replaces the old text)"),
1847
+ status: import_zod10.z.enum(["draft", "active"]).optional().describe("'active' serves it; 'draft' stops serving it")
1848
+ },
1849
+ outputSchema: {
1850
+ variantId: import_zod10.z.string(),
1851
+ componentId: import_zod10.z.string(),
1852
+ changed: import_zod10.z.array(import_zod10.z.string()).describe("Fields that were sent for update"),
1853
+ status: import_zod10.z.enum(["draft", "active"]).nullable().describe("The new status, or null when unchanged")
1854
+ },
1855
+ // Destructive: the previous text is overwritten, not versioned.
1856
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1857
+ },
1858
+ withApiErrorGuidance(async ({ projectId, componentId, variantId, displayName, content, status }) => {
1859
+ const changed = [
1860
+ ...displayName !== void 0 ? ["displayName"] : [],
1861
+ ...content !== void 0 ? ["content"] : [],
1862
+ ...status !== void 0 ? ["status"] : []
1863
+ ];
1864
+ if (changed.length === 0) {
1865
+ return {
1866
+ content: [{ type: "text", text: "Nothing to update \u2014 pass displayName, content and/or status." }],
1867
+ isError: true
1868
+ };
1869
+ }
1870
+ const id = encodeURIComponent(projectId);
1871
+ await client.patch(`/projects/${id}/variants`, { componentId, variantId, displayName, content, status });
1872
+ const statusNote = status === "active" ? " It is now ACTIVE and will be assigned traffic." : status === "draft" ? " It is now a draft and no longer serves traffic." : "";
1873
+ return {
1874
+ content: [{
1875
+ type: "text",
1876
+ text: `Managed variant ${untrusted(variantId)} in component ${untrusted(componentId)} updated (${changed.join(", ")}).${statusNote}`
1877
+ }],
1878
+ structuredContent: { variantId, componentId, changed, status: status != null ? status : null }
1879
+ };
1880
+ }, {
1881
+ // PATCH matches managed variants only, so a code-native id answers a
1882
+ // bare not_found that read as "that variant does not exist".
1883
+ not_found: "No MANAGED variant with that id exists in this component. Code-native variants (declared in <Adaptive variants>) are edited in the code, not here \u2014 check ids with list_components (a wrong projectId answers the same way)."
1884
+ })
1885
+ );
1886
+ server.registerTool(
1887
+ "delete_variant_draft",
1888
+ {
1889
+ title: "Delete draft variant",
1890
+ description: "Permanently delete a managed variant that is still a DRAFT (never activated). Active variants cannot be deleted \u2014 pause or retire them instead. CONFIRM with the user first: this cannot be undone.",
1891
+ inputSchema: {
1892
+ projectId: projectIdSchema,
1893
+ componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
1894
+ variantId: import_zod10.z.string().min(1).max(200).describe("The draft managed variant ID (dm_\u2026)")
1895
+ },
1896
+ outputSchema: {
1897
+ variantId: import_zod10.z.string(),
1898
+ componentId: import_zod10.z.string(),
1899
+ deleted: import_zod10.z.literal(true)
1900
+ },
1901
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
1902
+ },
1903
+ withApiErrorGuidance(async ({ projectId, componentId, variantId }) => {
1904
+ const id = encodeURIComponent(projectId);
1905
+ await client.delete(`/projects/${id}/variants`, { componentId, variantId });
1906
+ return {
1907
+ content: [{
1908
+ type: "text",
1909
+ text: `Draft variant ${untrusted(variantId)} in component ${untrusted(componentId)} was deleted.`
1910
+ }],
1911
+ structuredContent: { variantId, componentId, deleted: true }
1912
+ };
1913
+ }, {
1914
+ // The API's DELETE matches managed drafts only, so an active or
1915
+ // code-native variant answers a bare not_found — say why.
1916
+ not_found: "No managed DRAFT variant with that id exists in this component. Active and code-native variants cannot be deleted \u2014 use pause_variant or retire_variant instead (check ids with list_components; a wrong projectId answers the same way)."
1917
+ })
1918
+ );
1919
+ server.registerTool(
1920
+ "retire_variant",
1921
+ {
1922
+ title: "Retire variant",
1923
+ description: "Retire (archive) an ACTIVE variant: it stops serving and moves to the archived list. Works for code-native variants that were removed from the code and linger. A promoted (pinned) variant must be unpromoted in the dashboard first. CONFIRM with the user first; undo with restore_variant.",
1924
+ inputSchema: {
1925
+ projectId: projectIdSchema,
1926
+ componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
1927
+ variantId: import_zod10.z.string().min(1).max(200).describe("The active variant ID to retire")
1928
+ },
1929
+ outputSchema: {
1930
+ variantId: import_zod10.z.string(),
1931
+ componentId: import_zod10.z.string(),
1932
+ status: import_zod10.z.literal("retired")
1933
+ },
1934
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1935
+ },
1936
+ withApiErrorGuidance(async ({ projectId, componentId, variantId }) => {
1937
+ const id = encodeURIComponent(projectId);
1938
+ await client.post(`/projects/${id}/variants/retire`, { componentId, variantId });
1939
+ return {
1940
+ content: [{
1941
+ type: "text",
1942
+ text: `Variant ${untrusted(variantId)} in component ${untrusted(componentId)} is retired and no longer serves. Undo with restore_variant.`
1943
+ }],
1944
+ structuredContent: { variantId, componentId, status: "retired" }
1945
+ };
1946
+ }, {
1947
+ variant_promoted: "This variant is promoted (pinned as the winner), so it cannot be retired. Unpromote it in the dashboard first, then retry.",
1948
+ not_found: "No ACTIVE variant with that id exists in this component (it may already be retired, or still a draft), or the projectId is wrong. Check ids with list_components."
1949
+ })
1950
+ );
1951
+ server.registerTool(
1952
+ "restore_variant",
1953
+ {
1954
+ title: "Restore variant",
1955
+ description: "Bring a RETIRED variant back to active so it serves again. For a managed draft that was never activated use update_variant (status 'active') instead. Restoring does not clear a pause \u2014 use resume_variant for that. CONFIRM with the user first.",
1956
+ inputSchema: {
1957
+ projectId: projectIdSchema,
1958
+ componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
1959
+ variantId: import_zod10.z.string().min(1).max(200).describe("The retired variant ID to restore")
1960
+ },
1961
+ outputSchema: {
1962
+ variantId: import_zod10.z.string(),
1963
+ componentId: import_zod10.z.string(),
1964
+ status: import_zod10.z.literal("active")
1965
+ },
1966
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1967
+ },
1968
+ withApiErrorGuidance(async ({ projectId, componentId, variantId }) => {
1969
+ const id = encodeURIComponent(projectId);
1970
+ await client.post(`/projects/${id}/variants/restore`, { componentId, variantId });
1971
+ return {
1972
+ content: [{
1973
+ type: "text",
1974
+ text: `Variant ${untrusted(variantId)} in component ${untrusted(componentId)} is active again. If it was also paused, call resume_variant.`
1975
+ }],
1976
+ structuredContent: { variantId, componentId, status: "active" }
1977
+ };
1978
+ })
1979
+ );
1722
1980
  server.registerTool(
1723
1981
  "refresh_insights",
1724
1982
  {
1725
1983
  title: "Refresh insights",
1726
- description: "Trigger fresh AI insight generation for a project. Returns immediately; use get_insights in ~15 seconds to see results.",
1984
+ // The plan and role gates were undocumented, so agents on Free called it
1985
+ // and got a bare 403 they could not explain to the user.
1986
+ description: "Trigger fresh AI insight generation for a project (a paid model call). Needs the Starter plan or higher and an editor/admin role (anonymous demo tokens are read-only); advisor recommendations additionally need Growth or higher. Returns immediately; call get_insights in ~15 seconds to see results.",
1727
1987
  inputSchema: { projectId: projectIdSchema },
1728
1988
  outputSchema: {
1729
1989
  projectId: import_zod10.z.string(),
@@ -1775,11 +2035,21 @@ var BEST_PRACTICE_PRIORS = {
1775
2035
  "Make the CTA specific and benefit-led.",
1776
2036
  "Add a single strong proof point; remove competing distractions."
1777
2037
  ],
1778
- marketplace: [
1779
- "Reduce choice overload: guide the visitor to a clear next step.",
1780
- "Surface trust and liquidity signals (ratings, counts, recency).",
1781
- "Make the primary action on each listing obvious.",
1782
- "Reassure on safety/guarantees near the point of decision."
2038
+ // Keys must be the project context types the API accepts (projects.ts
2039
+ // VALID_CONTEXT_TYPES: saas | ecommerce | marketing | landing | internal).
2040
+ // A `marketplace` key sat here that no project could ever have, while
2041
+ // marketing and internal projects silently fell through to the generic list.
2042
+ marketing: [
2043
+ 'Make the page answer "what is this and why should I care" in the first screen.',
2044
+ "Give each section one job and one next step; cut competing CTAs.",
2045
+ "Put the strongest proof (named customers, a hard number) near the primary CTA.",
2046
+ "Match the copy to where the visitor came from (campaign, search intent)."
2047
+ ],
2048
+ internal: [
2049
+ "Optimize for task completion, not persuasion: make the next step obvious.",
2050
+ "Reduce steps and fields; prefill what the system already knows.",
2051
+ "Use plain, specific labels over marketing copy.",
2052
+ "Make errors and empty states say exactly what to do next."
1783
2053
  ]
1784
2054
  };
1785
2055
  var GENERIC_PRIORS = [
@@ -2310,12 +2580,12 @@ function registerAgentTrafficTools(server, client) {
2310
2580
  "get_agent_traffic",
2311
2581
  {
2312
2582
  title: "Agent traffic",
2313
- description: "Which AI agents and crawlers are reading this site: totals by type (passive crawlers, agentic browsers, agent API calls), engine breakdown, and the paths they fetch most. Defaults to the last 30 days; pass from/to for a custom period (agent logs are retained 30 days on Free/Starter, 90 on Growth+). Agent traffic is tracked separately and never counted in conversion rate." + UNTRUSTED_FIELDS_NOTE,
2314
- inputSchema: {
2315
- projectId: projectIdSchema,
2316
- from: import_zod14.z.string().optional().describe("Period start (ISO 8601 date or timestamp). Defaults to 30 days ago."),
2317
- to: import_zod14.z.string().optional().describe("Period end (ISO 8601 date or timestamp). Defaults to now.")
2318
- },
2583
+ description: "Which AI agents and crawlers are reading this site: totals by type (passive crawlers, agentic browsers, agent API calls), engine breakdown, and the paths they fetch most. Defaults to the last 30 calendar days; pass range (7d/30d/90d/all) or from/to (YYYY-MM-DD) like every other windowed tool (agent logs are retained 30 days on Free/Starter, 90 on Growth+). Agent traffic is tracked separately and never counted in conversion rate." + UNTRUSTED_FIELDS_NOTE,
2584
+ // This tool alone took ad-hoc ISO from/to with no range token and echoed
2585
+ // no window, while every other windowed tool used rangeInputSchema — so
2586
+ // agents passed range:'7d' here, it was silently dropped, and 30 days of
2587
+ // traffic was reported as a week. The endpoint uses the shared resolver.
2588
+ inputSchema: __spreadValues({ projectId: projectIdSchema }, rangeInputSchema("30d")),
2319
2589
  outputSchema: {
2320
2590
  totals: import_zod14.z.object({ crawler: import_zod14.z.number(), api: import_zod14.z.number(), browser: import_zod14.z.number() }),
2321
2591
  engines: import_zod14.z.array(
@@ -2329,20 +2599,18 @@ function registerAgentTrafficTools(server, client) {
2329
2599
  })
2330
2600
  ),
2331
2601
  intents: import_zod14.z.object({ user: import_zod14.z.number(), search: import_zod14.z.number(), training: import_zod14.z.number(), other: import_zod14.z.number() }).describe("Crawler fetches by purpose: user = an assistant answering a real person live"),
2332
- topPaths: import_zod14.z.array(import_zod14.z.object({ path: import_zod14.z.string(), count: import_zod14.z.number(), engines: import_zod14.z.number() }))
2602
+ topPaths: import_zod14.z.array(import_zod14.z.object({ path: import_zod14.z.string(), count: import_zod14.z.number(), engines: import_zod14.z.number() })),
2603
+ window: windowOutputSchema
2333
2604
  },
2334
2605
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2335
2606
  },
2336
- withApiErrorGuidance(async ({ projectId, from, to }) => {
2607
+ withApiErrorGuidance(async ({ projectId, range, from, to }) => {
2337
2608
  const id = encodeURIComponent(projectId);
2338
- const qs = new URLSearchParams();
2339
- if (from) qs.set("from", from);
2340
- if (to) qs.set("to", to);
2341
- const query = qs.toString();
2342
2609
  const s = await client.get(
2343
- `/projects/${id}/agent-activity/summary${query ? `?${query}` : ""}`
2610
+ `/projects/${id}/agent-activity/summary${rangeQuery({ range, from, to })}`
2344
2611
  );
2345
- const structuredContent = { totals: s.totals, engines: s.engines, intents: s.intents, topPaths: s.topPaths };
2612
+ const structuredContent = { totals: s.totals, engines: s.engines, intents: s.intents, topPaths: s.topPaths, window: s.window };
2613
+ const win = windowLine(s.window);
2346
2614
  const total = s.totals.crawler + s.totals.api + s.totals.browser;
2347
2615
  if (total === 0) {
2348
2616
  return {
@@ -2354,6 +2622,7 @@ function registerAgentTrafficTools(server, client) {
2354
2622
  };
2355
2623
  }
2356
2624
  const lines = [
2625
+ ...win ? [win] : [],
2357
2626
  `Agent traffic: ${s.totals.crawler} crawler fetches, ${s.totals.api} agent API calls, ${s.totals.browser} agentic browser sessions.`,
2358
2627
  `Live user fetches: ${s.intents.user} \xB7 Search index: ${s.intents.search} \xB7 Training: ${s.intents.training}${s.intents.other ? ` \xB7 Other: ${s.intents.other}` : ""} (a "live user fetch" = an AI assistant reading your site to answer a real person).`,
2359
2628
  "",
@@ -2444,7 +2713,7 @@ var personaOutput = import_zod15.z.object({
2444
2713
  var cellOutput = import_zod15.z.object({
2445
2714
  slotId: import_zod15.z.string(),
2446
2715
  persona: import_zod15.z.string(),
2447
- status: import_zod15.z.string().describe("pending | generating | live | error | rejected"),
2716
+ status: import_zod15.z.string().describe("pending | generating | review (waiting for approval) | live | error | rejected"),
2448
2717
  requestedBy: import_zod15.z.string(),
2449
2718
  armId: import_zod15.z.string().nullable(),
2450
2719
  rationale: import_zod15.z.string().nullable(),
@@ -2484,6 +2753,10 @@ function registerCellTools(server, client) {
2484
2753
  personas: import_zod15.z.array(personaOutput),
2485
2754
  cells: import_zod15.z.array(cellOutput),
2486
2755
  autoFill: import_zod15.z.boolean(),
2756
+ // The API has always sent this; without it in the schema zod stripped
2757
+ // it, and generate_cell had no way to know a version would wait for a
2758
+ // human instead of publishing.
2759
+ reviewMode: import_zod15.z.boolean().optional().describe("True when generated versions wait for approval (review_cell) instead of publishing automatically"),
2487
2760
  // Nullable: a project with no persona set at all reports null, not a
2488
2761
  // string. That is the NORMAL state since the default vocabulary was
2489
2762
  // emptied, so a non-nullable schema failed output validation on most
@@ -2500,7 +2773,7 @@ function registerCellTools(server, client) {
2500
2773
  const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
2501
2774
  const lines = [
2502
2775
  `Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
2503
- `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${(_a = data.personaSource) != null ? _a : 'none (one "everyone" row)'}`
2776
+ `Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 review before publishing: ${data.reviewMode ? "ON (approve with review_cell)" : "off"} \xB7 persona source: ${(_a = data.personaSource) != null ? _a : 'none (one "everyone" row)'}`
2504
2777
  ];
2505
2778
  for (const p of data.personas) {
2506
2779
  const states = data.slots.map((s) => {
@@ -2585,7 +2858,12 @@ function registerCellTools(server, client) {
2585
2858
  "generate_cell",
2586
2859
  {
2587
2860
  title: "Generate a cell version",
2588
- description: "Queue AI generation of a version for one (persona, slot) cell. The worker writes it asynchronously (usually under a minute), brand-locked, and it goes live as a named option the optimizer explores. Also works to regenerate or retry a failed cell. Requires a paid plan (anonymous demo tokens are read-only).",
2861
+ // The old copy promised "goes live, usually under a minute" — but with
2862
+ // review mode on the worker parks the version for a human (status
2863
+ // 'review') and nothing serves until someone approves it, and the queue
2864
+ // can defer a cell (retry backoff, the plan's monthly generation cap). An
2865
+ // agent relayed "it's live" for versions no visitor would ever see.
2866
+ description: "Queue AI generation of a version for one (persona, slot) cell; the worker writes it asynchronously, brand-locked. If the project has review mode on, the finished version WAITS for approval (list_cell_reviews / review_cell) and serves nobody until approved; otherwise it publishes automatically as a named option the optimizer explores. Also regenerates or retries a failed cell. Generation volume is capped per plan (Free included). Check get_cell_matrix for progress. Confirm with the user before calling \u2014 each generation is a model call against the plan quota.",
2589
2867
  inputSchema: {
2590
2868
  projectId: projectIdSchema,
2591
2869
  slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
@@ -2594,28 +2872,130 @@ function registerCellTools(server, client) {
2594
2872
  outputSchema: {
2595
2873
  slotId: import_zod15.z.string(),
2596
2874
  persona: import_zod15.z.string(),
2597
- status: import_zod15.z.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress")
2875
+ status: import_zod15.z.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress"),
2876
+ reviewMode: import_zod15.z.boolean().nullable().describe("True: the version will wait for review_cell approval. False: it publishes automatically. Null: could not be read.")
2598
2877
  },
2599
2878
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2600
2879
  },
2601
2880
  withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
2602
2881
  const id = encodeURIComponent(projectId);
2603
2882
  await client.post(`/projects/${id}/cells/generate`, { slotId, persona });
2883
+ const matrix = await settled(client.get(`/projects/${id}/cells`));
2884
+ const reviewMode = matrix.ok && typeof matrix.value.reviewMode === "boolean" ? matrix.value.reviewMode : null;
2885
+ const next = reviewMode === true ? "Review mode is ON: when it is written it will WAIT for approval and serve nobody until then \u2014 list it with list_cell_reviews and approve or reject it with review_cell (after the user decides)." : reviewMode === false ? "Review mode is off: once written it publishes automatically and the optimizer starts exploring it." : 'Could not read whether review mode is on \u2014 check get_cell_matrix: status "review" means it is waiting for approval (review_cell), "live" means it is serving.';
2604
2886
  return {
2605
2887
  content: [
2606
2888
  {
2607
2889
  type: "text",
2608
- text: `Writing a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)} \u2014 it usually goes live in under a minute. Check get_cell_matrix (status flips pending \u2192 live) or get_cell_detail for the story once it lands.`
2890
+ text: `Queued a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)}. It is written asynchronously (the queue may defer it \u2014 retries back off, and the plan caps generations). ${next} Track it with get_cell_matrix or get_cell_detail.`
2609
2891
  }
2610
2892
  ],
2611
- structuredContent: { slotId, persona, status: "pending" }
2893
+ structuredContent: { slotId, persona, status: "pending", reviewMode }
2894
+ };
2895
+ })
2896
+ );
2897
+ server.registerTool(
2898
+ "list_cell_reviews",
2899
+ {
2900
+ title: "Versions waiting for review",
2901
+ description: "Generated versions waiting for approval (review mode): which slot and visitor type, the proposed text or layout, and why it was written. Approve or reject each with review_cell after the user decides." + UNTRUSTED_FIELDS_NOTE,
2902
+ inputSchema: { projectId: projectIdSchema },
2903
+ outputSchema: {
2904
+ reviews: import_zod15.z.array(
2905
+ import_zod15.z.object({
2906
+ slotId: import_zod15.z.string(),
2907
+ slotName: import_zod15.z.string(),
2908
+ persona: import_zod15.z.string(),
2909
+ personaDisplay: import_zod15.z.string(),
2910
+ content: import_zod15.z.string().nullable().describe("Proposed text, when the version is text (null for a block/layout version)"),
2911
+ rationale: import_zod15.z.string().nullable(),
2912
+ refinedFrom: import_zod15.z.string().nullable().describe("Live version this one would replace; that version keeps serving until approval"),
2913
+ updatedAt: import_zod15.z.string()
2914
+ })
2915
+ )
2916
+ },
2917
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2918
+ },
2919
+ withApiErrorGuidance(async ({ projectId }) => {
2920
+ var _a;
2921
+ const id = encodeURIComponent(projectId);
2922
+ const data = await client.get(`/projects/${id}/cell-reviews`);
2923
+ const reviews = ((_a = data.reviews) != null ? _a : []).map((r) => {
2924
+ var _a2, _b, _c;
2925
+ return {
2926
+ slotId: r.slotId,
2927
+ slotName: r.slotName,
2928
+ persona: r.persona,
2929
+ personaDisplay: r.personaDisplay,
2930
+ content: typeof ((_a2 = r.pendingArm) == null ? void 0 : _a2.content) === "string" ? r.pendingArm.content : null,
2931
+ rationale: (_b = r.rationale) != null ? _b : null,
2932
+ refinedFrom: (_c = r.refinedFrom) != null ? _c : null,
2933
+ updatedAt: String(r.updatedAt)
2934
+ };
2935
+ });
2936
+ if (reviews.length === 0) {
2937
+ return {
2938
+ content: [{ type: "text", text: "No generated versions are waiting for review." }],
2939
+ structuredContent: { reviews }
2940
+ };
2941
+ }
2942
+ const lines = reviews.map(
2943
+ (r) => [
2944
+ `- ${untrusted(r.slotName, 60)} (slotId ${untrusted(r.slotId, 64)}) for ${untrusted(r.personaDisplay, 60)} (persona ${untrusted(r.persona, 64)})`,
2945
+ r.content !== null ? ` Proposed text: ${untrusted(r.content, 300)}` : " Proposed: a designed block/layout (preview it in the dashboard)",
2946
+ r.rationale ? ` Why: ${untrusted(r.rationale, 300)}` : null,
2947
+ r.refinedFrom ? ` Would replace ${untrusted(r.refinedFrom, 64)}, which keeps serving until approval.` : null
2948
+ ].filter((l) => l !== null).join("\n")
2949
+ );
2950
+ lines.push("", "Approve or reject each with review_cell (slotId + persona) once the user has decided.");
2951
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { reviews } };
2952
+ })
2953
+ );
2954
+ server.registerTool(
2955
+ "review_cell",
2956
+ {
2957
+ title: "Approve or reject a generated version",
2958
+ description: "Approve or reject ONE generated version waiting for review (see list_cell_reviews). 'approve' publishes it to live visitors of that type; 'reject' discards it (a rejected refinement leaves the current live version in place). This is the human decision review mode exists for \u2014 only call it with the user's explicit go-ahead.",
2959
+ inputSchema: {
2960
+ projectId: projectIdSchema,
2961
+ slotId: import_zod15.z.string().min(1).max(128).describe("The slot id of the version under review"),
2962
+ persona: import_zod15.z.string().min(1).max(64).describe("The persona key of the version under review"),
2963
+ action: import_zod15.z.enum(["approve", "reject"]),
2964
+ feedback: import_zod15.z.string().max(2e3).optional().describe("Optional reviewer note, stored with a rejection")
2965
+ },
2966
+ outputSchema: {
2967
+ slotId: import_zod15.z.string(),
2968
+ persona: import_zod15.z.string(),
2969
+ action: import_zod15.z.enum(["approve", "reject"]),
2970
+ status: import_zod15.z.string().describe("live (approved, or a rejected refinement whose incumbent keeps serving) | rejected"),
2971
+ armId: import_zod15.z.string().nullable().describe("The published option id, on approval")
2972
+ },
2973
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
2974
+ },
2975
+ withApiErrorGuidance(async ({ projectId, slotId, persona, action, feedback }) => {
2976
+ var _a, _b;
2977
+ const id = encodeURIComponent(projectId);
2978
+ const res = await client.post(`/projects/${id}/cells/review`, {
2979
+ slotId,
2980
+ persona,
2981
+ action,
2982
+ feedback
2983
+ });
2984
+ const status = (_a = res.status) != null ? _a : action === "approve" ? "live" : "rejected";
2985
+ const text = action === "approve" ? `Approved: the version for ${untrusted(persona, 64)} in ${untrusted(slotId, 64)} is now live${res.armId ? ` as option ${untrusted(res.armId, 64)}` : ""}.` : status === "live" ? `Rejected: the proposed refinement for ${untrusted(persona, 64)} in ${untrusted(slotId, 64)} was discarded; the current live version keeps serving.` : `Rejected: the version for ${untrusted(persona, 64)} in ${untrusted(slotId, 64)} was discarded; that visitor type keeps seeing the original.`;
2986
+ return {
2987
+ content: [{ type: "text", text }],
2988
+ structuredContent: { slotId, persona, action, status, armId: (_b = res.armId) != null ? _b : null }
2612
2989
  };
2990
+ }, {
2991
+ not_found: "No version is waiting for review in that cell (it may already have been approved or rejected). Check list_cell_reviews.",
2992
+ publish_failed: "Approval could not publish this version (the slot may be full of options, or its configuration changed). It stays in review \u2014 check the reason in get_cell_detail or the dashboard, then retry."
2613
2993
  })
2614
2994
  );
2615
2995
  }
2616
2996
 
2617
2997
  // src/server.ts
2618
- var PKG_VERSION = true ? "0.19.0" : "0.0.0-dev";
2998
+ var PKG_VERSION = true ? "0.20.0" : "0.0.0-dev";
2619
2999
  function createMcpServer(client) {
2620
3000
  const server = new import_mcp.McpServer(
2621
3001
  {
@@ -2653,10 +3033,57 @@ function createMcpServer(client) {
2653
3033
  registerCellTools(server, client);
2654
3034
  return server;
2655
3035
  }
3036
+
3037
+ // src/tool-catalog.ts
3038
+ var TOOL_CATALOG = [
3039
+ // Projects
3040
+ { name: "list_projects", access: "read", summary: "List the projects this credential can see (an sk_ key sees only its own project)" },
3041
+ { name: "create_project", access: "write", summary: "Create a project and return its pk_ public key (account login only \u2014 not an sk_ key or demo token)" },
3042
+ { name: "get_project_stats", access: "read", summary: "Ingestion health: status, 24h events and sessions, and /v1/agent API calls" },
3043
+ { name: "add_origin", access: "write", summary: "Allow-list a site origin so the SDK's events from it are accepted" },
3044
+ // Components and variants
3045
+ { name: "list_components", access: "read", summary: "Adaptive components with variant counts and impressions" },
3046
+ { name: "get_variant_performance", access: "read", summary: "Per-(component, variant) rates with their n and the server's evidence verdict vs the baseline" },
3047
+ { name: "get_variant_brief", access: "read", summary: "Evidence-driven brief for writing a new code-native variant" },
3048
+ { name: "get_test_brief", access: "read", summary: "Ready-to-paste test code that forces one of a component's real variants" },
3049
+ { name: "create_variant", access: "write", summary: "Create a no-code managed text variant as a draft" },
3050
+ { name: "update_variant", access: "write", summary: "Edit a managed variant's name or text, or activate a draft (status active) / move it back to draft" },
3051
+ { name: "delete_variant_draft", access: "write", summary: "Delete a managed variant that is still a draft" },
3052
+ { name: "pause_variant", access: "write", summary: "Pause a variant so no new traffic is assigned to it (undo with resume_variant)" },
3053
+ { name: "resume_variant", access: "write", summary: "Resume a paused variant \u2014 manual or guardrail pause" },
3054
+ { name: "retire_variant", access: "write", summary: "Retire (archive) an active variant (undo with restore_variant)" },
3055
+ { name: "restore_variant", access: "write", summary: "Bring a retired variant back to active" },
3056
+ // Goals, funnels, guardrails
3057
+ { name: "get_goal_funnel", access: "read", summary: "Per-goal conversion summary (windowed) with descriptive all-time per-variant rates \u2014 not a step funnel (see get_funnel_report)" },
3058
+ { name: "list_goals", access: "read", summary: "Defined goals (id, role, event, status), including ones with no conversions yet" },
3059
+ { name: "list_funnels", access: "read", summary: "Defined multi-step funnels" },
3060
+ { name: "get_funnel_report", access: "read", summary: "Step-by-step funnel drop-off" },
3061
+ { name: "list_guardrail_events", access: "read", summary: "Every variant currently paused by a guardrail (optionally only pauses from the last N days)" },
3062
+ // Audience and layout
3063
+ { name: "get_persona_breakdown", access: "read", summary: "Visitor clusters with session counts and reliability (all retained sessions)" },
3064
+ { name: "get_layout_stats", access: "read", summary: "Per-persona section order with bandit pulls and reward (cumulative state)" },
3065
+ // Insights
3066
+ { name: "get_insights", access: "read", summary: "Ranked measured findings with their sample, plus AI narrations marked unmeasured" },
3067
+ { name: "refresh_insights", access: "write", summary: "Regenerate AI insights (Starter+ and an editor/admin role; a paid model call)" },
3068
+ // Generated versions (persona x slot cells)
3069
+ { name: "get_cell_matrix", access: "read", summary: 'The "Who sees what" matrix of generated versions per visitor type and region' },
3070
+ { name: "get_cell_detail", access: "read", summary: "One cell's generated version, why it was written, and how it performs" },
3071
+ { name: "generate_cell", access: "write", summary: "Queue AI generation for one cell \u2014 publishes automatically, or waits for approval when review mode is on" },
3072
+ { name: "list_cell_reviews", access: "read", summary: "Generated versions waiting for approval (review mode)" },
3073
+ { name: "review_cell", access: "write", summary: "Approve (publish) or reject one generated version waiting for review" },
3074
+ // Agent traffic
3075
+ { name: "get_agent_traffic", access: "read", summary: "Which AI crawlers, agentic browsers and agent API callers read the site (windowed)" },
3076
+ { name: "get_agent_legibility", access: "read", summary: "Whether the pages agents read are machine-legible, with fixes" },
3077
+ // Setup
3078
+ { name: "get_integration_guide", access: "read", summary: "SDK setup guide (static \u2014 same for every project)" }
3079
+ ];
3080
+ var TOOL_NAMES = TOOL_CATALOG.map((t) => t.name);
2656
3081
  // Annotate the CommonJS export names for ESM import in node:
2657
3082
  0 && (module.exports = {
2658
3083
  ApiClient,
2659
3084
  ApiError,
2660
3085
  PUBLIC_UI_RESOURCE_URIS,
3086
+ TOOL_CATALOG,
3087
+ TOOL_NAMES,
2661
3088
  createMcpServer
2662
3089
  });