@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.
@@ -70,14 +70,29 @@ function createProjectGuidance(err) {
70
70
  }
71
71
  }
72
72
  function registerProjectTools(server, client) {
73
- server.tool(
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
- name: z.string().min(1).describe("Human-readable project name"),
78
- contextType: z.enum(["saas", "ecommerce", "marketing", "internal"]).optional().describe("What kind of product this is; defaults to saas"),
79
- framework: z.enum(["next-app", "next-pages", "react", "core"]).optional().describe("Frontend framework, used to tailor setup; defaults to next-app"),
80
- websiteUrl: z.string().optional().describe("Production site origin to allow-list so the SDK's events aren't origin-blocked on day one")
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: ${contextType ?? "saas"}).`,
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.tool(
133
+ server.registerTool(
112
134
  "list_projects",
113
- "List all SentientUI projects for the authenticated account.",
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 { content: [{ type: "text", text }] };
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.tool(
173
+ server.registerTool(
124
174
  "get_project_stats",
125
- "Get health stats for a project: event volume, session count, agent calls, and status.",
126
- { projectId: projectIdSchema },
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 { content: [{ type: "text", text }] };
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.tool(
220
+ server.registerTool(
147
221
  "list_components",
148
- "List all adaptive components in a project with variant counts and impression totals.",
149
- { projectId: projectIdSchema2 },
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 { content: [{ type: "text", text: "No components found for this project." }] };
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.tool(
265
+ server.registerTool(
163
266
  "get_variant_performance",
164
- "Get CVR and momentum for all variants in a project over the last 7 days vs prior 7 days.",
165
- { projectId: projectIdSchema2 },
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 { content: [{ type: "text", text: "No variant data available yet." }] };
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.tool(
319
+ server.registerTool(
186
320
  "get_insights",
187
- "Get the latest AI-generated insights: narrator observations and (Growth tier) advisor recommendations.",
188
- { projectId: projectIdSchema3 },
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 { content: [{ type: "text", text: "No insights generated yet. Use refresh_insights to generate." }] };
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
- (data.narratorBullets ?? []).forEach((b) => lines.push(`- ${b}`));
201
- if (data.advisorBullets?.length) {
360
+ observations.forEach((b) => lines.push(`- ${b}`));
361
+ if (recommendations.length) {
202
362
  lines.push("");
203
363
  lines.push("Recommendations:");
204
- data.advisorBullets.forEach((b) => lines.push(`- ${b}`));
364
+ recommendations.forEach((b) => lines.push(`- ${b}`));
205
365
  }
206
- return { content: [{ type: "text", text: lines.join("\n") }] };
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.tool(
384
+ server.registerTool(
216
385
  "get_persona_breakdown",
217
- "Get the distribution of visitor persona clusters with session counts and reliability scores.",
218
- { projectId: projectIdSchema4 },
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 { content: [{ type: "text", text: "No persona clusters yet. More visitor data is needed." }] };
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.tool(
443
+ server.registerTool(
244
444
  "get_goal_funnel",
245
- "Get goal hit counts, unique-session conversion rates, and per-variant breakdown.",
246
- { projectId: projectIdSchema5 },
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 { content: [{ type: "text", text: "No goals configured for this project." }] };
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.tool(
508
+ server.registerTool(
268
509
  "list_guardrail_events",
269
- "List variants currently paused by the guardrail in the last 24 hours.",
270
- { projectId: projectIdSchema6 },
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 { content: [{ type: "text", text: "No active guardrail events in the last 24 hours." }] };
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.tool(
557
+ server.registerTool(
290
558
  "get_layout_stats",
291
- "Get per-persona section layout rankings and bandit reward weights.",
292
- { projectId: projectIdSchema7 },
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 { content: [{ type: "text", text: "No layout data yet. More visitor sessions are needed." }] };
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.tool(
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
- projectId: projectIdSchema8,
316
- componentId: z8.string().describe("The component ID to add a variant to"),
317
- displayName: z8.string().describe("Human-readable name for the new variant"),
318
- 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.")
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.tool(
655
+ server.registerTool(
336
656
  "pause_variant",
337
- "Pause a variant, stopping traffic from being assigned to it.",
338
657
  {
339
- projectId: projectIdSchema8,
340
- componentId: z8.string().describe("The component ID"),
341
- variantId: z8.string().describe("The variant ID to pause")
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.tool(
689
+ server.registerTool(
355
690
  "refresh_insights",
356
- "Trigger fresh AI insight generation for a project. Returns immediately; use get_insights in ~15 seconds to see results.",
357
- { projectId: projectIdSchema8 },
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.tool(
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
- projectId: projectIdSchema9,
443
- componentId: z9.string().describe('The component ID to write a new variant for (matches <Adaptive id="...">).')
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
- return { content: [{ type: "text", text: lines.join("\n") }] };
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.tool(
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
- projectId: projectIdSchema10,
555
- componentId: z10.string().describe('The component ID to write a test for (matches <Adaptive id="...">).')
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
- return { content: [{ type: "text", text: lines.join("\n") }] };
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.tool(
1087
+ server.registerTool(
693
1088
  "get_integration_guide",
694
- "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.",
695
- {},
696
- async () => ({ content: [{ type: "text", text: GUIDE }] })
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