@sentientui/mcp 0.4.1 → 0.5.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/dist/{chunk-ZXI3OZKA.js → chunk-QH3RMHLM.js} +495 -86
- package/dist/index.cjs +495 -86
- package/dist/index.js +1 -1
- package/dist/lib.cjs +495 -86
- package/dist/lib.js +1 -1
- package/package.json +1 -1
|
@@ -70,14 +70,29 @@ function createProjectGuidance(err) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
function registerProjectTools(server, client) {
|
|
73
|
-
server.
|
|
73
|
+
server.registerTool(
|
|
74
74
|
"create_project",
|
|
75
|
-
"Create a NEW SentientUI project (onboarding). Returns the project id and its pk_ public key for the SDK. Requires an account login: this works when connected via OAuth (the hosted MCP URL) but NOT with a project-scoped sk_ server key or an anonymous demo token. After it succeeds, call get_integration_guide and help the user install @sentientui/react with the returned key.",
|
|
76
75
|
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
title: "Create project",
|
|
77
|
+
description: "Create a NEW SentientUI project (onboarding). Returns the project id and its pk_ public key for the SDK. Requires an account login: this works when connected via OAuth (the hosted MCP URL) but NOT with a project-scoped sk_ server key or an anonymous demo token. After it succeeds, call get_integration_guide and help the user install @sentientui/react with the returned key.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
name: z.string().min(1).describe("Human-readable project name"),
|
|
80
|
+
contextType: z.enum(["saas", "ecommerce", "marketing", "internal"]).optional().describe("What kind of product this is; defaults to saas"),
|
|
81
|
+
framework: z.enum(["next-app", "next-pages", "react", "core"]).optional().describe("Frontend framework, used to tailor setup; defaults to next-app"),
|
|
82
|
+
websiteUrl: z.string().optional().describe("Production site origin to allow-list so the SDK's events aren't origin-blocked on day one")
|
|
83
|
+
},
|
|
84
|
+
outputSchema: {
|
|
85
|
+
projectId: z.string().describe("The new project UUID"),
|
|
86
|
+
publicKey: z.string().describe("The pk_ public key to configure the SDK with"),
|
|
87
|
+
name: z.string().describe("The project name"),
|
|
88
|
+
contextType: z.string().describe("The resolved context type")
|
|
89
|
+
},
|
|
90
|
+
annotations: {
|
|
91
|
+
readOnlyHint: false,
|
|
92
|
+
destructiveHint: false,
|
|
93
|
+
idempotentHint: false,
|
|
94
|
+
openWorldHint: false
|
|
95
|
+
}
|
|
81
96
|
},
|
|
82
97
|
async ({ name, contextType, framework, websiteUrl }) => {
|
|
83
98
|
try {
|
|
@@ -87,15 +102,22 @@ function registerProjectTools(server, client) {
|
|
|
87
102
|
framework,
|
|
88
103
|
origin: websiteUrl
|
|
89
104
|
});
|
|
105
|
+
const resolvedContextType = contextType ?? "saas";
|
|
90
106
|
return {
|
|
91
107
|
content: [{
|
|
92
108
|
type: "text",
|
|
93
109
|
text: [
|
|
94
|
-
`Created project "${name}" (id: ${created.id}, type: ${
|
|
110
|
+
`Created project "${name}" (id: ${created.id}, type: ${resolvedContextType}).`,
|
|
95
111
|
`Public key: ${created.apiKey}`,
|
|
96
112
|
`Next: install @sentientui/react with this key. Ask me to pull the setup guide (get_integration_guide) and I'll wrap your first component.`
|
|
97
113
|
].join("\n")
|
|
98
|
-
}]
|
|
114
|
+
}],
|
|
115
|
+
structuredContent: {
|
|
116
|
+
projectId: created.id,
|
|
117
|
+
publicKey: created.apiKey,
|
|
118
|
+
name,
|
|
119
|
+
contextType: resolvedContextType
|
|
120
|
+
}
|
|
99
121
|
};
|
|
100
122
|
} catch (err) {
|
|
101
123
|
if (err instanceof ApiError) {
|
|
@@ -108,22 +130,65 @@ function registerProjectTools(server, client) {
|
|
|
108
130
|
}
|
|
109
131
|
}
|
|
110
132
|
);
|
|
111
|
-
server.
|
|
133
|
+
server.registerTool(
|
|
112
134
|
"list_projects",
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
{
|
|
136
|
+
title: "List projects",
|
|
137
|
+
description: "List all SentientUI projects for the authenticated account.",
|
|
138
|
+
inputSchema: {},
|
|
139
|
+
outputSchema: {
|
|
140
|
+
projects: z.array(
|
|
141
|
+
z.object({
|
|
142
|
+
id: z.string().describe("Project UUID"),
|
|
143
|
+
name: z.string(),
|
|
144
|
+
contextType: z.string(),
|
|
145
|
+
createdAt: z.string().describe("ISO date (YYYY-MM-DD)")
|
|
146
|
+
})
|
|
147
|
+
).describe("All projects for the account (empty if none)")
|
|
148
|
+
},
|
|
149
|
+
annotations: {
|
|
150
|
+
readOnlyHint: true,
|
|
151
|
+
idempotentHint: true,
|
|
152
|
+
openWorldHint: false
|
|
153
|
+
}
|
|
154
|
+
},
|
|
115
155
|
async () => {
|
|
116
156
|
const projects = await client.get("/projects");
|
|
117
157
|
const text = projects.length === 0 ? "No projects found." : projects.map(
|
|
118
158
|
(p) => `- ${p.name} (id: ${p.id}, type: ${p.context_type}, created: ${p.created_at.slice(0, 10)})`
|
|
119
159
|
).join("\n");
|
|
120
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text }],
|
|
162
|
+
structuredContent: {
|
|
163
|
+
projects: projects.map((p) => ({
|
|
164
|
+
id: p.id,
|
|
165
|
+
name: p.name,
|
|
166
|
+
contextType: p.context_type,
|
|
167
|
+
createdAt: p.created_at.slice(0, 10)
|
|
168
|
+
}))
|
|
169
|
+
}
|
|
170
|
+
};
|
|
121
171
|
}
|
|
122
172
|
);
|
|
123
|
-
server.
|
|
173
|
+
server.registerTool(
|
|
124
174
|
"get_project_stats",
|
|
125
|
-
|
|
126
|
-
|
|
175
|
+
{
|
|
176
|
+
title: "Project health stats",
|
|
177
|
+
description: "Get health stats for a project: event volume, session count, agent calls, and status.",
|
|
178
|
+
inputSchema: { projectId: projectIdSchema },
|
|
179
|
+
outputSchema: {
|
|
180
|
+
status: z.string().describe("Overall project health status"),
|
|
181
|
+
events24h: z.number().describe("Events in the last 24 hours"),
|
|
182
|
+
sessions24h: z.number().describe("Sessions in the last 24 hours"),
|
|
183
|
+
agentCalls: z.number().describe("Total agent (MCP/API) calls"),
|
|
184
|
+
lastEventAt: z.string().nullable().describe("ISO timestamp of the last event, or null")
|
|
185
|
+
},
|
|
186
|
+
annotations: {
|
|
187
|
+
readOnlyHint: true,
|
|
188
|
+
idempotentHint: true,
|
|
189
|
+
openWorldHint: false
|
|
190
|
+
}
|
|
191
|
+
},
|
|
127
192
|
async ({ projectId }) => {
|
|
128
193
|
const id = encodeURIComponent(projectId);
|
|
129
194
|
const stats = await client.get(`/projects/${id}/health`);
|
|
@@ -134,7 +199,16 @@ function registerProjectTools(server, client) {
|
|
|
134
199
|
`Agent calls (total): ${stats.agentCalls}`,
|
|
135
200
|
`Last event: ${stats.lastEventAt ?? "never"}`
|
|
136
201
|
].join("\n");
|
|
137
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text }],
|
|
204
|
+
structuredContent: {
|
|
205
|
+
status: stats.status,
|
|
206
|
+
events24h: stats.events24h,
|
|
207
|
+
sessions24h: stats.sessions24h,
|
|
208
|
+
agentCalls: stats.agentCalls,
|
|
209
|
+
lastEventAt: stats.lastEventAt
|
|
210
|
+
}
|
|
211
|
+
};
|
|
138
212
|
}
|
|
139
213
|
);
|
|
140
214
|
}
|
|
@@ -143,37 +217,97 @@ function registerProjectTools(server, client) {
|
|
|
143
217
|
import { z as z2 } from "zod";
|
|
144
218
|
var projectIdSchema2 = z2.string().uuid().describe("The project UUID");
|
|
145
219
|
function registerComponentTools(server, client) {
|
|
146
|
-
server.
|
|
220
|
+
server.registerTool(
|
|
147
221
|
"list_components",
|
|
148
|
-
|
|
149
|
-
|
|
222
|
+
{
|
|
223
|
+
title: "List components",
|
|
224
|
+
description: "List all adaptive components in a project with variant counts and impression totals.",
|
|
225
|
+
inputSchema: { projectId: projectIdSchema2 },
|
|
226
|
+
outputSchema: {
|
|
227
|
+
components: z2.array(
|
|
228
|
+
z2.object({
|
|
229
|
+
componentId: z2.string(),
|
|
230
|
+
variantCount: z2.number(),
|
|
231
|
+
impressions: z2.number(),
|
|
232
|
+
conversions: z2.number()
|
|
233
|
+
})
|
|
234
|
+
).describe("Adaptive components in the project (empty if none)")
|
|
235
|
+
},
|
|
236
|
+
annotations: {
|
|
237
|
+
readOnlyHint: true,
|
|
238
|
+
idempotentHint: true,
|
|
239
|
+
openWorldHint: false
|
|
240
|
+
}
|
|
241
|
+
},
|
|
150
242
|
async ({ projectId }) => {
|
|
151
243
|
const id = encodeURIComponent(projectId);
|
|
152
244
|
const components = await client.get(`/projects/${id}/components`);
|
|
245
|
+
const structuredContent = {
|
|
246
|
+
components: components.map((c) => ({
|
|
247
|
+
componentId: c.component_id,
|
|
248
|
+
variantCount: c.variants.length,
|
|
249
|
+
impressions: c.total_impressions,
|
|
250
|
+
conversions: c.total_conversions
|
|
251
|
+
}))
|
|
252
|
+
};
|
|
153
253
|
if (!components.length) {
|
|
154
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text", text: "No components found for this project." }],
|
|
256
|
+
structuredContent
|
|
257
|
+
};
|
|
155
258
|
}
|
|
156
259
|
const text = components.map(
|
|
157
260
|
(c) => `- ${c.component_id}: ${c.variants.length} variants, ${c.total_impressions} impressions, ${c.total_conversions} conversions`
|
|
158
261
|
).join("\n");
|
|
159
|
-
return { content: [{ type: "text", text }] };
|
|
262
|
+
return { content: [{ type: "text", text }], structuredContent };
|
|
160
263
|
}
|
|
161
264
|
);
|
|
162
|
-
server.
|
|
265
|
+
server.registerTool(
|
|
163
266
|
"get_variant_performance",
|
|
164
|
-
|
|
165
|
-
|
|
267
|
+
{
|
|
268
|
+
title: "Variant performance",
|
|
269
|
+
description: "Get CVR and momentum for all variants in a project over the last 7 days vs prior 7 days.",
|
|
270
|
+
inputSchema: { projectId: projectIdSchema2 },
|
|
271
|
+
outputSchema: {
|
|
272
|
+
variants: z2.array(
|
|
273
|
+
z2.object({
|
|
274
|
+
variantId: z2.string(),
|
|
275
|
+
currentCvr: z2.number().describe("Conversion rate over the last 7 days (0-1)"),
|
|
276
|
+
priorCvr: z2.number().describe("Conversion rate over the prior 7 days (0-1)"),
|
|
277
|
+
deltaPp: z2.number().describe("Change in percentage points"),
|
|
278
|
+
momentum: z2.string().describe("Momentum direction: gaining, losing, or stable")
|
|
279
|
+
})
|
|
280
|
+
).describe("Per-variant performance (empty if no data yet)")
|
|
281
|
+
},
|
|
282
|
+
annotations: {
|
|
283
|
+
readOnlyHint: true,
|
|
284
|
+
idempotentHint: true,
|
|
285
|
+
openWorldHint: false
|
|
286
|
+
}
|
|
287
|
+
},
|
|
166
288
|
async ({ projectId }) => {
|
|
167
289
|
const id = encodeURIComponent(projectId);
|
|
168
290
|
const data = await client.get(`/projects/${id}/trends`);
|
|
291
|
+
const momentumMap = new Map((data.momentum ?? []).map((m) => [m.variantId, m.direction]));
|
|
292
|
+
const structuredContent = {
|
|
293
|
+
variants: (data.cvr ?? []).map((v) => ({
|
|
294
|
+
variantId: v.variantId,
|
|
295
|
+
currentCvr: v.currentCvr,
|
|
296
|
+
priorCvr: v.priorCvr,
|
|
297
|
+
deltaPp: v.deltaPp,
|
|
298
|
+
momentum: momentumMap.get(v.variantId) ?? "stable"
|
|
299
|
+
}))
|
|
300
|
+
};
|
|
169
301
|
if (!data.cvr?.length) {
|
|
170
|
-
return {
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: "text", text: "No variant data available yet." }],
|
|
304
|
+
structuredContent
|
|
305
|
+
};
|
|
171
306
|
}
|
|
172
|
-
const momentumMap = new Map(data.momentum.map((m) => [m.variantId, m.direction]));
|
|
173
307
|
const text = data.cvr.map(
|
|
174
308
|
(v) => `- ${v.variantId}: CVR ${(v.currentCvr * 100).toFixed(2)}% (prior ${(v.priorCvr * 100).toFixed(2)}%, ${v.deltaPp > 0 ? "+" : ""}${v.deltaPp.toFixed(1)} pp, ${momentumMap.get(v.variantId) ?? "stable"})`
|
|
175
309
|
).join("\n");
|
|
176
|
-
return { content: [{ type: "text", text }] };
|
|
310
|
+
return { content: [{ type: "text", text }], structuredContent };
|
|
177
311
|
}
|
|
178
312
|
);
|
|
179
313
|
}
|
|
@@ -182,28 +316,63 @@ function registerComponentTools(server, client) {
|
|
|
182
316
|
import { z as z3 } from "zod";
|
|
183
317
|
var projectIdSchema3 = z3.string().uuid().describe("The project UUID");
|
|
184
318
|
function registerInsightTools(server, client) {
|
|
185
|
-
server.
|
|
319
|
+
server.registerTool(
|
|
186
320
|
"get_insights",
|
|
187
|
-
|
|
188
|
-
|
|
321
|
+
{
|
|
322
|
+
title: "Get insights",
|
|
323
|
+
description: "Get the latest AI-generated insights: narrator observations and (Growth tier) advisor recommendations.",
|
|
324
|
+
inputSchema: { projectId: projectIdSchema3 },
|
|
325
|
+
outputSchema: {
|
|
326
|
+
status: z3.enum(["ok", "empty"]).describe("Whether insights exist yet"),
|
|
327
|
+
observations: z3.array(z3.string()).describe("Narrator observations"),
|
|
328
|
+
recommendations: z3.array(z3.string()).describe("Advisor recommendations (Growth tier)"),
|
|
329
|
+
isStale: z3.boolean().describe("True when the insights are older than ~6h"),
|
|
330
|
+
generatedAt: z3.string().nullable().describe("ISO timestamp the insights were generated, or null")
|
|
331
|
+
},
|
|
332
|
+
annotations: {
|
|
333
|
+
readOnlyHint: true,
|
|
334
|
+
idempotentHint: true,
|
|
335
|
+
openWorldHint: false
|
|
336
|
+
}
|
|
337
|
+
},
|
|
189
338
|
async ({ projectId }) => {
|
|
190
339
|
const id = encodeURIComponent(projectId);
|
|
191
340
|
const data = await client.get(`/projects/${id}/insights`);
|
|
192
341
|
if (data.status === "empty") {
|
|
193
|
-
return {
|
|
342
|
+
return {
|
|
343
|
+
content: [{ type: "text", text: "No insights generated yet. Use refresh_insights to generate." }],
|
|
344
|
+
structuredContent: {
|
|
345
|
+
status: "empty",
|
|
346
|
+
observations: [],
|
|
347
|
+
recommendations: [],
|
|
348
|
+
isStale: false,
|
|
349
|
+
generatedAt: null
|
|
350
|
+
}
|
|
351
|
+
};
|
|
194
352
|
}
|
|
353
|
+
const observations = data.narratorBullets ?? [];
|
|
354
|
+
const recommendations = data.advisorBullets ?? [];
|
|
195
355
|
const lines = [];
|
|
196
356
|
if (data.isStale) lines.push("\u26A0 Insights are stale (>6h old). Consider calling refresh_insights.");
|
|
197
357
|
if (data.generatedAt) lines.push(`Generated: ${new Date(data.generatedAt).toUTCString()}`);
|
|
198
358
|
lines.push("");
|
|
199
359
|
lines.push("Observations:");
|
|
200
|
-
|
|
201
|
-
if (
|
|
360
|
+
observations.forEach((b) => lines.push(`- ${b}`));
|
|
361
|
+
if (recommendations.length) {
|
|
202
362
|
lines.push("");
|
|
203
363
|
lines.push("Recommendations:");
|
|
204
|
-
|
|
364
|
+
recommendations.forEach((b) => lines.push(`- ${b}`));
|
|
205
365
|
}
|
|
206
|
-
return {
|
|
366
|
+
return {
|
|
367
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
368
|
+
structuredContent: {
|
|
369
|
+
status: "ok",
|
|
370
|
+
observations,
|
|
371
|
+
recommendations,
|
|
372
|
+
isStale: data.isStale ?? false,
|
|
373
|
+
generatedAt: data.generatedAt ?? null
|
|
374
|
+
}
|
|
375
|
+
};
|
|
207
376
|
}
|
|
208
377
|
);
|
|
209
378
|
}
|
|
@@ -212,15 +381,46 @@ function registerInsightTools(server, client) {
|
|
|
212
381
|
import { z as z4 } from "zod";
|
|
213
382
|
var projectIdSchema4 = z4.string().uuid().describe("The project UUID");
|
|
214
383
|
function registerPersonaTools(server, client) {
|
|
215
|
-
server.
|
|
384
|
+
server.registerTool(
|
|
216
385
|
"get_persona_breakdown",
|
|
217
|
-
|
|
218
|
-
|
|
386
|
+
{
|
|
387
|
+
title: "Persona breakdown",
|
|
388
|
+
description: "Get the distribution of visitor persona clusters with session counts and reliability scores.",
|
|
389
|
+
inputSchema: { projectId: projectIdSchema4 },
|
|
390
|
+
outputSchema: {
|
|
391
|
+
totalSessions: z4.number().describe("Total sessions across all clusters"),
|
|
392
|
+
clusters: z4.array(
|
|
393
|
+
z4.object({
|
|
394
|
+
label: z4.string(),
|
|
395
|
+
sessionCount: z4.number(),
|
|
396
|
+
sharePct: z4.number().describe("Share of total traffic (0-100)"),
|
|
397
|
+
reliability: z4.number().describe("Average cluster reliability (0-1)")
|
|
398
|
+
})
|
|
399
|
+
).describe("Persona clusters (empty until enough visitor data)")
|
|
400
|
+
},
|
|
401
|
+
annotations: {
|
|
402
|
+
readOnlyHint: true,
|
|
403
|
+
idempotentHint: true,
|
|
404
|
+
openWorldHint: false
|
|
405
|
+
}
|
|
406
|
+
},
|
|
219
407
|
async ({ projectId }) => {
|
|
220
408
|
const id = encodeURIComponent(projectId);
|
|
221
409
|
const data = await client.get(`/projects/${id}/portraits`);
|
|
410
|
+
const structuredContent = {
|
|
411
|
+
totalSessions: data.totalSessions,
|
|
412
|
+
clusters: data.clusters.map((c) => ({
|
|
413
|
+
label: c.label,
|
|
414
|
+
sessionCount: c.sessionCount,
|
|
415
|
+
sharePct: data.totalSessions > 0 ? c.sessionCount / data.totalSessions * 100 : 0,
|
|
416
|
+
reliability: c.avgReliability
|
|
417
|
+
}))
|
|
418
|
+
};
|
|
222
419
|
if (!data.clusters.length) {
|
|
223
|
-
return {
|
|
420
|
+
return {
|
|
421
|
+
content: [{ type: "text", text: "No persona clusters yet. More visitor data is needed." }],
|
|
422
|
+
structuredContent
|
|
423
|
+
};
|
|
224
424
|
}
|
|
225
425
|
const lines = [
|
|
226
426
|
`Total sessions: ${data.totalSessions}`,
|
|
@@ -231,7 +431,7 @@ function registerPersonaTools(server, client) {
|
|
|
231
431
|
return `- ${c.label}: ${c.sessionCount} sessions (${pct.toFixed(1)}% of traffic, reliability ${(c.avgReliability * 100).toFixed(0)}%)`;
|
|
232
432
|
})
|
|
233
433
|
];
|
|
234
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
434
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
235
435
|
}
|
|
236
436
|
);
|
|
237
437
|
}
|
|
@@ -240,22 +440,63 @@ function registerPersonaTools(server, client) {
|
|
|
240
440
|
import { z as z5 } from "zod";
|
|
241
441
|
var projectIdSchema5 = z5.string().uuid().describe("The project UUID");
|
|
242
442
|
function registerGoalTools(server, client) {
|
|
243
|
-
server.
|
|
443
|
+
server.registerTool(
|
|
244
444
|
"get_goal_funnel",
|
|
245
|
-
|
|
246
|
-
|
|
445
|
+
{
|
|
446
|
+
title: "Goal funnel",
|
|
447
|
+
description: "Get goal hit counts, unique-session conversion rates, and per-variant breakdown.",
|
|
448
|
+
inputSchema: { projectId: projectIdSchema5 },
|
|
449
|
+
outputSchema: {
|
|
450
|
+
goals: z5.array(
|
|
451
|
+
z5.object({
|
|
452
|
+
goalName: z5.string(),
|
|
453
|
+
hits: z5.number(),
|
|
454
|
+
uniqueSessions: z5.number(),
|
|
455
|
+
conversionRate: z5.number().describe("Unique-session conversion rate (0-1)"),
|
|
456
|
+
variants: z5.array(
|
|
457
|
+
z5.object({
|
|
458
|
+
componentId: z5.string(),
|
|
459
|
+
variantId: z5.string(),
|
|
460
|
+
completionRate: z5.number().describe("Completion rate per assigned session (0-1)")
|
|
461
|
+
})
|
|
462
|
+
).describe("Per-variant breakdown")
|
|
463
|
+
})
|
|
464
|
+
).describe("Configured goals (empty if none)")
|
|
465
|
+
},
|
|
466
|
+
annotations: {
|
|
467
|
+
readOnlyHint: true,
|
|
468
|
+
idempotentHint: true,
|
|
469
|
+
openWorldHint: false
|
|
470
|
+
}
|
|
471
|
+
},
|
|
247
472
|
async ({ projectId }) => {
|
|
248
473
|
const id = encodeURIComponent(projectId);
|
|
249
474
|
const data = await client.get(`/projects/${id}/goals`);
|
|
475
|
+
const structuredContent = {
|
|
476
|
+
goals: data.goals.map((g) => ({
|
|
477
|
+
goalName: g.goalName,
|
|
478
|
+
hits: g.hits,
|
|
479
|
+
uniqueSessions: g.uniqueSessions,
|
|
480
|
+
conversionRate: g.pct,
|
|
481
|
+
variants: g.variants.map((v) => ({
|
|
482
|
+
componentId: v.componentId,
|
|
483
|
+
variantId: v.variantId,
|
|
484
|
+
completionRate: v.completionRate
|
|
485
|
+
}))
|
|
486
|
+
}))
|
|
487
|
+
};
|
|
250
488
|
if (!data.goals.length) {
|
|
251
|
-
return {
|
|
489
|
+
return {
|
|
490
|
+
content: [{ type: "text", text: "No goals configured for this project." }],
|
|
491
|
+
structuredContent
|
|
492
|
+
};
|
|
252
493
|
}
|
|
253
494
|
const lines = data.goals.flatMap((g) => [
|
|
254
495
|
`${g.goalName}: ${g.hits} hits, ${g.uniqueSessions} unique sessions, ${(g.pct * 100).toFixed(1)}% conversion`,
|
|
255
496
|
...g.variants.map((v) => ` ${v.componentId}/${v.variantId}: ${(v.completionRate * 100).toFixed(1)}% per assigned session`),
|
|
256
497
|
""
|
|
257
498
|
]);
|
|
258
|
-
return { content: [{ type: "text", text: lines.join("\n").trim() }] };
|
|
499
|
+
return { content: [{ type: "text", text: lines.join("\n").trim() }], structuredContent };
|
|
259
500
|
}
|
|
260
501
|
);
|
|
261
502
|
}
|
|
@@ -264,20 +505,47 @@ function registerGoalTools(server, client) {
|
|
|
264
505
|
import { z as z6 } from "zod";
|
|
265
506
|
var projectIdSchema6 = z6.string().uuid().describe("The project UUID");
|
|
266
507
|
function registerGuardrailTools(server, client) {
|
|
267
|
-
server.
|
|
508
|
+
server.registerTool(
|
|
268
509
|
"list_guardrail_events",
|
|
269
|
-
|
|
270
|
-
|
|
510
|
+
{
|
|
511
|
+
title: "List guardrail events",
|
|
512
|
+
description: "List variants currently paused by the guardrail in the last 24 hours.",
|
|
513
|
+
inputSchema: { projectId: projectIdSchema6 },
|
|
514
|
+
outputSchema: {
|
|
515
|
+
events: z6.array(
|
|
516
|
+
z6.object({
|
|
517
|
+
componentId: z6.string(),
|
|
518
|
+
variantIds: z6.array(z6.string()).describe("Variants paused by the guardrail"),
|
|
519
|
+
pausedAt: z6.string().nullable().describe("ISO timestamp the pause fired, or null")
|
|
520
|
+
})
|
|
521
|
+
).describe("Guardrail events in the last 24h (empty if none)")
|
|
522
|
+
},
|
|
523
|
+
annotations: {
|
|
524
|
+
readOnlyHint: true,
|
|
525
|
+
idempotentHint: true,
|
|
526
|
+
openWorldHint: false
|
|
527
|
+
}
|
|
528
|
+
},
|
|
271
529
|
async ({ projectId }) => {
|
|
272
530
|
const id = encodeURIComponent(projectId);
|
|
273
531
|
const data = await client.get(`/projects/${id}/guardrail-events`);
|
|
532
|
+
const structuredContent = {
|
|
533
|
+
events: data.guardrailEvents.map((e) => ({
|
|
534
|
+
componentId: e.componentId,
|
|
535
|
+
variantIds: e.variantIds,
|
|
536
|
+
pausedAt: e.pausedAt
|
|
537
|
+
}))
|
|
538
|
+
};
|
|
274
539
|
if (!data.guardrailEvents.length) {
|
|
275
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
content: [{ type: "text", text: "No active guardrail events in the last 24 hours." }],
|
|
542
|
+
structuredContent
|
|
543
|
+
};
|
|
276
544
|
}
|
|
277
545
|
const lines = data.guardrailEvents.map(
|
|
278
546
|
(e) => `- ${e.componentId}: variants [${e.variantIds.join(", ")}] paused${e.pausedAt ? ` at ${e.pausedAt}` : ""}`
|
|
279
547
|
);
|
|
280
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
548
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
281
549
|
}
|
|
282
550
|
);
|
|
283
551
|
}
|
|
@@ -286,20 +554,49 @@ function registerGuardrailTools(server, client) {
|
|
|
286
554
|
import { z as z7 } from "zod";
|
|
287
555
|
var projectIdSchema7 = z7.string().uuid().describe("The project UUID");
|
|
288
556
|
function registerLayoutTools(server, client) {
|
|
289
|
-
server.
|
|
557
|
+
server.registerTool(
|
|
290
558
|
"get_layout_stats",
|
|
291
|
-
|
|
292
|
-
|
|
559
|
+
{
|
|
560
|
+
title: "Layout stats",
|
|
561
|
+
description: "Get per-persona section layout rankings and bandit reward weights.",
|
|
562
|
+
inputSchema: { projectId: projectIdSchema7 },
|
|
563
|
+
outputSchema: {
|
|
564
|
+
layouts: z7.array(
|
|
565
|
+
z7.object({
|
|
566
|
+
persona: z7.string(),
|
|
567
|
+
layoutOrder: z7.array(z7.string()).describe("Ranked section order for this persona"),
|
|
568
|
+
pulls: z7.number().describe("Number of times this arm was served"),
|
|
569
|
+
avgReward: z7.number().describe("Average bandit reward weight")
|
|
570
|
+
})
|
|
571
|
+
).describe("Per-persona layout rankings (empty until enough sessions)")
|
|
572
|
+
},
|
|
573
|
+
annotations: {
|
|
574
|
+
readOnlyHint: true,
|
|
575
|
+
idempotentHint: true,
|
|
576
|
+
openWorldHint: false
|
|
577
|
+
}
|
|
578
|
+
},
|
|
293
579
|
async ({ projectId }) => {
|
|
294
580
|
const id = encodeURIComponent(projectId);
|
|
295
581
|
const stats = await client.get(`/projects/${id}/layout-stats`);
|
|
582
|
+
const structuredContent = {
|
|
583
|
+
layouts: stats.map((s) => ({
|
|
584
|
+
persona: s.persona,
|
|
585
|
+
layoutOrder: s.layoutOrder,
|
|
586
|
+
pulls: s.pulls,
|
|
587
|
+
avgReward: s.avgReward
|
|
588
|
+
}))
|
|
589
|
+
};
|
|
296
590
|
if (!stats.length) {
|
|
297
|
-
return {
|
|
591
|
+
return {
|
|
592
|
+
content: [{ type: "text", text: "No layout data yet. More visitor sessions are needed." }],
|
|
593
|
+
structuredContent
|
|
594
|
+
};
|
|
298
595
|
}
|
|
299
596
|
const text = stats.map(
|
|
300
597
|
(s) => `- ${s.persona}: [${s.layoutOrder.join(" \u2192 ")}] (avg reward: ${s.avgReward.toFixed(2)}, ${s.pulls} pulls)`
|
|
301
598
|
).join("\n");
|
|
302
|
-
return { content: [{ type: "text", text }] };
|
|
599
|
+
return { content: [{ type: "text", text }], structuredContent };
|
|
303
600
|
}
|
|
304
601
|
);
|
|
305
602
|
}
|
|
@@ -308,14 +605,30 @@ function registerLayoutTools(server, client) {
|
|
|
308
605
|
import { z as z8 } from "zod";
|
|
309
606
|
var projectIdSchema8 = z8.string().uuid().describe("The project UUID");
|
|
310
607
|
function registerVariantWriteTools(server, client) {
|
|
311
|
-
server.
|
|
608
|
+
server.registerTool(
|
|
312
609
|
"create_variant",
|
|
313
|
-
"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. Requires a paid plan (server keys are Starter+; anonymous demo tokens are read-only).",
|
|
314
610
|
{
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
611
|
+
title: "Create managed variant",
|
|
612
|
+
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. Requires a paid plan (server keys are Starter+; anonymous demo tokens are read-only).",
|
|
613
|
+
inputSchema: {
|
|
614
|
+
projectId: projectIdSchema8,
|
|
615
|
+
componentId: z8.string().describe("The component ID to add a variant to"),
|
|
616
|
+
displayName: z8.string().describe("Human-readable name for the new variant"),
|
|
617
|
+
content: z8.string().optional().describe("The text content for this managed variant (rendered by <AdaptiveText>). Generate it from get_variant_brief context; omit only to create an empty placeholder to fill in from the dashboard.")
|
|
618
|
+
},
|
|
619
|
+
outputSchema: {
|
|
620
|
+
variantId: z8.string().describe("The new variant ID"),
|
|
621
|
+
displayName: z8.string(),
|
|
622
|
+
componentId: z8.string(),
|
|
623
|
+
state: z8.literal("draft").describe("New managed variants start in draft state"),
|
|
624
|
+
hasContent: z8.boolean().describe("Whether text content was provided at creation")
|
|
625
|
+
},
|
|
626
|
+
annotations: {
|
|
627
|
+
readOnlyHint: false,
|
|
628
|
+
destructiveHint: false,
|
|
629
|
+
idempotentHint: false,
|
|
630
|
+
openWorldHint: false
|
|
631
|
+
}
|
|
319
632
|
},
|
|
320
633
|
async ({ projectId, componentId, displayName, content }) => {
|
|
321
634
|
const id = encodeURIComponent(projectId);
|
|
@@ -328,17 +641,38 @@ function registerVariantWriteTools(server, client) {
|
|
|
328
641
|
content: [{
|
|
329
642
|
type: "text",
|
|
330
643
|
text: `Managed text variant created: ${result.variantId} ("${result.displayName}") for component ${componentId}${contentNote}. It is in draft state \u2014 activate it from the dashboard. Reminder: this is a no-code managed variant; for code-native variants, edit the code instead (see get_variant_brief).`
|
|
331
|
-
}]
|
|
644
|
+
}],
|
|
645
|
+
structuredContent: {
|
|
646
|
+
variantId: result.variantId,
|
|
647
|
+
displayName: result.displayName,
|
|
648
|
+
componentId,
|
|
649
|
+
state: "draft",
|
|
650
|
+
hasContent: Boolean(content)
|
|
651
|
+
}
|
|
332
652
|
};
|
|
333
653
|
}
|
|
334
654
|
);
|
|
335
|
-
server.
|
|
655
|
+
server.registerTool(
|
|
336
656
|
"pause_variant",
|
|
337
|
-
"Pause a variant, stopping traffic from being assigned to it.",
|
|
338
657
|
{
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
658
|
+
title: "Pause variant",
|
|
659
|
+
description: "Pause a variant, stopping traffic from being assigned to it.",
|
|
660
|
+
inputSchema: {
|
|
661
|
+
projectId: projectIdSchema8,
|
|
662
|
+
componentId: z8.string().describe("The component ID"),
|
|
663
|
+
variantId: z8.string().describe("The variant ID to pause")
|
|
664
|
+
},
|
|
665
|
+
outputSchema: {
|
|
666
|
+
variantId: z8.string(),
|
|
667
|
+
componentId: z8.string(),
|
|
668
|
+
paused: z8.literal(true).describe("The variant is now paused")
|
|
669
|
+
},
|
|
670
|
+
annotations: {
|
|
671
|
+
readOnlyHint: false,
|
|
672
|
+
destructiveHint: false,
|
|
673
|
+
idempotentHint: true,
|
|
674
|
+
openWorldHint: false
|
|
675
|
+
}
|
|
342
676
|
},
|
|
343
677
|
async ({ projectId, componentId, variantId }) => {
|
|
344
678
|
const id = encodeURIComponent(projectId);
|
|
@@ -347,14 +681,28 @@ function registerVariantWriteTools(server, client) {
|
|
|
347
681
|
content: [{
|
|
348
682
|
type: "text",
|
|
349
683
|
text: `Variant ${variantId} in component ${componentId} has been paused. No new traffic will be assigned to it.`
|
|
350
|
-
}]
|
|
684
|
+
}],
|
|
685
|
+
structuredContent: { variantId, componentId, paused: true }
|
|
351
686
|
};
|
|
352
687
|
}
|
|
353
688
|
);
|
|
354
|
-
server.
|
|
689
|
+
server.registerTool(
|
|
355
690
|
"refresh_insights",
|
|
356
|
-
|
|
357
|
-
|
|
691
|
+
{
|
|
692
|
+
title: "Refresh insights",
|
|
693
|
+
description: "Trigger fresh AI insight generation for a project. Returns immediately; use get_insights in ~15 seconds to see results.",
|
|
694
|
+
inputSchema: { projectId: projectIdSchema8 },
|
|
695
|
+
outputSchema: {
|
|
696
|
+
projectId: z8.string(),
|
|
697
|
+
status: z8.literal("generating").describe("Generation has been triggered")
|
|
698
|
+
},
|
|
699
|
+
annotations: {
|
|
700
|
+
readOnlyHint: false,
|
|
701
|
+
destructiveHint: false,
|
|
702
|
+
idempotentHint: false,
|
|
703
|
+
openWorldHint: false
|
|
704
|
+
}
|
|
705
|
+
},
|
|
358
706
|
async ({ projectId }) => {
|
|
359
707
|
const id = encodeURIComponent(projectId);
|
|
360
708
|
await client.post(`/projects/${id}/insights/refresh`);
|
|
@@ -362,7 +710,8 @@ function registerVariantWriteTools(server, client) {
|
|
|
362
710
|
content: [{
|
|
363
711
|
type: "text",
|
|
364
712
|
text: `Insights are generating for project ${projectId}. Call get_insights in ~15 seconds to see the results.`
|
|
365
|
-
}]
|
|
713
|
+
}],
|
|
714
|
+
structuredContent: { projectId, status: "generating" }
|
|
366
715
|
};
|
|
367
716
|
}
|
|
368
717
|
);
|
|
@@ -435,12 +784,28 @@ async function settled(p) {
|
|
|
435
784
|
}
|
|
436
785
|
}
|
|
437
786
|
function registerVariantBriefTools(server, client) {
|
|
438
|
-
server.
|
|
787
|
+
server.registerTool(
|
|
439
788
|
"get_variant_brief",
|
|
440
|
-
"Get an insight-driven brief for creating a new CODE-NATIVE variant of a component. Returns current variant performance, audience, insights, a data-sufficiency assessment (with a best-practice fallback when there is no data yet), and step-by-step instructions for writing the variant in the customer's code. Use this instead of create_variant when the variant will live in the codebase.",
|
|
441
789
|
{
|
|
442
|
-
|
|
443
|
-
|
|
790
|
+
title: "Variant brief",
|
|
791
|
+
description: "Get an insight-driven brief for creating a new CODE-NATIVE variant of a component. Returns current variant performance, audience, insights, a data-sufficiency assessment (with a best-practice fallback when there is no data yet), and step-by-step instructions for writing the variant in the customer's code. Use this instead of create_variant when the variant will live in the codebase.",
|
|
792
|
+
inputSchema: {
|
|
793
|
+
projectId: projectIdSchema9,
|
|
794
|
+
componentId: z9.string().describe('The component ID to write a new variant for (matches <Adaptive id="...">).')
|
|
795
|
+
},
|
|
796
|
+
outputSchema: {
|
|
797
|
+
componentId: z9.string(),
|
|
798
|
+
contextType: z9.string().describe("The project's context type (or 'unknown')"),
|
|
799
|
+
dataState: z9.enum(["sufficient", "collecting", "empty"]).describe("Data-sufficiency assessment"),
|
|
800
|
+
existingVariantIds: z9.array(z9.string()).describe("Variant IDs already in use (do not reuse)"),
|
|
801
|
+
priors: z9.array(z9.string()).describe("Best-practice priors applied for this context type"),
|
|
802
|
+
markdown: z9.string().describe("The full variant brief in Markdown")
|
|
803
|
+
},
|
|
804
|
+
annotations: {
|
|
805
|
+
readOnlyHint: true,
|
|
806
|
+
idempotentHint: true,
|
|
807
|
+
openWorldHint: false
|
|
808
|
+
}
|
|
444
809
|
},
|
|
445
810
|
async ({ projectId, componentId }) => {
|
|
446
811
|
const id = encodeURIComponent(projectId);
|
|
@@ -531,7 +896,18 @@ function registerVariantBriefTools(server, client) {
|
|
|
531
896
|
lines.push("5. Commit, push, and deploy. Optionally enable shadow mode for this component first if you want to validate before serving real traffic.");
|
|
532
897
|
lines.push("");
|
|
533
898
|
lines.push("Make the change reflect the data sufficiency above: data-driven when SUFFICIENT, best-practice-led when COLLECTING or EMPTY.");
|
|
534
|
-
|
|
899
|
+
const markdown = lines.join("\n");
|
|
900
|
+
return {
|
|
901
|
+
content: [{ type: "text", text: markdown }],
|
|
902
|
+
structuredContent: {
|
|
903
|
+
componentId,
|
|
904
|
+
contextType,
|
|
905
|
+
dataState,
|
|
906
|
+
existingVariantIds,
|
|
907
|
+
priors: priorsFor(contextType),
|
|
908
|
+
markdown
|
|
909
|
+
}
|
|
910
|
+
};
|
|
535
911
|
}
|
|
536
912
|
);
|
|
537
913
|
}
|
|
@@ -547,12 +923,26 @@ async function settled2(p) {
|
|
|
547
923
|
}
|
|
548
924
|
}
|
|
549
925
|
function registerTestBriefTools(server, client) {
|
|
550
|
-
server.
|
|
926
|
+
server.registerTool(
|
|
551
927
|
"get_test_brief",
|
|
552
|
-
"Get a ready-to-paste test for a SentientUI-wrapped component, populated with the component's real variants and goals. This project uses @sentientui/react/testing. Use this so your tests force a specific variant/layout deterministically and never break when the optimizer serves a different version. Returns a React Testing Library example plus the URL-param recipe for E2E (Playwright/Cypress).",
|
|
553
928
|
{
|
|
554
|
-
|
|
555
|
-
|
|
929
|
+
title: "Test brief",
|
|
930
|
+
description: "Get a ready-to-paste test for a SentientUI-wrapped component, populated with the component's real variants and goals. This project uses @sentientui/react/testing. Use this so your tests force a specific variant/layout deterministically and never break when the optimizer serves a different version. Returns a React Testing Library example plus the URL-param recipe for E2E (Playwright/Cypress).",
|
|
931
|
+
inputSchema: {
|
|
932
|
+
projectId: projectIdSchema10,
|
|
933
|
+
componentId: z10.string().describe('The component ID to write a test for (matches <Adaptive id="...">).')
|
|
934
|
+
},
|
|
935
|
+
outputSchema: {
|
|
936
|
+
componentId: z10.string(),
|
|
937
|
+
forcedVariantId: z10.string().describe("The non-control variant the example forces"),
|
|
938
|
+
goalName: z10.string().describe("The goal the example asserts fires"),
|
|
939
|
+
markdown: z10.string().describe("The full test brief in Markdown")
|
|
940
|
+
},
|
|
941
|
+
annotations: {
|
|
942
|
+
readOnlyHint: true,
|
|
943
|
+
idempotentHint: true,
|
|
944
|
+
openWorldHint: false
|
|
945
|
+
}
|
|
556
946
|
},
|
|
557
947
|
async ({ projectId, componentId }) => {
|
|
558
948
|
const id = encodeURIComponent(projectId);
|
|
@@ -616,12 +1006,17 @@ function registerTestBriefTools(server, client) {
|
|
|
616
1006
|
lines.push("```ts");
|
|
617
1007
|
lines.push(`await page.goto('/?sentient_variant=${componentId}:${forcedId}');`);
|
|
618
1008
|
lines.push("```");
|
|
619
|
-
|
|
1009
|
+
const markdown = lines.join("\n");
|
|
1010
|
+
return {
|
|
1011
|
+
content: [{ type: "text", text: markdown }],
|
|
1012
|
+
structuredContent: { componentId, forcedVariantId: forcedId, goalName, markdown }
|
|
1013
|
+
};
|
|
620
1014
|
}
|
|
621
1015
|
);
|
|
622
1016
|
}
|
|
623
1017
|
|
|
624
1018
|
// src/tools/integration-guide.ts
|
|
1019
|
+
import { z as z11 } from "zod";
|
|
625
1020
|
var GUIDE = `# SentientUI integration guide \u2014 the adaptive ladder
|
|
626
1021
|
|
|
627
1022
|
SentientUI adapts a site per visitor type (personas: buyer, researcher, deal_seeker, browser,
|
|
@@ -689,11 +1084,25 @@ Use '@sentientui/react/testing': renderWithSentient(ui, { variants, slots, perso
|
|
|
689
1084
|
deterministic outcomes so tests never depend on what the optimizer serves.
|
|
690
1085
|
`;
|
|
691
1086
|
function registerIntegrationGuideTools(server) {
|
|
692
|
-
server.
|
|
1087
|
+
server.registerTool(
|
|
693
1088
|
"get_integration_guide",
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1089
|
+
{
|
|
1090
|
+
title: "Integration guide",
|
|
1091
|
+
description: "Get the SentientUI adaptive-ladder integration guide: setup (keyless and keyed) plus copy-pasteable examples for every rung (Style, Swap, Reorder). Use this to integrate SentientUI into a codebase.",
|
|
1092
|
+
inputSchema: {},
|
|
1093
|
+
outputSchema: {
|
|
1094
|
+
guide: z11.string().describe("The full integration guide in Markdown")
|
|
1095
|
+
},
|
|
1096
|
+
annotations: {
|
|
1097
|
+
readOnlyHint: true,
|
|
1098
|
+
idempotentHint: true,
|
|
1099
|
+
openWorldHint: false
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
async () => ({
|
|
1103
|
+
content: [{ type: "text", text: GUIDE }],
|
|
1104
|
+
structuredContent: { guide: GUIDE }
|
|
1105
|
+
})
|
|
697
1106
|
);
|
|
698
1107
|
}
|
|
699
1108
|
|