@sentientui/mcp 0.20.0 → 0.22.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/AGENTS.md CHANGED
@@ -51,6 +51,7 @@ Every write tool changes what real visitors see or what the project accepts —
51
51
  | `restore_variant` | write | Brings a retired variant back to active. Does not unpause it |
52
52
  | `get_goal_funnel` | read | Per-goal conversion summary (windowed) + descriptive per-variant all-time completion rates, each with its n and a low-sample flag (descriptive — no verdict). Not a step funnel; for drop-off use `get_funnel_report` |
53
53
  | `list_goals` | read | Defined goals, including ones with no conversions yet |
54
+ | `define_goal` | write | Create a new goal. Create-only: an existing id (even archived) is refused, and it never sets or changes the primary goal — that stays a dashboard decision |
54
55
  | `list_funnels` | read | Defined multi-step funnels |
55
56
  | `get_funnel_report` | read | Step-by-step funnel drop-off |
56
57
  | `list_guardrail_events` | read | Every variant still paused by a guardrail; `days` narrows to recent pauses |
@@ -84,7 +85,7 @@ Every write tool changes what real visitors see or what the project accepts —
84
85
  4. Do **not** call `create_variant`. Commit, push, deploy — the variant auto-registers on the first assignment and goes live.
85
86
  5. Optional: enable shadow mode (project-level, in dashboard settings — it applies to the whole project, not a single component) first to validate before serving.
86
87
 
87
- The evidence state (from the brief) drives the change. It is keyed on the server's evidence tiers, never on a traffic count: only **DECIDED** (an arm reliably ahead of or behind the baseline, at the multiple-comparison-corrected strong bar) is a result to build on. **DIRECTIONAL** (early/moderate/inconclusive signals), **COLLECTING** (below the 100-visit floor per arm) and **EMPTY** all mean: apply the best-practice priors for the project's context type and make one conservative change. Never call an arm better or worse from its raw rate.
88
+ The evidence state (from the brief) drives the change. It is keyed on the server's evidence tiers, never on a traffic count: only **DECIDED** (an arm reliably ahead of or behind the baseline, at the multiple-comparison-corrected strong bar) is a result to build on. **DIRECTIONAL** (early/moderate/likely-worse/inconclusive signals — likely-worse means probably behind the baseline, wrong about one time in five, so do not build on that arm but do not treat it as decided either), **COLLECTING** (below the 100-visit floor per arm) and **EMPTY** all mean: apply the best-practice priors for the project's context type and make one conservative change. Never call an arm better or worse from its raw rate.
88
89
 
89
90
  **Add a no-code managed text variant (fallback — text only, less powerful)**
90
91
  1. `list_components` — confirm the component ID
