@sentientui/mcp 0.14.1 → 0.15.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.
@@ -65,196 +65,6 @@ var ApiClient = class {
65
65
  }
66
66
  };
67
67
 
68
- // src/server.ts
69
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
70
-
71
- // src/tools/projects.ts
72
- import { z as z2 } from "zod";
73
-
74
- // src/tools/common.ts
75
- import { z } from "zod";
76
- var projectIdSchema = z.string().uuid().describe("The project UUID");
77
- function apiErrorGuidance(err, extra) {
78
- if (extra && Object.prototype.hasOwnProperty.call(extra, err.message)) {
79
- return extra[err.message];
80
- }
81
- switch (err.message) {
82
- case "insufficient_scope":
83
- return "This action needs an account login. Connect via the hosted MCP URL (https://api.sentient-ui.com/mcp) and sign in \u2014 a project-scoped server key (sk_\u2026) or anonymous demo token cannot do this.";
84
- case "demo_read_only":
85
- return "Demo mode is read-only. Create a SentientUI account and sign in (or use a project server key) to make changes.";
86
- case "insufficient_role":
87
- return "Your account role does not permit this action \u2014 it needs the account owner or an admin.";
88
- default:
89
- break;
90
- }
91
- if (err.status === 402) {
92
- return `This feature requires a higher plan (${err.message}). Upgrade your SentientUI plan, then try again.`;
93
- }
94
- if (err.status === 403) {
95
- return `Access denied (${err.message}). Check that your key or login has access to this project.`;
96
- }
97
- return null;
98
- }
99
- function withApiErrorGuidance(fn, extra) {
100
- return async (args) => {
101
- try {
102
- return await fn(args);
103
- } catch (err) {
104
- if (err instanceof ApiError) {
105
- const guidance = apiErrorGuidance(err, extra);
106
- if (guidance) {
107
- return { content: [{ type: "text", text: guidance }], isError: true };
108
- }
109
- }
110
- throw err;
111
- }
112
- };
113
- }
114
-
115
- // src/tools/projects.ts
116
- var CREATE_PROJECT_GUIDANCE = {
117
- project_limit_reached: "You've reached your plan's project limit. Upgrade your plan or remove an existing project, then try again.",
118
- name_required: "A project name is required to create a project."
119
- };
120
- function registerProjectTools(server, client) {
121
- server.registerTool(
122
- "create_project",
123
- {
124
- title: "Create project",
125
- 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.",
126
- inputSchema: {
127
- name: z2.string().min(1).describe("Human-readable project name"),
128
- contextType: z2.enum(["saas", "ecommerce", "marketing", "landing", "internal"]).optional().describe("What kind of product this is; defaults to saas"),
129
- framework: z2.enum(["next", "react", "core"]).optional().describe("How the site is built \u2014 next, react, or core (website builder/CMS); defaults to next"),
130
- websiteUrl: z2.string().optional().describe("Production site origin to allow-list so the SDK's events aren't origin-blocked on day one")
131
- },
132
- outputSchema: {
133
- projectId: z2.string().describe("The new project UUID"),
134
- publicKey: z2.string().describe("The pk_ public key to configure the SDK with"),
135
- name: z2.string().describe("The project name"),
136
- contextType: z2.string().describe("The resolved context type")
137
- },
138
- annotations: {
139
- readOnlyHint: false,
140
- destructiveHint: false,
141
- idempotentHint: false,
142
- openWorldHint: false
143
- }
144
- },
145
- withApiErrorGuidance(async ({ name, contextType, framework, websiteUrl }) => {
146
- const created = await client.post("/projects", {
147
- name,
148
- contextType,
149
- framework,
150
- origin: websiteUrl
151
- });
152
- const resolvedContextType = contextType != null ? contextType : "saas";
153
- return {
154
- content: [{
155
- type: "text",
156
- text: [
157
- `Created project "${name}" (id: ${created.id}, type: ${resolvedContextType}).`,
158
- `Public key: ${created.apiKey}`,
159
- `Next: install @sentientui/react with this key. Ask me to pull the setup guide (get_integration_guide) and I'll wrap your first component.`
160
- ].join("\n")
161
- }],
162
- structuredContent: {
163
- projectId: created.id,
164
- publicKey: created.apiKey,
165
- name,
166
- contextType: resolvedContextType
167
- }
168
- };
169
- }, CREATE_PROJECT_GUIDANCE)
170
- );
171
- server.registerTool(
172
- "list_projects",
173
- {
174
- title: "List projects",
175
- description: "List all SentientUI projects for the authenticated account.",
176
- inputSchema: {},
177
- outputSchema: {
178
- projects: z2.array(
179
- z2.object({
180
- id: z2.string().describe("Project UUID"),
181
- name: z2.string(),
182
- contextType: z2.string(),
183
- createdAt: z2.string().describe("ISO date (YYYY-MM-DD)")
184
- })
185
- ).describe("All projects for the account (empty if none)")
186
- },
187
- annotations: {
188
- readOnlyHint: true,
189
- idempotentHint: true,
190
- openWorldHint: false
191
- }
192
- },
193
- withApiErrorGuidance(async () => {
194
- const projects = await client.get("/projects");
195
- const text = projects.length === 0 ? "No projects found." : projects.map(
196
- (p) => `- ${p.name} (id: ${p.id}, type: ${p.context_type}, created: ${p.created_at.slice(0, 10)})`
197
- ).join("\n");
198
- return {
199
- content: [{ type: "text", text }],
200
- structuredContent: {
201
- projects: projects.map((p) => ({
202
- id: p.id,
203
- name: p.name,
204
- contextType: p.context_type,
205
- createdAt: p.created_at.slice(0, 10)
206
- }))
207
- }
208
- };
209
- })
210
- );
211
- server.registerTool(
212
- "get_project_stats",
213
- {
214
- title: "Project health stats",
215
- description: "Get health stats for a project: event volume, session count, agent calls, and status.",
216
- inputSchema: { projectId: projectIdSchema },
217
- outputSchema: {
218
- status: z2.string().describe("Overall project health status"),
219
- events24h: z2.number().describe("Events in the last 24 hours"),
220
- sessions24h: z2.number().describe("Sessions in the last 24 hours"),
221
- agentCalls: z2.number().describe("Total agent (MCP/API) calls"),
222
- lastEventAt: z2.string().nullable().describe("ISO timestamp of the last event, or null")
223
- },
224
- annotations: {
225
- readOnlyHint: true,
226
- idempotentHint: true,
227
- openWorldHint: false
228
- }
229
- },
230
- withApiErrorGuidance(async ({ projectId }) => {
231
- var _a;
232
- const id = encodeURIComponent(projectId);
233
- const stats = await client.get(`/projects/${id}/health`);
234
- const text = [
235
- `Status: ${stats.status}`,
236
- `Events (24h): ${stats.events24h}`,
237
- `Sessions (24h): ${stats.sessions24h}`,
238
- `Agent calls (total): ${stats.agentCalls}`,
239
- `Last event: ${(_a = stats.lastEventAt) != null ? _a : "never"}`
240
- ].join("\n");
241
- return {
242
- content: [{ type: "text", text }],
243
- structuredContent: {
244
- status: stats.status,
245
- events24h: stats.events24h,
246
- sessions24h: stats.sessions24h,
247
- agentCalls: stats.agentCalls,
248
- lastEventAt: stats.lastEventAt
249
- }
250
- };
251
- })
252
- );
253
- }
254
-
255
- // src/tools/components.ts
256
- import { z as z3 } from "zod";
257
-
258
68
  // src/ui/templates.ts
