@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 +2 -1
- package/README.md +1 -0
- package/dist/{chunk-35I5EJTS.js → chunk-K7JN3LOH.js} +58 -4
- package/dist/index.cjs +58 -4
- package/dist/index.js +1 -1
- package/dist/lib.cjs +59 -4
- 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
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
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
|
|
@@ -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
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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",
|