@sentientui/mcp 0.20.0 → 0.21.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
@@ -2052,7 +2103,10 @@ function guidanceFor(dataState, contextType, arms) {
2052
2103
  }
2053
2104
  case "directional": {
2054
2105
  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.";
2106
+ const likelyWorse = arms.filter((a) => !a.isBaseline && a.state === "likely_worse").map((a) => untrusted(a.variantId));
2107
+ 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
2108
+ // an equality the evidence never measured.
2109
+ (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
2110
  }
2057
2111
  case "collecting":
2058
2112
  if (!arms.some((a) => !a.isBaseline)) {
@@ -2965,7 +3019,7 @@ function registerCellTools(server, client) {
2965
3019
  }
2966
3020
 
2967
3021
  // src/server.ts
2968
- var PKG_VERSION = true ? "0.20.0" : "0.0.0-dev";
3022
+ var PKG_VERSION = true ? "0.21.0" : "0.0.0-dev";
2969
3023
  function createMcpServer(client) {
2970
3024
  const server = new McpServer(
2971
3025
  {
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
@@ -2056,7 +2107,10 @@ function guidanceFor(dataState, contextType, arms) {
2056
2107
  }
2057
2108
  case "directional": {
2058
2109
  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.";
2110
+ const likelyWorse = arms.filter((a) => !a.isBaseline && a.state === "likely_worse").map((a) => untrusted(a.variantId));
2111
+ 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
2112
+ // an equality the evidence never measured.
2113
+ (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
2114
  }
2061
2115
  case "collecting":
2062
2116
  if (!arms.some((a) => !a.isBaseline)) {
@@ -2969,7 +3023,7 @@ function registerCellTools(server, client) {
2969
3023
  }
2970
3024
 
2971
3025
  // src/server.ts
2972
- var PKG_VERSION = true ? "0.20.0" : "0.0.0-dev";
3026
+ var PKG_VERSION = true ? "0.21.0" : "0.0.0-dev";
2973
3027
  function createMcpServer(client) {
2974
3028
  const server = new import_mcp.McpServer(
2975
3029
  {
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-K7JN3LOH.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
@@ -2082,7 +2133,10 @@ function guidanceFor(dataState, contextType, arms) {
2082
2133
  }
2083
2134
  case "directional": {
2084
2135
  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.";
2136
+ const likelyWorse = arms.filter((a) => !a.isBaseline && a.state === "likely_worse").map((a) => untrusted(a.variantId));
2137
+ 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
2138
+ // an equality the evidence never measured.
2139
+ (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
2140
  }
2087
2141
  case "collecting":
2088
2142
  if (!arms.some((a) => !a.isBaseline)) {
@@ -2995,7 +3049,7 @@ function registerCellTools(server, client) {
2995
3049
  }
2996
3050
 
2997
3051
  // src/server.ts
2998
- var PKG_VERSION = true ? "0.20.0" : "0.0.0-dev";
3052
+ var PKG_VERSION = true ? "0.21.0" : "0.0.0-dev";
2999
3053
  function createMcpServer(client) {
3000
3054
  const server = new import_mcp.McpServer(
3001
3055
  {
@@ -3056,6 +3110,7 @@ var TOOL_CATALOG = [
3056
3110
  // Goals, funnels, guardrails
3057
3111
  { 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
3112
  { name: "list_goals", access: "read", summary: "Defined goals (id, role, event, status), including ones with no conversions yet" },
3113
+ { name: "define_goal", access: "write", summary: "Create a new goal \u2014 never overwrites an existing goal and never changes the primary goal" },
3059
3114
  { name: "list_funnels", access: "read", summary: "Defined multi-step funnels" },
3060
3115
  { name: "get_funnel_report", access: "read", summary: "Step-by-step funnel drop-off" },
3061
3116
  { 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-K7JN3LOH.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.21.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",