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