@firstdistro/mcp 1.2.3 → 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.
- package/README.md +61 -7
- package/dist/server.js +575 -52
- package/package.json +3 -4
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
|
-
| `
|
|
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
|
|
89
|
-
| React + Vite | Full
|
|
90
|
-
|
|
|
91
|
-
|
|
|
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`
|
|
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:
|
|
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
|
|
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
|
|
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/
|
|
323
|
-
function
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
};
|
|
331
|
-
|
|
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.
|
|
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(
|
|
954
|
-
'Target framework.
|
|
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
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstdistro/mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server
|
|
3
|
+
"version": "1.3.0",
|
|
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": {
|
|
7
7
|
"firstdistro-mcp": "./bin/firstdistro-mcp.js"
|
|
@@ -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 <
|
|
59
|
+
"author": "FirstDistro <hello@firstdistro.com>"
|
|
61
60
|
}
|