@firstdistro/mcp 1.2.4 → 1.3.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.
Files changed (3) hide show
  1. package/README.md +61 -7
  2. package/dist/server.js +575 -52
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -49,19 +49,48 @@ That's it! Try asking:
49
49
  - "Show me my FirstDistro experiences"
50
50
  - "Who's stuck in onboarding?"
51
51
  - "What's Acme Corp's health score?"
52
+ - "Which accounts need attention right now?"
53
+ - "Search for Acme Corp"
52
54
 
53
55
  ## Available Tools
54
56
 
57
+ 16 read-only tools (v1.3.0). All require a server API key (`sk_live_...` / `sk_test_...`).
58
+
59
+ ### SDK & setup
60
+
55
61
  | Tool | Description |
56
62
  |------|-------------|
57
63
  | `get_sdk_config` | Get your installation token and SDK setup snippets |
58
64
  | `setup_sdk` | Generate files to set up FirstDistro SDK in your project |
65
+ | `check_events_flowing` | Verify SDK is sending events |
66
+
67
+ ### Experiences
68
+
69
+ | Tool | Description |
70
+ |------|-------------|
59
71
  | `list_experiences` | List all configured user journeys |
72
+ | `create_experience` | Create a new experience to track a customer journey |
60
73
  | `get_experience_stats` | Get funnel metrics for an experience |
61
74
  | `get_stuck_customers` | Find customers stuck in a journey |
75
+
76
+ ### Customer success
77
+
78
+ | Tool | Description |
79
+ |------|-------------|
62
80
  | `get_customer_health` | Get health score for an account |
63
81
  | `list_at_risk_accounts` | List critical and at-risk customers |
64
- | `check_events_flowing` | Verify SDK is sending events |
82
+ | `get_priorities` | List accounts that need attention, sorted by urgency |
83
+ | `list_upcoming_renewals` | Summarize the renewal pipeline by bucket |
84
+ | `get_portfolio_pulse` | Snapshot of healthy/at-risk/critical account counts |
85
+
86
+ ### Account intelligence & CRM
87
+
88
+ | Tool | Description |
89
+ |------|-------------|
90
+ | `get_churn_risk` | Get the deterministic churn-risk baseline for an account |
91
+ | `get_customer_contacts` | List the contacts recorded for a customer account |
92
+ | `get_integration_status` | Check which integrations (CRM, Slack, SDK) are connected |
93
+ | `search_customers` | Search customer accounts and users by name or email |
65
94
 
66
95
  ## SDK Setup with AI
67
96
 
@@ -85,14 +114,15 @@ to verify the setup.
85
114
 
86
115
  | Framework | Status |
87
116
  |-----------|--------|
88
- | Next.js (App Router) | Full support |
89
- | React + Vite | Full support |
90
- | Next.js (Pages Router) | Coming soon |
91
- | Create React App | Coming soon |
117
+ | Next.js (App Router) | Full scaffold via `setup_sdk` |
118
+ | React + Vite | Full scaffold via `setup_sdk` |
119
+ | Vanilla JavaScript (script tag) | Full scaffold via `setup_sdk` — no npm required |
120
+ | Next.js (Pages Router) | Manual README via `setup_sdk` — prefer App Router or script tag |
121
+ | Create React App | Manual README via `setup_sdk` — prefer Vite or script tag |
92
122
 
93
123
  ### Auth Integrations
94
124
 
95
- The `setup_sdk` tool can generate user identification code for:
125
+ The `setup_sdk` `authPattern` option applies to **React scaffolds** (`nextjs-app`, `react-vite`) only. Vanilla uses a generic script-tag `setup()` snippet regardless of auth library.
96
126
 
97
127
  - **NextAuth.js** — Uses `useSession` hook
98
128
  - **Clerk** — Uses `useUser` hook
@@ -209,10 +239,34 @@ node bin/firstdistro-mcp.js
209
239
  node bin/firstdistro-mcp.js init