package/README.md CHANGED
@@ -102,6 +102,7 @@ A demo token is provisioned automatically — 10 calls/month, read-only (write t
102
102
  | `restore_variant` | write | Bring a retired variant back to active (does not unpause it) |
103
103
  | `get_goal_funnel` | read | Per-goal conversion summary (windowed; default 30d) with descriptive all-time per-variant rates — not a step funnel (use `get_funnel_report` for drop-off); the id is kept for compatibility |
104
104
  | `list_goals` | read | Defined goals (id, role, event, status), including ones with no conversions yet |
105
+ | `define_goal` | write | Create a new goal (secondary or guardrail). Create-only: refuses an existing goal id and never sets or changes the primary goal |
105
106
  | `list_funnels` | read | Defined multi-step funnels |
106
107
  | `get_funnel_report` | read | Step-by-step funnel drop-off |
107
108
  | `list_guardrail_events` | read | Every variant currently paused by a guardrail (pass `days` to see only pauses from the last N days) |
@@ -720,6 +720,10 @@ var EVIDENCE_LABEL = {
720
720
  early_signal: "Early signal (still moving \u2014 not a result)",
721
721
  moderate_evidence: "Moderate evidence (about one call in five of this strength is wrong \u2014 not decided)",
722
722
  strong_evidence: "Strong evidence",
723
+ // The mirror of moderate_evidence. Before the server had it, an arm clearly
724
+ // behind its baseline read "Inconclusive (… without separating)" — it HAD
725
+ // separated, downward.
726
+ likely_worse: "Likely worse than the baseline (about one call in five of this strength is wrong \u2014 not decided)",
723
727
  inconclusive: "Inconclusive (ran a full period without separating from the baseline)"
724
728
  };
725
729
  var STATES = new Set(Object.keys(EVIDENCE_LABEL));
@@ -770,7 +774,9 @@ function verdictOf(state, stats, isBaseline) {
770
774
  var VERDICT_TEXT = {
771
775
  ahead: "reliably AHEAD of the baseline",
772
776
  behind: "reliably BEHIND the baseline",
773
- unclear: "not separated from the baseline",
777
+ // "not RELIABLY": this is printed beside moderate_evidence and likely_worse
778
+ // labels too, and a bare "not separated" contradicted the label next to it.
779
+ unclear: "not reliably separated from the baseline",
774
780
  no_evidence: "no evidence available from the server"
775
781
  };
776
782
  function rateWithN(conversions, n, unit = "sessions") {
@@ -856,7 +862,7 @@ function registerComponentTools(server, client) {
856
862
  conversions: z3.number().describe("Converting sessions in the window"),
857
863
  currentCvr: z3.number().nullable().describe("conversions / sessions over the window (0-1); null at 0 sessions"),
858
864
  lowSample: z3.boolean().describe(`True under ${MIN_EVIDENCE_SAMPLE} sessions \u2014 the rate is not interpretable on its own`),
859
- evidenceState: z3.string().nullable().describe("Reliability (all time): the server's ALL-TIME evidence state vs the baseline on direct evidence \u2014 not_enough_data | early_signal | moderate_evidence | strong_evidence | inconclusive; null for the baseline or when not provided. Not a verdict on the windowed rate"),
865
+ evidenceState: z3.string().nullable().describe("Reliability (all time): the server's ALL-TIME evidence state vs the baseline on direct evidence \u2014 not_enough_data | early_signal | moderate_evidence | strong_evidence | likely_worse | inconclusive; null for the baseline or when not provided. Not a verdict on the windowed rate"),
860
866
  probBeatsBaseline: z3.number().nullable().describe("P(this arm > baseline), all-time, from the server; null when not provided"),
861
867
  reliabilityVerdict: z3.enum(["baseline", "ahead", "behind", "unclear", "no_evidence"]).describe("The all-time verdict read from evidenceState \u2014 reliability (all time), which can differ from `verdict` on the window"),
862
868
  windowEvidenceState: z3.string().nullable().describe("The server's evidence state on the WINDOWED session conversion rate vs the baseline; null for the baseline, when the arm was not scored in the window, or on older APIs"),
@@ -1425,6 +1431,51 @@ function registerGoalTools(server, client) {
1425
1431
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
1426
1432
  })
1427
1433
  );
1434
+ server.registerTool(
1435
+ "define_goal",
1436
+ {
1437
+ title: "Define a new goal",
1438
+ description: "Create a NEW goal (conversion signal) for a project. Create-only: it never changes an existing goal \u2014 an id that already exists, including an archived one, is refused (goal_exists); call list_goals first. It never sets or changes the PRIMARY goal (what experiments optimize): the goal is created as 'secondary' (default) or 'guardrail'; choosing the primary stays a dashboard decision. A guardrail goal can pause variants that hurt it \u2014 say so when proposing one. Event types: url_reached (needs urlPattern, e.g. /thank-you), click / form_submit (need a locator, usually easier to pick in the dashboard on-site editor, or fire from code with client.goal('<goalId>') instead), scroll_depth (needs threshold in (0, 1]). Needs an editor/admin login or the project's sk_ server key. Confirm the goal id, event and pattern with the user before calling.",
1439
+ inputSchema: {
1440
+ projectId: projectIdSchema,
1441
+ goalId: z6.string().min(1).max(128).refine((s) => !s.startsWith("funnel:"), "'funnel:' is reserved for funnel bindings").describe("Stable id code will reference, e.g. 'signup' or 'checkout-complete'. Must not exist yet."),
1442
+ event: z6.enum(["url_reached", "click", "form_submit", "scroll_depth"]).describe("What counts as a conversion"),
1443
+ urlPattern: z6.string().min(1).max(512).optional().describe("url_reached only: the path pattern, e.g. '/thank-you' or '/orders/*'"),
1444
+ locator: z6.record(z6.unknown()).optional().describe('click / form_submit only: the element locator (e.g. { "id": "signup-button" })'),
1445
+ threshold: z6.number().gt(0).max(1).optional().describe("scroll_depth only: how far down the page counts, in (0, 1]"),
1446
+ displayName: z6.string().min(1).max(128).optional().describe("Human-readable name shown in the dashboard"),
1447
+ role: z6.enum(["secondary", "guardrail"]).optional().describe("secondary (default) is tracked and reported; guardrail can also pause variants that hurt it. Never 'primary'."),
1448
+ defaultValue: z6.number().positive().max(1e9).optional().describe("Fixed worth of one conversion in the project currency (revenue goals)")
1449
+ },
1450
+ outputSchema: {
1451
+ goalId: z6.string(),
1452
+ role: z6.enum(["secondary", "guardrail"]).describe("Never primary \u2014 this tool cannot change the primary goal")
1453
+ },
1454
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1455
+ },
1456
+ withApiErrorGuidance(async ({ projectId, goalId, event, urlPattern, locator, threshold, displayName, role, defaultValue }) => {
1457
+ var _a, _b, _c;
1458
+ const id = encodeURIComponent(projectId);
1459
+ const res = await client.post(`/projects/${id}/goal-definitions`, __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
1460
+ goalId,
1461
+ event
1462
+ }, urlPattern !== void 0 ? { urlPattern } : {}), locator !== void 0 ? { locator } : {}), threshold !== void 0 ? { threshold } : {}), displayName !== void 0 ? { displayName } : {}), role !== void 0 ? { role } : {}), defaultValue !== void 0 ? { defaultValue } : {}));
1463
+ const structuredContent = { goalId: (_a = res.goalId) != null ? _a : goalId, role: (_c = (_b = res.role) != null ? _b : role) != null ? _c : "secondary" };
1464
+ return {
1465
+ content: [{
1466
+ type: "text",
1467
+ text: `Created goal ${untrusted(structuredContent.goalId)} (${structuredContent.role}, ${event}). It is active now. The primary goal was not changed \u2014 if this should be what experiments optimize, the owner can make it primary in the dashboard. Reference the goalId verbatim from code (client.goal('<goalId>')) if the event is not tracked automatically.`
1468
+ }],
1469
+ structuredContent
1470
+ };
1471
+ }, {
1472
+ goal_exists: "A goal with this id already exists (it may be archived). define_goal never changes an existing goal \u2014 pick a new id (see list_goals), or edit that goal in the dashboard.",
1473
+ primary_not_allowed: "define_goal never sets the primary goal. Create it as secondary; the owner can make it primary in the dashboard.",
1474
+ invalid_goal_id: "That goal id is not allowed: use 1-128 characters, and not the reserved 'funnel:' prefix.",
1475
+ invalid_goal: "The goal definition is incomplete for its event type: url_reached needs urlPattern, click/form_submit need a locator, scroll_depth needs threshold.",
1476
+ "Not Found": "This SentientUI API does not support creating goals from MCP yet (it predates create-only goals). Define the goal in the dashboard instead."
1477
+ })
1478
+ );
1428
1479
  }
1429
1480
 
1430
1481
  // src/tools/funnels.ts
