@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 +2 -1
- package/README.md +1 -0
- package/dist/{chunk-35I5EJTS.js → chunk-A7LJWTYQ.js} +76 -15
- package/dist/index.cjs +76 -15
- package/dist/index.js +1 -1
- package/dist/lib.cjs +77 -15
- package/dist/lib.js +2 -1
- package/package.json +3 -2
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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",
|