210
240
  ```
211
241
 
242
+ ## Changelog
243
+
244
+ ### 1.3.0 (2026-06-06)
245
+
246
+ **Customer success & CRM tools (7 new)**
247
+
248
+ - `get_priorities` — "Needs Your Attention" queue
249
+ - `list_upcoming_renewals` — renewal pipeline buckets
250
+ - `get_portfolio_pulse` — portfolio health snapshot
251
+ - `get_churn_risk` — deterministic churn baseline per account
252
+ - `get_customer_contacts` — account contacts list
253
+ - `get_integration_status` — CRM / Slack / SDK connection status
254
+ - `search_customers` — search accounts and users by name or email
255
+
256
+ **`setup_sdk` framework router**
257
+
258
+ - Full scaffold for **vanilla** (script tag + `setup()`, no npm)
259
+ - Honest manual README for **Next.js Pages Router** and **Create React App** (no more "coming soon")
260
+ - `authPattern` documented as React-scaffold-only; vanilla ignores auth-specific branches
261
+
262
+ ### 1.2.4
263
+
264
+ - Nine core tools: experiences, health, at-risk, SDK setup, event verification
265
+
212
266
  ## Support
213
267
 
214
268
  - Documentation: https://firstdistro.com/documentation
215
- - Email: support@firstdistro.com
269
+ - Email: hello@firstdistro.com
216
270
 
217
271
  ## License
218
272
 
package/dist/server.js CHANGED
@@ -45,6 +45,94 @@ function loadConfig() {
45
45
  }
46
46
  }
47
47
 
48
+ // src/templates/frameworks.ts
49
+ var SCAFFOLD_FRAMEWORKS = ["nextjs-app", "react-vite", "vanilla"];
50
+ var MANUAL_FRAMEWORKS = ["nextjs-pages", "react-cra"];
51
+ var SETUP_SDK_FRAMEWORKS = [
52
+ ...SCAFFOLD_FRAMEWORKS,
53
+ ...MANUAL_FRAMEWORKS
54
+ ];
55
+ function isManualFramework(framework) {
56
+ return MANUAL_FRAMEWORKS.includes(framework);
57
+ }
58
+ function frameworkUsesNpmInstall(framework) {
59
+ return framework === "nextjs-app" || framework === "react-vite";
60
+ }
61
+ function getFrameworkDisplayName(framework) {
62
+ const names = {
63
+ "nextjs-app": "Next.js (App Router)",
64
+ "nextjs-pages": "Next.js (Pages Router)",
65
+ "react-vite": "React + Vite",
66
+ "react-cra": "Create React App",
67
+ "vanilla": "Vanilla JavaScript"
68
+ };
69
+ return names[framework] || framework;
70
+ }
71
+
72
+ // src/templates/install-snippet.ts
73
+ var INSTALL_SCRIPT_BASE = "https://firstdistro.com/sdk/install";
74
+ function buildInstallScriptTag(installationToken) {
75
+ return `<script src="${INSTALL_SCRIPT_BASE}/${installationToken}.js"></script>`;
76
+ }
77
+
78
+ // src/templates/manual-readme.ts
79
+ function generateManualSetupReadme(framework, installationToken) {
80
+ const scriptTag = buildInstallScriptTag(installationToken);
81
+ const frameworkNote = framework === "nextjs-pages" ? "We do not auto-scaffold the Pages Router. Prefer **Next.js App Router** (`setup_sdk` with `nextjs-app`) for file generation, or follow the manual steps below." : "Create React App is legacy/deprecated. Prefer **React + Vite** (`react-vite`) or **vanilla** (script tag) for new installs.";
82
+ return [
83
+ {
84
+ path: "README-FIRSTDISTRO.md",
85
+ action: "create",
86
+ content: `# FirstDistro SDK Setup \u2014 ${getFrameworkDisplayName(framework)}
87
+
88
+ ${frameworkNote}
89
+
90
+ ## Option A \u2014 Script tag (any HTML app)
91
+
92
+ Add before \`</body>\`:
93
+
94
+ \`\`\`html
95
+ ${scriptTag}
96
+ <script>
97
+ FirstDistro.setup({
98
+ user: { id: 'user-id', email: 'user@company.com', name: 'Display Name' },
99
+ });
100
+ </script>
101
+ \`\`\`
102
+
103
+ Page views (\`$pageview\`) are captured automatically after \`setup()\` \u2014 no \`track()\` for navigation.
104
+
105
+ ## Option B \u2014 npm + React provider
106
+
107
+ 1. \`npm install @firstdistro/sdk\`
108
+ 2. Wrap your app:
109
+
110
+ \`\`\`tsx
111
+ import { FirstDistroProvider } from '@firstdistro/sdk/react'
112
+
113
+ <FirstDistroProvider token="${installationToken}">
114
+ {children}
115
+ </FirstDistroProvider>
116
+ \`\`\`
117
+
118
+ 3. After login:
119
+
120
+ \`\`\`tsx
121
+ import { useFirstDistroSetup } from '@firstdistro/sdk/react'
122
+
123
+ useFirstDistroSetup({
124
+ userId: user.id,
125
+ userEmail: user.email,
126
+ })
127
+ \`\`\`
128
+
129
+ Verify with the MCP \`check_events_flowing\` tool after you log in locally.
130
+ `,
131
+ description: `Manual setup path for ${getFrameworkDisplayName(framework)}`
132
+ }
133
+ ];
134
+ }
135
+
48
136
  // src/templates/nextjs-app.ts
