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