@sentientui/mcp 0.19.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 +60 -27
- package/README.md +45 -28
- package/dist/{chunk-HT23NB6F.js → chunk-K7JN3LOH.js} +481 -49
- package/dist/index.cjs +486 -49
- package/dist/index.js +6 -1
- package/dist/lib.cjs +531 -49
- package/dist/lib.d.cts +29 -2
- package/dist/lib.d.ts +29 -2
- package/dist/lib.js +49 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -25,9 +25,10 @@ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
25
25
|
|
|
26
26
|
// src/api-client.ts
|
|
27
27
|
var ApiError = class extends Error {
|
|
28
|
-
constructor(status, message) {
|
|
28
|
+
constructor(status, message, detail) {
|
|
29
29
|
super(message);
|
|
30
30
|
this.status = status;
|
|
31
|
+
this.detail = detail;
|
|
31
32
|
this.name = "ApiError";
|
|
32
33
|
}
|
|
33
34
|
};
|
|
@@ -44,8 +45,14 @@ var ApiClient = class {
|
|
|
44
45
|
async post(path, body) {
|
|
45
46
|
return this.request("POST", path, body);
|
|
46
47
|
}
|
|
48
|
+
async patch(path, body) {
|
|
49
|
+
return this.request("PATCH", path, body);
|
|
50
|
+
}
|
|
51
|
+
async delete(path, body) {
|
|
52
|
+
return this.request("DELETE", path, body);
|
|
53
|
+
}
|
|
47
54
|
async request(method, path, body, isRetry = false) {
|
|
48
|
-
var _a2, _b;
|
|
55
|
+
var _a2, _b, _c;
|
|
49
56
|
const res = await fetch(`${this.baseUrl}/v1/mgmt${path}`, {
|
|
50
57
|
method,
|
|
51
58
|
headers: __spreadValues({
|
|
@@ -62,7 +69,9 @@ var ApiClient = class {
|
|
|
62
69
|
return this.request(method, path, body, true);
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
|
-
|
|
72
|
+
const code = String((_b = (_a2 = errBody.error) != null ? _a2 : errBody.message) != null ? _b : res.statusText);
|
|
73
|
+
const human = (_c = errBody.message) != null ? _c : errBody.reason;
|
|
74
|
+
throw new ApiError(res.status, code, typeof human === "string" && human !== code ? human : void 0);
|
|
66
75
|
}
|
|
67
76
|
};
|
|
68
77
|
|
|
@@ -141,7 +150,7 @@ function localDate(iso, timeZone) {
|
|
|
141
150
|
}
|
|
142
151
|
function apiErrorGuidance(err, extra) {
|
|
143
152
|
if (extra && Object.prototype.hasOwnProperty.call(extra, err.message)) {
|
|
144
|
-
return extra[err.message];
|
|
153
|
+
return extra[err.message] + detailSuffix(err);
|
|
145
154
|
}
|
|
146
155
|
switch (err.message) {
|
|
147
156
|
case "insufficient_scope":
|
|
@@ -150,6 +159,15 @@ function apiErrorGuidance(err, extra) {
|
|
|
150
159
|
return "Demo mode is read-only. Create a SentientUI account and sign in (or use a project server key) to make changes.";
|
|
151
160
|
case "insufficient_role":
|
|
152
161
|
return "Your account role does not permit this action \u2014 it needs the account owner or an admin.";
|
|
162
|
+
// The mgmt write routes answer a viewer with `role_required` (and origins
|
|
163
|
+
// with `not_authorized`), not `insufficient_role` — so every role refusal
|
|
164
|
+
// fell through to the generic 403 "check your key has access" advice and
|
|
165
|
+
// sent the agent re-authenticating for a permission it already had wrong.
|
|
166
|
+
case "role_required":
|
|
167
|
+
case "not_authorized":
|
|
168
|
+
return `Your role on this project cannot make this change \u2014 it needs an editor or admin.${detailSuffix(err)}`;
|
|
169
|
+
case "ai_unavailable":
|
|
170
|
+
return "AI generation is unavailable on the SentientUI server right now (no model is configured). Nothing was queued \u2014 retry later.";
|
|
153
171
|
default:
|
|
154
172
|
break;
|
|
155
173
|
}
|
|
@@ -181,10 +199,25 @@ function apiErrorGuidance(err, extra) {
|
|
|
181
199
|
return `This feature requires a higher plan (${err.message}). Upgrade your SentientUI plan, then try again.`;
|
|
182
200
|
}
|
|
183
201
|
if (err.status === 403) {
|
|
184
|
-
return `Access denied (${err.message}). Check that your key or login has access to this project
|
|
202
|
+
return `Access denied (${err.message}). Check that your key or login has access to this project.${detailSuffix(err)}`;
|
|
203
|
+
}
|
|
204
|
+
if (err.status === 404) {
|
|
205
|
+
return `Not found (${err.message}). The project, component, variant, goal or cell does not exist, or this credential cannot see it (an sk_ server key sees only its own project). Re-read the ids with list_projects, list_components, list_goals or get_cell_matrix and retry with an exact id.` + detailSuffix(err);
|
|
206
|
+
}
|
|
207
|
+
if (err.status === 400) {
|
|
208
|
+
return `The API rejected the arguments (${err.message}).${detailSuffix(err)} Correct the input and retry \u2014 repeating the same call will fail the same way.`;
|
|
209
|
+
}
|
|
210
|
+
if (err.status === 409) {
|
|
211
|
+
return `Conflict (${err.message}): the target's current state does not allow this.${detailSuffix(err)} Read its current state first, then retry the step that makes this valid.`;
|
|
212
|
+
}
|
|
213
|
+
if (err.status === 502 || err.status === 503 || err.status === 504) {
|
|
214
|
+
return `SentientUI is temporarily unavailable (${err.status} ${err.message}). Nothing changed \u2014 wait a moment and retry.`;
|
|
185
215
|
}
|
|
186
216
|
return null;
|
|
187
217
|
}
|
|
218
|
+
function detailSuffix(err) {
|
|
219
|
+
return err.detail ? ` Server says: ${untrusted(err.detail, 300)}` : "";
|
|
220
|
+
}
|
|
188
221
|
async function settled(p) {
|
|
189
222
|
try {
|
|
190
223
|
return { ok: true, value: await p };
|
|
@@ -297,6 +330,37 @@ function registerProjectTools(server, client) {
|
|
|
297
330
|
};
|
|
298
331
|
}, CREATE_PROJECT_GUIDANCE)
|
|
299
332
|
);
|
|
333
|
+
server.registerTool(
|
|
334
|
+
"add_origin",
|
|
335
|
+
{
|
|
336
|
+
title: "Add allowed origin",
|
|
337
|
+
description: "Allow-list a site origin (scheme + host, e.g. https://www.example.com) so the SDK's events from it are accepted. Idempotent: an origin already on the list is left as is. Removing an origin is dashboard-only. Needs an editor/admin login or the project's sk_ server key. Confirm the exact origin with the user first \u2014 an allowed origin can send events into this project.",
|
|
338
|
+
inputSchema: {
|
|
339
|
+
projectId: projectIdSchema,
|
|
340
|
+
origin: import_zod2.z.string().min(1).max(500).describe("The site origin, e.g. https://www.example.com (a pasted page URL is reduced to its origin)")
|
|
341
|
+
},
|
|
342
|
+
outputSchema: {
|
|
343
|
+
origins: import_zod2.z.array(import_zod2.z.string()).describe("The full allow-list after the change")
|
|
344
|
+
},
|
|
345
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
346
|
+
},
|
|
347
|
+
withApiErrorGuidance(async ({ projectId, origin }) => {
|
|
348
|
+
var _a2;
|
|
349
|
+
const id = encodeURIComponent(projectId);
|
|
350
|
+
const res = await client.post(`/projects/${id}/origins`, { origin });
|
|
351
|
+
const origins = (_a2 = res.origins) != null ? _a2 : [];
|
|
352
|
+
return {
|
|
353
|
+
content: [{
|
|
354
|
+
type: "text",
|
|
355
|
+
text: `Allowed origins: ${origins.map((o) => untrusted(o)).join(", ") || "(none)"}. Events from these origins are accepted (other API instances pick the change up within ~30 seconds, when their key cache expires).`
|
|
356
|
+
}],
|
|
357
|
+
structuredContent: { origins }
|
|
358
|
+
};
|
|
359
|
+
}, {
|
|
360
|
+
invalid_origin: "That is not a usable origin. Pass a scheme and host, e.g. https://www.example.com.",
|
|
361
|
+
origin_required: "An origin is required, e.g. https://www.example.com."
|
|
362
|
+
})
|
|
363
|
+
);
|
|
300
364
|
server.registerTool(
|
|
301
365
|
"list_projects",
|
|
302
366
|
{
|
|
@@ -341,13 +405,16 @@ function registerProjectTools(server, client) {
|
|
|
341
405
|
"get_project_stats",
|
|
342
406
|
{
|
|
343
407
|
title: "Project health stats",
|
|
344
|
-
description: "Get health
|
|
408
|
+
description: "Get ingestion health for a project: status, events and sessions in the last 24 hours, the time of the last event, and the all-time count of calls to the SentientUI agent API (/v1/agent) \u2014 NOT MCP tool calls.",
|
|
345
409
|
inputSchema: { projectId: projectIdSchema },
|
|
346
410
|
outputSchema: {
|
|
347
411
|
status: import_zod2.z.string().describe("Overall project health status"),
|
|
348
412
|
events24h: import_zod2.z.number().describe("Events in the last 24 hours"),
|
|
349
413
|
sessions24h: import_zod2.z.number().describe("Sessions in the last 24 hours"),
|
|
350
|
-
|
|
414
|
+
// Was "Total agent (MCP/API) calls" — but the count comes from
|
|
415
|
+
// agent_events, which only the /v1/agent endpoint writes. MCP tool calls
|
|
416
|
+
// are never in it, so an agent read its own activity as missing.
|
|
417
|
+
agentCalls: import_zod2.z.number().describe("All-time calls to the /v1/agent agent API (not MCP tool calls)"),
|
|
351
418
|
lastEventAt: import_zod2.z.string().nullable().describe("ISO timestamp of the last event, or null")
|
|
352
419
|
},
|
|
353
420
|
annotations: {
|
|
@@ -364,7 +431,7 @@ function registerProjectTools(server, client) {
|
|
|
364
431
|
`Status: ${stats.status}`,
|
|
365
432
|
`Events (24h): ${stats.events24h}`,
|
|
366
433
|
`Sessions (24h): ${stats.sessions24h}`,
|
|
367
|
-
`Agent calls (
|
|
434
|
+
`Agent API calls (/v1/agent, all-time): ${stats.agentCalls}`,
|
|
368
435
|
`Last event: ${(_a2 = stats.lastEventAt) != null ? _a2 : "never"}`
|
|
369
436
|
].join("\n");
|
|
370
437
|
return {
|
|
@@ -657,6 +724,10 @@ var EVIDENCE_LABEL = {
|
|
|
657
724
|
early_signal: "Early signal (still moving \u2014 not a result)",
|
|
658
725
|
moderate_evidence: "Moderate evidence (about one call in five of this strength is wrong \u2014 not decided)",
|
|
659
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)",
|
|
660
731
|
inconclusive: "Inconclusive (ran a full period without separating from the baseline)"
|
|
661
732
|
};
|
|
662
733
|
var STATES = new Set(Object.keys(EVIDENCE_LABEL));
|
|
@@ -707,7 +778,9 @@ function verdictOf(state, stats, isBaseline) {
|
|
|
707
778
|
var VERDICT_TEXT = {
|
|
708
779
|
ahead: "reliably AHEAD of the baseline",
|
|
709
780
|
behind: "reliably BEHIND the baseline",
|
|
710
|
-
|
|
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",
|
|
711
784
|
no_evidence: "no evidence available from the server"
|
|
712
785
|
};
|
|
713
786
|
function rateWithN(conversions, n, unit = "sessions") {
|
|
@@ -793,7 +866,7 @@ function registerComponentTools(server, client) {
|
|
|
793
866
|
conversions: import_zod3.z.number().describe("Converting sessions in the window"),
|
|
794
867
|
currentCvr: import_zod3.z.number().nullable().describe("conversions / sessions over the window (0-1); null at 0 sessions"),
|
|
795
868
|
lowSample: import_zod3.z.boolean().describe(`True under ${MIN_EVIDENCE_SAMPLE} sessions \u2014 the rate is not interpretable on its own`),
|
|
796
|
-
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"),
|
|
797
870
|
probBeatsBaseline: import_zod3.z.number().nullable().describe("P(this arm > baseline), all-time, from the server; null when not provided"),
|
|
798
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"),
|
|
799
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"),
|
|
@@ -1111,7 +1184,10 @@ function registerPersonaTools(server, client) {
|
|
|
1111
1184
|
"get_persona_breakdown",
|
|
1112
1185
|
{
|
|
1113
1186
|
title: "Persona breakdown",
|
|
1114
|
-
|
|
1187
|
+
// Takes no range/from/to on purpose: the API computes it over every
|
|
1188
|
+
// retained session and has no window. Saying so stops an agent from
|
|
1189
|
+
// reading it beside a 7d get_variant_performance as the same period.
|
|
1190
|
+
description: "Get the distribution of visitor persona clusters with session counts and reliability scores, over ALL retained human sessions (not windowed \u2014 this has no range input, unlike the conversion tools)." + UNTRUSTED_FIELDS_NOTE,
|
|
1115
1191
|
inputSchema: { projectId: projectIdSchema },
|
|
1116
1192
|
_meta: uiMeta("persona-breakdown"),
|
|
1117
1193
|
outputSchema: {
|
|
@@ -1172,8 +1248,13 @@ function registerGoalTools(server, client) {
|
|
|
1172
1248
|
server.registerTool(
|
|
1173
1249
|
"get_goal_funnel",
|
|
1174
1250
|
{
|
|
1175
|
-
|
|
1176
|
-
|
|
1251
|
+
// Named get_goal_funnel for compatibility (renaming a published tool
|
|
1252
|
+
// breaks every saved prompt and host allow-list), but it is NOT a funnel:
|
|
1253
|
+
// agents asked it for step drop-off and reported per-goal rates as funnel
|
|
1254
|
+
// stages. The title and the first words of the description now say what
|
|
1255
|
+
// it is, and point to the real funnel tool.
|
|
1256
|
+
title: "Goal conversions (per goal \u2014 not a step funnel)",
|
|
1257
|
+
description: "Per-goal conversion summary \u2014 NOT a step funnel (for step-by-step drop-off use list_funnels + get_funnel_report). Gets goal hit counts and unique-session conversion rates for a window (default: last 30 calendar days), plus a per-variant breakdown. NOTE: the per-variant completionRate values are ALL-TIME (bounded only by plan retention), NOT windowed \u2014 a variant completion rate needs the full assignment history as its denominator, so it does not change when you narrow `range`, and retired components keep appearing in it. Only hits/uniqueSessions/conversionRate/revenue respect the window. Per-variant rates are descriptive, not joined to the evidence verdicts \u2014 for whether an arm is ahead, use get_variant_performance." + UNTRUSTED_FIELDS_NOTE,
|
|
1177
1258
|
inputSchema: __spreadValues({ projectId: projectIdSchema }, rangeInputSchema("30d")),
|
|
1178
1259
|
_meta: uiMeta("goal-funnel"),
|
|
1179
1260
|
outputSchema: {
|
|
@@ -1354,6 +1435,51 @@ function registerGoalTools(server, client) {
|
|
|
1354
1435
|
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
1355
1436
|
})
|
|
1356
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
|
+
);
|
|
1357
1483
|
}
|
|
1358
1484
|
|
|
1359
1485
|
// src/tools/funnels.ts
|
|
@@ -1563,7 +1689,9 @@ function registerLayoutTools(server, client) {
|
|
|
1563
1689
|
"get_layout_stats",
|
|
1564
1690
|
{
|
|
1565
1691
|
title: "Layout stats",
|
|
1566
|
-
|
|
1692
|
+
// No window by design: these are the bandit's cumulative pulls and reward,
|
|
1693
|
+
// the state serving reads — not a report over a period.
|
|
1694
|
+
description: "Get per-persona section layout rankings with bandit pulls and average reward \u2014 the cumulative learning state serving uses (not windowed; there is no range input). avgReward is a reward signal, not a conversion rate." + UNTRUSTED_FIELDS_NOTE,
|
|
1567
1695
|
inputSchema: { projectId: projectIdSchema },
|
|
1568
1696
|
_meta: uiMeta("layout-stats"),
|
|
1569
1697
|
outputSchema: {
|
|
@@ -1615,7 +1743,7 @@ function registerVariantWriteTools(server, client) {
|
|
|
1615
1743
|
"create_variant",
|
|
1616
1744
|
{
|
|
1617
1745
|
title: "Create managed variant",
|
|
1618
|
-
description: "Create a NO-CODE managed text variant for a component (content stored in SentientUI, rendered by <AdaptiveText>). Use this ONLY for text-only variants the user wants without a code change. For variants that will live in the codebase (full components \u2014 copy, markup, styling), use get_variant_brief and write the variant in code instead; those auto-register on deploy and do not need create_variant.
|
|
1746
|
+
description: "Create a NO-CODE managed text variant for a component (content stored in SentientUI, rendered by <AdaptiveText>). Use this ONLY for text-only variants the user wants without a code change. For variants that will live in the codebase (full components \u2014 copy, markup, styling), use get_variant_brief and write the variant in code instead; those auto-register on deploy and do not need create_variant. Works on every plan with an editor/admin login or the project's sk_ server key (anonymous demo tokens are read-only). The variant is created as a DRAFT \u2014 it serves no traffic until activated with update_variant (status 'active'). Confirm the text with the user before calling.",
|
|
1619
1747
|
inputSchema: {
|
|
1620
1748
|
projectId: projectIdSchema,
|
|
1621
1749
|
componentId: import_zod10.z.string().min(1).max(200).describe("The component ID to add a variant to"),
|
|
@@ -1649,7 +1777,7 @@ function registerVariantWriteTools(server, client) {
|
|
|
1649
1777
|
return {
|
|
1650
1778
|
content: [{
|
|
1651
1779
|
type: "text",
|
|
1652
|
-
text: `Managed text variant created: ${result.variantId} ("${result.displayName}") for component ${componentId}${contentNote}. It is
|
|
1780
|
+
text: `Managed text variant created: ${result.variantId} ("${result.displayName}") for component ${componentId}${contentNote}. It is a draft and serves no traffic yet \u2014 after the user confirms, activate it with update_variant (status 'active'), or delete it with delete_variant_draft. Reminder: this is a no-code managed variant; for code-native variants, edit the code instead (see get_variant_brief).`
|
|
1653
1781
|
}],
|
|
1654
1782
|
structuredContent: {
|
|
1655
1783
|
variantId: result.variantId,
|
|
@@ -1665,20 +1793,24 @@ function registerVariantWriteTools(server, client) {
|
|
|
1665
1793
|
"pause_variant",
|
|
1666
1794
|
{
|
|
1667
1795
|
title: "Pause variant",
|
|
1668
|
-
description: "Pause a variant
|
|
1796
|
+
description: "Pause a variant: no new traffic is assigned to it until it is resumed. This changes what live visitors see, so CONFIRM with the user before calling. Undo with resume_variant.",
|
|
1669
1797
|
inputSchema: {
|
|
1670
1798
|
projectId: projectIdSchema,
|
|
1671
|
-
componentId: import_zod10.z.string().describe("The component ID"),
|
|
1672
|
-
variantId: import_zod10.z.string().describe("The variant ID to pause")
|
|
1799
|
+
componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
|
|
1800
|
+
variantId: import_zod10.z.string().min(1).max(200).describe("The variant ID to pause")
|
|
1673
1801
|
},
|
|
1674
1802
|
outputSchema: {
|
|
1675
1803
|
variantId: import_zod10.z.string(),
|
|
1676
1804
|
componentId: import_zod10.z.string(),
|
|
1677
1805
|
paused: import_zod10.z.literal(true).describe("The variant is now paused")
|
|
1678
1806
|
},
|
|
1807
|
+
// destructiveHint was false, so hosts that gate destructive tools behind a
|
|
1808
|
+
// confirmation ran this without asking — yet it pulls a live arm out of
|
|
1809
|
+
// serving for every visitor. It is reversible (resume_variant), but the
|
|
1810
|
+
// hint is about the change to the environment, not about undo.
|
|
1679
1811
|
annotations: {
|
|
1680
1812
|
readOnlyHint: false,
|
|
1681
|
-
destructiveHint:
|
|
1813
|
+
destructiveHint: true,
|
|
1682
1814
|
idempotentHint: true,
|
|
1683
1815
|
openWorldHint: false
|
|
1684
1816
|
}
|
|
@@ -1689,17 +1821,194 @@ function registerVariantWriteTools(server, client) {
|
|
|
1689
1821
|
return {
|
|
1690
1822
|
content: [{
|
|
1691
1823
|
type: "text",
|
|
1692
|
-
text: `Variant ${variantId} in component ${componentId}
|
|
1824
|
+
text: `Variant ${untrusted(variantId)} in component ${untrusted(componentId)} is paused. No new traffic will be assigned to it. Undo with resume_variant.`
|
|
1693
1825
|
}],
|
|
1694
1826
|
structuredContent: { variantId, componentId, paused: true }
|
|
1695
1827
|
};
|
|
1696
1828
|
})
|
|
1697
1829
|
);
|
|
1830
|
+
server.registerTool(
|
|
1831
|
+
"resume_variant",
|
|
1832
|
+
{
|
|
1833
|
+
title: "Resume variant",
|
|
1834
|
+
description: "Resume a paused variant so it is assigned traffic again \u2014 works for a manual pause (pause_variant) and for a guardrail auto-pause (see list_guardrail_events). Resuming a guardrail-paused arm puts a variant the guardrail judged harmful back in front of visitors, so CONFIRM with the user first.",
|
|
1835
|
+
inputSchema: {
|
|
1836
|
+
projectId: projectIdSchema,
|
|
1837
|
+
componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
|
|
1838
|
+
variantId: import_zod10.z.string().min(1).max(200).describe("The paused variant ID to resume")
|
|
1839
|
+
},
|
|
1840
|
+
outputSchema: {
|
|
1841
|
+
variantId: import_zod10.z.string(),
|
|
1842
|
+
componentId: import_zod10.z.string(),
|
|
1843
|
+
paused: import_zod10.z.literal(false).describe("The variant is no longer paused")
|
|
1844
|
+
},
|
|
1845
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1846
|
+
},
|
|
1847
|
+
withApiErrorGuidance(async ({ projectId, componentId, variantId }) => {
|
|
1848
|
+
const id = encodeURIComponent(projectId);
|
|
1849
|
+
await client.post(`/projects/${id}/guardrails/${encodeURIComponent(componentId)}/unpause`, {
|
|
1850
|
+
variantIds: [variantId]
|
|
1851
|
+
});
|
|
1852
|
+
return {
|
|
1853
|
+
content: [{
|
|
1854
|
+
type: "text",
|
|
1855
|
+
text: `Variant ${untrusted(variantId)} in component ${untrusted(componentId)} is resumed and will be assigned traffic again.`
|
|
1856
|
+
}],
|
|
1857
|
+
structuredContent: { variantId, componentId, paused: false }
|
|
1858
|
+
};
|
|
1859
|
+
})
|
|
1860
|
+
);
|
|
1861
|
+
server.registerTool(
|
|
1862
|
+
"update_variant",
|
|
1863
|
+
{
|
|
1864
|
+
title: "Update managed variant",
|
|
1865
|
+
description: "Edit a NO-CODE managed variant (one created with create_variant or in the dashboard): its display name, its text, or its status \u2014 'active' starts serving it to visitors, 'draft' takes it back out of rotation. Code-native variants cannot be edited here (change the code). Overwriting text or activating changes what live visitors see, so CONFIRM with the user first. Omit a field to leave it unchanged.",
|
|
1866
|
+
inputSchema: {
|
|
1867
|
+
projectId: projectIdSchema,
|
|
1868
|
+
componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
|
|
1869
|
+
variantId: import_zod10.z.string().min(1).max(200).describe("The managed variant ID (dm_\u2026)"),
|
|
1870
|
+
displayName: import_zod10.z.string().min(1).max(200).optional().describe("New display name"),
|
|
1871
|
+
content: import_zod10.z.string().max(1e4).optional().describe("New text content (replaces the old text)"),
|
|
1872
|
+
status: import_zod10.z.enum(["draft", "active"]).optional().describe("'active' serves it; 'draft' stops serving it")
|
|
1873
|
+
},
|
|
1874
|
+
outputSchema: {
|
|
1875
|
+
variantId: import_zod10.z.string(),
|
|
1876
|
+
componentId: import_zod10.z.string(),
|
|
1877
|
+
changed: import_zod10.z.array(import_zod10.z.string()).describe("Fields that were sent for update"),
|
|
1878
|
+
status: import_zod10.z.enum(["draft", "active"]).nullable().describe("The new status, or null when unchanged")
|
|
1879
|
+
},
|
|
1880
|
+
// Destructive: the previous text is overwritten, not versioned.
|
|
1881
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
1882
|
+
},
|
|
1883
|
+
withApiErrorGuidance(async ({ projectId, componentId, variantId, displayName, content, status }) => {
|
|
1884
|
+
const changed = [
|
|
1885
|
+
...displayName !== void 0 ? ["displayName"] : [],
|
|
1886
|
+
...content !== void 0 ? ["content"] : [],
|
|
1887
|
+
...status !== void 0 ? ["status"] : []
|
|
1888
|
+
];
|
|
1889
|
+
if (changed.length === 0) {
|
|
1890
|
+
return {
|
|
1891
|
+
content: [{ type: "text", text: "Nothing to update \u2014 pass displayName, content and/or status." }],
|
|
1892
|
+
isError: true
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
const id = encodeURIComponent(projectId);
|
|
1896
|
+
await client.patch(`/projects/${id}/variants`, { componentId, variantId, displayName, content, status });
|
|
1897
|
+
const statusNote = status === "active" ? " It is now ACTIVE and will be assigned traffic." : status === "draft" ? " It is now a draft and no longer serves traffic." : "";
|
|
1898
|
+
return {
|
|
1899
|
+
content: [{
|
|
1900
|
+
type: "text",
|
|
1901
|
+
text: `Managed variant ${untrusted(variantId)} in component ${untrusted(componentId)} updated (${changed.join(", ")}).${statusNote}`
|
|
1902
|
+
}],
|
|
1903
|
+
structuredContent: { variantId, componentId, changed, status: status != null ? status : null }
|
|
1904
|
+
};
|
|
1905
|
+
}, {
|
|
1906
|
+
// PATCH matches managed variants only, so a code-native id answers a
|
|
1907
|
+
// bare not_found that read as "that variant does not exist".
|
|
1908
|
+
not_found: "No MANAGED variant with that id exists in this component. Code-native variants (declared in <Adaptive variants>) are edited in the code, not here \u2014 check ids with list_components (a wrong projectId answers the same way)."
|
|
1909
|
+
})
|
|
1910
|
+
);
|
|
1911
|
+
server.registerTool(
|
|
1912
|
+
"delete_variant_draft",
|
|
1913
|
+
{
|
|
1914
|
+
title: "Delete draft variant",
|
|
1915
|
+
description: "Permanently delete a managed variant that is still a DRAFT (never activated). Active variants cannot be deleted \u2014 pause or retire them instead. CONFIRM with the user first: this cannot be undone.",
|
|
1916
|
+
inputSchema: {
|
|
1917
|
+
projectId: projectIdSchema,
|
|
1918
|
+
componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
|
|
1919
|
+
variantId: import_zod10.z.string().min(1).max(200).describe("The draft managed variant ID (dm_\u2026)")
|
|
1920
|
+
},
|
|
1921
|
+
outputSchema: {
|
|
1922
|
+
variantId: import_zod10.z.string(),
|
|
1923
|
+
componentId: import_zod10.z.string(),
|
|
1924
|
+
deleted: import_zod10.z.literal(true)
|
|
1925
|
+
},
|
|
1926
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
|
|
1927
|
+
},
|
|
1928
|
+
withApiErrorGuidance(async ({ projectId, componentId, variantId }) => {
|
|
1929
|
+
const id = encodeURIComponent(projectId);
|
|
1930
|
+
await client.delete(`/projects/${id}/variants`, { componentId, variantId });
|
|
1931
|
+
return {
|
|
1932
|
+
content: [{
|
|
1933
|
+
type: "text",
|
|
1934
|
+
text: `Draft variant ${untrusted(variantId)} in component ${untrusted(componentId)} was deleted.`
|
|
1935
|
+
}],
|
|
1936
|
+
structuredContent: { variantId, componentId, deleted: true }
|
|
1937
|
+
};
|
|
1938
|
+
}, {
|
|
1939
|
+
// The API's DELETE matches managed drafts only, so an active or
|
|
1940
|
+
// code-native variant answers a bare not_found — say why.
|
|
1941
|
+
not_found: "No managed DRAFT variant with that id exists in this component. Active and code-native variants cannot be deleted \u2014 use pause_variant or retire_variant instead (check ids with list_components; a wrong projectId answers the same way)."
|
|
1942
|
+
})
|
|
1943
|
+
);
|
|
1944
|
+
server.registerTool(
|
|
1945
|
+
"retire_variant",
|
|
1946
|
+
{
|
|
1947
|
+
title: "Retire variant",
|
|
1948
|
+
description: "Retire (archive) an ACTIVE variant: it stops serving and moves to the archived list. Works for code-native variants that were removed from the code and linger. A promoted (pinned) variant must be unpromoted in the dashboard first. CONFIRM with the user first; undo with restore_variant.",
|
|
1949
|
+
inputSchema: {
|
|
1950
|
+
projectId: projectIdSchema,
|
|
1951
|
+
componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
|
|
1952
|
+
variantId: import_zod10.z.string().min(1).max(200).describe("The active variant ID to retire")
|
|
1953
|
+
},
|
|
1954
|
+
outputSchema: {
|
|
1955
|
+
variantId: import_zod10.z.string(),
|
|
1956
|
+
componentId: import_zod10.z.string(),
|
|
1957
|
+
status: import_zod10.z.literal("retired")
|
|
1958
|
+
},
|
|
1959
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
1960
|
+
},
|
|
1961
|
+
withApiErrorGuidance(async ({ projectId, componentId, variantId }) => {
|
|
1962
|
+
const id = encodeURIComponent(projectId);
|
|
1963
|
+
await client.post(`/projects/${id}/variants/retire`, { componentId, variantId });
|
|
1964
|
+
return {
|
|
1965
|
+
content: [{
|
|
1966
|
+
type: "text",
|
|
1967
|
+
text: `Variant ${untrusted(variantId)} in component ${untrusted(componentId)} is retired and no longer serves. Undo with restore_variant.`
|
|
1968
|
+
}],
|
|
1969
|
+
structuredContent: { variantId, componentId, status: "retired" }
|
|
1970
|
+
};
|
|
1971
|
+
}, {
|
|
1972
|
+
variant_promoted: "This variant is promoted (pinned as the winner), so it cannot be retired. Unpromote it in the dashboard first, then retry.",
|
|
1973
|
+
not_found: "No ACTIVE variant with that id exists in this component (it may already be retired, or still a draft), or the projectId is wrong. Check ids with list_components."
|
|
1974
|
+
})
|
|
1975
|
+
);
|
|
1976
|
+
server.registerTool(
|
|
1977
|
+
"restore_variant",
|
|
1978
|
+
{
|
|
1979
|
+
title: "Restore variant",
|
|
1980
|
+
description: "Bring a RETIRED variant back to active so it serves again. For a managed draft that was never activated use update_variant (status 'active') instead. Restoring does not clear a pause \u2014 use resume_variant for that. CONFIRM with the user first.",
|
|
1981
|
+
inputSchema: {
|
|
1982
|
+
projectId: projectIdSchema,
|
|
1983
|
+
componentId: import_zod10.z.string().min(1).max(200).describe("The component ID"),
|
|
1984
|
+
variantId: import_zod10.z.string().min(1).max(200).describe("The retired variant ID to restore")
|
|
1985
|
+
},
|
|
1986
|
+
outputSchema: {
|
|
1987
|
+
variantId: import_zod10.z.string(),
|
|
1988
|
+
componentId: import_zod10.z.string(),
|
|
1989
|
+
status: import_zod10.z.literal("active")
|
|
1990
|
+
},
|
|
1991
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1992
|
+
},
|
|
1993
|
+
withApiErrorGuidance(async ({ projectId, componentId, variantId }) => {
|
|
1994
|
+
const id = encodeURIComponent(projectId);
|
|
1995
|
+
await client.post(`/projects/${id}/variants/restore`, { componentId, variantId });
|
|
1996
|
+
return {
|
|
1997
|
+
content: [{
|
|
1998
|
+
type: "text",
|
|
1999
|
+
text: `Variant ${untrusted(variantId)} in component ${untrusted(componentId)} is active again. If it was also paused, call resume_variant.`
|
|
2000
|
+
}],
|
|
2001
|
+
structuredContent: { variantId, componentId, status: "active" }
|
|
2002
|
+
};
|
|
2003
|
+
})
|
|
2004
|
+
);
|
|
1698
2005
|
server.registerTool(
|
|
1699
2006
|
"refresh_insights",
|
|
1700
2007
|
{
|
|
1701
2008
|
title: "Refresh insights",
|
|
1702
|
-
|
|
2009
|
+
// The plan and role gates were undocumented, so agents on Free called it
|
|
2010
|
+
// and got a bare 403 they could not explain to the user.
|
|
2011
|
+
description: "Trigger fresh AI insight generation for a project (a paid model call). Needs the Starter plan or higher and an editor/admin role (anonymous demo tokens are read-only); advisor recommendations additionally need Growth or higher. Returns immediately; call get_insights in ~15 seconds to see results.",
|
|
1703
2012
|
inputSchema: { projectId: projectIdSchema },
|
|
1704
2013
|
outputSchema: {
|
|
1705
2014
|
projectId: import_zod10.z.string(),
|
|
@@ -1751,11 +2060,21 @@ var BEST_PRACTICE_PRIORS = {
|
|
|
1751
2060
|
"Make the CTA specific and benefit-led.",
|
|
1752
2061
|
"Add a single strong proof point; remove competing distractions."
|
|
1753
2062
|
],
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2063
|
+
// Keys must be the project context types the API accepts (projects.ts
|
|
2064
|
+
// VALID_CONTEXT_TYPES: saas | ecommerce | marketing | landing | internal).
|
|
2065
|
+
// A `marketplace` key sat here that no project could ever have, while
|
|
2066
|
+
// marketing and internal projects silently fell through to the generic list.
|
|
2067
|
+
marketing: [
|
|
2068
|
+
'Make the page answer "what is this and why should I care" in the first screen.',
|
|
2069
|
+
"Give each section one job and one next step; cut competing CTAs.",
|
|
2070
|
+
"Put the strongest proof (named customers, a hard number) near the primary CTA.",
|
|
2071
|
+
"Match the copy to where the visitor came from (campaign, search intent)."
|
|
2072
|
+
],
|
|
2073
|
+
internal: [
|
|
2074
|
+
"Optimize for task completion, not persuasion: make the next step obvious.",
|
|
2075
|
+
"Reduce steps and fields; prefill what the system already knows.",
|
|
2076
|
+
"Use plain, specific labels over marketing copy.",
|
|
2077
|
+
"Make errors and empty states say exactly what to do next."
|
|
1759
2078
|
]
|
|
1760
2079
|
};
|
|
1761
2080
|
var GENERIC_PRIORS = [
|
|
@@ -1788,7 +2107,10 @@ function guidanceFor(dataState, contextType, arms) {
|
|
|
1788
2107
|
}
|
|
1789
2108
|
case "directional": {
|
|
1790
2109
|
const inconclusive = arms.some((a) => a.state === "inconclusive");
|
|
1791
|
-
|
|
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.";
|
|
1792
2114
|
}
|
|
1793
2115
|
case "collecting":
|
|
1794
2116
|
if (!arms.some((a) => !a.isBaseline)) {
|
|
@@ -2286,12 +2608,12 @@ function registerAgentTrafficTools(server, client) {
|
|
|
2286
2608
|
"get_agent_traffic",
|
|
2287
2609
|
{
|
|
2288
2610
|
title: "Agent traffic",
|
|
2289
|
-
description: "Which AI agents and crawlers are reading this site: totals by type (passive crawlers, agentic browsers, agent API calls), engine breakdown, and the paths they fetch most. Defaults to the last 30 days; pass from/to
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
},
|
|
2611
|
+
description: "Which AI agents and crawlers are reading this site: totals by type (passive crawlers, agentic browsers, agent API calls), engine breakdown, and the paths they fetch most. Defaults to the last 30 calendar days; pass range (7d/30d/90d/all) or from/to (YYYY-MM-DD) like every other windowed tool (agent logs are retained 30 days on Free/Starter, 90 on Growth+). Agent traffic is tracked separately and never counted in conversion rate." + UNTRUSTED_FIELDS_NOTE,
|
|
2612
|
+
// This tool alone took ad-hoc ISO from/to with no range token and echoed
|
|
2613
|
+
// no window, while every other windowed tool used rangeInputSchema — so
|
|
2614
|
+
// agents passed range:'7d' here, it was silently dropped, and 30 days of
|
|
2615
|
+
// traffic was reported as a week. The endpoint uses the shared resolver.
|
|
2616
|
+
inputSchema: __spreadValues({ projectId: projectIdSchema }, rangeInputSchema("30d")),
|
|
2295
2617
|
outputSchema: {
|
|
2296
2618
|
totals: import_zod14.z.object({ crawler: import_zod14.z.number(), api: import_zod14.z.number(), browser: import_zod14.z.number() }),
|
|
2297
2619
|
engines: import_zod14.z.array(
|
|
@@ -2305,20 +2627,18 @@ function registerAgentTrafficTools(server, client) {
|
|
|
2305
2627
|
})
|
|
2306
2628
|
),
|
|
2307
2629
|
intents: import_zod14.z.object({ user: import_zod14.z.number(), search: import_zod14.z.number(), training: import_zod14.z.number(), other: import_zod14.z.number() }).describe("Crawler fetches by purpose: user = an assistant answering a real person live"),
|
|
2308
|
-
topPaths: import_zod14.z.array(import_zod14.z.object({ path: import_zod14.z.string(), count: import_zod14.z.number(), engines: import_zod14.z.number() }))
|
|
2630
|
+
topPaths: import_zod14.z.array(import_zod14.z.object({ path: import_zod14.z.string(), count: import_zod14.z.number(), engines: import_zod14.z.number() })),
|
|
2631
|
+
window: windowOutputSchema
|
|
2309
2632
|
},
|
|
2310
2633
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2311
2634
|
},
|
|
2312
|
-
withApiErrorGuidance(async ({ projectId, from, to }) => {
|
|
2635
|
+
withApiErrorGuidance(async ({ projectId, range, from, to }) => {
|
|
2313
2636
|
const id = encodeURIComponent(projectId);
|
|
2314
|
-
const qs = new URLSearchParams();
|
|
2315
|
-
if (from) qs.set("from", from);
|
|
2316
|
-
if (to) qs.set("to", to);
|
|
2317
|
-
const query = qs.toString();
|
|
2318
2637
|
const s = await client.get(
|
|
2319
|
-
`/projects/${id}/agent-activity/summary${
|
|
2638
|
+
`/projects/${id}/agent-activity/summary${rangeQuery({ range, from, to })}`
|
|
2320
2639
|
);
|
|
2321
|
-
const structuredContent = { totals: s.totals, engines: s.engines, intents: s.intents, topPaths: s.topPaths };
|
|
2640
|
+
const structuredContent = { totals: s.totals, engines: s.engines, intents: s.intents, topPaths: s.topPaths, window: s.window };
|
|
2641
|
+
const win = windowLine(s.window);
|
|
2322
2642
|
const total = s.totals.crawler + s.totals.api + s.totals.browser;
|
|
2323
2643
|
if (total === 0) {
|
|
2324
2644
|
return {
|
|
@@ -2330,6 +2650,7 @@ function registerAgentTrafficTools(server, client) {
|
|
|
2330
2650
|
};
|
|
2331
2651
|
}
|
|
2332
2652
|
const lines = [
|
|
2653
|
+
...win ? [win] : [],
|
|
2333
2654
|
`Agent traffic: ${s.totals.crawler} crawler fetches, ${s.totals.api} agent API calls, ${s.totals.browser} agentic browser sessions.`,
|
|
2334
2655
|
`Live user fetches: ${s.intents.user} \xB7 Search index: ${s.intents.search} \xB7 Training: ${s.intents.training}${s.intents.other ? ` \xB7 Other: ${s.intents.other}` : ""} (a "live user fetch" = an AI assistant reading your site to answer a real person).`,
|
|
2335
2656
|
"",
|
|
@@ -2420,7 +2741,7 @@ var personaOutput = import_zod15.z.object({
|
|
|
2420
2741
|
var cellOutput = import_zod15.z.object({
|
|
2421
2742
|
slotId: import_zod15.z.string(),
|
|
2422
2743
|
persona: import_zod15.z.string(),
|
|
2423
|
-
status: import_zod15.z.string().describe("pending | generating | live | error | rejected"),
|
|
2744
|
+
status: import_zod15.z.string().describe("pending | generating | review (waiting for approval) | live | error | rejected"),
|
|
2424
2745
|
requestedBy: import_zod15.z.string(),
|
|
2425
2746
|
armId: import_zod15.z.string().nullable(),
|
|
2426
2747
|
rationale: import_zod15.z.string().nullable(),
|
|
@@ -2460,6 +2781,10 @@ function registerCellTools(server, client) {
|
|
|
2460
2781
|
personas: import_zod15.z.array(personaOutput),
|
|
2461
2782
|
cells: import_zod15.z.array(cellOutput),
|
|
2462
2783
|
autoFill: import_zod15.z.boolean(),
|
|
2784
|
+
// The API has always sent this; without it in the schema zod stripped
|
|
2785
|
+
// it, and generate_cell had no way to know a version would wait for a
|
|
2786
|
+
// human instead of publishing.
|
|
2787
|
+
reviewMode: import_zod15.z.boolean().optional().describe("True when generated versions wait for approval (review_cell) instead of publishing automatically"),
|
|
2463
2788
|
// Nullable: a project with no persona set at all reports null, not a
|
|
2464
2789
|
// string. That is the NORMAL state since the default vocabulary was
|
|
2465
2790
|
// emptied, so a non-nullable schema failed output validation on most
|
|
@@ -2476,7 +2801,7 @@ function registerCellTools(server, client) {
|
|
|
2476
2801
|
const byCell = new Map(data.cells.map((c) => [`${c.slotId}::${c.persona}`, c]));
|
|
2477
2802
|
const lines = [
|
|
2478
2803
|
`Slots: ${data.slots.map((s) => `${untrusted(s.displayName)}${s.live === false ? " [draft]" : ""}`).join(", ") || "(none published)"}`,
|
|
2479
|
-
`Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 persona source: ${(_a2 = data.personaSource) != null ? _a2 : 'none (one "everyone" row)'}`
|
|
2804
|
+
`Auto-fill on first miss: ${data.autoFill ? "ON" : "off"} \xB7 review before publishing: ${data.reviewMode ? "ON (approve with review_cell)" : "off"} \xB7 persona source: ${(_a2 = data.personaSource) != null ? _a2 : 'none (one "everyone" row)'}`
|
|
2480
2805
|
];
|
|
2481
2806
|
for (const p of data.personas) {
|
|
2482
2807
|
const states = data.slots.map((s) => {
|
|
@@ -2561,7 +2886,12 @@ function registerCellTools(server, client) {
|
|
|
2561
2886
|
"generate_cell",
|
|
2562
2887
|
{
|
|
2563
2888
|
title: "Generate a cell version",
|
|
2564
|
-
|
|
2889
|
+
// The old copy promised "goes live, usually under a minute" — but with
|
|
2890
|
+
// review mode on the worker parks the version for a human (status
|
|
2891
|
+
// 'review') and nothing serves until someone approves it, and the queue
|
|
2892
|
+
// can defer a cell (retry backoff, the plan's monthly generation cap). An
|
|
2893
|
+
// agent relayed "it's live" for versions no visitor would ever see.
|
|
2894
|
+
description: "Queue AI generation of a version for one (persona, slot) cell; the worker writes it asynchronously, brand-locked. If the project has review mode on, the finished version WAITS for approval (list_cell_reviews / review_cell) and serves nobody until approved; otherwise it publishes automatically as a named option the optimizer explores. Also regenerates or retries a failed cell. Generation volume is capped per plan (Free included). Check get_cell_matrix for progress. Confirm with the user before calling \u2014 each generation is a model call against the plan quota.",
|
|
2565
2895
|
inputSchema: {
|
|
2566
2896
|
projectId: projectIdSchema,
|
|
2567
2897
|
slotId: import_zod15.z.string().min(1).max(128).describe("The slot id (a matrix column)"),
|
|
@@ -2570,28 +2900,130 @@ function registerCellTools(server, client) {
|
|
|
2570
2900
|
outputSchema: {
|
|
2571
2901
|
slotId: import_zod15.z.string(),
|
|
2572
2902
|
persona: import_zod15.z.string(),
|
|
2573
|
-
status: import_zod15.z.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress")
|
|
2903
|
+
status: import_zod15.z.literal("pending").describe("Queued \u2014 check get_cell_matrix for progress"),
|
|
2904
|
+
reviewMode: import_zod15.z.boolean().nullable().describe("True: the version will wait for review_cell approval. False: it publishes automatically. Null: could not be read.")
|
|
2574
2905
|
},
|
|
2575
2906
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
2576
2907
|
},
|
|
2577
2908
|
withApiErrorGuidance(async ({ projectId, slotId, persona }) => {
|
|
2578
2909
|
const id = encodeURIComponent(projectId);
|
|
2579
2910
|
await client.post(`/projects/${id}/cells/generate`, { slotId, persona });
|
|
2911
|
+
const matrix = await settled(client.get(`/projects/${id}/cells`));
|
|
2912
|
+
const reviewMode = matrix.ok && typeof matrix.value.reviewMode === "boolean" ? matrix.value.reviewMode : null;
|
|
2913
|
+
const next = reviewMode === true ? "Review mode is ON: when it is written it will WAIT for approval and serve nobody until then \u2014 list it with list_cell_reviews and approve or reject it with review_cell (after the user decides)." : reviewMode === false ? "Review mode is off: once written it publishes automatically and the optimizer starts exploring it." : 'Could not read whether review mode is on \u2014 check get_cell_matrix: status "review" means it is waiting for approval (review_cell), "live" means it is serving.';
|
|
2580
2914
|
return {
|
|
2581
2915
|
content: [
|
|
2582
2916
|
{
|
|
2583
2917
|
type: "text",
|
|
2584
|
-
text: `
|
|
2918
|
+
text: `Queued a version of ${untrusted(slotId, 64)} for ${untrusted(persona, 64)}. It is written asynchronously (the queue may defer it \u2014 retries back off, and the plan caps generations). ${next} Track it with get_cell_matrix or get_cell_detail.`
|
|
2585
2919
|
}
|
|
2586
2920
|
],
|
|
2587
|
-
structuredContent: { slotId, persona, status: "pending" }
|
|
2921
|
+
structuredContent: { slotId, persona, status: "pending", reviewMode }
|
|
2922
|
+
};
|
|
2923
|
+
})
|
|
2924
|
+
);
|
|
2925
|
+
server.registerTool(
|
|
2926
|
+
"list_cell_reviews",
|
|
2927
|
+
{
|
|
2928
|
+
title: "Versions waiting for review",
|
|
2929
|
+
description: "Generated versions waiting for approval (review mode): which slot and visitor type, the proposed text or layout, and why it was written. Approve or reject each with review_cell after the user decides." + UNTRUSTED_FIELDS_NOTE,
|
|
2930
|
+
inputSchema: { projectId: projectIdSchema },
|
|
2931
|
+
outputSchema: {
|
|
2932
|
+
reviews: import_zod15.z.array(
|
|
2933
|
+
import_zod15.z.object({
|
|
2934
|
+
slotId: import_zod15.z.string(),
|
|
2935
|
+
slotName: import_zod15.z.string(),
|
|
2936
|
+
persona: import_zod15.z.string(),
|
|
2937
|
+
personaDisplay: import_zod15.z.string(),
|
|
2938
|
+
content: import_zod15.z.string().nullable().describe("Proposed text, when the version is text (null for a block/layout version)"),
|
|
2939
|
+
rationale: import_zod15.z.string().nullable(),
|
|
2940
|
+
refinedFrom: import_zod15.z.string().nullable().describe("Live version this one would replace; that version keeps serving until approval"),
|
|
2941
|
+
updatedAt: import_zod15.z.string()
|
|
2942
|
+
})
|
|
2943
|
+
)
|
|
2944
|
+
},
|
|
2945
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
2946
|
+
},
|
|
2947
|
+
withApiErrorGuidance(async ({ projectId }) => {
|
|
2948
|
+
var _a2;
|
|
2949
|
+
const id = encodeURIComponent(projectId);
|
|
2950
|
+
const data = await client.get(`/projects/${id}/cell-reviews`);
|
|
2951
|
+
const reviews = ((_a2 = data.reviews) != null ? _a2 : []).map((r) => {
|
|
2952
|
+
var _a3, _b, _c;
|
|
2953
|
+
return {
|
|
2954
|
+
slotId: r.slotId,
|
|
2955
|
+
slotName: r.slotName,
|
|
2956
|
+
persona: r.persona,
|
|
2957
|
+
personaDisplay: r.personaDisplay,
|
|
2958
|
+
content: typeof ((_a3 = r.pendingArm) == null ? void 0 : _a3.content) === "string" ? r.pendingArm.content : null,
|
|
2959
|
+
rationale: (_b = r.rationale) != null ? _b : null,
|
|
2960
|
+
refinedFrom: (_c = r.refinedFrom) != null ? _c : null,
|
|
2961
|
+
updatedAt: String(r.updatedAt)
|
|
2962
|
+
};
|
|
2963
|
+
});
|
|
2964
|
+
if (reviews.length === 0) {
|
|
2965
|
+
return {
|
|
2966
|
+
content: [{ type: "text", text: "No generated versions are waiting for review." }],
|
|
2967
|
+
structuredContent: { reviews }
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
const lines = reviews.map(
|
|
2971
|
+
(r) => [
|
|
2972
|
+
`- ${untrusted(r.slotName, 60)} (slotId ${untrusted(r.slotId, 64)}) for ${untrusted(r.personaDisplay, 60)} (persona ${untrusted(r.persona, 64)})`,
|
|
2973
|
+
r.content !== null ? ` Proposed text: ${untrusted(r.content, 300)}` : " Proposed: a designed block/layout (preview it in the dashboard)",
|
|
2974
|
+
r.rationale ? ` Why: ${untrusted(r.rationale, 300)}` : null,
|
|
2975
|
+
r.refinedFrom ? ` Would replace ${untrusted(r.refinedFrom, 64)}, which keeps serving until approval.` : null
|
|
2976
|
+
].filter((l) => l !== null).join("\n")
|
|
2977
|
+
);
|
|
2978
|
+
lines.push("", "Approve or reject each with review_cell (slotId + persona) once the user has decided.");
|
|
2979
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { reviews } };
|
|
2980
|
+
})
|
|
2981
|
+
);
|
|
2982
|
+
server.registerTool(
|
|
2983
|
+
"review_cell",
|
|
2984
|
+
{
|
|
2985
|
+
title: "Approve or reject a generated version",
|
|
2986
|
+
description: "Approve or reject ONE generated version waiting for review (see list_cell_reviews). 'approve' publishes it to live visitors of that type; 'reject' discards it (a rejected refinement leaves the current live version in place). This is the human decision review mode exists for \u2014 only call it with the user's explicit go-ahead.",
|
|
2987
|
+
inputSchema: {
|
|
2988
|
+
projectId: projectIdSchema,
|
|
2989
|
+
slotId: import_zod15.z.string().min(1).max(128).describe("The slot id of the version under review"),
|
|
2990
|
+
persona: import_zod15.z.string().min(1).max(64).describe("The persona key of the version under review"),
|
|
2991
|
+
action: import_zod15.z.enum(["approve", "reject"]),
|
|
2992
|
+
feedback: import_zod15.z.string().max(2e3).optional().describe("Optional reviewer note, stored with a rejection")
|
|
2993
|
+
},
|
|
2994
|
+
outputSchema: {
|
|
2995
|
+
slotId: import_zod15.z.string(),
|
|
2996
|
+
persona: import_zod15.z.string(),
|
|
2997
|
+
action: import_zod15.z.enum(["approve", "reject"]),
|
|
2998
|
+
status: import_zod15.z.string().describe("live (approved, or a rejected refinement whose incumbent keeps serving) | rejected"),
|
|
2999
|
+
armId: import_zod15.z.string().nullable().describe("The published option id, on approval")
|
|
3000
|
+
},
|
|
3001
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
|
|
3002
|
+
},
|
|
3003
|
+
withApiErrorGuidance(async ({ projectId, slotId, persona, action, feedback }) => {
|
|
3004
|
+
var _a2, _b;
|
|
3005
|
+
const id = encodeURIComponent(projectId);
|
|
3006
|
+
const res = await client.post(`/projects/${id}/cells/review`, {
|
|
3007
|
+
slotId,
|
|
3008
|
+
persona,
|
|
3009
|
+
action,
|
|
3010
|
+
feedback
|
|
3011
|
+
});
|
|
3012
|
+
const status = (_a2 = res.status) != null ? _a2 : action === "approve" ? "live" : "rejected";
|
|
3013
|
+
const text = action === "approve" ? `Approved: the version for ${untrusted(persona, 64)} in ${untrusted(slotId, 64)} is now live${res.armId ? ` as option ${untrusted(res.armId, 64)}` : ""}.` : status === "live" ? `Rejected: the proposed refinement for ${untrusted(persona, 64)} in ${untrusted(slotId, 64)} was discarded; the current live version keeps serving.` : `Rejected: the version for ${untrusted(persona, 64)} in ${untrusted(slotId, 64)} was discarded; that visitor type keeps seeing the original.`;
|
|
3014
|
+
return {
|
|
3015
|
+
content: [{ type: "text", text }],
|
|
3016
|
+
structuredContent: { slotId, persona, action, status, armId: (_b = res.armId) != null ? _b : null }
|
|
2588
3017
|
};
|
|
3018
|
+
}, {
|
|
3019
|
+
not_found: "No version is waiting for review in that cell (it may already have been approved or rejected). Check list_cell_reviews.",
|
|
3020
|
+
publish_failed: "Approval could not publish this version (the slot may be full of options, or its configuration changed). It stays in review \u2014 check the reason in get_cell_detail or the dashboard, then retry."
|
|
2589
3021
|
})
|
|
2590
3022
|
);
|
|
2591
3023
|
}
|
|
2592
3024
|
|
|
2593
3025
|
// src/server.ts
|
|
2594
|
-
var PKG_VERSION = true ? "0.
|
|
3026
|
+
var PKG_VERSION = true ? "0.21.0" : "0.0.0-dev";
|
|
2595
3027
|
function createMcpServer(client) {
|
|
2596
3028
|
const server = new import_mcp.McpServer(
|
|
2597
3029
|
{
|
|
@@ -2706,6 +3138,11 @@ async function main() {
|
|
|
2706
3138
|
"SENTIENTUI_API_KEY is set but empty. Set it to a real server key (sk_\u2026), or unset it entirely to use anonymous demo mode."
|
|
2707
3139
|
);
|
|
2708
3140
|
}
|
|
3141
|
+
if (apiKey && /^pk_/i.test(apiKey)) {
|
|
3142
|
+
throw new Error(
|
|
3143
|
+
"SENTIENTUI_API_KEY is a pk_ publishable key \u2014 that key is for the browser SDK and cannot read or manage project data. Use the project's sk_ server key (Dashboard \u2192 Settings \u2192 API keys \u2192 Server key), or connect by URL (https://api.sentient-ui.com/mcp) and sign in."
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
2709
3146
|
if (!apiKey) {
|
|
2710
3147
|
apiKey = await resolveDemoToken();
|
|
2711
3148
|
refreshApiKey = refreshDemoToken;
|