@sentientui/mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/index.ts
5
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+
7
+ // src/api-client.ts
8
+ var ApiError = class extends Error {
9
+ constructor(status, message) {
10
+ super(message);
11
+ this.status = status;
12
+ this.name = "ApiError";
13
+ }
14
+ status;
15
+ };
16
+ var ApiClient = class {
17
+ baseUrl;
18
+ apiKey;
19
+ constructor(opts) {
20
+ this.apiKey = opts.apiKey;
21
+ this.baseUrl = (opts.baseUrl ?? "https://api.sentient-ui.com").replace(/\/$/, "");
22
+ }
23
+ async get(path) {
24
+ const res = await fetch(`${this.baseUrl}/v1/mgmt${path}`, {
25
+ headers: {
26
+ authorization: `Bearer ${this.apiKey}`,
27
+ "content-type": "application/json"
28
+ }
29
+ });
30
+ if (!res.ok) {
31
+ const body = await res.json().catch(() => ({}));
32
+ throw new ApiError(res.status, String(body.error ?? res.statusText));
33
+ }
34
+ return res.json();
35
+ }
36
+ async post(path, body) {
37
+ const res = await fetch(`${this.baseUrl}/v1/mgmt${path}`, {
38
+ method: "POST",
39
+ headers: {
40
+ authorization: `Bearer ${this.apiKey}`,
41
+ "content-type": "application/json"
42
+ },
43
+ body: body !== void 0 ? JSON.stringify(body) : void 0
44
+ });
45
+ if (!res.ok) {
46
+ const errBody = await res.json().catch(() => ({}));
47
+ throw new ApiError(res.status, String(errBody.error ?? res.statusText));
48
+ }
49
+ return res.json();
50
+ }
51
+ };
52
+
53
+ // src/server.ts
54
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
55
+
56
+ // src/tools/projects.ts
57
+ var import_zod = require("zod");
58
+ var projectIdSchema = import_zod.z.string().uuid().describe("The project UUID");
59
+ function registerProjectTools(server, client) {
60
+ server.tool(
61
+ "list_projects",
62
+ "List all SentientUI projects for the authenticated account.",
63
+ {},
64
+ async () => {
65
+ const projects = await client.get("/projects");
66
+ const text = projects.length === 0 ? "No projects found." : projects.map(
67
+ (p) => `- ${p.name} (id: ${p.id}, type: ${p.context_type}, created: ${p.created_at.slice(0, 10)})`
68
+ ).join("\n");
69
+ return { content: [{ type: "text", text }] };
70
+ }
71
+ );
72
+ server.tool(
73
+ "get_project_stats",
74
+ "Get health stats for a project: event volume, session count, agent calls, and status.",
75
+ { projectId: projectIdSchema },
76
+ async ({ projectId }) => {
77
+ const id = encodeURIComponent(projectId);
78
+ const stats = await client.get(`/projects/${id}/health`);
79
+ const text = [
80
+ `Status: ${stats.status}`,
81
+ `Events (24h): ${stats.events24h}`,
82
+ `Sessions (24h): ${stats.sessions24h}`,
83
+ `Agent calls (total): ${stats.agentCalls}`,
84
+ `Last event: ${stats.lastEventAt ?? "never"}`
85
+ ].join("\n");
86
+ return { content: [{ type: "text", text }] };
87
+ }
88
+ );
89
+ }
90
+
91
+ // src/tools/components.ts
92
+ var import_zod2 = require("zod");
93
+ var projectIdSchema2 = import_zod2.z.string().uuid().describe("The project UUID");
94
+ function registerComponentTools(server, client) {
95
+ server.tool(
96
+ "list_components",
97
+ "List all adaptive components in a project with variant counts and impression totals.",
98
+ { projectId: projectIdSchema2 },
99
+ async ({ projectId }) => {
100
+ const id = encodeURIComponent(projectId);
101
+ const components = await client.get(`/projects/${id}/components`);
102
+ if (!components.length) {
103
+ return { content: [{ type: "text", text: "No components found for this project." }] };
104
+ }
105
+ const text = components.map(
106
+ (c) => `- ${c.component_id}: ${c.variants.length} variants, ${c.total_impressions} impressions, ${c.total_conversions} conversions`
107
+ ).join("\n");
108
+ return { content: [{ type: "text", text }] };
109
+ }
110
+ );
111
+ server.tool(
112
+ "get_variant_performance",
113
+ "Get CVR and momentum for all variants in a project over the last 7 days vs prior 7 days.",
114
+ { projectId: projectIdSchema2 },
115
+ async ({ projectId }) => {
116
+ const id = encodeURIComponent(projectId);
117
+ const data = await client.get(`/projects/${id}/trends`);
118
+ if (!data.cvr?.length) {
119
+ return { content: [{ type: "text", text: "No variant data available yet." }] };
120
+ }
121
+ const momentumMap = new Map(data.momentum.map((m) => [m.variantId, m.direction]));
122
+ const text = data.cvr.map(
123
+ (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"})`
124
+ ).join("\n");
125
+ return { content: [{ type: "text", text }] };
126
+ }
127
+ );
128
+ }
129
+
130
+ // src/tools/insights.ts
131
+ var import_zod3 = require("zod");
132
+ var projectIdSchema3 = import_zod3.z.string().uuid().describe("The project UUID");
133
+ function registerInsightTools(server, client) {
134
+ server.tool(
135
+ "get_insights",
136
+ "Get the latest AI-generated insights: narrator observations and (Growth tier) advisor recommendations.",
137
+ { projectId: projectIdSchema3 },
138
+ async ({ projectId }) => {
139
+ const id = encodeURIComponent(projectId);
140
+ const data = await client.get(`/projects/${id}/insights`);
141
+ if (data.status === "empty") {
142
+ return { content: [{ type: "text", text: "No insights generated yet. Use refresh_insights to generate." }] };
143
+ }
144
+ const lines = [];
145
+ if (data.isStale) lines.push("\u26A0 Insights are stale (>6h old). Consider calling refresh_insights.");
146
+ if (data.generatedAt) lines.push(`Generated: ${new Date(data.generatedAt).toUTCString()}`);
147
+ lines.push("");
148
+ lines.push("Observations:");
149
+ (data.narratorBullets ?? []).forEach((b) => lines.push(`- ${b}`));
150
+ if (data.advisorBullets?.length) {
151
+ lines.push("");
152
+ lines.push("Recommendations:");
153
+ data.advisorBullets.forEach((b) => lines.push(`- ${b}`));
154
+ }
155
+ return { content: [{ type: "text", text: lines.join("\n") }] };
156
+ }
157
+ );
158
+ }
159
+
160
+ // src/tools/personas.ts
161
+ var import_zod4 = require("zod");
162
+ var projectIdSchema4 = import_zod4.z.string().uuid().describe("The project UUID");
163
+ function registerPersonaTools(server, client) {
164
+ server.tool(
165
+ "get_persona_breakdown",
166
+ "Get the distribution of visitor persona clusters with session counts and reliability scores.",
167
+ { projectId: projectIdSchema4 },
168
+ async ({ projectId }) => {
169
+ const id = encodeURIComponent(projectId);
170
+ const data = await client.get(`/projects/${id}/portraits`);
171
+ if (!data.clusters.length) {
172
+ return { content: [{ type: "text", text: "No persona clusters yet. More visitor data is needed." }] };
173
+ }
174
+ const lines = [
175
+ `Total sessions: ${data.totalSessions}`,
176
+ "",
177
+ "Clusters:",
178
+ ...data.clusters.map(
179
+ (c) => `- ${c.label}: ${c.sessionCount} sessions (${(c.sessionCount / data.totalSessions * 100).toFixed(1)}% of traffic, reliability ${(c.avgReliability * 100).toFixed(0)}%)`
180
+ )
181
+ ];
182
+ return { content: [{ type: "text", text: lines.join("\n") }] };
183
+ }
184
+ );
185
+ }
186
+
187
+ // src/tools/goals.ts
188
+ var import_zod5 = require("zod");
189
+ var projectIdSchema5 = import_zod5.z.string().uuid().describe("The project UUID");
190
+ function registerGoalTools(server, client) {
191
+ server.tool(
192
+ "get_goal_funnel",
193
+ "Get goal hit counts, unique-session conversion rates, and per-variant breakdown.",
194
+ { projectId: projectIdSchema5 },
195
+ async ({ projectId }) => {
196
+ const id = encodeURIComponent(projectId);
197
+ const data = await client.get(`/projects/${id}/goals`);
198
+ if (!data.goals.length) {
199
+ return { content: [{ type: "text", text: "No goals configured for this project." }] };
200
+ }
201
+ const lines = data.goals.flatMap((g) => [
202
+ `${g.goalName}: ${g.hits} hits, ${g.uniqueSessions} unique sessions, ${(g.pct * 100).toFixed(1)}% conversion`,
203
+ ...g.variants.map((v) => ` ${v.componentId}/${v.variantId}: ${(v.completionRate * 100).toFixed(1)}% per assigned session`),
204
+ ""
205
+ ]);
206
+ return { content: [{ type: "text", text: lines.join("\n").trim() }] };
207
+ }
208
+ );
209
+ }
210
+
211
+ // src/tools/guardrails.ts
212
+ var import_zod6 = require("zod");
213
+ var projectIdSchema6 = import_zod6.z.string().uuid().describe("The project UUID");
214
+ function registerGuardrailTools(server, client) {
215
+ server.tool(
216
+ "list_guardrail_events",
217
+ "List variants currently paused by the guardrail in the last 24 hours.",
218
+ { projectId: projectIdSchema6 },
219
+ async ({ projectId }) => {
220
+ const id = encodeURIComponent(projectId);
221
+ const data = await client.get(`/projects/${id}/guardrail-events`);
222
+ if (!data.guardrailEvents.length) {
223
+ return { content: [{ type: "text", text: "No active guardrail events in the last 24 hours." }] };
224
+ }
225
+ const lines = data.guardrailEvents.map(
226
+ (e) => `- ${e.componentId}: variants [${e.variantIds.join(", ")}] paused${e.pausedAt ? ` at ${e.pausedAt}` : ""}`
227
+ );
228
+ return { content: [{ type: "text", text: lines.join("\n") }] };
229
+ }
230
+ );
231
+ }
232
+
233
+ // src/tools/layout.ts
234
+ var import_zod7 = require("zod");
235
+ var projectIdSchema7 = import_zod7.z.string().uuid().describe("The project UUID");
236
+ function registerLayoutTools(server, client) {
237
+ server.tool(
238
+ "get_layout_stats",
239
+ "Get per-persona section layout rankings and bandit reward weights.",
240
+ { projectId: projectIdSchema7 },
241
+ async ({ projectId }) => {
242
+ const id = encodeURIComponent(projectId);
243
+ const stats = await client.get(`/projects/${id}/layout-stats`);
244
+ if (!stats.length) {
245
+ return { content: [{ type: "text", text: "No layout data yet. More visitor sessions are needed." }] };
246
+ }
247
+ const text = stats.map(
248
+ (s) => `- ${s.persona}: [${s.layoutOrder.join(" \u2192 ")}] (avg reward: ${s.avgReward.toFixed(2)}, ${s.pulls} pulls)`
249
+ ).join("\n");
250
+ return { content: [{ type: "text", text }] };
251
+ }
252
+ );
253
+ }
254
+
255
+ // src/tools/variants.ts
256
+ var import_zod8 = require("zod");
257
+ var projectIdSchema8 = import_zod8.z.string().uuid().describe("The project UUID");
258
+ function registerVariantWriteTools(server, client) {
259
+ server.tool(
260
+ "create_variant",
261
+ "Create a new draft variant for a component. Requires Starter or Growth plan.",
262
+ {
263
+ projectId: projectIdSchema8,
264
+ componentId: import_zod8.z.string().describe("The component ID to add a variant to"),
265
+ displayName: import_zod8.z.string().describe("Human-readable name for the new variant")
266
+ },
267
+ async ({ projectId, componentId, displayName }) => {
268
+ const id = encodeURIComponent(projectId);
269
+ const result = await client.post(
270
+ `/projects/${id}/variants`,
271
+ { componentId, displayName }
272
+ );
273
+ return {
274
+ content: [{
275
+ type: "text",
276
+ text: `Variant created: ${result.variantId} ("${result.displayName}") for component ${componentId}. It is in draft state \u2014 activate it from the dashboard.`
277
+ }]
278
+ };
279
+ }
280
+ );
281
+ server.tool(
282
+ "pause_variant",
283
+ "Pause a variant, stopping traffic from being assigned to it.",
284
+ {
285
+ projectId: projectIdSchema8,
286
+ componentId: import_zod8.z.string().describe("The component ID"),
287
+ variantId: import_zod8.z.string().describe("The variant ID to pause")
288
+ },
289
+ async ({ projectId, componentId, variantId }) => {
290
+ const id = encodeURIComponent(projectId);
291
+ await client.post(`/projects/${id}/variants/pause`, { componentId, variantId });
292
+ return {
293
+ content: [{
294
+ type: "text",
295
+ text: `Variant ${variantId} in component ${componentId} has been paused. No new traffic will be assigned to it.`
296
+ }]
297
+ };
298
+ }
299
+ );
300
+ server.tool(
301
+ "refresh_insights",
302
+ "Trigger fresh AI insight generation for a project. Returns immediately; use get_insights in ~15 seconds to see results.",
303
+ { projectId: projectIdSchema8 },
304
+ async ({ projectId }) => {
305
+ const id = encodeURIComponent(projectId);
306
+ await client.post(`/projects/${id}/insights/refresh`);
307
+ return {
308
+ content: [{
309
+ type: "text",
310
+ text: `Insights are generating for project ${projectId}. Call get_insights in ~15 seconds to see the results.`
311
+ }]
312
+ };
313
+ }
314
+ );
315
+ }
316
+
317
+ // src/server.ts
318
+ function createMcpServer(client) {
319
+ const server = new import_mcp.McpServer({
320
+ name: "@sentientui/mcp",
321
+ version: "0.1.0"
322
+ });
323
+ registerProjectTools(server, client);
324
+ registerComponentTools(server, client);
325
+ registerInsightTools(server, client);
326
+ registerPersonaTools(server, client);
327
+ registerGoalTools(server, client);
328
+ registerGuardrailTools(server, client);
329
+ registerLayoutTools(server, client);
330
+ registerVariantWriteTools(server, client);
331
+ return server;
332
+ }
333
+
334
+ // src/demo.ts
335
+ var import_node_fs = require("fs");
336
+ var import_node_path = require("path");
337
+ var import_node_os = require("os");
338
+ var CONFIG_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "sentientui");
339
+ var CONFIG_FILE = (0, import_node_path.join)(CONFIG_DIR, "mcp-anon.json");
340
+ var API_BASE = process.env.SENTIENTUI_API_URL ?? "https://api.sentient-ui.com";
341
+ function readCachedToken() {
342
+ try {
343
+ return JSON.parse((0, import_node_fs.readFileSync)(CONFIG_FILE, "utf-8"));
344
+ } catch {
345
+ return null;
346
+ }
347
+ }
348
+ function writeCachedToken(cfg) {
349
+ (0, import_node_fs.mkdirSync)(CONFIG_DIR, { recursive: true, mode: 448 });
350
+ (0, import_node_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2), { encoding: "utf-8", mode: 384 });
351
+ }
352
+ async function resolveDemoToken() {
353
+ const cached = readCachedToken();
354
+ if (cached?.token) {
355
+ process.stderr.write(
356
+ `[sentientui-mcp] Running in demo mode (${CONFIG_FILE}). Set SENTIENTUI_API_KEY for full access.
357
+ `
358
+ );
359
+ return cached.token;
360
+ }
361
+ process.stderr.write("[sentientui-mcp] No API key found. Provisioning anonymous demo token...\n");
362
+ const res = await fetch(`${API_BASE}/v1/mcp/demo`, { method: "POST" });
363
+ if (!res.ok) {
364
+ const body = await res.json().catch(() => ({}));
365
+ throw new Error(`Demo provisioning failed: ${String(body.error ?? res.statusText)}`);
366
+ }
367
+ const data = await res.json();
368
+ writeCachedToken({ token: data.token, projectId: data.projectId });
369
+ process.stderr.write(
370
+ `[sentientui-mcp] Demo token provisioned (${data.callsRemaining} calls/month). Set SENTIENTUI_API_KEY to remove this limit.
371
+ `
372
+ );
373
+ return data.token;
374
+ }
375
+
376
+ // src/index.ts
377
+ async function main() {
378
+ let apiKey = process.env.SENTIENTUI_API_KEY;
379
+ if (!apiKey) {
380
+ apiKey = await resolveDemoToken();
381
+ }
382
+ const client = new ApiClient({ apiKey });
383
+ const server = createMcpServer(client);
384
+ const transport = new import_stdio.StdioServerTransport();
385
+ await server.connect(transport);
386
+ }
387
+ main().catch((err) => {
388
+ process.stderr.write(`[sentientui-mcp] fatal: ${String(err)}
389
+ `);
390
+ process.exit(1);
391
+ });
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/api-client.ts
7
+ var ApiError = class extends Error {
8
+ constructor(status, message) {
9
+ super(message);
10
+ this.status = status;
11
+ this.name = "ApiError";
12
+ }
13
+ status;
14
+ };
15
+ var ApiClient = class {
16
+ baseUrl;
17
+ apiKey;
18
+ constructor(opts) {
19
+ this.apiKey = opts.apiKey;
20
+ this.baseUrl = (opts.baseUrl ?? "https://api.sentient-ui.com").replace(/\/$/, "");
21
+ }
22
+ async get(path) {
23
+ const res = await fetch(`${this.baseUrl}/v1/mgmt${path}`, {
24
+ headers: {
25
+ authorization: `Bearer ${this.apiKey}`,
26
+ "content-type": "application/json"
27
+ }
28
+ });
29
+ if (!res.ok) {
30
+ const body = await res.json().catch(() => ({}));
31
+ throw new ApiError(res.status, String(body.error ?? res.statusText));
32
+ }
33
+ return res.json();
34
+ }
35
+ async post(path, body) {
36
+ const res = await fetch(`${this.baseUrl}/v1/mgmt${path}`, {
37
+ method: "POST",
38
+ headers: {
39
+ authorization: `Bearer ${this.apiKey}`,
40
+ "content-type": "application/json"
41
+ },
42
+ body: body !== void 0 ? JSON.stringify(body) : void 0
43
+ });
44
+ if (!res.ok) {
45
+ const errBody = await res.json().catch(() => ({}));
46
+ throw new ApiError(res.status, String(errBody.error ?? res.statusText));
47
+ }
48
+ return res.json();
49
+ }
50
+ };
51
+
52
+ // src/server.ts
53
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
54
+
55
+ // src/tools/projects.ts
56
+ import { z } from "zod";
57
+ var projectIdSchema = z.string().uuid().describe("The project UUID");
58
+ function registerProjectTools(server, client) {
59
+ server.tool(
60
+ "list_projects",
61
+ "List all SentientUI projects for the authenticated account.",
62
+ {},
63
+ async () => {
64
+ const projects = await client.get("/projects");
65
+ const text = projects.length === 0 ? "No projects found." : projects.map(
66
+ (p) => `- ${p.name} (id: ${p.id}, type: ${p.context_type}, created: ${p.created_at.slice(0, 10)})`
67
+ ).join("\n");
68
+ return { content: [{ type: "text", text }] };
69
+ }
70
+ );
71
+ server.tool(
72
+ "get_project_stats",
73
+ "Get health stats for a project: event volume, session count, agent calls, and status.",
74
+ { projectId: projectIdSchema },
75
+ async ({ projectId }) => {
76
+ const id = encodeURIComponent(projectId);
77
+ const stats = await client.get(`/projects/${id}/health`);
78
+ const text = [
79
+ `Status: ${stats.status}`,
80
+ `Events (24h): ${stats.events24h}`,
81
+ `Sessions (24h): ${stats.sessions24h}`,
82
+ `Agent calls (total): ${stats.agentCalls}`,
83
+ `Last event: ${stats.lastEventAt ?? "never"}`
84
+ ].join("\n");
85
+ return { content: [{ type: "text", text }] };
86
+ }
87
+ );
88
+ }
89
+
90
+ // src/tools/components.ts
91
+ import { z as z2 } from "zod";
92
+ var projectIdSchema2 = z2.string().uuid().describe("The project UUID");
93
+ function registerComponentTools(server, client) {
94
+ server.tool(
95
+ "list_components",
96
+ "List all adaptive components in a project with variant counts and impression totals.",
97
+ { projectId: projectIdSchema2 },
98
+ async ({ projectId }) => {
99
+ const id = encodeURIComponent(projectId);
100
+ const components = await client.get(`/projects/${id}/components`);
101
+ if (!components.length) {
102
+ return { content: [{ type: "text", text: "No components found for this project." }] };
103
+ }
104
+ const text = components.map(
105
+ (c) => `- ${c.component_id}: ${c.variants.length} variants, ${c.total_impressions} impressions, ${c.total_conversions} conversions`
106
+ ).join("\n");
107
+ return { content: [{ type: "text", text }] };
108
+ }
109
+ );
110
+ server.tool(
111
+ "get_variant_performance",
112
+ "Get CVR and momentum for all variants in a project over the last 7 days vs prior 7 days.",
113
+ { projectId: projectIdSchema2 },
114
+ async ({ projectId }) => {
115
+ const id = encodeURIComponent(projectId);
116
+ const data = await client.get(`/projects/${id}/trends`);
117
+ if (!data.cvr?.length) {
118
+ return { content: [{ type: "text", text: "No variant data available yet." }] };
119
+ }
120
+ const momentumMap = new Map(data.momentum.map((m) => [m.variantId, m.direction]));
121
+ const text = data.cvr.map(
122
+ (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"})`
123
+ ).join("\n");
124
+ return { content: [{ type: "text", text }] };
125
+ }
126
+ );
127
+ }
128
+
129
+ // src/tools/insights.ts
130
+ import { z as z3 } from "zod";
131
+ var projectIdSchema3 = z3.string().uuid().describe("The project UUID");
132
+ function registerInsightTools(server, client) {
133
+ server.tool(
134
+ "get_insights",
135
+ "Get the latest AI-generated insights: narrator observations and (Growth tier) advisor recommendations.",
136
+ { projectId: projectIdSchema3 },
137
+ async ({ projectId }) => {
138
+ const id = encodeURIComponent(projectId);
139
+ const data = await client.get(`/projects/${id}/insights`);
140
+ if (data.status === "empty") {
141
+ return { content: [{ type: "text", text: "No insights generated yet. Use refresh_insights to generate." }] };
142
+ }
143
+ const lines = [];
144
+ if (data.isStale) lines.push("\u26A0 Insights are stale (>6h old). Consider calling refresh_insights.");
145
+ if (data.generatedAt) lines.push(`Generated: ${new Date(data.generatedAt).toUTCString()}`);
146
+ lines.push("");
147
+ lines.push("Observations:");
148
+ (data.narratorBullets ?? []).forEach((b) => lines.push(`- ${b}`));
149
+ if (data.advisorBullets?.length) {
150
+ lines.push("");
151
+ lines.push("Recommendations:");
152
+ data.advisorBullets.forEach((b) => lines.push(`- ${b}`));
153
+ }
154
+ return { content: [{ type: "text", text: lines.join("\n") }] };
155
+ }
156
+ );
157
+ }
158
+
159
+ // src/tools/personas.ts
160
+ import { z as z4 } from "zod";
161
+ var projectIdSchema4 = z4.string().uuid().describe("The project UUID");
162
+ function registerPersonaTools(server, client) {
163
+ server.tool(
164
+ "get_persona_breakdown",
165
+ "Get the distribution of visitor persona clusters with session counts and reliability scores.",
166
+ { projectId: projectIdSchema4 },
167
+ async ({ projectId }) => {
168
+ const id = encodeURIComponent(projectId);
169
+ const data = await client.get(`/projects/${id}/portraits`);
170
+ if (!data.clusters.length) {
171
+ return { content: [{ type: "text", text: "No persona clusters yet. More visitor data is needed." }] };
172
+ }
173
+ const lines = [
174
+ `Total sessions: ${data.totalSessions}`,
175
+ "",
176
+ "Clusters:",
177
+ ...data.clusters.map(
178
+ (c) => `- ${c.label}: ${c.sessionCount} sessions (${(c.sessionCount / data.totalSessions * 100).toFixed(1)}% of traffic, reliability ${(c.avgReliability * 100).toFixed(0)}%)`
179
+ )
180
+ ];
181
+ return { content: [{ type: "text", text: lines.join("\n") }] };
182
+ }
183
+ );
184
+ }
185
+
186
+ // src/tools/goals.ts
187
+ import { z as z5 } from "zod";
188
+ var projectIdSchema5 = z5.string().uuid().describe("The project UUID");
189
+ function registerGoalTools(server, client) {
190
+ server.tool(
191
+ "get_goal_funnel",
192
+ "Get goal hit counts, unique-session conversion rates, and per-variant breakdown.",
193
+ { projectId: projectIdSchema5 },
194
+ async ({ projectId }) => {
195
+ const id = encodeURIComponent(projectId);
196
+ const data = await client.get(`/projects/${id}/goals`);
197
+ if (!data.goals.length) {
198
+ return { content: [{ type: "text", text: "No goals configured for this project." }] };
199
+ }
200
+ const lines = data.goals.flatMap((g) => [
201
+ `${g.goalName}: ${g.hits} hits, ${g.uniqueSessions} unique sessions, ${(g.pct * 100).toFixed(1)}% conversion`,
202
+ ...g.variants.map((v) => ` ${v.componentId}/${v.variantId}: ${(v.completionRate * 100).toFixed(1)}% per assigned session`),
203
+ ""
204
+ ]);
205
+ return { content: [{ type: "text", text: lines.join("\n").trim() }] };
206
+ }
207
+ );
208
+ }
209
+
210
+ // src/tools/guardrails.ts
211
+ import { z as z6 } from "zod";
212
+ var projectIdSchema6 = z6.string().uuid().describe("The project UUID");
213
+ function registerGuardrailTools(server, client) {
214
+ server.tool(
215
+ "list_guardrail_events",
216
+ "List variants currently paused by the guardrail in the last 24 hours.",
217
+ { projectId: projectIdSchema6 },
218
+ async ({ projectId }) => {
219
+ const id = encodeURIComponent(projectId);
220
+ const data = await client.get(`/projects/${id}/guardrail-events`);
221
+ if (!data.guardrailEvents.length) {
222
+ return { content: [{ type: "text", text: "No active guardrail events in the last 24 hours." }] };
223
+ }
224
+ const lines = data.guardrailEvents.map(
225
+ (e) => `- ${e.componentId}: variants [${e.variantIds.join(", ")}] paused${e.pausedAt ? ` at ${e.pausedAt}` : ""}`
226
+ );
227
+ return { content: [{ type: "text", text: lines.join("\n") }] };
228
+ }
229
+ );
230
+ }
231
+
232
+ // src/tools/layout.ts
233
+ import { z as z7 } from "zod";
234
+ var projectIdSchema7 = z7.string().uuid().describe("The project UUID");
235
+ function registerLayoutTools(server, client) {
236
+ server.tool(
237
+ "get_layout_stats",
238
+ "Get per-persona section layout rankings and bandit reward weights.",
239
+ { projectId: projectIdSchema7 },
240
+ async ({ projectId }) => {
241
+ const id = encodeURIComponent(projectId);
242
+ const stats = await client.get(`/projects/${id}/layout-stats`);
243
+ if (!stats.length) {
244
+ return { content: [{ type: "text", text: "No layout data yet. More visitor sessions are needed." }] };
245
+ }
246
+ const text = stats.map(
247
+ (s) => `- ${s.persona}: [${s.layoutOrder.join(" \u2192 ")}] (avg reward: ${s.avgReward.toFixed(2)}, ${s.pulls} pulls)`
248
+ ).join("\n");
249
+ return { content: [{ type: "text", text }] };
250
+ }
251
+ );
252
+ }
253
+
254
+ // src/tools/variants.ts
255
+ import { z as z8 } from "zod";
256
+ var projectIdSchema8 = z8.string().uuid().describe("The project UUID");
257
+ function registerVariantWriteTools(server, client) {
258
+ server.tool(
259
+ "create_variant",
260
+ "Create a new draft variant for a component. Requires Starter or Growth plan.",
261
+ {
262
+ projectId: projectIdSchema8,
263
+ componentId: z8.string().describe("The component ID to add a variant to"),
264
+ displayName: z8.string().describe("Human-readable name for the new variant")
265
+ },
266
+ async ({ projectId, componentId, displayName }) => {
267
+ const id = encodeURIComponent(projectId);
268
+ const result = await client.post(
269
+ `/projects/${id}/variants`,
270
+ { componentId, displayName }
271
+ );
272
+ return {
273
+ content: [{
274
+ type: "text",
275
+ text: `Variant created: ${result.variantId} ("${result.displayName}") for component ${componentId}. It is in draft state \u2014 activate it from the dashboard.`
276
+ }]
277
+ };
278
+ }
279
+ );
280
+ server.tool(
281
+ "pause_variant",
282
+ "Pause a variant, stopping traffic from being assigned to it.",
283
+ {
284
+ projectId: projectIdSchema8,
285
+ componentId: z8.string().describe("The component ID"),
286
+ variantId: z8.string().describe("The variant ID to pause")
287
+ },
288
+ async ({ projectId, componentId, variantId }) => {
289
+ const id = encodeURIComponent(projectId);
290
+ await client.post(`/projects/${id}/variants/pause`, { componentId, variantId });
291
+ return {
292
+ content: [{
293
+ type: "text",
294
+ text: `Variant ${variantId} in component ${componentId} has been paused. No new traffic will be assigned to it.`
295
+ }]
296
+ };
297
+ }
298
+ );
299
+ server.tool(
300
+ "refresh_insights",
301
+ "Trigger fresh AI insight generation for a project. Returns immediately; use get_insights in ~15 seconds to see results.",
302
+ { projectId: projectIdSchema8 },
303
+ async ({ projectId }) => {
304
+ const id = encodeURIComponent(projectId);
305
+ await client.post(`/projects/${id}/insights/refresh`);
306
+ return {
307
+ content: [{
308
+ type: "text",
309
+ text: `Insights are generating for project ${projectId}. Call get_insights in ~15 seconds to see the results.`
310
+ }]
311
+ };
312
+ }
313
+ );
314
+ }
315
+
316
+ // src/server.ts
317
+ function createMcpServer(client) {
318
+ const server = new McpServer({
319
+ name: "@sentientui/mcp",
320
+ version: "0.1.0"
321
+ });
322
+ registerProjectTools(server, client);
323
+ registerComponentTools(server, client);
324
+ registerInsightTools(server, client);
325
+ registerPersonaTools(server, client);
326
+ registerGoalTools(server, client);
327
+ registerGuardrailTools(server, client);
328
+ registerLayoutTools(server, client);
329
+ registerVariantWriteTools(server, client);
330
+ return server;
331
+ }
332
+
333
+ // src/demo.ts
334
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
335
+ import { join } from "path";
336
+ import { homedir } from "os";
337
+ var CONFIG_DIR = join(homedir(), ".config", "sentientui");
338
+ var CONFIG_FILE = join(CONFIG_DIR, "mcp-anon.json");
339
+ var API_BASE = process.env.SENTIENTUI_API_URL ?? "https://api.sentient-ui.com";
340
+ function readCachedToken() {
341
+ try {
342
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
347
+ function writeCachedToken(cfg) {
348
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
349
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { encoding: "utf-8", mode: 384 });
350
+ }
351
+ async function resolveDemoToken() {
352
+ const cached = readCachedToken();
353
+ if (cached?.token) {
354
+ process.stderr.write(
355
+ `[sentientui-mcp] Running in demo mode (${CONFIG_FILE}). Set SENTIENTUI_API_KEY for full access.
356
+ `
357
+ );
358
+ return cached.token;
359
+ }
360
+ process.stderr.write("[sentientui-mcp] No API key found. Provisioning anonymous demo token...\n");
361
+ const res = await fetch(`${API_BASE}/v1/mcp/demo`, { method: "POST" });
362
+ if (!res.ok) {
363
+ const body = await res.json().catch(() => ({}));
364
+ throw new Error(`Demo provisioning failed: ${String(body.error ?? res.statusText)}`);
365
+ }
366
+ const data = await res.json();
367
+ writeCachedToken({ token: data.token, projectId: data.projectId });
368
+ process.stderr.write(
369
+ `[sentientui-mcp] Demo token provisioned (${data.callsRemaining} calls/month). Set SENTIENTUI_API_KEY to remove this limit.
370
+ `
371
+ );
372
+ return data.token;
373
+ }
374
+
375
+ // src/index.ts
376
+ async function main() {
377
+ let apiKey = process.env.SENTIENTUI_API_KEY;
378
+ if (!apiKey) {
379
+ apiKey = await resolveDemoToken();
380
+ }
381
+ const client = new ApiClient({ apiKey });
382
+ const server = createMcpServer(client);
383
+ const transport = new StdioServerTransport();
384
+ await server.connect(transport);
385
+ }
386
+ main().catch((err) => {
387
+ process.stderr.write(`[sentientui-mcp] fatal: ${String(err)}
388
+ `);
389
+ process.exit(1);
390
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@sentientui/mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for SentientUI — exposes project data and actions to AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "sentientui-mcp": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.mjs",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc --noEmit",
27
+ "dev": "tsx src/index.ts",
28
+ "prepublishOnly": "pnpm build && pnpm typecheck"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "zod": "^3.23.8"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.10.2",
39
+ "tsup": "^8.3.5",
40
+ "tsx": "^4.19.2",
41
+ "typescript": "^5.7.2",
42
+ "vitest": "^2.1.8"
43
+ }
44
+ }