49
137
  function generateProvidersFile(installationToken) {
50
138
  return `'use client'
@@ -147,9 +235,8 @@ export function FirstDistroSetup() {
147
235
  userId: user.id,
148
236
  userEmail: user.email,
149
237
  userName: user.name,
150
- // Optional: Add account info for B2B
238
+ // Optional override for multi-domain orgs:
151
239
  // accountId: user.organizationId,
152
- // accountName: user.organizationName,
153
240
  } : null)
154
241
 
155
242
  return null
@@ -270,7 +357,7 @@ export function FirstDistroSetup() {
270
357
  userId: user.id,
271
358
  userEmail: user.email,
272
359
  userName: user.name,
273
- // Optional: Add account info for B2B
360
+ // Optional override for multi-domain orgs:
274
361
  // accountId: user.organizationId,
275
362
  // accountName: user.organizationName,
276
363
  } : null)
@@ -319,69 +406,89 @@ function generateReactViteFiles(installationToken, options = {}) {
319
406
  return files;
320
407
  }
321
408
 
322
- // src/templates/index.ts
323
- function getFrameworkDisplayName(framework) {
324
- const names = {
325
- "nextjs-app": "Next.js (App Router)",
326
- "nextjs-pages": "Next.js (Pages Router)",
327
- "react-vite": "React + Vite",
328
- "react-cra": "Create React App",
329
- "vanilla": "Vanilla JavaScript"
330
- };
331
- return names[framework] || framework;
409
+ // src/templates/vanilla.ts
410
+ function buildSetupSnippet(options) {
411
+ const { includeUserSetup = true, authPattern = "none" } = options;
412
+ if (!includeUserSetup || authPattern === "none") {
413
+ return `<script>
414
+ // Call once when the logged-in user is known:
415
+ // FirstDistro.setup({
416
+ // user: { id: 'user-id', email: 'user@company.com', name: 'Display Name' },
417
+ // });
418
+ // Page views ($pageview) flow automatically after setup() \u2014 no track() for navigation.
419
+ </script>`;
420
+ }
421
+ if (authPattern === "custom") {
422
+ return `<script>
423
+ // CUSTOMIZE: run after your app knows the logged-in user
424
+ function identifyUserForFirstDistro(user) {
425
+ if (!user?.id || !user?.email) return;
426
+ FirstDistro.setup({
427
+ user: { id: user.id, email: user.email, name: user.name },
428
+ // account: { id, name, plan } // optional override for multi-domain orgs
429
+ });
430
+ }
431
+ </script>`;
432
+ }
433
+ return `<script>
434
+ // CUSTOMIZE: call once after your app knows the logged-in user \u2014 email is required
435
+ FirstDistro.setup({
436
+ user: { id: 'user-id', email: 'user@company.com', name: 'Display Name' },
437
+ });
438
+ // Page views ($pageview) flow automatically after setup() \u2014 no track() for navigation.
439
+ </script>`;
332
440
  }
441
+ function generateVanillaFiles(installationToken, options = {}) {
442
+ const scriptTag = buildInstallScriptTag(installationToken);
443
+ const setupSnippet = buildSetupSnippet(options);
444
+ const bodyInjection = `${scriptTag}
445
+ ${setupSnippet}`;
446
+ return [
447
+ {
448
+ path: "index.html",
449
+ action: "modify",
450
+ replace: {
451
+ old: "</body>",
452
+ new: `${bodyInjection}
453
+ </body>`
454
+ },
455
+ description: "Add the FirstDistro install script and setup snippet before the closing </body> tag (or the main HTML template in your CMS)"
456
+ },
457
+ {
458
+ path: "firstdistro-install-snippet.html",
459
+ action: "create",
460
+ content: `<!-- Paste before </body> on every page (or your site-wide template) -->
461
+ ${bodyInjection}
462
+ `,
463
+ description: "Copy-paste fragment if you cannot edit index.html directly (WordPress, Webflow, etc.)"
464
+ }
465
+ ];
466
+ }
467
+
468
+ // src/templates/index.ts
333
469
  function generateSetupFiles(framework, installationToken, options = {}) {
470
+ if (isManualFramework(framework)) {
471
+ return generateManualSetupReadme(framework, installationToken);
472
+ }
334
473
  switch (framework) {
335
474
  case "nextjs-app":
336
475
  return generateNextjsAppFiles(installationToken, options);
337
476
  case "react-vite":
338
477
  return generateReactViteFiles(installationToken, options);
339
- case "nextjs-pages":
340
- case "react-cra":
341
478
  case "vanilla":
342
- return [{
343
- path: "README-FIRSTDISTRO.md",
344
- action: "create",
345
- content: `# FirstDistro SDK Setup
346
-
347
- Framework "${getFrameworkDisplayName(framework)}" template is coming soon.
348
-
349
- For now, please follow the manual setup instructions:
350
-
351
- 1. Install the SDK:
352
- npm install @firstdistro/sdk
353
-
354
- 2. Add the provider to your app:
355
- import { FirstDistroProvider } from '@firstdistro/sdk/react'
356
-
357
- <FirstDistroProvider token="${installationToken}">
358
- {/* your app */}
359
- </FirstDistroProvider>
360
-
361
- 3. Identify users after login:
362
- import { useFirstDistroSetup } from '@firstdistro/sdk/react'
363
-
364
- useFirstDistroSetup({
365
- userId: user.id,
366
- userEmail: user.email,
367
- })
368
-
369
- 4. Verify setup by asking: "Are events flowing?"
370
- `,
371
- description: "Manual setup instructions for unsupported framework"
372
- }];
479
+ return generateVanillaFiles(installationToken, options);
373
480
  default:
374
481
  throw new Error(`Unsupported framework: ${framework}`);
375
482
  }
376
483
  }
377
484
  function generateSetupResult(framework, installationToken, options = {}) {
378
485
  const files = generateSetupFiles(framework, installationToken, options);
379
- const commands = [
486
+ const commands = frameworkUsesNpmInstall(framework) ? [
380
487
  {
381
488
  run: "npm install @firstdistro/sdk",
382
489
  description: "Install the FirstDistro SDK"
383
490
  }
384
- ];
491
+ ] : [];
385
492
  return {
386
493
  installationToken,
387
494
  framework,
@@ -401,7 +508,7 @@ function generateSetupResult(framework, installationToken, options = {}) {
401
508
  }
402
509
 
403
510
  // src/server.ts
404
- var MCP_VERSION = "1.1.0";
511
+ var MCP_VERSION = "1.3.0";
405
512
  function detectMcpClient() {
406
513
  if (process.env.CLAUDE_CODE || process.env.CLAUDE_PROJECT_ROOT) {
407
514
  return "claude-code";
@@ -950,11 +1057,11 @@ After setup, use 'check_events_flowing' to verify events are being received.`
950
1057
  "setup_sdk",
951
1058
  "Generate all files and commands needed to set up the FirstDistro SDK in your project. Returns framework-specific setup files, install commands, and verification steps.",
952
1059
  {
953
- framework: z.enum(["nextjs-app", "nextjs-pages", "react-vite", "react-cra", "vanilla"]).describe(
954
- 'Target framework. Use "nextjs-app" for Next.js 13+ with app directory, "react-vite" for React + Vite projects.'
1060
+ framework: z.enum(SETUP_SDK_FRAMEWORKS).describe(
1061
+ 'Target framework. Full scaffold: "nextjs-app" (App Router), "react-vite", "vanilla" (script tag, no npm). Manual README only: "nextjs-pages", "react-cra".'
955
1062
  ),
956
1063
  includeUserSetup: z.boolean().optional().describe("Include user identification code for tracking logged-in users (default: true)"),
957
- authPattern: z.enum(["nextauth", "clerk", "supabase", "custom", "none"]).optional().describe("Auth library in use. Generates appropriate user setup code for the auth pattern.")
1064
+ authPattern: z.enum(["nextauth", "clerk", "supabase", "custom", "none"]).optional().describe("Auth library in use. Generates auth-specific user setup code for React scaffolds (nextjs-app, react-vite); vanilla uses a generic script-tag setup regardless.")
958
1065
  },
959
1066
  async ({ framework, includeUserSetup, authPattern }) => {
960
1067
  try {
@@ -1148,6 +1255,415 @@ Use \`check_events_flowing\` after deploying to verify events are arriving.`
1148
1255
  }
1149
1256
  }
1150
1257
  );
1258
+ server.tool(
1259
+ "get_priorities",
1260
+ 'List the customer accounts that need attention right now, sorted by urgency (the "Needs Your Attention" queue). Surfaces at-risk health, stalled outreach, champion risk, and expansion opportunities.',
1261
+ {
1262
+ segment: z.enum(["save", "grow"]).optional().describe('Filter by segment: "save" (retention/risk) or "grow" (expansion). Default: all.'),
1263
+ limit: z.number().optional().describe("Maximum number of priorities to return (default: 10, max: 50)")
1264
+ },
1265
+ async ({ segment, limit }) => {
1266
+ try {
1267
+ const params = new URLSearchParams();
1268
+ if (segment) params.set("segment", segment);
1269
+ if (limit) params.set("limit", String(limit));
1270
+ const url = `${config.baseUrl}/api/vendor/priorities${params.toString() ? "?" + params.toString() : ""}`;
1271
+ const response = await fetch(url, {
1272
+ headers: getApiHeaders(config.apiKey)
1273
+ });
1274
+ if (!response.ok) {
1275
+ return {
1276
+ content: [
1277
+ {
1278
+ type: "text",
1279
+ text: formatHttpError(response.status, response.statusText, "Priorities")
1280
+ }
1281
+ ],
1282
+ isError: true
1283
+ };
1284
+ }
1285
+ const rawData = await response.json();
1286
+ const data = unwrapApiResponse(rawData);
1287
+ const priorities = data.priorities || [];
1288
+ if (priorities.length === 0) {
1289
+ return {
1290
+ content: [
1291
+ {
1292
+ type: "text",
1293
+ text: "No priorities right now. Nothing needs your attention."
1294
+ }
1295
+ ]
1296
+ };
1297
+ }
1298
+ const list = priorities.map(
1299
+ (p) => `- ${p.account_name || p.account_id} [${p.priority_type}] ${p.reason_detail || p.reason}${typeof p.health_score === "number" ? ` (health ${p.health_score}/100)` : ""}`
1300
+ ).join("\n");
1301
+ return {
1302
+ content: [
1303
+ {
1304
+ type: "text",
1305
+ text: `Priorities (${data.total ?? priorities.length} total, showing ${priorities.length}):
1306
+
1307
+ ${list}`
1308
+ }
1309
+ ]
1310
+ };
1311
+ } catch (error) {
1312
+ return {
1313
+ content: [
1314
+ {
1315
+ type: "text",
1316
+ text: formatNetworkError(error)
1317
+ }
1318
+ ],
1319
+ isError: true
1320
+ };
1321
+ }
1322
+ }
1323
+ );
1324
+ server.tool(
1325
+ "list_upcoming_renewals",
1326
+ "Summarize the renewal pipeline: ARR and account counts for overdue renewals, the next 30 days, the rest of this quarter, and accounts missing renewal data.",
1327
+ async () => {
1328
+ try {
1329
+ const response = await fetch(`${config.baseUrl}/api/vendor/renewals`, {
1330
+ headers: getApiHeaders(config.apiKey)
1331
+ });
1332
+ if (!response.ok) {
1333
+ return {
1334
+ content: [
1335
+ {
1336
+ type: "text",
1337
+ text: formatHttpError(response.status, response.statusText, "Renewals")
1338
+ }
1339
+ ],
1340
+ isError: true
1341
+ };
1342
+ }
1343
+ const rawData = await response.json();
1344
+ const data = unwrapApiResponse(rawData);
1345
+ const buckets = data.buckets || {};
1346
+ const totals = data.totals || {};
1347
+ const currency = data.currency || "USD";
1348
+ const fmt = (bucket) => `${bucket?.count ?? 0} account(s), ${currency} ${Math.round(bucket?.arr ?? 0).toLocaleString()}`;
1349
+ return {
1350
+ content: [
1351
+ {
1352
+ type: "text",
1353
+ text: `Renewal Pipeline (${currency}):
1354
+ - Overdue: ${fmt(buckets.overdue)}
1355
+ - Next 30 days: ${fmt(buckets.next30Days)}
1356
+ - This quarter: ${fmt(buckets.thisQuarter)}
1357
+ - Needs data: ${fmt(buckets.needsData)}
1358
+
1359
+ Pending ARR: ${currency} ${Math.round(totals.pendingArr ?? 0).toLocaleString()} (${totals.pendingCount ?? 0} accounts)
1360
+ Renewed ARR: ${currency} ${Math.round(totals.renewedArr ?? 0).toLocaleString()} (${totals.renewedCount ?? 0} accounts)`
1361
+ }
1362
+ ]
1363
+ };
1364
+ } catch (error) {
1365
+ return {
1366
+ content: [
1367
+ {
1368
+ type: "text",
1369
+ text: formatNetworkError(error)
1370
+ }
1371
+ ],
1372
+ isError: true
1373
+ };
1374
+ }
1375
+ }
1376
+ );
1377
+ server.tool(
1378
+ "get_portfolio_pulse",
1379
+ "Get a snapshot of overall customer portfolio health: how many accounts are healthy, at-risk, and critical right now.",
1380
+ async () => {
1381
+ try {
1382
+ const response = await fetch(`${config.baseUrl}/api/vendor/pulse`, {
1383
+ headers: getApiHeaders(config.apiKey)
1384
+ });
1385
+ if (!response.ok) {
1386
+ return {
1387
+ content: [
1388
+ {
1389
+ type: "text",
1390
+ text: formatHttpError(response.status, response.statusText, "Portfolio pulse")
1391
+ }
1392
+ ],
1393
+ isError: true
1394
+ };
1395
+ }
1396
+ const rawData = await response.json();
1397
+ const data = unwrapApiResponse(rawData);
1398
+ if (!data.hasData) {
1399
+ return {
1400
+ content: [
1401
+ {
1402
+ type: "text",
1403
+ text: "No portfolio data yet. Health scores are calculated during the hourly health pass once events are flowing."
1404
+ }
1405
+ ]
1406
+ };
1407
+ }
1408
+ return {
1409
+ content: [
1410
+ {
1411
+ type: "text",
1412
+ text: `Portfolio Pulse (as of ${data.date}):
1413
+ - Healthy: ${data.healthy ?? 0}
1414
+ - At-Risk: ${data.atRisk ?? 0}
1415
+ - Critical: ${data.critical ?? 0}
1416
+ - Total scored accounts: ${data.total ?? 0}`
1417
+ }
1418
+ ]
1419
+ };
1420
+ } catch (error) {
1421
+ return {
1422
+ content: [
1423
+ {
1424
+ type: "text",
1425
+ text: formatNetworkError(error)
1426
+ }
1427
+ ],
1428
+ isError: true
1429
+ };
1430
+ }
1431
+ }
1432
+ );
1433
+ server.tool(
1434
+ "get_churn_risk",
1435
+ "Get the deterministic churn-risk baseline for a specific customer account: a 0-100 risk score, risk band, confidence, and the top contributing factors across product, CRM, and billing signals.",
1436
+ {
1437
+ accountId: z.string().describe("The account ID to look up")
1438
+ },
1439
+ async ({ accountId }) => {
1440
+ try {
1441
+ const response = await fetch(
1442
+ `${config.baseUrl}/api/vendor/customers/${accountId}/churn`,
1443
+ {
1444
+ headers: getApiHeaders(config.apiKey)
1445
+ }
1446
+ );
1447
+ if (!response.ok) {
1448
+ return {
1449
+ content: [
1450
+ {
1451
+ type: "text",
1452
+ text: formatHttpError(response.status, response.statusText, `Customer "${accountId}"`)
1453
+ }
1454
+ ],
1455
+ isError: true
1456
+ };
1457
+ }
1458
+ const rawData = await response.json();
1459
+ const data = unwrapApiResponse(rawData);
1460
+ const factors = data.topFactors || [];
1461
+ const factorList = factors.length > 0 ? factors.map((f) => `- ${f.label} (${f.source}, impact ${f.impact}): ${f.reason}`).join("\n") : "- No significant risk factors detected.";
1462
+ return {
1463
+ content: [
1464
+ {
1465
+ type: "text",
1466
+ text: `Churn Risk for "${accountId}":
1467
+ Score: ${data.score ?? "N/A"}/100 (${data.riskBand || "unknown"})
1468
+ Confidence: ${typeof data.confidence === "number" ? `${Math.round(data.confidence * 100)}%` : "N/A"}
1469
+ Source mode: ${data.sourceMode || "unknown"}
1470
+
1471
+ Top factors:
1472
+ ${factorList}`
1473
+ }
1474
+ ]
1475
+ };
1476
+ } catch (error) {
1477
+ return {
1478
+ content: [
1479
+ {
1480
+ type: "text",
1481
+ text: formatNetworkError(error)
1482
+ }
1483
+ ],
1484
+ isError: true
1485
+ };
1486
+ }
1487
+ }
1488
+ );
1489
+ server.tool(
1490
+ "get_customer_contacts",
1491
+ "List the known contacts (people) recorded for a specific customer account, including their role, job title, and email.",
1492
+ {
1493
+ accountId: z.string().describe("The account ID to look up")
1494
+ },
1495
+ async ({ accountId }) => {
1496
+ try {
1497
+ const response = await fetch(
1498
+ `${config.baseUrl}/api/vendor/customers/${accountId}/contacts`,
1499
+ {
1500
+ headers: getApiHeaders(config.apiKey)
1501
+ }
1502
+ );
1503
+ if (!response.ok) {
1504
+ return {
1505
+ content: [
1506
+ {
1507
+ type: "text",
1508
+ text: formatHttpError(response.status, response.statusText, `Customer "${accountId}"`)
1509
+ }
1510
+ ],
1511
+ isError: true
1512
+ };
1513
+ }
1514
+ const rawData = await response.json();
1515
+ const data = unwrapApiResponse(rawData);
1516
+ const contacts = data.contacts || [];
1517
+ if (contacts.length === 0) {
1518
+ return {
1519
+ content: [
1520
+ {
1521
+ type: "text",
1522
+ text: `No contacts recorded for "${accountId}" yet.`
1523
+ }
1524
+ ]
1525
+ };
1526
+ }
1527
+ const contactList = contacts.map(
1528
+ (c) => `- ${c.display_name || c.email || "Unknown"}${c.job_title ? ` (${c.job_title})` : ""}${c.primary_role ? ` [${c.primary_role}]` : ""}${c.email ? ` \u2014 ${c.email}` : ""}`
1529
+ ).join("\n");
1530
+ return {
1531
+ content: [
1532
+ {
1533
+ type: "text",
1534
+ text: `Contacts for "${accountId}" (${contacts.length}):
1535
+
1536
+ ${contactList}`
1537
+ }
1538
+ ]
1539
+ };
1540
+ } catch (error) {
1541
+ return {
1542
+ content: [
1543
+ {
1544
+ type: "text",
1545
+ text: formatNetworkError(error)
1546
+ }
1547
+ ],
1548
+ isError: true
1549
+ };
1550
+ }
1551
+ }
1552
+ );
1553
+ server.tool(
1554
+ "get_integration_status",
1555
+ "Check which integrations are connected for your account: CRM provider(s), Slack notifications, and whether the SDK is receiving events.",
1556
+ async () => {
1557
+ try {
1558
+ const response = await fetch(`${config.baseUrl}/api/vendor/integrations`, {
1559
+ headers: getApiHeaders(config.apiKey)
1560
+ });
1561
+ if (!response.ok) {
1562
+ return {
1563
+ content: [
1564
+ {
1565
+ type: "text",
1566
+ text: formatHttpError(response.status, response.statusText, "Integration status")
1567
+ }
1568
+ ],
1569
+ isError: true
1570
+ };
1571
+ }
1572
+ const rawData = await response.json();
1573
+ const data = unwrapApiResponse(rawData);
1574
+ const crm = data.crm || {};
1575
+ const crmList = Object.keys(crm).map((provider) => `- ${provider}: ${crm[provider]?.connected ? "connected" : "not connected"}`).join("\n");
1576
+ return {
1577
+ content: [
1578
+ {
1579
+ type: "text",
1580
+ text: `Integration Status:
1581
+
1582
+ CRM (active: ${data.activeCrm || "none"}):
1583
+ ${crmList || "- No CRM providers available"}
1584
+
1585
+ Slack: ${data.slack?.connected ? "connected" : "not connected"}
1586
+ SDK events received: ${data.sdk?.eventsReceived ? "yes" : "no"}`
1587
+ }
1588
+ ]
1589
+ };
1590
+ } catch (error) {
1591
+ return {
1592
+ content: [
1593
+ {
1594
+ type: "text",
1595
+ text: formatNetworkError(error)
1596
+ }
1597
+ ],
1598
+ isError: true
1599
+ };
1600
+ }
1601
+ }
1602
+ );
1603
+ server.tool(
1604
+ "search_customers",
1605
+ "Search your customer accounts and users by name or email. Useful for finding the account ID to pass to other tools.",
1606
+ {
1607
+ query: z.string().describe("Search text (minimum 2 characters)")
1608
+ },
1609
+ async ({ query }) => {
1610
+ try {
1611
+ const params = new URLSearchParams();
1612
+ params.set("q", query);
1613
+ const url = `${config.baseUrl}/api/vendor/search?${params.toString()}`;
1614
+ const response = await fetch(url, {
1615
+ headers: getApiHeaders(config.apiKey)
1616
+ });
1617
+ if (!response.ok) {
1618
+ return {
1619
+ content: [
1620
+ {
1621
+ type: "text",
1622
+ text: formatHttpError(response.status, response.statusText, "Customer search")
1623
+ }
1624
+ ],
1625
+ isError: true
1626
+ };
1627
+ }
1628
+ const rawData = await response.json();
1629
+ const data = unwrapApiResponse(rawData);
1630
+ const results = data.results || [];
1631
+ if (results.length === 0) {
1632
+ return {
1633
+ content: [
1634
+ {
1635
+ type: "text",
1636
+ text: `No matches for "${query}".`
1637
+ }
1638
+ ]
1639
+ };
1640
+ }
1641
+ const resultList = results.map(
1642
+ (r) => r.type === "account" ? `- [account] ${r.name || r.account_id} (id: ${r.account_id})` : `- [user] ${r.name || r.user_id}${r.email ? ` \u2014 ${r.email}` : ""} (account: ${r.account_id})`
1643
+ ).join("\n");
1644
+ return {
1645
+ content: [
1646
+ {
1647
+ type: "text",
1648
+ text: `Found ${results.length} result(s) for "${query}":
1649
+
1650
+ ${resultList}`
1651
+ }
1652
+ ]
1653
+ };
1654
+ } catch (error) {
1655
+ return {
1656
+ content: [
1657
+ {
1658
+ type: "text",
1659
+ text: formatNetworkError(error)
1660
+ }
1661
+ ],
1662
+ isError: true
1663
+ };
1664
+ }
1665
+ }
1666
+ );
1151
1667
  }