@@ -1576,7 +1627,7 @@ function registerGuardrailTools(server, client) {
1576
1627
  "list_guardrail_events",
1577
1628
  {
1578
1629
  title: "List guardrail events",
1579
- description: "List every variant currently paused by a guardrail (a paused variant stops serving until unpaused, however long ago the pause fired). Pass `days` to narrow to pauses fired within the last N days." + UNTRUSTED_FIELDS_NOTE,
1630
+ description: "List every variant currently paused (a paused variant stops serving until unpaused, however long ago the pause fired). `source` says who paused it: 'guardrail' = automatically, for converting clearly worse; 'manual' = a teammate paused it by hand (do not suggest resuming those without asking who paused them). Pass `days` to narrow to pauses fired within the last N days." + UNTRUSTED_FIELDS_NOTE,
1580
1631
  inputSchema: {
1581
1632
  projectId: projectIdSchema,
1582
1633
  days: z8.number().int().min(1).max(365).optional().describe("Only pauses fired within the last N days. Omit for every still-paused variant.")
@@ -1585,9 +1636,10 @@ function registerGuardrailTools(server, client) {
1585
1636
  events: z8.array(
1586
1637
  z8.object({
1587
1638
  componentId: z8.string(),
1588
- variantIds: z8.array(z8.string()).describe("Variants paused by the guardrail"),
1639
+ variantIds: z8.array(z8.string()).describe("Variants paused together"),
1589
1640
  pausedAt: z8.string().nullable().describe("ISO timestamp the pause fired, or null"),
1590
- funnelId: z8.string().nullable().describe("Set when the pause came from a funnel guardrail")
1641
+ funnelId: z8.string().nullable().describe("Set when the pause came from a funnel guardrail"),
1642
+ source: z8.enum(["guardrail", "manual"]).describe("'guardrail' = paused automatically; 'manual' = paused by a teammate")
1591
1643
  })
1592
1644
  ).describe("Currently paused variants (empty if none)")
1593
1645
  },
@@ -1603,24 +1655,25 @@ function registerGuardrailTools(server, client) {
1603
1655
  const data = await client.get(`/projects/${id}/guardrail-events${query}`);
1604
1656
  const structuredContent = {
1605
1657
  events: data.guardrailEvents.map((e) => {
1606
- var _a;
1658
+ var _a, _b;
1607
1659
  return {
1608
1660
  componentId: e.componentId,
1609
1661
  variantIds: e.variantIds,
1610
1662
  pausedAt: e.pausedAt,
1611
- funnelId: (_a = e.funnelId) != null ? _a : null
1663
+ funnelId: (_a = e.funnelId) != null ? _a : null,
1664
+ source: (_b = e.source) != null ? _b : "guardrail"
1612
1665
  };
1613
1666
  })
1614
1667
  };
1615
1668
  if (!data.guardrailEvents.length) {
1616
- const scope = days != null ? `paused by a guardrail in the last ${days} days` : "currently paused by a guardrail";
1669
+ const scope = days != null ? `paused in the last ${days} days` : "currently paused";
1617
1670
  return {
1618
1671
  content: [{ type: "text", text: `No variants ${scope}.` }],
1619
1672
  structuredContent
1620
1673
  };
1621
1674
  }
1622
- const lines = data.guardrailEvents.map(
1623
- (e) => `- ${untrusted(e.componentId)}: variants [${e.variantIds.map((v) => untrusted(v)).join(", ")}] paused${e.pausedAt ? ` at ${e.pausedAt}` : ""}${e.funnelId ? ` (protecting the ${untrusted(e.funnelId)} funnel)` : ""}`
1675
+ const lines = structuredContent.events.map(
1676
+ (e) => `- ${untrusted(e.componentId)}: variants [${e.variantIds.map((v) => untrusted(v)).join(", ")}] ${e.source === "manual" ? "paused by a teammate" : "paused automatically by a guardrail"}${e.pausedAt ? ` at ${e.pausedAt}` : ""}${e.funnelId ? ` (protecting the ${untrusted(e.funnelId)} funnel)` : ""}`
1624
1677
  );
1625
1678
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
1626
1679
  })
@@ -2052,7 +2105,10 @@ function guidanceFor(dataState, contextType, arms) {
2052
2105
  }
2053
2106
  case "directional": {
2054
2107
  const inconclusive = arms.some((a) => a.state === "inconclusive");
2055
- return 'No comparison on this component is decided. Early and moderate signals are hypotheses, not results (moderate evidence is wrong about one time in five), so do not target an arm as "under-" or "out-performing". ' + (inconclusive ? "At least one arm ran a full period without separating from the baseline \u2014 the existing versions do not differ meaningfully, so make a clearly different change rather than a tweak. " : "") + "Lean on the best-practice priors below and make one focused change.";
2108
+ const likelyWorse = arms.filter((a) => !a.isBaseline && a.state === "likely_worse").map((a) => untrusted(a.variantId));
2109
+ return 'No comparison on this component is decided. Early, moderate and likely-worse signals are hypotheses, not results (each is wrong about one time in five), so do not target an arm as "under-" or "out-performing". ' + (likelyWorse.length ? `Probably behind the baseline (not decided): ${likelyWorse.join(", ")} \u2014 do not build on ${likelyWorse.length === 1 ? "it" : "them"}. ` : "") + // "did not separate" is not "do not differ": the old wording asserted
2110
+ // an equality the evidence never measured.
2111
+ (inconclusive ? "At least one arm ran a full period without separating from the baseline \u2014 any difference between the existing versions is too small to detect, so make a clearly different change rather than a tweak. " : "") + "Lean on the best-practice priors below and make one focused change.";
2056
2112
  }
2057
2113
  case "collecting":
2058
2114
  if (!arms.some((a) => !a.isBaseline)) {
@@ -2882,7 +2938,8 @@ function registerCellTools(server, client) {
2882
2938
  refinedFrom: z15.string().nullable().describe("Live version this one would replace; that version keeps serving until approval"),
2883
2939
  updatedAt: z15.string()
2884
2940
  })
2885
- )
2941
+ ),
2942
+ total: z15.number().int().describe("How many versions are waiting in all. The list holds at most the oldest 200; decide those to see the rest.")
2886
2943
  },
2887
2944
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2888
2945
  },
@@ -2903,10 +2960,11 @@ function registerCellTools(server, client) {
2903
2960
  updatedAt: String(r.updatedAt)
2904
2961
  };
2905
2962
  });
2963
+ const total = typeof data.total === "number" ? data.total : reviews.length;
2906
2964
  if (reviews.length === 0) {
2907
2965
  return {
2908
2966
  content: [{ type: "text", text: "No generated versions are waiting for review." }],
2909
- structuredContent: { reviews }
2967
+ structuredContent: { reviews, total }
2910
2968
  };
2911
2969
  }
2912
2970
  const lines = reviews.map(
@@ -2917,8 +2975,11 @@ function registerCellTools(server, client) {
2917
2975
  r.refinedFrom ? ` Would replace ${untrusted(r.refinedFrom, 64)}, which keeps serving until approval.` : null
2918
2976
  ].filter((l) => l !== null).join("\n")
2919
2977
  );
2978
+ if (total > reviews.length) {
2979
+ lines.unshift(`Showing the ${reviews.length} oldest of ${total} versions waiting. Decide on these to see the rest.`, "");
2980
+ }
2920
2981
  lines.push("", "Approve or reject each with review_cell (slotId + persona) once the user has decided.");
2921
- return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { reviews } };
2982
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { reviews, total } };
2922
2983
  })
2923
2984
  );
