@firstdistro/mcp 1.0.0 → 1.1.1

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 +76 -21
  2. package/dist/server.js +641 -40
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,26 +1,33 @@
1
1
  # @firstdistro/mcp
2
2
 
3
- MCP server for [FirstDistro](https://firstdistro.com) — Query and manage customer health from AI assistants like Claude.
3
+ MCP server for [FirstDistro](https://firstdistro.com) — Query customer health and set up the SDK from AI assistants like Claude.
4
4
 
5
5
  ## Quick Start
6
6
 
7
- ### 1. Install and configure
7
+ ### 1. Create a FirstDistro Account
8
8
 
9
- **Interactive setup (recommended):**
9
+ Sign up at [firstdistro.com/auth/register](https://firstdistro.com/auth/register) and get your API key from **Settings → API Keys**.
10
+
11
+ ### 2. Configure the MCP Server
12
+
13
+ **Option A: Interactive setup (recommended)**
10
14
  ```bash
11
15
  npx @firstdistro/mcp init
12
16
  ```
13
17
 
14
- **Or with API key directly:**
18
+ **Option B: With API key directly**
15
19
  ```bash
16
20
  npx @firstdistro/mcp init --api-key sk_live_xxxxx
17
21
  ```
18
22
 
19
- Get your API key from [Settings → API Keys](https://firstdistro.com/dashboard/settings/sdk-configuration) in your FirstDistro dashboard.
23
+ **Option C: Claude Code CLI**
24
+ ```bash
25
+ claude mcp add firstdistro -e FIRSTDISTRO_API_KEY=sk_live_xxx -- npx -y @firstdistro/mcp
26
+ ```
20
27
 
21
28
  > **Note:** Use an **API Key** (`sk_live_...` or `sk_test_...`), not an Installation Token (`fd_...`). Installation Tokens are for the browser SDK only.
22
29
 
23
- ### 2. Add to Claude Code
30
+ ### 3. Add to Claude Code
24
31
 
25
32
  Add to your `~/.claude/settings.json`:
26
33
 
@@ -35,18 +42,20 @@ Add to your `~/.claude/settings.json`:
35
42
  }
36
43
  ```
37
44
 
38
- ### 3. Restart Claude Code
45
+ ### 4. Restart Claude Code
39
46
 
40
47
  That's it! Try asking:
48
+ - "Set up FirstDistro SDK in my Next.js app"
41
49
  - "Show me my FirstDistro experiences"
42
50
  - "Who's stuck in onboarding?"
43
51
  - "What's Acme Corp's health score?"
44
- - "List my at-risk customers"
45
52
 
46
53
  ## Available Tools
47
54
 
48
55
  | Tool | Description |
49
56
  |------|-------------|
57
+ | `get_sdk_config` | Get your installation token and SDK setup snippets |
58
+ | `setup_sdk` | Generate files to set up FirstDistro SDK in your project |
50
59
  | `list_experiences` | List all configured user journeys |
51
60
  | `get_experience_stats` | Get funnel metrics for an experience |
52
61
  | `get_stuck_customers` | Find customers stuck in a journey |
@@ -54,7 +63,50 @@ That's it! Try asking:
54
63
  | `list_at_risk_accounts` | List critical and at-risk customers |
55
64
  | `check_events_flowing` | Verify SDK is sending events |
56
65
 
57
- ### Example Usage
66
+ ## SDK Setup with AI
67
+
68
+ Ask your AI assistant to set up the FirstDistro SDK:
69
+
70
+ ```
71
+ User: "Add FirstDistro to my Next.js app to track user activity"
72
+
73
+ Claude: I'll set up FirstDistro in your project.
74
+
75
+ [Runs npm install @firstdistro/sdk]
76
+ [Creates app/providers.tsx with your installation token]
77
+ [Updates app/layout.tsx to use the provider]
78
+
79
+ Done! FirstDistro is now installed. Try refreshing your app
80
+ and interacting with it, then ask me "Are events flowing?"
81
+ to verify the setup.
82
+ ```
83
+
84
+ ### Supported Frameworks
85
+
86
+ | Framework | Status |
87
+ |-----------|--------|
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 |
92
+
93
+ ### Auth Integrations
94
+
95
+ The `setup_sdk` tool can generate user identification code for:
96
+
97
+ - **NextAuth.js** — Uses `useSession` hook
98
+ - **Clerk** — Uses `useUser` hook
99
+ - **Supabase Auth** — Uses `onAuthStateChange`
100
+ - **Custom** — Provides template with TODOs
101
+
102
+ ## Example Usage
103
+
104
+ **Set up the SDK:**
105
+ ```
106
+ User: "Set up FirstDistro in my React + Vite project with Clerk auth"
107
+
108
+ Claude: [Generates provider component, main.tsx updates, and Clerk user setup]
109
+ ```
58
110
 
59
111
  **Check customer health:**
60
112
  ```
@@ -82,18 +134,20 @@ Found 3 stuck customer(s):
82
134
  - DataFlow: mike@dataflow.com (stuck 18 min)
83
135
  ```
84
136
 
85
- **List at-risk accounts:**
137
+ **Verify events are flowing:**
86
138
  ```
87
- User: "Show me customers at risk of churning"
139
+ User: "Are events flowing from my app?"
88
140
 
89
- Claude: At-Risk Accounts Summary:
90
- • Critical: 2
91
- • At-Risk: 5
92
- • Healthy: 43
141
+ Claude: Events are flowing!
93
142
 
94
- - Acme Corp: Score 45/100 (critical) 📉
95
- - TechStart: Score 52/100 (at-risk) 📉
96
- - DataFlow: Score 58/100 (at-risk)
143
+ Last event: 2 minutes ago
144
+ Events (24h): 1,234
145
+ Unique users (24h): 56
146
+
147
+ Top Events (24h):
148
+ • page_view: 892
149
+ • button_click: 234
150
+ • form_submit: 108
97
151
  ```
98
152
 
99
153
  ## Configuration
@@ -116,12 +170,10 @@ You can also configure via environment variables (takes priority over config fil
116
170
  | `FIRSTDISTRO_API_KEY` | Your API key |
117
171
  | `FIRSTDISTRO_BASE_URL` | API base URL (default: https://firstdistro.com) |
118
172
 
119
- > **Important:** API keys start with `sk_live_` (production) or `sk_test_` (sandbox). Installation Tokens (`fd_...`) are for the browser SDK and won't work with the MCP server.
120
-
121
173
  ## Troubleshooting
122
174
 
123
175
  ### "Not configured" error
124
- Run `npx @firstdistro/mcp init` to set up your API key.
176
+ Run `npx @firstdistro/mcp init` to set up your API key, or follow the setup message which includes a link to sign up.
125
177
 
126
178
  ### "Invalid API key" error
127
179
  1. Check you're using an API Key (`sk_live_...`), not an Installation Token (`fd_...`)
@@ -147,6 +199,9 @@ npm install
147
199
  # Build
148
200
  npm run build
149
201
 
202
+ # Run tests
203
+ npm test
204
+
150
205
  # Run locally
151
206
  node bin/firstdistro-mcp.js
152
207
 
package/dist/server.js CHANGED
@@ -7,7 +7,8 @@ import { z } from "zod";
7
7
  import { readFileSync, existsSync } from "fs";
8
8
  import { homedir } from "os";
9
9
  import { join } from "path";
10
- var CONFIG_PATH = join(homedir(), ".firstdistro", "config.json");
10
+ var CONFIG_DIR = join(homedir(), ".firstdistro");
11
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
11
12
  var DEFAULT_BASE_URL = "https://firstdistro.com";
12
13
  function loadConfig() {
13
14
  const envKey = process.env.FIRSTDISTRO_API_KEY;
@@ -44,7 +45,386 @@ function loadConfig() {
44
45
  }
45
46
  }
46
47
 
48
+ // src/templates/nextjs-app.ts
49
+ function generateProvidersFile(installationToken) {
50
+ return `'use client'
51
+
52
+ import { FirstDistroProvider } from '@firstdistro/sdk/react'
53
+
54
+ export function Providers({ children }: { children: React.ReactNode }) {
55
+ return (
56
+ <FirstDistroProvider token="${installationToken}">
57
+ {children}
58
+ </FirstDistroProvider>
59
+ )
60
+ }
61
+ `;
62
+ }
63
+ function generateUserSetupComponent(authPattern) {
64
+ switch (authPattern) {
65
+ case "nextauth":
66
+ return `'use client'
67
+
68
+ import { useSession } from 'next-auth/react'
69
+ import { useFirstDistroSetup } from '@firstdistro/sdk/react'
70
+
71
+ export function FirstDistroSetup() {
72
+ const { data: session } = useSession()
73
+
74
+ useFirstDistroSetup(session?.user ? {
75
+ userId: session.user.id,
76
+ userEmail: session.user.email || undefined,
77
+ userName: session.user.name || undefined,
78
+ } : null)
79
+
80
+ return null
81
+ }
82
+ `;
83
+ case "clerk":
84
+ return `'use client'
85
+
86
+ import { useUser } from '@clerk/nextjs'
87
+ import { useFirstDistroSetup } from '@firstdistro/sdk/react'
88
+
89
+ export function FirstDistroSetup() {
90
+ const { user, isLoaded } = useUser()
91
+
92
+ useFirstDistroSetup(isLoaded && user ? {
93
+ userId: user.id,
94
+ userEmail: user.primaryEmailAddress?.emailAddress,
95
+ userName: user.fullName || undefined,
96
+ } : null)
97
+
98
+ return null
99
+ }
100
+ `;
101
+ case "supabase":
102
+ return `'use client'
103
+
104
+ import { useEffect, useState } from 'react'
105
+ import { createClient } from '@/lib/supabase/client'
106
+ import { useFirstDistroSetup } from '@firstdistro/sdk/react'
107
+ import type { User } from '@supabase/supabase-js'
108
+
109
+ // Requires @supabase/ssr setup. See: https://supabase.com/docs/guides/auth/server-side/creating-a-client
110
+
111
+ export function FirstDistroSetup() {
112
+ const [user, setUser] = useState<User | null>(null)
113
+ const supabase = createClient()
114
+
115
+ useEffect(() => {
116
+ supabase.auth.getUser().then(({ data: { user } }) => setUser(user))
117
+
118
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
119
+ (_event, session) => setUser(session?.user ?? null)
120
+ )
121
+
122
+ return () => subscription.unsubscribe()
123
+ }, [supabase])
124
+
125
+ useFirstDistroSetup(user ? {
126
+ userId: user.id,
127
+ userEmail: user.email,
128
+ } : null)
129
+
130
+ return null
131
+ }
132
+ `;
133
+ case "custom":
134
+ return `'use client'
135
+
136
+ import { useFirstDistroSetup } from '@firstdistro/sdk/react'
137
+
138
+ // CUSTOMIZE: Import your auth hook or context
139
+ // import { useAuth } from './your-auth-provider'
140
+
141
+ export function FirstDistroSetup() {
142
+ // CUSTOMIZE: Replace with your auth state
143
+ // const { user } = useAuth()
144
+ const user = null // Replace with your user object
145
+
146
+ useFirstDistroSetup(user ? {
147
+ userId: user.id,
148
+ userEmail: user.email,
149
+ userName: user.name,
150
+ // Optional: Add account info for B2B
151
+ // accountId: user.organizationId,
152
+ // accountName: user.organizationName,
153
+ } : null)
154
+
155
+ return null
156
+ }
157
+ `;
158
+ case "none":
159
+ default:
160
+ return null;
161
+ }
162
+ }
163
+ function generateNextjsAppFiles(installationToken, options = {}) {
164
+ const { includeUserSetup = true, authPattern = "none" } = options;
165
+ const files = [];
166
+ files.push({
167
+ path: "app/providers.tsx",
168
+ action: "create",
169
+ content: generateProvidersFile(installationToken),
170
+ description: "FirstDistro provider component that wraps your app"
171
+ });
172
+ files.push({
173
+ path: "app/layout.tsx",
174
+ action: "modify",
175
+ insertAfter: "import",
176
+ replace: {
177
+ old: "{children}",
178
+ new: "<Providers>{children}</Providers>"
179
+ },
180
+ description: 'Wrap your app with the Providers component. Add: import { Providers } from "./providers"'
181
+ });
182
+ if (includeUserSetup && authPattern !== "none") {
183
+ const userSetupContent = generateUserSetupComponent(authPattern);
184
+ if (userSetupContent) {
185
+ files.push({
186
+ path: "components/FirstDistroSetup.tsx",
187
+ action: "create",
188
+ content: userSetupContent,
189
+ description: `User identification component for ${authPattern}. Add <FirstDistroSetup /> inside your Providers.`
190
+ });
191
+ }
192
+ }
193
+ return files;
194
+ }
195
+
196
+ // src/templates/react-vite.ts
197
+ function generateProviderFile(installationToken) {
198
+ return `import { FirstDistroProvider as FDProvider } from '@firstdistro/sdk/react'
199
+
200
+ interface Props {
201
+ children: React.ReactNode
202
+ }
203
+
204
+ export function FirstDistroProvider({ children }: Props) {
205
+ return (
206
+ <FDProvider token="${installationToken}">
207
+ {children}
208
+ </FDProvider>
209
+ )
210
+ }
211
+ `;
212
+ }
213
+ function generateUserSetupComponent2(authPattern) {
214
+ switch (authPattern) {
215
+ case "supabase":
216
+ return `import { useEffect, useState } from 'react'
217
+ import { supabase } from '../lib/supabase' // Adjust path as needed
218
+ import { useFirstDistroSetup } from '@firstdistro/sdk/react'
219
+ import type { User } from '@supabase/supabase-js'
220
+
221
+ export function FirstDistroSetup() {
222
+ const [user, setUser] = useState<User | null>(null)
223
+
224
+ useEffect(() => {
225
+ supabase.auth.getUser().then(({ data: { user } }) => setUser(user))
226
+
227
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
228
+ (_event, session) => setUser(session?.user ?? null)
229
+ )
230
+
231
+ return () => subscription.unsubscribe()
232
+ }, [])
233
+
234
+ useFirstDistroSetup(user ? {
235
+ userId: user.id,
236
+ userEmail: user.email,
237
+ } : null)
238
+
239
+ return null
240
+ }
241
+ `;
242
+ case "clerk":
243
+ return `import { useUser } from '@clerk/clerk-react'
244
+ import { useFirstDistroSetup } from '@firstdistro/sdk/react'
245
+
246
+ export function FirstDistroSetup() {
247
+ const { user, isLoaded } = useUser()
248
+
249
+ useFirstDistroSetup(isLoaded && user ? {
250
+ userId: user.id,
251
+ userEmail: user.primaryEmailAddress?.emailAddress,
252
+ userName: user.fullName || undefined,
253
+ } : null)
254
+
255
+ return null
256
+ }
257
+ `;
258
+ case "custom":
259
+ return `import { useFirstDistroSetup } from '@firstdistro/sdk/react'
260
+
261
+ // CUSTOMIZE: Import your auth hook or context
262
+ // import { useAuth } from './your-auth-provider'
263
+
264
+ export function FirstDistroSetup() {
265
+ // CUSTOMIZE: Replace with your auth state
266
+ // const { user } = useAuth()
267
+ const user = null // Replace with your user object
268
+
269
+ useFirstDistroSetup(user ? {
270
+ userId: user.id,
271
+ userEmail: user.email,
272
+ userName: user.name,
273
+ // Optional: Add account info for B2B
274
+ // accountId: user.organizationId,
275
+ // accountName: user.organizationName,
276
+ } : null)
277
+
278
+ return null
279
+ }
280
+ `;
281
+ case "nextauth":
282
+ case "none":
283
+ default:
284
+ return null;
285
+ }
286
+ }
287
+ function generateReactViteFiles(installationToken, options = {}) {
288
+ const { includeUserSetup = true, authPattern = "none" } = options;
289
+ const files = [];
290
+ files.push({
291
+ path: "src/providers/FirstDistroProvider.tsx",
292
+ action: "create",
293
+ content: generateProviderFile(installationToken),
294
+ description: "FirstDistro provider component"
295
+ });
296
+ files.push({
297
+ path: "src/main.tsx",
298
+ action: "modify",
299
+ insertAfter: "import",
300
+ replace: {
301
+ old: "<App />",
302
+ new: `<FirstDistroProvider>
303
+ <App />
304
+ </FirstDistroProvider>`
305
+ },
306
+ description: 'Wrap your App with FirstDistroProvider. Add: import { FirstDistroProvider } from "./providers/FirstDistroProvider"'
307
+ });
308
+ if (includeUserSetup && authPattern !== "none" && authPattern !== "nextauth") {
309
+ const userSetupContent = generateUserSetupComponent2(authPattern);
310
+ if (userSetupContent) {
311
+ files.push({
312
+ path: "src/components/FirstDistroSetup.tsx",
313
+ action: "create",
314
+ content: userSetupContent,
315
+ description: `User identification component for ${authPattern}. Add <FirstDistroSetup /> inside your FirstDistroProvider.`
316
+ });
317
+ }
318
+ }
319
+ return files;
320
+ }
321
+
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;
332
+ }
333
+ function generateSetupFiles(framework, installationToken, options = {}) {
334
+ switch (framework) {
335
+ case "nextjs-app":
336
+ return generateNextjsAppFiles(installationToken, options);
337
+ case "react-vite":
338
+ return generateReactViteFiles(installationToken, options);
339
+ case "nextjs-pages":
340
+ case "react-cra":
341
+ 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
+ }];
373
+ default:
374
+ throw new Error(`Unsupported framework: ${framework}`);
375
+ }
376
+ }
377
+ function generateSetupResult(framework, installationToken, options = {}) {
378
+ const files = generateSetupFiles(framework, installationToken, options);
379
+ const commands = [
380
+ {
381
+ run: "npm install @firstdistro/sdk",
382
+ description: "Install the FirstDistro SDK"
383
+ }
384
+ ];
385
+ return {
386
+ installationToken,
387
+ framework,
388
+ commands,
389
+ files,
390
+ verification: {
391
+ tool: "check_events_flowing",
392
+ successMessage: "Events are flowing! FirstDistro is working.",
393
+ failureHint: "No events yet. Try refreshing your app and interacting with it."
394
+ },
395
+ nextSteps: [
396
+ 'Create an Experience to track a user journey (e.g., "User Onboarding")',
397
+ 'Ask: "Show my at-risk customers" to see health insights',
398
+ 'Ask: "Who is stuck in onboarding?" to find users who need help'
399
+ ]
400
+ };
401
+ }
402
+
47
403
  // src/server.ts
404
+ var MCP_VERSION = "1.1.0";
405
+ function detectMcpClient() {
406
+ if (process.env.CLAUDE_CODE || process.env.CLAUDE_PROJECT_ROOT) {
407
+ return "claude-code";
408
+ }
409
+ if (process.env.CURSOR_SESSION_ID || process.env.CURSOR_TRACE_ID) {
410
+ return "cursor";
411
+ }
412
+ if (process.env.WINDSURF_SESSION_ID || process.env.CODEIUM_API_KEY) {
413
+ return "windsurf";
414
+ }
415
+ if (process.env.CONTINUE_GLOBAL_DIR) {
416
+ return "continue";
417
+ }
418
+ return "unknown";
419
+ }
420
+ function getApiHeaders(apiKey) {
421
+ const client = detectMcpClient();
422
+ return {
423
+ "X-API-Key": apiKey,
424
+ "Content-Type": "application/json",
425
+ "User-Agent": `firstdistro-mcp/${MCP_VERSION} (${client})`
426
+ };
427
+ }
48
428
  function formatHttpError(status, statusText, context) {
49
429
  switch (status) {
50
430
  case 401:
@@ -79,28 +459,46 @@ function formatNetworkError(error) {
79
459
  }
80
460
  return "Unknown network error occurred.";
81
461
  }
462
+ function unwrapApiResponse(response) {
463
+ if (response && typeof response === "object" && "success" in response && "data" in response) {
464
+ return response.data;
465
+ }
466
+ return response;
467
+ }
82
468
  async function startServer() {
83
- const config = loadConfig();
469
+ let config = null;
470
+ let configError = null;
471
+ try {
472
+ config = loadConfig();
473
+ } catch (error) {
474
+ const errorMessage = error instanceof Error ? error.message : String(error);
475
+ if (errorMessage.includes("not configured")) {
476
+ configError = null;
477
+ } else {
478
+ configError = errorMessage;
479
+ }
480
+ }
84
481
  const server = new McpServer({
85
482
  name: "firstdistro",
86
- version: "1.0.0"
483
+ version: MCP_VERSION
87
484
  });
88
- registerTools(server, config);
485
+ if (config) {
486
+ registerAuthenticatedTools(server, config);
487
+ } else {
488
+ registerUnconfiguredTools(server, configError);
489
+ }
89
490
  const transport = new StdioServerTransport();
90
491
  await server.connect(transport);
91
492
  console.error("[FirstDistro MCP] Server started");
92
493
  }
93
- function registerTools(server, config) {
494
+ function registerAuthenticatedTools(server, config) {
94
495
  server.tool(
95
496
  "list_experiences",
96
- "List all configured experiences (user journeys) for your FirstDistro account",
497
+ `List all user journeys/funnels you're tracking (called "experiences" in FirstDistro). Examples: onboarding flow, checkout funnel, feature adoption.`,
97
498
  async () => {
98
499
  try {
99
500
  const response = await fetch(`${config.baseUrl}/api/vendor/experiences`, {
100
- headers: {
101
- "X-API-Key": config.apiKey,
102
- "Content-Type": "application/json"
103
- }
501
+ headers: getApiHeaders(config.apiKey)
104
502
  });
105
503
  if (!response.ok) {
106
504
  return {
@@ -113,8 +511,9 @@ function registerTools(server, config) {
113
511
  isError: true
114
512
  };
115
513
  }
116
- const data = await response.json();
117
- const experiences = data.experiences || data;
514
+ const rawData = await response.json();
515
+ const data = unwrapApiResponse(rawData);
516
+ const experiences = data.experiences || [];
118
517
  if (!experiences || experiences.length === 0) {
119
518
  return {
120
519
  content: [
@@ -155,10 +554,7 @@ ${summary}`
155
554
  async () => {
156
555
  try {
157
556
  const response = await fetch(`${config.baseUrl}/api/mcp/events-status`, {
158
- headers: {
159
- "X-API-Key": config.apiKey,
160
- "Content-Type": "application/json"
161
- }
557
+ headers: getApiHeaders(config.apiKey)
162
558
  });
163
559
  if (!response.ok) {
164
560
  return {
@@ -171,7 +567,8 @@ ${summary}`
171
567
  isError: true
172
568
  };
173
569
  }
174
- const data = await response.json();
570
+ const rawData = await response.json();
571
+ const data = unwrapApiResponse(rawData);
175
572
  if (data.eventsFlowing || data.hasEvents) {
176
573
  let topEventsText = "";
177
574
  if (data.topEvents && data.topEvents.length > 0) {
@@ -227,10 +624,7 @@ Unique users (24h): ${data.uniqueUsersLast24h ?? "N/A"}${topEventsText}`
227
624
  const response = await fetch(
228
625
  `${config.baseUrl}/api/vendor/customers/${accountId}`,
229
626
  {
230
- headers: {
231
- "X-API-Key": config.apiKey,
232
- "Content-Type": "application/json"
233
- }
627
+ headers: getApiHeaders(config.apiKey)
234
628
  }
235
629
  );
236
630
  if (!response.ok) {
@@ -244,8 +638,9 @@ Unique users (24h): ${data.uniqueUsersLast24h ?? "N/A"}${topEventsText}`
244
638
  isError: true
245
639
  };
246
640
  }
247
- const data = await response.json();
248
- const health = data.health || data;
641
+ const rawData = await response.json();
642
+ const data = unwrapApiResponse(rawData);
643
+ const health = data.health || {};
249
644
  return {
250
645
  content: [
251
646
  {
@@ -274,7 +669,7 @@ Last Seen: ${data.account?.lastSeenAt || "Unknown"}`
274
669
  );
275
670
  server.tool(
276
671
  "get_experience_stats",
277
- "Get funnel statistics for a specific experience (conversion rates, drop-offs)",
672
+ "Get funnel statistics for a user journey: how many started, completed, conversion rate, and average time to complete.",
278
673
  {
279
674
  experienceId: z.string().describe("The experience ID or slug"),
280
675
  range: z.enum(["7d", "30d", "90d"]).optional().describe("Time range for stats (default: 7d)")
@@ -285,10 +680,7 @@ Last Seen: ${data.account?.lastSeenAt || "Unknown"}`
285
680
  if (range) params.set("range", range);
286
681
  const url = `${config.baseUrl}/api/vendor/experiences/${experienceId}/stats${params.toString() ? "?" + params.toString() : ""}`;
287
682
  const response = await fetch(url, {
288
- headers: {
289
- "X-API-Key": config.apiKey,
290
- "Content-Type": "application/json"
291
- }
683
+ headers: getApiHeaders(config.apiKey)
292
684
  });
293
685
  if (!response.ok) {
294
686
  return {
@@ -301,7 +693,8 @@ Last Seen: ${data.account?.lastSeenAt || "Unknown"}`
301
693
  isError: true
302
694
  };
303
695
  }
304
- const data = await response.json();
696
+ const rawData = await response.json();
697
+ const data = unwrapApiResponse(rawData);
305
698
  const stats = data.stats || {};
306
699
  const exp = data.experience || {};
307
700
  let avgTimeDisplay = "N/A";
@@ -344,7 +737,7 @@ Avg Time to Complete: ${avgTimeDisplay}`
344
737
  );
345
738
  server.tool(
346
739
  "get_stuck_customers",
347
- "Find customers who are stuck in a specific experience (not progressing)",
740
+ "Find customers who started a journey but stopped progressing. Useful for identifying users who need help completing onboarding, checkout, or other flows.",
348
741
  {
349
742
  experienceId: z.string().describe("The experience ID or slug"),
350
743
  limit: z.number().optional().describe("Maximum number of results (default: 20, max: 100)")
@@ -355,10 +748,7 @@ Avg Time to Complete: ${avgTimeDisplay}`
355
748
  if (limit) params.set("limit", String(limit));
356
749
  const url = `${config.baseUrl}/api/vendor/experiences/${experienceId}/stuck${params.toString() ? "?" + params.toString() : ""}`;
357
750
  const response = await fetch(url, {
358
- headers: {
359
- "X-API-Key": config.apiKey,
360
- "Content-Type": "application/json"
361
- }
751
+ headers: getApiHeaders(config.apiKey)
362
752
  });
363
753
  if (!response.ok) {
364
754
  return {
@@ -371,7 +761,8 @@ Avg Time to Complete: ${avgTimeDisplay}`
371
761
  isError: true
372
762
  };
373
763
  }
374
- const data = await response.json();
764
+ const rawData = await response.json();
765
+ const data = unwrapApiResponse(rawData);
375
766
  const customers = data.stuckCustomers || [];
376
767
  const exp = data.experience || {};
377
768
  if (customers.length === 0) {
@@ -431,10 +822,7 @@ ${customerList}${customers.length > 10 ? `
431
822
  if (sortBy) params.set("sortBy", sortBy);
432
823
  const url = `${config.baseUrl}/api/vendor/customers/at-risk${params.toString() ? "?" + params.toString() : ""}`;
433
824
  const response = await fetch(url, {
434
- headers: {
435
- "X-API-Key": config.apiKey,
436
- "Content-Type": "application/json"
437
- }
825
+ headers: getApiHeaders(config.apiKey)
438
826
  });
439
827
  if (!response.ok) {
440
828
  return {
@@ -447,7 +835,8 @@ ${customerList}${customers.length > 10 ? `
447
835
  isError: true
448
836
  };
449
837
  }
450
- const data = await response.json();
838
+ const rawData = await response.json();
839
+ const data = unwrapApiResponse(rawData);
451
840
  const accounts = data.accounts || [];
452
841
  const summary = data.summary || {};
453
842
  if (accounts.length === 0) {
@@ -493,6 +882,218 @@ ${accountList}${accounts.length > 15 ? `
493
882
  }
494
883
  }
495
884
  );
885
+ server.tool(
886
+ "get_sdk_config",
887
+ "Get your FirstDistro installation token and SDK configuration snippets for setting up the SDK in your project.",
888
+ async () => {
889
+ try {
890
+ const response = await fetch(`${config.baseUrl}/api/vendor/sdk-config`, {
891
+ headers: getApiHeaders(config.apiKey)
892
+ });
893
+ if (!response.ok) {
894
+ return {
895
+ content: [
896
+ {
897
+ type: "text",
898
+ text: formatHttpError(response.status, response.statusText, "SDK config")
899
+ }
900
+ ],
901
+ isError: true
902
+ };
903
+ }
904
+ const rawData = await response.json();
905
+ const result = unwrapApiResponse(rawData);
906
+ const snippets = result.snippets || {};
907
+ return {
908
+ content: [
909
+ {
910
+ type: "text",
911
+ text: `FirstDistro SDK Configuration
912
+
913
+ Installation Token: ${result.installationToken}
914
+ Vendor: ${result.vendorName || result.vendorSlug}
915
+
916
+ Quick Start:
917
+ 1. Install: ${snippets.install || "npm install @firstdistro/sdk"}
918
+
919
+ 2. Add the provider to your app:
920
+ ${snippets.providerImport || "import { FirstDistroProvider } from '@firstdistro/sdk/react'"}
921
+ ${snippets.providerJsx || `<FirstDistroProvider token="${result.installationToken}">{children}</FirstDistroProvider>`}
922
+
923
+ 3. Identify users (after login):
924
+ ${snippets.setupHook || "import { useFirstDistroSetup } from '@firstdistro/sdk/react'"}
925
+ ${snippets.setupCall || "useFirstDistroSetup({ userId: user.id, userEmail: user.email })"}
926
+
927
+ 4. Track custom events:
928
+ ${snippets.trackCall || "FirstDistro.track('event_name', { property: 'value' })"}
929
+
930
+ After setup, use 'check_events_flowing' to verify events are being received.`
931
+ }
932
+ ]
933
+ };
934
+ } catch (error) {
935
+ return {
936
+ content: [
937
+ {
938
+ type: "text",
939
+ text: formatNetworkError(error)
940
+ }
941
+ ],
942
+ isError: true
943
+ };
944
+ }
945
+ }
946
+ );
947
+ server.tool(
948
+ "setup_sdk",
949
+ "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.",
950
+ {
951
+ framework: z.enum(["nextjs-app", "nextjs-pages", "react-vite", "react-cra", "vanilla"]).describe(
952
+ 'Target framework. Use "nextjs-app" for Next.js 13+ with app directory, "react-vite" for React + Vite projects.'
953
+ ),
954
+ includeUserSetup: z.boolean().optional().describe("Include user identification code for tracking logged-in users (default: true)"),
955
+ authPattern: z.enum(["nextauth", "clerk", "supabase", "custom", "none"]).optional().describe("Auth library in use. Generates appropriate user setup code for the auth pattern.")
956
+ },
957
+ async ({ framework, includeUserSetup, authPattern }) => {
958
+ try {
959
+ const response = await fetch(`${config.baseUrl}/api/vendor/sdk-config`, {
960
+ headers: getApiHeaders(config.apiKey)
961
+ });
962
+ if (!response.ok) {
963
+ return {
964
+ content: [
965
+ {
966
+ type: "text",
967
+ text: formatHttpError(response.status, response.statusText, "SDK config")
968
+ }
969
+ ],
970
+ isError: true
971
+ };
972
+ }
973
+ const rawData = await response.json();
974
+ const data = unwrapApiResponse(rawData);
975
+ const installationToken = data.installationToken;
976
+ if (!installationToken) {
977
+ return {
978
+ content: [
979
+ {
980
+ type: "text",
981
+ text: "Could not retrieve installation token. Please check your API key and try again."
982
+ }
983
+ ],
984
+ isError: true
985
+ };
986
+ }
987
+ const result = generateSetupResult(framework, installationToken, {
988
+ includeUserSetup: includeUserSetup ?? true,
989
+ authPattern: authPattern || "none"
990
+ });
991
+ const filesOutput = result.files.map((file) => {
992
+ if (file.action === "create") {
993
+ return `### ${file.path} (CREATE)
994
+ ${file.description}
995
+
996
+ \`\`\`tsx
997
+ ${file.content}
998
+ \`\`\``;
999
+ } else {
1000
+ return `### ${file.path} (MODIFY)
1001
+ ${file.description}
1002
+ ${file.replace ? `
1003
+ Find this code:
1004
+ \`\`\`
1005
+ ${file.replace.old}
1006
+ \`\`\`
1007
+
1008
+ Replace with:
1009
+ \`\`\`tsx
1010
+ ${file.replace.new}
1011
+ \`\`\`
1012
+ ` : ""}`;
1013
+ }
1014
+ }).join("\n\n");
1015
+ const commandsOutput = result.commands.map((cmd) => `- \`${cmd.run}\` \u2014 ${cmd.description}`).join("\n");
1016
+ const nextStepsOutput = result.nextSteps.map((step) => `- ${step}`).join("\n");
1017
+ return {
1018
+ content: [
1019
+ {
1020
+ type: "text",
1021
+ text: `# FirstDistro SDK Setup for ${getFrameworkDisplayName(framework)}
1022
+
1023
+ ## Installation Token
1024
+ \`${result.installationToken}\`
1025
+
1026
+ ## Commands to Run
1027
+ ${commandsOutput}
1028
+
1029
+ ## Files to Create/Modify
1030
+
1031
+ ${filesOutput}
1032
+
1033
+ ## Verification
1034
+ After setup, use the \`${result.verification.tool}\` tool to verify:
1035
+ - Success: ${result.verification.successMessage}
1036
+ - If not working: ${result.verification.failureHint}
1037
+
1038
+ ## Next Steps
1039
+ ${nextStepsOutput}`
1040
+ }
1041
+ ]
1042
+ };
1043
+ } catch (error) {
1044
+ return {
1045
+ content: [
1046
+ {
1047
+ type: "text",
1048
+ text: formatNetworkError(error)
1049
+ }
1050
+ ],
1051
+ isError: true
1052
+ };
1053
+ }
1054
+ }
1055
+ );
1056
+ }
1057
+ function registerUnconfiguredTools(server, configError = null) {
1058
+ const message = configError ? `FirstDistro configuration error:
1059
+
1060
+ ${configError}
1061
+
1062
+ To fix this, run:
1063
+ npx @firstdistro/mcp init --api-key YOUR_KEY
1064
+
1065
+ Then restart your IDE.` : `FirstDistro is not configured yet.
1066
+
1067
+ To get started:
1068
+
1069
+ 1. Sign up at https://firstdistro.com/auth/register
1070
+
1071
+ 2. Get your API key from Settings \u2192 API Keys
1072
+
1073
+ 3. Configure MCP:
1074
+ npx @firstdistro/mcp init --api-key YOUR_KEY
1075
+
1076
+ Then restart your IDE to use FirstDistro tools.`;
1077
+ const placeholderTools = [
1078
+ { name: "list_experiences", description: "List all user journeys/funnels you're tracking" },
1079
+ { name: "check_events_flowing", description: "Check if events are being received" },
1080
+ { name: "get_customer_health", description: "Get health score for a customer" },
1081
+ { name: "get_experience_stats", description: "Get funnel statistics for a user journey" },
1082
+ { name: "get_stuck_customers", description: "Find customers who stopped progressing in a journey" },
1083
+ { name: "list_at_risk_accounts", description: "List at-risk customer accounts" },
1084
+ { name: "get_sdk_config", description: "Get your installation token and SDK setup code" },
1085
+ { name: "setup_sdk", description: "Generate files to set up FirstDistro SDK in your project" }
1086
+ ];
1087
+ for (const tool of placeholderTools) {
1088
+ server.tool(tool.name, tool.description, async () => ({
1089
+ content: [
1090
+ {
1091
+ type: "text",
1092
+ text: message
1093
+ }
1094
+ ]
1095
+ }));
1096
+ }
496
1097
  }
497
1098
  export {
498
1099
  startServer
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firstdistro/mcp",
3
- "version": "1.0.0",
4
- "description": "MCP server for FirstDistro - query and manage customer health from AI assistants",
3
+ "version": "1.1.1",
4
+ "description": "MCP server that brings customer health scores, stuck users, and funnel analytics into Claude, Cursor, and other AI tools — catch churn risks early",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "firstdistro-mcp": "./bin/firstdistro-mcp.js"