1152
1668
  function registerUnconfiguredTools(server, configError = null) {
1153
1669
  const message = configError ? `FirstDistro configuration error:
@@ -1176,6 +1692,13 @@ Then restart your IDE to use FirstDistro tools.`;
1176
1692
  { name: "get_experience_stats", description: "Get funnel statistics for a user journey" },
1177
1693
  { name: "get_stuck_customers", description: "Find customers who stopped progressing in a journey" },
1178
1694
  { name: "list_at_risk_accounts", description: "List at-risk customer accounts" },
1695
+ { name: "get_priorities", description: "List accounts that need attention, sorted by urgency" },
1696
+ { name: "list_upcoming_renewals", description: "Summarize the renewal pipeline by bucket" },
1697
+ { name: "get_portfolio_pulse", description: "Snapshot of healthy/at-risk/critical account counts" },
1698
+ { name: "get_churn_risk", description: "Get the churn-risk baseline for a customer account" },
1699
+ { name: "get_customer_contacts", description: "List the contacts recorded for a customer account" },
1700
+ { name: "get_integration_status", description: "Check which integrations (CRM, Slack, SDK) are connected" },
1701
+ { name: "search_customers", description: "Search customer accounts and users by name or email" },
1179
1702
  { name: "get_sdk_config", description: "Get your installation token and SDK setup code" },
1180
1703
  { name: "setup_sdk", description: "Generate files to set up FirstDistro SDK in your project" },
1181
1704
  { name: "create_experience", description: "Create a new experience to track customer journeys and find stuck users" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstdistro/mcp",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "description": "FirstDistro MCP server — manage customer health, churn risk, and journeys from Claude, Cursor, and other AI tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,6 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^20.0.0",
34
- "cac": "^6.7.14",
35
34
  "tsup": "^8.0.0",
36
35
  "typescript": "^5.0.0",
37
36
  "vitest": "^4.0.18"
@@ -57,5 +56,5 @@
57
56
  ],
58
57
  "license": "MIT",
59
58
  "homepage": "https://firstdistro.com/documentation",
60
- "author": "FirstDistro <support@firstdistro.com>"
59
+ "author": "FirstDistro <hello@firstdistro.com>"
61
60
  }