2924
2985
  server.registerTool(
@@ -2965,7 +3026,7 @@ function registerCellTools(server, client) {
2965
3026
  }
2966
3027
 
2967
3028
  // src/server.ts
2968
- var PKG_VERSION = true ? "0.20.0" : "0.0.0-dev";
3029
+ var PKG_VERSION = true ? "0.22.0" : "0.0.0-dev";
2969
3030
  function createMcpServer(client) {
2970
3031
  const server = new McpServer(
2971
3032
  {
package/dist/index.cjs CHANGED
@@ -724,6 +724,10 @@ var EVIDENCE_LABEL = {
724
724
  early_signal: "Early signal (still moving \u2014 not a result)",
725
725
  moderate_evidence: "Moderate evidence (about one call in five of this strength is wrong \u2014 not decided)",
726
726
  strong_evidence: "Strong evidence",
727
+ // The mirror of moderate_evidence. Before the server had it, an arm clearly
728
+ // behind its baseline read "Inconclusive (… without separating)" — it HAD
729
+ // separated, downward.
730
+ likely_worse: "Likely worse than the baseline (about one call in five of this strength is wrong \u2014 not decided)",
727
731
  inconclusive: "Inconclusive (ran a full period without separating from the baseline)"
728
732
  };
729
733
  var STATES = new Set(Object.keys(EVIDENCE_LABEL));
@@ -774,7 +778,9 @@ function verdictOf(state, stats, isBaseline) {
774
778
  var VERDICT_TEXT = {
775
779
  ahead: "reliably AHEAD of the baseline",
776
780
  behind: "reliably BEHIND the baseline",
777
- unclear: "not separated from the baseline",
781
+ // "not RELIABLY": this is printed beside moderate_evidence and likely_worse
782
+ // labels too, and a bare "not separated" contradicted the label next to it.
783
+ unclear: "not reliably separated from the baseline",
778
784
  no_evidence: "no evidence available from the server"
779
785
  };
780
786
  function rateWithN(conversions, n, unit = "sessions") {
@@ -860,7 +866,7 @@ function registerComponentTools(server, client) {
860
866
  conversions: import_zod3.z.number().describe("Converting sessions in the window"),
861
867
  currentCvr: import_zod3.z.number().nullable().describe("conversions / sessions over the window (0-1); null at 0 sessions"),
862
868
  lowSample: import_zod3.z.boolean().describe(`True under ${MIN_EVIDENCE_SAMPLE} sessions \u2014 the rate is not interpretable on its own`),
863
- evidenceState: import_zod3.z.string().nullable().describe("Reliability (all time): the server's ALL-TIME evidence state vs the baseline on direct evidence \u2014 not_enough_data | early_signal | moderate_evidence | strong_evidence | inconclusive; null for the baseline or when not provided. Not a verdict on the windowed rate"),
869
+ evidenceState: import_zod3.z.string().nullable().describe("Reliability (all time): the server's ALL-TIME evidence state vs the baseline on direct evidence \u2014 not_enough_data | early_signal | moderate_evidence | strong_evidence | likely_worse | inconclusive; null for the baseline or when not provided. Not a verdict on the windowed rate"),
864
870
  probBeatsBaseline: import_zod3.z.number().nullable().describe("P(this arm > baseline), all-time, from the server; null when not provided"),
865
871
  reliabilityVerdict: import_zod3.z.enum(["baseline", "ahead", "behind", "unclear", "no_evidence"]).describe("The all-time verdict read from evidenceState \u2014 reliability (all time), which can differ from `verdict` on the window"),
866
872
  windowEvidenceState: import_zod3.z.string().nullable().describe("The server's evidence state on the WINDOWED session conversion rate vs the baseline; null for the baseline, when the arm was not scored in the window, or on older APIs"),
@@ -1429,6 +1435,51 @@ function registerGoalTools(server, client) {
1429
1435
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
1430
1436
  })
1431
1437
  );
1438
+ server.registerTool(
1439
+ "define_goal",
1440
+ {
1441
+ title: "Define a new goal",
1442
+ description: "Create a NEW goal (conversion signal) for a project. Create-only: it never changes an existing goal \u2014 an id that already exists, including an archived one, is refused (goal_exists); call list_goals first. It never sets or changes the PRIMARY goal (what experiments optimize): the goal is created as 'secondary' (default) or 'guardrail'; choosing the primary stays a dashboard decision. A guardrail goal can pause variants that hurt it \u2014 say so when proposing one. Event types: url_reached (needs urlPattern, e.g. /thank-you), click / form_submit (need a locator, usually easier to pick in the dashboard on-site editor, or fire from code with client.goal('<goalId>') instead), scroll_depth (needs threshold in (0, 1]). Needs an editor/admin login or the project's sk_ server key. Confirm the goal id, event and pattern with the user before calling.",
1443
+ inputSchema: {
1444
+ projectId: projectIdSchema,
1445
+ goalId: import_zod6.z.string().min(1).max(128).refine((s) => !s.startsWith("funnel:"), "'funnel:' is reserved for funnel bindings").describe("Stable id code will reference, e.g. 'signup' or 'checkout-complete'. Must not exist yet."),
1446
+ event: import_zod6.z.enum(["url_reached", "click", "form_submit", "scroll_depth"]).describe("What counts as a conversion"),
1447
+ urlPattern: import_zod6.z.string().min(1).max(512).optional().describe("url_reached only: the path pattern, e.g. '/thank-you' or '/orders/*'"),
1448
+ locator: import_zod6.z.record(import_zod6.z.unknown()).optional().describe('click / form_submit only: the element locator (e.g. { "id": "signup-button" })'),
1449
+ threshold: import_zod6.z.number().gt(0).max(1).optional().describe("scroll_depth only: how far down the page counts, in (0, 1]"),
1450
+ displayName: import_zod6.z.string().min(1).max(128).optional().describe("Human-readable name shown in the dashboard"),
1451
+ role: import_zod6.z.enum(["secondary", "guardrail"]).optional().describe("secondary (default) is tracked and reported; guardrail can also pause variants that hurt it. Never 'primary'."),
1452
+ defaultValue: import_zod6.z.number().positive().max(1e9).optional().describe("Fixed worth of one conversion in the project currency (revenue goals)")
1453
+ },
1454
+ outputSchema: {
1455
+ goalId: import_zod6.z.string(),
1456
+ role: import_zod6.z.enum(["secondary", "guardrail"]).describe("Never primary \u2014 this tool cannot change the primary goal")
1457
+ },
1458
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1459
+ },
1460
+ withApiErrorGuidance(async ({ projectId, goalId, event, urlPattern, locator, threshold, displayName, role, defaultValue }) => {
1461
+ var _a2, _b, _c;
1462
+ const id = encodeURIComponent(projectId);
1463
+ const res = await client.post(`/projects/${id}/goal-definitions`, __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
1464
+ goalId,
1465
+ event
1466
+ }, urlPattern !== void 0 ? { urlPattern } : {}), locator !== void 0 ? { locator } : {}), threshold !== void 0 ? { threshold } : {}), displayName !== void 0 ? { displayName } : {}), role !== void 0 ? { role } : {}), defaultValue !== void 0 ? { defaultValue } : {}));
1467
+ const structuredContent = { goalId: (_a2 = res.goalId) != null ? _a2 : goalId, role: (_c = (_b = res.role) != null ? _b : role) != null ? _c : "secondary" };
1468
+ return {
1469
+ content: [{
1470
+ type: "text",
1471
+ text: `Created goal ${untrusted(structuredContent.goalId)} (${structuredContent.role}, ${event}). It is active now. The primary goal was not changed \u2014 if this should be what experiments optimize, the owner can make it primary in the dashboard. Reference the goalId verbatim from code (client.goal('<goalId>')) if the event is not tracked automatically.`
1472
+ }],
1473
+ structuredContent
1474
+ };
1475
+ }, {
1476
+ goal_exists: "A goal with this id already exists (it may be archived). define_goal never changes an existing goal \u2014 pick a new id (see list_goals), or edit that goal in the dashboard.",
1477
+ primary_not_allowed: "define_goal never sets the primary goal. Create it as secondary; the owner can make it primary in the dashboard.",
1478
+ invalid_goal_id: "That goal id is not allowed: use 1-128 characters, and not the reserved 'funnel:' prefix.",
1479
+ invalid_goal: "The goal definition is incomplete for its event type: url_reached needs urlPattern, click/form_submit need a locator, scroll_depth needs threshold.",
1480
+ "Not Found": "This SentientUI API does not support creating goals from MCP yet (it predates create-only goals). Define the goal in the dashboard instead."
1481
+ })
1482
+ );
1432
1483
  }