259
69
  var VIZ_TITLES = {
260
70
  "persona-breakdown": "Persona breakdown",
@@ -488,6 +298,7 @@ function uiMeta(id) {
488
298
  "openai/outputTemplate": uri
489
299
  };
490
300
  }
301
+ var PUBLIC_UI_RESOURCE_URIS = Object.values(UI_TOOL_VIZ).map(uiResourceUri);
491
302
  function registerUiResources(server) {
492
303
  for (const id of Object.values(UI_TOOL_VIZ)) {
493
304
  server.registerResource(
@@ -511,7 +322,195 @@ function registerUiResources(server) {
511
322
  }
512
323
  }
513
324
 
325
+ // src/server.ts
326
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
327
+
328
+ // src/tools/projects.ts
329
+ import { z as z2 } from "zod";
330
+
331
+ // src/tools/common.ts
332
+ import { z } from "zod";
333
+ var projectIdSchema = z.string().uuid().describe("The project UUID");
334
+ function apiErrorGuidance(err, extra) {
335
+ if (extra && Object.prototype.hasOwnProperty.call(extra, err.message)) {
336
+ return extra[err.message];
337
+ }
338
+ switch (err.message) {
339
+ case "insufficient_scope":
340
+ return "This action needs an account login. Connect via the hosted MCP URL (https://api.sentient-ui.com/mcp) and sign in \u2014 a project-scoped server key (sk_\u2026) or anonymous demo token cannot do this.";
341
+ case "demo_read_only":
342
+ return "Demo mode is read-only. Create a SentientUI account and sign in (or use a project server key) to make changes.";
343
+ case "insufficient_role":
344
+ return "Your account role does not permit this action \u2014 it needs the account owner or an admin.";
345
+ default:
346
+ break;
347
+ }
348
+ if (err.status === 402) {
349
+ return `This feature requires a higher plan (${err.message}). Upgrade your SentientUI plan, then try again.`;
350
+ }
351
+ if (err.status === 403) {
352
+ return `Access denied (${err.message}). Check that your key or login has access to this project.`;
353
+ }
354
+ return null;
355
+ }
356
+ function withApiErrorGuidance(fn, extra) {
357
+ return async (args) => {
358
+ try {
359
+ return await fn(args);
360
+ } catch (err) {
361
+ if (err instanceof ApiError) {
362
+ const guidance = apiErrorGuidance(err, extra);
363
+ if (guidance) {
364
+ return { content: [{ type: "text", text: guidance }], isError: true };
365
+ }
366
+ }
367
+ throw err;
368
+ }
369
+ };
370
+ }
371
+
372
+ // src/tools/projects.ts
373
+ var CREATE_PROJECT_GUIDANCE = {
374
+ project_limit_reached: "You've reached your plan's project limit. Upgrade your plan or remove an existing project, then try again.",
375
+ name_required: "A project name is required to create a project."
376
+ };
377
+ function registerProjectTools(server, client) {
378
+ server.registerTool(
379
+ "create_project",
380
+ {
381
+ title: "Create project",
382
+ 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.",
383
+ inputSchema: {
384
+ name: z2.string().min(1).describe("Human-readable project name"),
385
+ contextType: z2.enum(["saas", "ecommerce", "marketing", "landing", "internal"]).optional().describe("What kind of product this is; defaults to saas"),
386
+ framework: z2.enum(["next", "react", "core"]).optional().describe("How the site is built \u2014 next, react, or core (website builder/CMS); defaults to next"),
387
+ websiteUrl: z2.string().optional().describe("Production site origin to allow-list so the SDK's events aren't origin-blocked on day one")
388
+ },
389
+ outputSchema: {
390
+ projectId: z2.string().describe("The new project UUID"),
391
+ publicKey: z2.string().describe("The pk_ public key to configure the SDK with"),
392
+ name: z2.string().describe("The project name"),
393
+ contextType: z2.string().describe("The resolved context type")
394
+ },
395
+ annotations: {
396
+ readOnlyHint: false,
397
+ destructiveHint: false,
398
+ idempotentHint: false,
399
+ openWorldHint: false
400
+ }
401
+ },
402
+ withApiErrorGuidance(async ({ name, contextType, framework, websiteUrl }) => {
403
+ const created = await client.post("/projects", {
404
+ name,
405
+ contextType,
406
+ framework,
407
+ origin: websiteUrl
408
+ });
409
+ const resolvedContextType = contextType != null ? contextType : "saas";
410
+ return {
411
+ content: [{
412
+ type: "text",
413
+ text: [
414
+ `Created project "${name}" (id: ${created.id}, type: ${resolvedContextType}).`,
415
+ `Public key: ${created.apiKey}`,
416
+ `Next: install @sentientui/react with this key. Ask me to pull the setup guide (get_integration_guide) and I'll wrap your first component.`
417
+ ].join("\n")
418
+ }],
419
+ structuredContent: {
420
+ projectId: created.id,
421
+ publicKey: created.apiKey,
422
+ name,
423
+ contextType: resolvedContextType
424
+ }
425
+ };
426
+ }, CREATE_PROJECT_GUIDANCE)
427
+ );
428
+ server.registerTool(
429
+ "list_projects",
430
+ {
431
+ title: "List projects",
432
+ description: "List all SentientUI projects for the authenticated account.",
433
+ inputSchema: {},
434
+ outputSchema: {
435
+ projects: z2.array(
436
+ z2.object({
437
+ id: z2.string().describe("Project UUID"),
438
+ name: z2.string(),
439
+ contextType: z2.string(),
440
+ createdAt: z2.string().describe("ISO date (YYYY-MM-DD)")
441
+ })
442
+ ).describe("All projects for the account (empty if none)")
443
+ },
444
+ annotations: {
445
+ readOnlyHint: true,
446
+ idempotentHint: true,
447
+ openWorldHint: false
448
+ }
449
+ },
450
+ withApiErrorGuidance(async () => {
451
+ const projects = await client.get("/projects");
452
+ const text = projects.length === 0 ? "No projects found." : projects.map(
453
+ (p) => `- ${p.name} (id: ${p.id}, type: ${p.context_type}, created: ${p.created_at.slice(0, 10)})`
454
+ ).join("\n");
455
+ return {
456
+ content: [{ type: "text", text }],
457
+ structuredContent: {
458
+ projects: projects.map((p) => ({
459
+ id: p.id,
460
+ name: p.name,
461
+ contextType: p.context_type,
462
+ createdAt: p.created_at.slice(0, 10)
463
+ }))
464
+ }
465
+ };
466
+ })
467
+ );
468
+ server.registerTool(
469
+ "get_project_stats",
470
+ {
471
+ title: "Project health stats",
472
+ description: "Get health stats for a project: event volume, session count, agent calls, and status.",
473
+ inputSchema: { projectId: projectIdSchema },
474
+ outputSchema: {
475
+ status: z2.string().describe("Overall project health status"),
476
+ events24h: z2.number().describe("Events in the last 24 hours"),
477
+ sessions24h: z2.number().describe("Sessions in the last 24 hours"),
478
+ agentCalls: z2.number().describe("Total agent (MCP/API) calls"),
479
+ lastEventAt: z2.string().nullable().describe("ISO timestamp of the last event, or null")
480
+ },
481
+ annotations: {
482
+ readOnlyHint: true,
483
+ idempotentHint: true,
484
+ openWorldHint: false
485
+ }
486
+ },
487
+ withApiErrorGuidance(async ({ projectId }) => {
488
+ var _a;
489
+ const id = encodeURIComponent(projectId);
490
+ const stats = await client.get(`/projects/${id}/health`);
491
+ const text = [
492
+ `Status: ${stats.status}`,
493
+ `Events (24h): ${stats.events24h}`,
494
+ `Sessions (24h): ${stats.sessions24h}`,
495
+ `Agent calls (total): ${stats.agentCalls}`,
496
+ `Last event: ${(_a = stats.lastEventAt) != null ? _a : "never"}`
497
+ ].join("\n");
498
+ return {
499
+ content: [{ type: "text", text }],
500
+ structuredContent: {
501
+ status: stats.status,
502
+ events24h: stats.events24h,
503
+ sessions24h: stats.sessions24h,
504
+ agentCalls: stats.agentCalls,
505
+ lastEventAt: stats.lastEventAt
506
+ }
507
+ };
508
+ })
509
+ );
510
+ }
511
+
514
512
  // src/tools/components.ts
513
+ import { z as z3 } from "zod";
515
514
  function registerComponentTools(server, client) {
516
515
  server.registerTool(
517
516
  "list_components",
@@ -619,19 +618,38 @@ function registerComponentTools(server, client) {
619
618
 
620
619
  // src/tools/insights.ts
621
620
  import { z as z4 } from "zod";
621
+ var EMPTY_REASON_TEXT = {
622
+ no_events: "The snippet has not fired yet \u2014 nothing has reached us.",
623
+ no_traffic_yet: "Test traffic only so far; no genuine visitors yet.",
624
+ awaiting_profiles: "Still building visitor profiles before visitors can be grouped.",
625
+ no_variants: "Nothing to compare yet \u2014 no adaptive component is running.",
626
+ nothing_moved: "Enough data, and nothing moved this period. That is a result.",
627
+ job_never_ran: "The analysis job has not run yet.",
628
+ job_stale: "These numbers are out of date \u2014 the analysis job has not run recently."
629
+ };
622
630
  function registerInsightTools(server, client) {
623
631
  server.registerTool(
624
632
  "get_insights",
625
633
  {
626
634
  title: "Get insights",
627
- description: "Get the latest AI-generated insights: narrator observations and (Growth tier) advisor recommendations.",
635
+ description: "What is known about this project right now: ranked findings with the evidence behind each one, plus (Growth tier) advisor recommendations. Says why it is empty when it is.",
628
636
  inputSchema: { projectId: projectIdSchema },
629
637
  outputSchema: {
630
- status: z4.enum(["ok", "empty"]).describe("Whether insights exist yet"),
631
- observations: z4.array(z4.string()).describe("Narrator observations"),
632
- recommendations: z4.array(z4.string()).describe("Advisor recommendations (Growth tier)"),
633
- isStale: z4.boolean().describe("True when the insights are older than ~6h"),
634
- generatedAt: z4.string().nullable().describe("ISO timestamp the insights were generated, or null")
638
+ status: z4.enum(["ok", "empty"]).describe("Whether any findings exist yet"),
639
+ reached: z4.string().describe("Highest confidence tier earned: observed | patterned | tested"),
640
+ findings: z4.array(z4.object({
641
+ tier: z4.string().describe("observed | patterned | tested"),
642
+ kind: z4.string(),
643
+ headline: z4.string(),
644
+ audience: z4.string().nullable(),
645
+ sample: z4.number().describe("How many the finding is based on"),
646
+ coverage: z4.number().nullable().describe("Fraction of traffic this finding describes, when applicable")
647
+ })).describe("Ranked findings, each with the evidence behind it"),
648
+ emptyReason: z4.string().nullable().describe("Why there is nothing to report, when there is nothing"),
649
+ observations: z4.array(z4.string()).describe("Headlines of measured findings"),
650
+ recommendations: z4.array(z4.string()).describe("Headlines of interpreted findings (Growth tier)"),
651
+ isStale: z4.boolean().describe("True when the analysis has not run recently"),
652
+ generatedAt: z4.string().nullable().describe("ISO timestamp, or null")
635
653
  },
636
654
  annotations: {
637
655
  readOnlyHint: true,
@@ -640,42 +658,54 @@ function registerInsightTools(server, client) {
640
658
  }
641
659
  },
642
660
  withApiErrorGuidance(async ({ projectId }) => {
643
- var _a, _b, _c, _d;
661
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
644
662
  const id = encodeURIComponent(projectId);
645
- const data = await client.get(`/projects/${id}/insights`);
646
- if (data.status === "empty") {
647
- return {
648
- content: [{ type: "text", text: "No insights generated yet. Use refresh_insights to generate." }],
649
- structuredContent: {
650
- status: "empty",
651
- observations: [],
652
- recommendations: [],
653
- isStale: false,
654
- generatedAt: null
655
- }
656
- };
657
- }
658
- const observations = (_a = data.narratorBullets) != null ? _a : [];
659
- const recommendations = (_b = data.advisorBullets) != null ? _b : [];
663
+ const data = await client.get(`/projects/${id}/evidence-report`);
664
+ const findings = (_a = data.findings) != null ? _a : [];
665
+ const measured = findings.filter((f) => !f.interpreted);
666
+ const interpreted = findings.filter((f) => f.interpreted);
667
+ const observations = measured.map((f) => f.headline);
668
+ const recommendations = interpreted.map((f) => f.headline);
669
+ const isStale = (_c = (_b = data.freshness) == null ? void 0 : _b.isStale) != null ? _c : false;
670
+ const emptyReason = (_d = data.emptyReason) != null ? _d : null;
660
671
  const lines = [];
661
- if (data.isStale) lines.push("\u26A0 Insights are stale (>6h old). Consider calling refresh_insights.");
672
+ if (emptyReason) lines.push((_e = EMPTY_REASON_TEXT[emptyReason]) != null ? _e : emptyReason);
673
+ if (isStale) lines.push("\u26A0 The analysis job has not run recently, so these numbers may have moved.");
674
+ if ((_f = data.interpretation) == null ? void 0 : _f.locked) {
675
+ lines.push(`${data.interpretation.count || "Some"} recommendations are available on the Growth plan.`);
676
+ } else if (((_g = data.interpretation) == null ? void 0 : _g.emptyReason) && !emptyReason) {
677
+ lines.push(`Recommendations: ${(_h = EMPTY_REASON_TEXT[data.interpretation.emptyReason]) != null ? _h : data.interpretation.emptyReason}`);
678
+ }
662
679
  if (data.generatedAt) lines.push(`Generated: ${new Date(data.generatedAt).toUTCString()}`);
663
- lines.push("");
664
- lines.push("Observations:");
665
- observations.forEach((b) => lines.push(`- ${b}`));
680
+ if (measured.length) {
681
+ lines.push("", "Findings:");
682
+ for (const f of measured) {
683
+ const cov = f.provenance.coverage !== null && f.provenance.coverage < 0.999 ? `, covering ${Math.round(f.provenance.coverage * 100)}% of visitors` : "";
684
+ lines.push(`- [${f.tier}] ${f.headline} (${f.provenance.sample} ${f.provenance.denominatorLabel}${cov})`);
685
+ }
686
+ }
666
687
  if (recommendations.length) {
667
- lines.push("");
668
- lines.push("Recommendations:");
669
- recommendations.forEach((b) => lines.push(`- ${b}`));
688
+ lines.push("", "Recommendations:");
689
+ for (const r of recommendations) lines.push(`- ${r}`);
670
690
  }
671
691
  return {
672
- content: [{ type: "text", text: lines.join("\n") }],
692
+ content: [{ type: "text", text: lines.join("\n").trim() || "No findings yet." }],
673
693
  structuredContent: {
674
- status: "ok",
694
+ status: findings.length ? "ok" : "empty",
695
+ reached: (_i = data.reached) != null ? _i : "observed",
696
+ findings: findings.map((f) => ({
697
+ tier: f.tier,
698
+ kind: f.kind,
699
+ headline: f.headline,
700
+ audience: f.audience,
701
+ sample: f.provenance.sample,
702
+ coverage: f.provenance.coverage
703
+ })),
704
+ emptyReason,
675
705
  observations,
676
706
  recommendations,
677
- isStale: (_c = data.isStale) != null ? _c : false,
678
- generatedAt: (_d = data.generatedAt) != null ? _d : null
707
+ isStale,
708
+ generatedAt: (_j = data.generatedAt) != null ? _j : null
679
709
  }
680
710
  };
681
711
  })
@@ -1772,7 +1802,7 @@ function registerAgentTrafficTools(server, client) {
1772
1802
  }
1773
1803
 
1774
1804
  // src/server.ts
1775
- var PKG_VERSION = true ? "0.14.1" : "0.0.0-dev";
1805
+ var PKG_VERSION = true ? "0.15.0" : "0.0.0-dev";
1776
1806
  function createMcpServer(client) {
1777
1807
  const server = new McpServer(
1778
1808
  {
@@ -1813,5 +1843,6 @@ function createMcpServer(client) {
1813
1843
  export {
1814
1844
  ApiError,
1815
1845
  ApiClient,
1846
+ PUBLIC_UI_RESOURCE_URIS,
1816
1847
  createMcpServer
1817
1848
  };
package/dist/index.cjs CHANGED
@@ -492,6 +492,7 @@ function uiMeta(id) {
492
492
  "openai/outputTemplate": uri
493
493
  };
494
494
  }
495
+ var PUBLIC_UI_RESOURCE_URIS = Object.values(UI_TOOL_VIZ).map(uiResourceUri);
495
496
  function registerUiResources(server) {
496
497
  for (const id of Object.values(UI_TOOL_VIZ)) {
497
498
  server.registerResource(
@@ -623,19 +624,38 @@ function registerComponentTools(server, client) {
623
624
 
624
625
  // src/tools/insights.ts
625
626
  var import_zod4 = require("zod");
627
+ var EMPTY_REASON_TEXT = {
628
+ no_events: "The snippet has not fired yet \u2014 nothing has reached us.",
629
+ no_traffic_yet: "Test traffic only so far; no genuine visitors yet.",
630
+ awaiting_profiles: "Still building visitor profiles before visitors can be grouped.",
631
+ no_variants: "Nothing to compare yet \u2014 no adaptive component is running.",
632
+ nothing_moved: "Enough data, and nothing moved this period. That is a result.",
633
+ job_never_ran: "The analysis job has not run yet.",
634
+ job_stale: "These numbers are out of date \u2014 the analysis job has not run recently."
635
+ };
626
636
  function registerInsightTools(server, client) {
627
637
  server.registerTool(
628
638
  "get_insights",
629
639
  {
630
640
  title: "Get insights",
631
- description: "Get the latest AI-generated insights: narrator observations and (Growth tier) advisor recommendations.",
641
+ description: "What is known about this project right now: ranked findings with the evidence behind each one, plus (Growth tier) advisor recommendations. Says why it is empty when it is.",
632
642
  inputSchema: { projectId: projectIdSchema },
633
643
  outputSchema: {
634
- status: import_zod4.z.enum(["ok", "empty"]).describe("Whether insights exist yet"),
635
- observations: import_zod4.z.array(import_zod4.z.string()).describe("Narrator observations"),
636
- recommendations: import_zod4.z.array(import_zod4.z.string()).describe("Advisor recommendations (Growth tier)"),
637
- isStale: import_zod4.z.boolean().describe("True when the insights are older than ~6h"),
638
- generatedAt: import_zod4.z.string().nullable().describe("ISO timestamp the insights were generated, or null")
644
+ status: import_zod4.z.enum(["ok", "empty"]).describe("Whether any findings exist yet"),
645
+ reached: import_zod4.z.string().describe("Highest confidence tier earned: observed | patterned | tested"),
646
+ findings: import_zod4.z.array(import_zod4.z.object({
647
+ tier: import_zod4.z.string().describe("observed | patterned | tested"),
648
+ kind: import_zod4.z.string(),
649
+ headline: import_zod4.z.string(),
650
+ audience: import_zod4.z.string().nullable(),
651
+ sample: import_zod4.z.number().describe("How many the finding is based on"),
652
+ coverage: import_zod4.z.number().nullable().describe("Fraction of traffic this finding describes, when applicable")
653
+ })).describe("Ranked findings, each with the evidence behind it"),
654
+ emptyReason: import_zod4.z.string().nullable().describe("Why there is nothing to report, when there is nothing"),
655
+ observations: import_zod4.z.array(import_zod4.z.string()).describe("Headlines of measured findings"),
656
+ recommendations: import_zod4.z.array(import_zod4.z.string()).describe("Headlines of interpreted findings (Growth tier)"),
657
+ isStale: import_zod4.z.boolean().describe("True when the analysis has not run recently"),
658
+ generatedAt: import_zod4.z.string().nullable().describe("ISO timestamp, or null")
639
659
  },
640
660
  annotations: {
641
661
  readOnlyHint: true,
@@ -644,42 +664,54 @@ function registerInsightTools(server, client) {
644
664
  }
645
665
  },
646
666
  withApiErrorGuidance(async ({ projectId }) => {
647
- var _a2, _b, _c, _d;
667
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j;
648
668
  const id = encodeURIComponent(projectId);
649
- const data = await client.get(`/projects/${id}/insights`);
650
- if (data.status === "empty") {
651
- return {
652
- content: [{ type: "text", text: "No insights generated yet. Use refresh_insights to generate." }],
653
- structuredContent: {
654
- status: "empty",
655
- observations: [],
656
- recommendations: [],
657
- isStale: false,
658
- generatedAt: null
659
- }
660
- };
661
- }
662
- const observations = (_a2 = data.narratorBullets) != null ? _a2 : [];
663
- const recommendations = (_b = data.advisorBullets) != null ? _b : [];
669
+ const data = await client.get(`/projects/${id}/evidence-report`);
670
+ const findings = (_a2 = data.findings) != null ? _a2 : [];
671
+ const measured = findings.filter((f) => !f.interpreted);
672
+ const interpreted = findings.filter((f) => f.interpreted);
673
+ const observations = measured.map((f) => f.headline);
674
+ const recommendations = interpreted.map((f) => f.headline);
675
+ const isStale = (_c = (_b = data.freshness) == null ? void 0 : _b.isStale) != null ? _c : false;
676
+ const emptyReason = (_d = data.emptyReason) != null ? _d : null;
664
677
  const lines = [];
665
- if (data.isStale) lines.push("\u26A0 Insights are stale (>6h old). Consider calling refresh_insights.");
678
+ if (emptyReason) lines.push((_e = EMPTY_REASON_TEXT[emptyReason]) != null ? _e : emptyReason);
679
+ if (isStale) lines.push("\u26A0 The analysis job has not run recently, so these numbers may have moved.");
680
+ if ((_f = data.interpretation) == null ? void 0 : _f.locked) {
681
+ lines.push(`${data.interpretation.count || "Some"} recommendations are available on the Growth plan.`);
682
+ } else if (((_g = data.interpretation) == null ? void 0 : _g.emptyReason) && !emptyReason) {
683
+ lines.push(`Recommendations: ${(_h = EMPTY_REASON_TEXT[data.interpretation.emptyReason]) != null ? _h : data.interpretation.emptyReason}`);
684
+ }
666
685
  if (data.generatedAt) lines.push(`Generated: ${new Date(data.generatedAt).toUTCString()}`);
667
- lines.push("");
668
- lines.push("Observations:");
669
- observations.forEach((b) => lines.push(`- ${b}`));
686
+ if (measured.length) {
687
+ lines.push("", "Findings:");
688
+ for (const f of measured) {
689
+ const cov = f.provenance.coverage !== null && f.provenance.coverage < 0.999 ? `, covering ${Math.round(f.provenance.coverage * 100)}% of visitors` : "";
690
+ lines.push(`- [${f.tier}] ${f.headline} (${f.provenance.sample} ${f.provenance.denominatorLabel}${cov})`);
691
+ }
692
+ }
670
693
  if (recommendations.length) {
671
- lines.push("");
672
- lines.push("Recommendations:");
673
- recommendations.forEach((b) => lines.push(`- ${b}`));
694
+ lines.push("", "Recommendations:");
695
+ for (const r of recommendations) lines.push(`- ${r}`);
674
696
  }
675
697
  return {
676
- content: [{ type: "text", text: lines.join("\n") }],
698
+ content: [{ type: "text", text: lines.join("\n").trim() || "No findings yet." }],
677
699
  structuredContent: {
678
- status: "ok",
700
+ status: findings.length ? "ok" : "empty",
701
+ reached: (_i = data.reached) != null ? _i : "observed",
702
+ findings: findings.map((f) => ({
703
+ tier: f.tier,
704
+ kind: f.kind,
705
+ headline: f.headline,
706
+ audience: f.audience,
707
+ sample: f.provenance.sample,
708
+ coverage: f.provenance.coverage
709
+ })),
710
+ emptyReason,
679
711
  observations,
680
712
  recommendations,
681
- isStale: (_c = data.isStale) != null ? _c : false,
682
- generatedAt: (_d = data.generatedAt) != null ? _d : null
713
+ isStale,
714
+ generatedAt: (_j = data.generatedAt) != null ? _j : null
683
715
  }
684
716
  };
685
717
  })
@@ -1776,7 +1808,7 @@ function registerAgentTrafficTools(server, client) {
1776
1808
  }
1777
1809
 
1778
1810
  // src/server.ts
1779
- var PKG_VERSION = true ? "0.14.1" : "0.0.0-dev";
1811
+ var PKG_VERSION = true ? "0.15.0" : "0.0.0-dev";
1780
1812
  function createMcpServer(client) {
1781
1813
  const server = new import_mcp.McpServer(
1782
1814
  {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  ApiClient,
4
4
  createMcpServer
5
- } from "./chunk-O335LMWF.js";
5
+ } from "./chunk-3RJUYHO7.js";
6
6
 
7
7
  // src/index.ts
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/dist/lib.cjs CHANGED
@@ -40,6 +40,7 @@ var lib_exports = {};
40
40
  __export(lib_exports, {
41
41
  ApiClient: () => ApiClient,
42
42
  ApiError: () => ApiError,
43
+ PUBLIC_UI_RESOURCE_URIS: () => PUBLIC_UI_RESOURCE_URIS,
43
44
  createMcpServer: () => createMcpServer
44
45
  });
45
46
  module.exports = __toCommonJS(lib_exports);
@@ -515,6 +516,7 @@ function uiMeta(id) {
515
516
  "openai/outputTemplate": uri
516
517
  };
517
518
  }
519
+ var PUBLIC_UI_RESOURCE_URIS = Object.values(UI_TOOL_VIZ).map(uiResourceUri);
518
520
  function registerUiResources(server) {
519
521
  for (const id of Object.values(UI_TOOL_VIZ)) {
520
522
  server.registerResource(
@@ -646,19 +648,38 @@ function registerComponentTools(server, client) {
646
648
 
647
649
  // src/tools/insights.ts
648
650
  var import_zod4 = require("zod");
651
+ var EMPTY_REASON_TEXT = {
652
+ no_events: "The snippet has not fired yet \u2014 nothing has reached us.",
653
+ no_traffic_yet: "Test traffic only so far; no genuine visitors yet.",
654
+ awaiting_profiles: "Still building visitor profiles before visitors can be grouped.",
655
+ no_variants: "Nothing to compare yet \u2014 no adaptive component is running.",
656
+ nothing_moved: "Enough data, and nothing moved this period. That is a result.",
657
+ job_never_ran: "The analysis job has not run yet.",
658
+ job_stale: "These numbers are out of date \u2014 the analysis job has not run recently."
659
+ };
649
660
  function registerInsightTools(server, client) {
650
661
  server.registerTool(
651
662
  "get_insights",
652
663
  {
653
664
  title: "Get insights",
654
- description: "Get the latest AI-generated insights: narrator observations and (Growth tier) advisor recommendations.",
665
+ description: "What is known about this project right now: ranked findings with the evidence behind each one, plus (Growth tier) advisor recommendations. Says why it is empty when it is.",
655
666
  inputSchema: { projectId: projectIdSchema },
656
667
  outputSchema: {
657
- status: import_zod4.z.enum(["ok", "empty"]).describe("Whether insights exist yet"),
658
- observations: import_zod4.z.array(import_zod4.z.string()).describe("Narrator observations"),
659
- recommendations: import_zod4.z.array(import_zod4.z.string()).describe("Advisor recommendations (Growth tier)"),
660
- isStale: import_zod4.z.boolean().describe("True when the insights are older than ~6h"),
661
- generatedAt: import_zod4.z.string().nullable().describe("ISO timestamp the insights were generated, or null")
668
+ status: import_zod4.z.enum(["ok", "empty"]).describe("Whether any findings exist yet"),
669
+ reached: import_zod4.z.string().describe("Highest confidence tier earned: observed | patterned | tested"),
670
+ findings: import_zod4.z.array(import_zod4.z.object({
671
+ tier: import_zod4.z.string().describe("observed | patterned | tested"),
672
+ kind: import_zod4.z.string(),
673
+ headline: import_zod4.z.string(),
674
+ audience: import_zod4.z.string().nullable(),
675
+ sample: import_zod4.z.number().describe("How many the finding is based on"),
676
+ coverage: import_zod4.z.number().nullable().describe("Fraction of traffic this finding describes, when applicable")
677
+ })).describe("Ranked findings, each with the evidence behind it"),
678
+ emptyReason: import_zod4.z.string().nullable().describe("Why there is nothing to report, when there is nothing"),
679
+ observations: import_zod4.z.array(import_zod4.z.string()).describe("Headlines of measured findings"),
680
+ recommendations: import_zod4.z.array(import_zod4.z.string()).describe("Headlines of interpreted findings (Growth tier)"),
681
+ isStale: import_zod4.z.boolean().describe("True when the analysis has not run recently"),
682
+ generatedAt: import_zod4.z.string().nullable().describe("ISO timestamp, or null")
662
683
  },
663
684
  annotations: {
664
685
  readOnlyHint: true,
@@ -667,42 +688,54 @@ function registerInsightTools(server, client) {
667
688
  }
668
689
  },
669
690
  withApiErrorGuidance(async ({ projectId }) => {
670
- var _a, _b, _c, _d;
691
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
671
692
  const id = encodeURIComponent(projectId);
672
- const data = await client.get(`/projects/${id}/insights`);
673
- if (data.status === "empty") {
674
- return {
675
- content: [{ type: "text", text: "No insights generated yet. Use refresh_insights to generate." }],
676
- structuredContent: {
677
- status: "empty",
678
- observations: [],
679
- recommendations: [],
680
- isStale: false,
681
- generatedAt: null
682
- }
683
- };
684
- }
685
- const observations = (_a = data.narratorBullets) != null ? _a : [];
686
- const recommendations = (_b = data.advisorBullets) != null ? _b : [];
693
+ const data = await client.get(`/projects/${id}/evidence-report`);
694
+ const findings = (_a = data.findings) != null ? _a : [];
695
+ const measured = findings.filter((f) => !f.interpreted);
696
+ const interpreted = findings.filter((f) => f.interpreted);
697
+ const observations = measured.map((f) => f.headline);
698
+ const recommendations = interpreted.map((f) => f.headline);
699
+ const isStale = (_c = (_b = data.freshness) == null ? void 0 : _b.isStale) != null ? _c : false;
700
+ const emptyReason = (_d = data.emptyReason) != null ? _d : null;
687
701
  const lines = [];
688
- if (data.isStale) lines.push("\u26A0 Insights are stale (>6h old). Consider calling refresh_insights.");
702
+ if (emptyReason) lines.push((_e = EMPTY_REASON_TEXT[emptyReason]) != null ? _e : emptyReason);
703
+ if (isStale) lines.push("\u26A0 The analysis job has not run recently, so these numbers may have moved.");
704
+ if ((_f = data.interpretation) == null ? void 0 : _f.locked) {
705
+ lines.push(`${data.interpretation.count || "Some"} recommendations are available on the Growth plan.`);
706
+ } else if (((_g = data.interpretation) == null ? void 0 : _g.emptyReason) && !emptyReason) {
707
+ lines.push(`Recommendations: ${(_h = EMPTY_REASON_TEXT[data.interpretation.emptyReason]) != null ? _h : data.interpretation.emptyReason}`);
708
+ }
689
709
  if (data.generatedAt) lines.push(`Generated: ${new Date(data.generatedAt).toUTCString()}`);
690
- lines.push("");
691
- lines.push("Observations:");
692
- observations.forEach((b) => lines.push(`- ${b}`));
710
+ if (measured.length) {
711
+ lines.push("", "Findings:");
712
+ for (const f of measured) {
713
+ const cov = f.provenance.coverage !== null && f.provenance.coverage < 0.999 ? `, covering ${Math.round(f.provenance.coverage * 100)}% of visitors` : "";
714
+ lines.push(`- [${f.tier}] ${f.headline} (${f.provenance.sample} ${f.provenance.denominatorLabel}${cov})`);
715
+ }
716
+ }
693
717
  if (recommendations.length) {
694
- lines.push("");
695
- lines.push("Recommendations:");
696
- recommendations.forEach((b) => lines.push(`- ${b}`));
718
+ lines.push("", "Recommendations:");
719
+ for (const r of recommendations) lines.push(`- ${r}`);
697
720
  }
698
721
  return {
699
- content: [{ type: "text", text: lines.join("\n") }],
722
+ content: [{ type: "text", text: lines.join("\n").trim() || "No findings yet." }],
700
723
  structuredContent: {
701
- status: "ok",
724
+ status: findings.length ? "ok" : "empty",
725
+ reached: (_i = data.reached) != null ? _i : "observed",
726
+ findings: findings.map((f) => ({
727
+ tier: f.tier,
728
+ kind: f.kind,
729
+ headline: f.headline,
730
+ audience: f.audience,
731
+ sample: f.provenance.sample,
732
+ coverage: f.provenance.coverage
733
+ })),
734
+ emptyReason,
702
735
  observations,
703
736
  recommendations,
704
- isStale: (_c = data.isStale) != null ? _c : false,
705
- generatedAt: (_d = data.generatedAt) != null ? _d : null
737
+ isStale,
738
+ generatedAt: (_j = data.generatedAt) != null ? _j : null
706
739
  }
707
740
  };
708
741
  })
@@ -1799,7 +1832,7 @@ function registerAgentTrafficTools(server, client) {
1799
1832
  }
1800
1833
 
1801
1834
  // src/server.ts
1802
- var PKG_VERSION = true ? "0.14.1" : "0.0.0-dev";
1835
+ var PKG_VERSION = true ? "0.15.0" : "0.0.0-dev";
1803
1836
  function createMcpServer(client) {
1804
1837
  const server = new import_mcp.McpServer(
1805
1838
  {
@@ -1840,5 +1873,6 @@ function createMcpServer(client) {
1840
1873
  0 && (module.exports = {
1841
1874
  ApiClient,
1842
1875
  ApiError,
1876
+ PUBLIC_UI_RESOURCE_URIS,
1843
1877
  createMcpServer
1844
1878
  });
package/dist/lib.d.cts CHANGED
@@ -17,4 +17,15 @@ declare class ApiClient {
17
17
 
18
18
  declare function createMcpServer(client: ApiClient): McpServer;
19
19
 
20
- export { ApiClient, ApiError, createMcpServer };
20
+ /**
21
+ * Every `ui://` resource this server registers. These templates are static HTML
22
+ * — no project data, no API call — so a host may read them before the user has
23
+ * authenticated. The remote /mcp endpoint uses this set to allow an
24
+ * unauthenticated `resources/read` for exactly these URIs (see
25
+ * apps/api/src/routes/mcp.ts): without it, `resources/list` advertised four
26
+ * resources that every reader got a 401 for, which reads to an agent as a
27
+ * server whose resources are all broken.
28
+ */
29
+ declare const PUBLIC_UI_RESOURCE_URIS: readonly string[];
30
+
31
+ export { ApiClient, ApiError, PUBLIC_UI_RESOURCE_URIS, createMcpServer };
package/dist/lib.d.ts CHANGED
@@ -17,4 +17,15 @@ declare class ApiClient {
17
17
 
18
18
  declare function createMcpServer(client: ApiClient): McpServer;
19
19
 
20
- export { ApiClient, ApiError, createMcpServer };
20
+ /**
21
+ * Every `ui://` resource this server registers. These templates are static HTML
22
+ * — no project data, no API call — so a host may read them before the user has
23
+ * authenticated. The remote /mcp endpoint uses this set to allow an
24
+ * unauthenticated `resources/read` for exactly these URIs (see
25
+ * apps/api/src/routes/mcp.ts): without it, `resources/list` advertised four
26
+ * resources that every reader got a 401 for, which reads to an agent as a
27
+ * server whose resources are all broken.
28
+ */
29
+ declare const PUBLIC_UI_RESOURCE_URIS: readonly string[];
30
+
31
+ export { ApiClient, ApiError, PUBLIC_UI_RESOURCE_URIS, createMcpServer };
package/dist/lib.js CHANGED
@@ -2,10 +2,12 @@
2
2
  import {
3
3
  ApiClient,
4
4
  ApiError,
5
+ PUBLIC_UI_RESOURCE_URIS,
5
6
  createMcpServer
6
- } from "./chunk-O335LMWF.js";
7
+ } from "./chunk-3RJUYHO7.js";
7
8
  export {
8
9
  ApiClient,
9
10
  ApiError,
11
+ PUBLIC_UI_RESOURCE_URIS,
10
12
  createMcpServer
11
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentientui/mcp",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "MCP server for SentientUI — exposes project data and actions to AI agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentient-ui.com",