1433
1484
 
1434
1485
  // src/tools/funnels.ts
@@ -1580,7 +1631,7 @@ function registerGuardrailTools(server, client) {
1580
1631
  "list_guardrail_events",
1581
1632
  {
1582
1633
  title: "List guardrail events",
1583
- description: "List every variant currently paused by a guardrail (a paused variant stops serving until unpaused, however long ago the pause fired). Pass `days` to narrow to pauses fired within the last N days." + UNTRUSTED_FIELDS_NOTE,
1634
+ description: "List every variant currently paused (a paused variant stops serving until unpaused, however long ago the pause fired). `source` says who paused it: 'guardrail' = automatically, for converting clearly worse; 'manual' = a teammate paused it by hand (do not suggest resuming those without asking who paused them). Pass `days` to narrow to pauses fired within the last N days." + UNTRUSTED_FIELDS_NOTE,
1584
1635
  inputSchema: {
1585
1636
  projectId: projectIdSchema,
1586
1637
  days: import_zod8.z.number().int().min(1).max(365).optional().describe("Only pauses fired within the last N days. Omit for every still-paused variant.")
@@ -1589,9 +1640,10 @@ function registerGuardrailTools(server, client) {
1589
1640
  events: import_zod8.z.array(
1590
1641
  import_zod8.z.object({
1591
1642
  componentId: import_zod8.z.string(),
1592
- variantIds: import_zod8.z.array(import_zod8.z.string()).describe("Variants paused by the guardrail"),
1643
+ variantIds: import_zod8.z.array(import_zod8.z.string()).describe("Variants paused together"),
1593
1644
  pausedAt: import_zod8.z.string().nullable().describe("ISO timestamp the pause fired, or null"),
1594
- funnelId: import_zod8.z.string().nullable().describe("Set when the pause came from a funnel guardrail")
1645
+ funnelId: import_zod8.z.string().nullable().describe("Set when the pause came from a funnel guardrail"),
1646
+ source: import_zod8.z.enum(["guardrail", "manual"]).describe("'guardrail' = paused automatically; 'manual' = paused by a teammate")
1595
1647
  })
1596
1648
  ).describe("Currently paused variants (empty if none)")
1597
1649
  },
@@ -1607,24 +1659,25 @@ function registerGuardrailTools(server, client) {
1607
1659
  const data = await client.get(`/projects/${id}/guardrail-events${query}`);
1608
1660
  const structuredContent = {
1609
1661
  events: data.guardrailEvents.map((e) => {
1610
- var _a2;
1662
+ var _a2, _b;
1611
1663
  return {
1612
1664
  componentId: e.componentId,
1613
1665
  variantIds: e.variantIds,
1614
1666
  pausedAt: e.pausedAt,
1615
- funnelId: (_a2 = e.funnelId) != null ? _a2 : null
1667
+ funnelId: (_a2 = e.funnelId) != null ? _a2 : null,
1668
+ source: (_b = e.source) != null ? _b : "guardrail"
1616
1669
  };
1617
1670
  })
1618
1671
  };
1619
1672
  if (!data.guardrailEvents.length) {
1620
- const scope = days != null ? `paused by a guardrail in the last ${days} days` : "currently paused by a guardrail";
1673
+ const scope = days != null ? `paused in the last ${days} days` : "currently paused";
1621
1674
  return {
1622
1675
  content: [{ type: "text", text: `No variants ${scope}.` }],
1623
1676
  structuredContent
1624
1677
  };
1625
1678
  }
1626
- const lines = data.guardrailEvents.map(
1627
- (e) => `- ${untrusted(e.componentId)}: variants [${e.variantIds.map((v) => untrusted(v)).join(", ")}] paused${e.pausedAt ? ` at ${e.pausedAt}` : ""}${e.funnelId ? ` (protecting the ${untrusted(e.funnelId)} funnel)` : ""}`
1679
+ const lines = structuredContent.events.map(
1680
+ (e) => `- ${untrusted(e.componentId)}: variants [${e.variantIds.map((v) => untrusted(v)).join(", ")}] ${e.source === "manual" ? "paused by a teammate" : "paused automatically by a guardrail"}${e.pausedAt ? ` at ${e.pausedAt}` : ""}${e.funnelId ? ` (protecting the ${untrusted(e.funnelId)} funnel)` : ""}`
1628
1681
  );
1629
1682
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
1630
1683
  })
@@ -2056,7 +2109,10 @@ function guidanceFor(dataState, contextType, arms) {
2056
2109
  }
2057
2110
  case "directional": {
2058
2111
  const inconclusive = arms.some((a) => a.state === "inconclusive");
2059
- return 'No comparison on this component is decided. Early and moderate signals are hypotheses, not results (moderate evidence is wrong about one time in five), so do not target an arm as "under-" or "out-performing". ' + (inconclusive ? "At least one arm ran a full period without separating from the baseline \u2014 the existing versions do not differ meaningfully, so make a clearly different change rather than a tweak. " : "") + "Lean on the best-practice priors below and make one focused change.";
2112
+ const likelyWorse = arms.filter((a) => !a.isBaseline && a.state === "likely_worse").map((a) => untrusted(a.variantId));
2113
+ return 'No comparison on this component is decided. Early, moderate and likely-worse signals are hypotheses, not results (each is wrong about one time in five), so do not target an arm as "under-" or "out-performing". ' + (likelyWorse.length ? `Probably behind the baseline (not decided): ${likelyWorse.join(", ")} \u2014 do not build on ${likelyWorse.length === 1 ? "it" : "them"}. ` : "") + // "did not separate" is not "do not differ": the old wording asserted
2114
+ // an equality the evidence never measured.
2115
+ (inconclusive ? "At least one arm ran a full period without separating from the baseline \u2014 any difference between the existing versions is too small to detect, so make a clearly different change rather than a tweak. " : "") + "Lean on the best-practice priors below and make one focused change.";
2060
2116
  }
2061
2117
  case "collecting":
2062
2118
  if (!arms.some((a) => !a.isBaseline)) {
@@ -2886,7 +2942,8 @@ function registerCellTools(server, client) {
2886
2942
  refinedFrom: import_zod15.z.string().nullable().describe("Live version this one would replace; that version keeps serving until approval"),
2887
2943
  updatedAt: import_zod15.z.string()
2888
2944
  })
2889
- )
2945
+ ),
2946
+ total: import_zod15.z.number().int().describe("How many versions are waiting in all. The list holds at most the oldest 200; decide those to see the rest.")
2890
2947
  },
2891
2948
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2892
2949
  },
@@ -2907,10 +2964,11 @@ function registerCellTools(server, client) {
2907
2964
  updatedAt: String(r.updatedAt)
2908
2965
  };
2909
2966
  });
2967
+ const total = typeof data.total === "number" ? data.total : reviews.length;
2910
2968
  if (reviews.length === 0) {
2911
2969
  return {
2912
2970
  content: [{ type: "text", text: "No generated versions are waiting for review." }],
2913
- structuredContent: { reviews }
2971
+ structuredContent: { reviews, total }
2914
2972
  };
2915
2973
  }
2916
2974
  const lines = reviews.map(
@@ -2921,8 +2979,11 @@ function registerCellTools(server, client) {
2921
2979
  r.refinedFrom ? ` Would replace ${untrusted(r.refinedFrom, 64)}, which keeps serving until approval.` : null
2922
2980
  ].filter((l) => l !== null).join("\n")
2923
2981
  );
2982
+ if (total > reviews.length) {
2983
+ lines.unshift(`Showing the ${reviews.length} oldest of ${total} versions waiting. Decide on these to see the rest.`, "");
2984
+ }
2924
2985
  lines.push("", "Approve or reject each with review_cell (slotId + persona) once the user has decided.");
2925
- return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { reviews } };
2986
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { reviews, total } };
2926
2987
  })
2927
2988
  );
2928
2989
  server.registerTool(
@@ -2969,7 +3030,7 @@ function registerCellTools(server, client) {
2969
3030
  }
2970
3031
 
2971
3032
  // src/server.ts
2972
- var PKG_VERSION = true ? "0.20.0" : "0.0.0-dev";
3033
+ var PKG_VERSION = true ? "0.22.0" : "0.0.0-dev";
2973
3034
  function createMcpServer(client) {
2974
3035
  const server = new import_mcp.McpServer(
2975
3036
  {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ApiClient,
4
4
  createMcpServer
5
- } from "./chunk-35I5EJTS.js";
5
+ } from "./chunk-A7LJWTYQ.js";
6
6
 
7
7
  // src/index.ts
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/dist/lib.cjs CHANGED
@@ -750,6 +750,10 @@ var EVIDENCE_LABEL = {
750
750
  early_signal: "Early signal (still moving \u2014 not a result)",
751
751
  moderate_evidence: "Moderate evidence (about one call in five of this strength is wrong \u2014 not decided)",
752
752
  strong_evidence: "Strong evidence",
753
+ // The mirror of moderate_evidence. Before the server had it, an arm clearly
754
+ // behind its baseline read "Inconclusive (… without separating)" — it HAD
755
+ // separated, downward.
756
+ likely_worse: "Likely worse than the baseline (about one call in five of this strength is wrong \u2014 not decided)",
753
757
  inconclusive: "Inconclusive (ran a full period without separating from the baseline)"
754
758
  };
755
759
  var STATES = new Set(Object.keys(EVIDENCE_LABEL));
@@ -800,7 +804,9 @@ function verdictOf(state, stats, isBaseline) {
800
804
  var VERDICT_TEXT = {
801
805
  ahead: "reliably AHEAD of the baseline",
802
806
  behind: "reliably BEHIND the baseline",
803
- unclear: "not separated from the baseline",
807
+ // "not RELIABLY": this is printed beside moderate_evidence and likely_worse
808
+ // labels too, and a bare "not separated" contradicted the label next to it.
809
+ unclear: "not reliably separated from the baseline",
804
810
  no_evidence: "no evidence available from the server"
805
811
  };
806
812
  function rateWithN(conversions, n, unit = "sessions") {
@@ -886,7 +892,7 @@ function registerComponentTools(server, client) {
886
892
  conversions: import_zod3.z.number().describe("Converting sessions in the window"),
887
893
  currentCvr: import_zod3.z.number().nullable().describe("conversions / sessions over the window (0-1); null at 0 sessions"),
888
894
  lowSample: import_zod3.z.boolean().describe(`True under ${MIN_EVIDENCE_SAMPLE} sessions \u2014 the rate is not interpretable on its own`),
889
- evidenceState: import_zod3.z.string().nullable().describe("Reliability (all time): the server's ALL-TIME evidence state vs the baseline on direct evidence \u2014 not_enough_data | early_signal | moderate_evidence | strong_evidence | inconclusive; null for the baseline or when not provided. Not a verdict on the windowed rate"),
895
+ evidenceState: import_zod3.z.string().nullable().describe("Reliability (all time): the server's ALL-TIME evidence state vs the baseline on direct evidence \u2014 not_enough_data | early_signal | moderate_evidence | strong_evidence | likely_worse | inconclusive; null for the baseline or when not provided. Not a verdict on the windowed rate"),
890
896
  probBeatsBaseline: import_zod3.z.number().nullable().describe("P(this arm > baseline), all-time, from the server; null when not provided"),
891
897
  reliabilityVerdict: import_zod3.z.enum(["baseline", "ahead", "behind", "unclear", "no_evidence"]).describe("The all-time verdict read from evidenceState \u2014 reliability (all time), which can differ from `verdict` on the window"),
892
898
  windowEvidenceState: import_zod3.z.string().nullable().describe("The server's evidence state on the WINDOWED session conversion rate vs the baseline; null for the baseline, when the arm was not scored in the window, or on older APIs"),
@@ -1455,6 +1461,51 @@ function registerGoalTools(server, client) {
1455
1461
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
1456
1462
  })
1457
1463
  );
1464
+ server.registerTool(
1465
+ "define_goal",
1466
+ {
1467
+ title: "Define a new goal",
1468
+ description: "Create a NEW goal (conversion signal) for a project. Create-only: it never changes an existing goal \u2014 an id that already exists, including an archived one, is refused (goal_exists); call list_goals first. It never sets or changes the PRIMARY goal (what experiments optimize): the goal is created as 'secondary' (default) or 'guardrail'; choosing the primary stays a dashboard decision. A guardrail goal can pause variants that hurt it \u2014 say so when proposing one. Event types: url_reached (needs urlPattern, e.g. /thank-you), click / form_submit (need a locator, usually easier to pick in the dashboard on-site editor, or fire from code with client.goal('<goalId>') instead), scroll_depth (needs threshold in (0, 1]). Needs an editor/admin login or the project's sk_ server key. Confirm the goal id, event and pattern with the user before calling.",
1469
+ inputSchema: {
1470
+ projectId: projectIdSchema,
1471
+ goalId: import_zod6.z.string().min(1).max(128).refine((s) => !s.startsWith("funnel:"), "'funnel:' is reserved for funnel bindings").describe("Stable id code will reference, e.g. 'signup' or 'checkout-complete'. Must not exist yet."),
1472
+ event: import_zod6.z.enum(["url_reached", "click", "form_submit", "scroll_depth"]).describe("What counts as a conversion"),
1473
+ urlPattern: import_zod6.z.string().min(1).max(512).optional().describe("url_reached only: the path pattern, e.g. '/thank-you' or '/orders/*'"),
1474
+ locator: import_zod6.z.record(import_zod6.z.unknown()).optional().describe('click / form_submit only: the element locator (e.g. { "id": "signup-button" })'),
1475
+ threshold: import_zod6.z.number().gt(0).max(1).optional().describe("scroll_depth only: how far down the page counts, in (0, 1]"),
1476
+ displayName: import_zod6.z.string().min(1).max(128).optional().describe("Human-readable name shown in the dashboard"),
1477
+ role: import_zod6.z.enum(["secondary", "guardrail"]).optional().describe("secondary (default) is tracked and reported; guardrail can also pause variants that hurt it. Never 'primary'."),
1478
+ defaultValue: import_zod6.z.number().positive().max(1e9).optional().describe("Fixed worth of one conversion in the project currency (revenue goals)")
1479
+ },
1480
+ outputSchema: {
1481
+ goalId: import_zod6.z.string(),
1482
+ role: import_zod6.z.enum(["secondary", "guardrail"]).describe("Never primary \u2014 this tool cannot change the primary goal")
1483
+ },
1484
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1485
+ },
1486
+ withApiErrorGuidance(async ({ projectId, goalId, event, urlPattern, locator, threshold, displayName, role, defaultValue }) => {
1487
+ var _a, _b, _c;
1488
+ const id = encodeURIComponent(projectId);
1489
+ const res = await client.post(`/projects/${id}/goal-definitions`, __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
1490
+ goalId,
1491
+ event
1492
+ }, urlPattern !== void 0 ? { urlPattern } : {}), locator !== void 0 ? { locator } : {}), threshold !== void 0 ? { threshold } : {}), displayName !== void 0 ? { displayName } : {}), role !== void 0 ? { role } : {}), defaultValue !== void 0 ? { defaultValue } : {}));
1493
+ const structuredContent = { goalId: (_a = res.goalId) != null ? _a : goalId, role: (_c = (_b = res.role) != null ? _b : role) != null ? _c : "secondary" };
1494
+ return {
1495
+ content: [{
1496
+ type: "text",
1497
+ text: `Created goal ${untrusted(structuredContent.goalId)} (${structuredContent.role}, ${event}). It is active now. The primary goal was not changed \u2014 if this should be what experiments optimize, the owner can make it primary in the dashboard. Reference the goalId verbatim from code (client.goal('<goalId>')) if the event is not tracked automatically.`
1498
+ }],
1499
+ structuredContent
1500
+ };
1501
+ }, {
1502
+ goal_exists: "A goal with this id already exists (it may be archived). define_goal never changes an existing goal \u2014 pick a new id (see list_goals), or edit that goal in the dashboard.",
1503
+ primary_not_allowed: "define_goal never sets the primary goal. Create it as secondary; the owner can make it primary in the dashboard.",
1504
+ invalid_goal_id: "That goal id is not allowed: use 1-128 characters, and not the reserved 'funnel:' prefix.",
1505
+ invalid_goal: "The goal definition is incomplete for its event type: url_reached needs urlPattern, click/form_submit need a locator, scroll_depth needs threshold.",
1506
+ "Not Found": "This SentientUI API does not support creating goals from MCP yet (it predates create-only goals). Define the goal in the dashboard instead."
1507
+ })
1508
+ );
1458
1509
  }
1459
1510
 
1460
1511
  // src/tools/funnels.ts
@@ -1606,7 +1657,7 @@ function registerGuardrailTools(server, client) {
1606
1657
  "list_guardrail_events",
1607
1658
  {
1608
1659
  title: "List guardrail events",
1609
- description: "List every variant currently paused by a guardrail (a paused variant stops serving until unpaused, however long ago the pause fired). Pass `days` to narrow to pauses fired within the last N days." + UNTRUSTED_FIELDS_NOTE,
1660
+ description: "List every variant currently paused (a paused variant stops serving until unpaused, however long ago the pause fired). `source` says who paused it: 'guardrail' = automatically, for converting clearly worse; 'manual' = a teammate paused it by hand (do not suggest resuming those without asking who paused them). Pass `days` to narrow to pauses fired within the last N days." + UNTRUSTED_FIELDS_NOTE,
1610
1661
  inputSchema: {
1611
1662
  projectId: projectIdSchema,
1612
1663
  days: import_zod8.z.number().int().min(1).max(365).optional().describe("Only pauses fired within the last N days. Omit for every still-paused variant.")
@@ -1615,9 +1666,10 @@ function registerGuardrailTools(server, client) {
1615
1666
  events: import_zod8.z.array(
1616
1667
  import_zod8.z.object({
1617
1668
  componentId: import_zod8.z.string(),
1618
- variantIds: import_zod8.z.array(import_zod8.z.string()).describe("Variants paused by the guardrail"),
1669
+ variantIds: import_zod8.z.array(import_zod8.z.string()).describe("Variants paused together"),
1619
1670
  pausedAt: import_zod8.z.string().nullable().describe("ISO timestamp the pause fired, or null"),
1620
- funnelId: import_zod8.z.string().nullable().describe("Set when the pause came from a funnel guardrail")
1671
+ funnelId: import_zod8.z.string().nullable().describe("Set when the pause came from a funnel guardrail"),
1672
+ source: import_zod8.z.enum(["guardrail", "manual"]).describe("'guardrail' = paused automatically; 'manual' = paused by a teammate")
1621
1673
  })
1622
1674
  ).describe("Currently paused variants (empty if none)")
1623
1675
  },
@@ -1633,24 +1685,25 @@ function registerGuardrailTools(server, client) {
1633
1685
  const data = await client.get(`/projects/${id}/guardrail-events${query}`);
1634
1686
  const structuredContent = {
1635
1687
  events: data.guardrailEvents.map((e) => {
1636
- var _a;
1688
+ var _a, _b;
1637
1689
  return {
1638
1690
  componentId: e.componentId,
1639
1691
  variantIds: e.variantIds,
1640
1692
  pausedAt: e.pausedAt,
1641
- funnelId: (_a = e.funnelId) != null ? _a : null
1693
+ funnelId: (_a = e.funnelId) != null ? _a : null,
1694
+ source: (_b = e.source) != null ? _b : "guardrail"
1642
1695
  };
1643
1696
  })
1644
1697
  };
1645
1698
  if (!data.guardrailEvents.length) {
1646
- const scope = days != null ? `paused by a guardrail in the last ${days} days` : "currently paused by a guardrail";
1699
+ const scope = days != null ? `paused in the last ${days} days` : "currently paused";
1647
1700
  return {
1648
1701
  content: [{ type: "text", text: `No variants ${scope}.` }],
1649
1702
  structuredContent
1650
1703
  };
1651
1704
  }
1652
- const lines = data.guardrailEvents.map(
1653
- (e) => `- ${untrusted(e.componentId)}: variants [${e.variantIds.map((v) => untrusted(v)).join(", ")}] paused${e.pausedAt ? ` at ${e.pausedAt}` : ""}${e.funnelId ? ` (protecting the ${untrusted(e.funnelId)} funnel)` : ""}`
1705
+ const lines = structuredContent.events.map(
1706
+ (e) => `- ${untrusted(e.componentId)}: variants [${e.variantIds.map((v) => untrusted(v)).join(", ")}] ${e.source === "manual" ? "paused by a teammate" : "paused automatically by a guardrail"}${e.pausedAt ? ` at ${e.pausedAt}` : ""}${e.funnelId ? ` (protecting the ${untrusted(e.funnelId)} funnel)` : ""}`
1654
1707
  );
1655
1708
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
1656
1709
  })
@@ -2082,7 +2135,10 @@ function guidanceFor(dataState, contextType, arms) {
2082
2135
  }
2083
2136
  case "directional": {
2084
2137
  const inconclusive = arms.some((a) => a.state === "inconclusive");
2085
- return 'No comparison on this component is decided. Early and moderate signals are hypotheses, not results (moderate evidence is wrong about one time in five), so do not target an arm as "under-" or "out-performing". ' + (inconclusive ? "At least one arm ran a full period without separating from the baseline \u2014 the existing versions do not differ meaningfully, so make a clearly different change rather than a tweak. " : "") + "Lean on the best-practice priors below and make one focused change.";
2138
+ const likelyWorse = arms.filter((a) => !a.isBaseline && a.state === "likely_worse").map((a) => untrusted(a.variantId));
2139
+ return 'No comparison on this component is decided. Early, moderate and likely-worse signals are hypotheses, not results (each is wrong about one time in five), so do not target an arm as "under-" or "out-performing". ' + (likelyWorse.length ? `Probably behind the baseline (not decided): ${likelyWorse.join(", ")} \u2014 do not build on ${likelyWorse.length === 1 ? "it" : "them"}. ` : "") + // "did not separate" is not "do not differ": the old wording asserted
2140
+ // an equality the evidence never measured.
2141
+ (inconclusive ? "At least one arm ran a full period without separating from the baseline \u2014 any difference between the existing versions is too small to detect, so make a clearly different change rather than a tweak. " : "") + "Lean on the best-practice priors below and make one focused change.";
2086
2142
  }
2087
2143
  case "collecting":
2088
2144
  if (!arms.some((a) => !a.isBaseline)) {
@@ -2912,7 +2968,8 @@ function registerCellTools(server, client) {
2912
2968
  refinedFrom: import_zod15.z.string().nullable().describe("Live version this one would replace; that version keeps serving until approval"),
2913
2969
  updatedAt: import_zod15.z.string()
2914
2970
  })
2915
- )
2971
+ ),
2972
+ total: import_zod15.z.number().int().describe("How many versions are waiting in all. The list holds at most the oldest 200; decide those to see the rest.")
2916
2973
  },
2917
2974
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
2918
2975
  },
@@ -2933,10 +2990,11 @@ function registerCellTools(server, client) {
2933
2990
  updatedAt: String(r.updatedAt)
2934
2991
  };
2935
2992
  });
2993
+ const total = typeof data.total === "number" ? data.total : reviews.length;
2936
2994
  if (reviews.length === 0) {
2937
2995
  return {
2938
2996
  content: [{ type: "text", text: "No generated versions are waiting for review." }],
2939
- structuredContent: { reviews }
2997
+ structuredContent: { reviews, total }
2940
2998
  };
2941
2999
  }
2942
3000
  const lines = reviews.map(
@@ -2947,8 +3005,11 @@ function registerCellTools(server, client) {
2947
3005
  r.refinedFrom ? ` Would replace ${untrusted(r.refinedFrom, 64)}, which keeps serving until approval.` : null
2948
3006
  ].filter((l) => l !== null).join("\n")
2949
3007
  );
3008
+ if (total > reviews.length) {
3009
+ lines.unshift(`Showing the ${reviews.length} oldest of ${total} versions waiting. Decide on these to see the rest.`, "");
3010
+ }
2950
3011
  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 } };
3012
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { reviews, total } };
2952
3013
  })
2953
3014
  );
2954
3015
  server.registerTool(
@@ -2995,7 +3056,7 @@ function registerCellTools(server, client) {
2995
3056
  }
2996
3057
 
2997
3058
  // src/server.ts
2998
- var PKG_VERSION = true ? "0.20.0" : "0.0.0-dev";
3059
+ var PKG_VERSION = true ? "0.22.0" : "0.0.0-dev";
2999
3060
  function createMcpServer(client) {
3000
3061
  const server = new import_mcp.McpServer(
3001
3062
  {
@@ -3056,6 +3117,7 @@ var TOOL_CATALOG = [
3056
3117
  // Goals, funnels, guardrails
3057
3118
  { 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
3119
  { name: "list_goals", access: "read", summary: "Defined goals (id, role, event, status), including ones with no conversions yet" },
3120
+ { name: "define_goal", access: "write", summary: "Create a new goal \u2014 never overwrites an existing goal and never changes the primary goal" },
3059
3121
  { name: "list_funnels", access: "read", summary: "Defined multi-step funnels" },
3060
3122
  { name: "get_funnel_report", access: "read", summary: "Step-by-step funnel drop-off" },
3061
3123
  { name: "list_guardrail_events", access: "read", summary: "Every variant currently paused by a guardrail (optionally only pauses from the last N days)" },
package/dist/lib.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  ApiError,
5
5
  PUBLIC_UI_RESOURCE_URIS,
6
6
  createMcpServer
7
- } from "./chunk-35I5EJTS.js";
7
+ } from "./chunk-A7LJWTYQ.js";
8
8
 
9
9
  // src/tool-catalog.ts
10
10
  var TOOL_CATALOG = [
@@ -28,6 +28,7 @@ var TOOL_CATALOG = [
28
28
  // Goals, funnels, guardrails
29
29
  { 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)" },
30
30
  { name: "list_goals", access: "read", summary: "Defined goals (id, role, event, status), including ones with no conversions yet" },
31
+ { name: "define_goal", access: "write", summary: "Create a new goal \u2014 never overwrites an existing goal and never changes the primary goal" },
31
32
  { name: "list_funnels", access: "read", summary: "Defined multi-step funnels" },
32
33
  { name: "get_funnel_report", access: "read", summary: "Step-by-step funnel drop-off" },
33
34
  { name: "list_guardrail_events", access: "read", summary: "Every variant currently paused by a guardrail (optionally only pauses from the last N days)" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentientui/mcp",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "MCP server for SentientUI — exposes project data and actions to AI agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentient-ui.com",
@@ -35,7 +35,8 @@
35
35
  "types": "./dist/lib.d.ts",
36
36
  "import": "./dist/lib.js",
37
37
  "require": "./dist/lib.cjs"
38
- }
38
+ },
39
+ "./package.json": "./package.json"
39
40
  },
40
41
  "files": [
41
42
  "dist",