@firstdistro/mcp 1.0.0 → 1.1.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 +76 -21
  2. package/dist/server.js +620 -32
  3. package/package.json +1 -1
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:
@@ -80,27 +460,39 @@ function formatNetworkError(error) {
80
460
  return "Unknown network error occurred.";
81
461
  }
82
462
  async function startServer() {
83
- const config = loadConfig();
463
+ let config = null;
464
+ let configError = null;
465
+ try {
466
+ config = loadConfig();
467
+ } catch (error) {
468
+ const errorMessage = error instanceof Error ? error.message : String(error);
469
+ if (errorMessage.includes("not configured")) {
470
+ configError = null;
471
+ } else {
472
+ configError = errorMessage;
473
+ }
474
+ }
84
475
  const server = new McpServer({
85
476
  name: "firstdistro",
86
- version: "1.0.0"
477
+ version: MCP_VERSION
87
478
  });
88
- registerTools(server, config);
479
+ if (config) {
480
+ registerAuthenticatedTools(server, config);
481
+ } else {
482
+ registerUnconfiguredTools(server, configError);
483
+ }
89
484
  const transport = new StdioServerTransport();
90
485
  await server.connect(transport);
91
486
  console.error("[FirstDistro MCP] Server started");
92
487
  }
93
- function registerTools(server, config) {
488
+ function registerAuthenticatedTools(server, config) {
94
489
  server.tool(
95
490
  "list_experiences",
96
- "List all configured experiences (user journeys) for your FirstDistro account",
491
+ `List all user journeys/funnels you're tracking (called "experiences" in FirstDistro). Examples: onboarding flow, checkout funnel, feature adoption.`,
97
492
  async () => {
98
493
  try {
99
494
  const response = await fetch(`${config.baseUrl}/api/vendor/experiences`, {
100
- headers: {
101
- "X-API-Key": config.apiKey,
102
- "Content-Type": "application/json"
103
- }
495
+ headers: getApiHeaders(config.apiKey)
104
496
  });
105
497
  if (!response.ok) {
106
498
  return {
@@ -155,10 +547,7 @@ ${summary}`
155
547
  async () => {
156
548
  try {
157
549
  const response = await fetch(`${config.baseUrl}/api/mcp/events-status`, {
158
- headers: {
159
- "X-API-Key": config.apiKey,
160
- "Content-Type": "application/json"
161
- }
550
+ headers: getApiHeaders(config.apiKey)
162
551
  });
163
552
  if (!response.ok) {
164
553
  return {
@@ -227,10 +616,7 @@ Unique users (24h): ${data.uniqueUsersLast24h ?? "N/A"}${topEventsText}`
227
616
  const response = await fetch(
228
617
  `${config.baseUrl}/api/vendor/customers/${accountId}`,
229
618
  {
230
- headers: {
231
- "X-API-Key": config.apiKey,
232
- "Content-Type": "application/json"
233
- }
619
+ headers: getApiHeaders(config.apiKey)
234
620
  }
235
621
  );
236
622
  if (!response.ok) {
@@ -274,7 +660,7 @@ Last Seen: ${data.account?.lastSeenAt || "Unknown"}`
274
660
  );
275
661
  server.tool(
276
662
  "get_experience_stats",
277
- "Get funnel statistics for a specific experience (conversion rates, drop-offs)",
663
+ "Get funnel statistics for a user journey: how many started, completed, conversion rate, and average time to complete.",
278
664
  {
279
665
  experienceId: z.string().describe("The experience ID or slug"),
280
666
  range: z.enum(["7d", "30d", "90d"]).optional().describe("Time range for stats (default: 7d)")
@@ -285,10 +671,7 @@ Last Seen: ${data.account?.lastSeenAt || "Unknown"}`
285
671
  if (range) params.set("range", range);
286
672
  const url = `${config.baseUrl}/api/vendor/experiences/${experienceId}/stats${params.toString() ? "?" + params.toString() : ""}`;
287
673
  const response = await fetch(url, {
288
- headers: {
289
- "X-API-Key": config.apiKey,
290
- "Content-Type": "application/json"
291
- }
674
+ headers: getApiHeaders(config.apiKey)
292
675
  });
293
676
  if (!response.ok) {
294
677
  return {
@@ -344,7 +727,7 @@ Avg Time to Complete: ${avgTimeDisplay}`
344
727
  );
345
728
  server.tool(
346
729
  "get_stuck_customers",
347
- "Find customers who are stuck in a specific experience (not progressing)",
730
+ "Find customers who started a journey but stopped progressing. Useful for identifying users who need help completing onboarding, checkout, or other flows.",
348
731
  {
349
732
  experienceId: z.string().describe("The experience ID or slug"),
350
733
  limit: z.number().optional().describe("Maximum number of results (default: 20, max: 100)")
@@ -355,10 +738,7 @@ Avg Time to Complete: ${avgTimeDisplay}`
355
738
  if (limit) params.set("limit", String(limit));
356
739
  const url = `${config.baseUrl}/api/vendor/experiences/${experienceId}/stuck${params.toString() ? "?" + params.toString() : ""}`;
357
740
  const response = await fetch(url, {
358
- headers: {
359
- "X-API-Key": config.apiKey,
360
- "Content-Type": "application/json"
361
- }
741
+ headers: getApiHeaders(config.apiKey)
362
742
  });
363
743
  if (!response.ok) {
364
744
  return {
@@ -431,10 +811,7 @@ ${customerList}${customers.length > 10 ? `
431
811
  if (sortBy) params.set("sortBy", sortBy);
432
812
  const url = `${config.baseUrl}/api/vendor/customers/at-risk${params.toString() ? "?" + params.toString() : ""}`;
433
813
  const response = await fetch(url, {
434
- headers: {
435
- "X-API-Key": config.apiKey,
436
- "Content-Type": "application/json"
437
- }
814
+ headers: getApiHeaders(config.apiKey)
438
815
  });
439
816
  if (!response.ok) {
440
817
  return {
@@ -493,6 +870,217 @@ ${accountList}${accounts.length > 15 ? `
493
870
  }
494
871
  }
495
872
  );
873
+ server.tool(
874
+ "get_sdk_config",
875
+ "Get your FirstDistro installation token and SDK configuration snippets for setting up the SDK in your project.",
876
+ async () => {
877
+ try {
878
+ const response = await fetch(`${config.baseUrl}/api/vendor/sdk-config`, {
879
+ headers: getApiHeaders(config.apiKey)
880
+ });
881
+ if (!response.ok) {
882
+ return {
883
+ content: [
884
+ {
885
+ type: "text",
886
+ text: formatHttpError(response.status, response.statusText, "SDK config")
887
+ }
888
+ ],
889
+ isError: true
890
+ };
891
+ }
892
+ const data = await response.json();
893
+ const result = data.data || data;
894
+ const snippets = result.snippets || {};
895
+ return {
896
+ content: [
897
+ {
898
+ type: "text",
899
+ text: `FirstDistro SDK Configuration
900
+
901
+ Installation Token: ${result.installationToken}
902
+ Vendor: ${result.vendorName || result.vendorSlug}
903
+
904
+ Quick Start:
905
+ 1. Install: ${snippets.install || "npm install @firstdistro/sdk"}
906
+
907
+ 2. Add the provider to your app:
908
+ ${snippets.providerImport || "import { FirstDistroProvider } from '@firstdistro/sdk/react'"}
909
+ ${snippets.providerJsx || `<FirstDistroProvider token="${result.installationToken}">{children}</FirstDistroProvider>`}
910
+
911
+ 3. Identify users (after login):
912
+ ${snippets.setupHook || "import { useFirstDistroSetup } from '@firstdistro/sdk/react'"}
913
+ ${snippets.setupCall || "useFirstDistroSetup({ userId: user.id, userEmail: user.email })"}
914
+
915
+ 4. Track custom events:
916
+ ${snippets.trackCall || "FirstDistro.track('event_name', { property: 'value' })"}
917
+
918
+ After setup, use 'check_events_flowing' to verify events are being received.`
919
+ }
920
+ ]
921
+ };
922
+ } catch (error) {
923
+ return {
924
+ content: [
925
+ {
926
+ type: "text",
927
+ text: formatNetworkError(error)
928
+ }
929
+ ],
930
+ isError: true
931
+ };
932
+ }
933
+ }
934
+ );
935
+ server.tool(
936
+ "setup_sdk",
937
+ "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.",
938
+ {
939
+ framework: z.enum(["nextjs-app", "nextjs-pages", "react-vite", "react-cra", "vanilla"]).describe(
940
+ 'Target framework. Use "nextjs-app" for Next.js 13+ with app directory, "react-vite" for React + Vite projects.'
941
+ ),
942
+ includeUserSetup: z.boolean().optional().describe("Include user identification code for tracking logged-in users (default: true)"),
943
+ authPattern: z.enum(["nextauth", "clerk", "supabase", "custom", "none"]).optional().describe("Auth library in use. Generates appropriate user setup code for the auth pattern.")
944
+ },
945
+ async ({ framework, includeUserSetup, authPattern }) => {
946
+ try {
947
+ const response = await fetch(`${config.baseUrl}/api/vendor/sdk-config`, {
948
+ headers: getApiHeaders(config.apiKey)
949
+ });
950
+ if (!response.ok) {
951
+ return {
952
+ content: [
953
+ {
954
+ type: "text",
955
+ text: formatHttpError(response.status, response.statusText, "SDK config")
956
+ }
957
+ ],
958
+ isError: true
959
+ };
960
+ }
961
+ const data = await response.json();
962
+ const installationToken = data.data?.installationToken || data.installationToken;
963
+ if (!installationToken) {
964
+ return {
965
+ content: [
966
+ {
967
+ type: "text",
968
+ text: "Could not retrieve installation token. Please check your API key and try again."
969
+ }
970
+ ],
971
+ isError: true
972
+ };
973
+ }
974
+ const result = generateSetupResult(framework, installationToken, {
975
+ includeUserSetup: includeUserSetup ?? true,
976
+ authPattern: authPattern || "none"
977
+ });
978
+ const filesOutput = result.files.map((file) => {
979
+ if (file.action === "create") {
980
+ return `### ${file.path} (CREATE)
981
+ ${file.description}
982
+
983
+ \`\`\`tsx
984
+ ${file.content}
985
+ \`\`\``;
986
+ } else {
987
+ return `### ${file.path} (MODIFY)
988
+ ${file.description}
989
+ ${file.replace ? `
990
+ Find this code:
991
+ \`\`\`
992
+ ${file.replace.old}
993
+ \`\`\`
994
+
995
+ Replace with:
996
+ \`\`\`tsx
997
+ ${file.replace.new}
998
+ \`\`\`
999
+ ` : ""}`;
1000
+ }
1001
+ }).join("\n\n");
1002
+ const commandsOutput = result.commands.map((cmd) => `- \`${cmd.run}\` \u2014 ${cmd.description}`).join("\n");
1003
+ const nextStepsOutput = result.nextSteps.map((step) => `- ${step}`).join("\n");
1004
+ return {
1005
+ content: [
1006
+ {
1007
+ type: "text",
1008
+ text: `# FirstDistro SDK Setup for ${getFrameworkDisplayName(framework)}
1009
+
1010
+ ## Installation Token
1011
+ \`${result.installationToken}\`
1012
+
1013
+ ## Commands to Run
1014
+ ${commandsOutput}
1015
+
1016
+ ## Files to Create/Modify
1017
+
1018
+ ${filesOutput}
1019
+
1020
+ ## Verification
1021
+ After setup, use the \`${result.verification.tool}\` tool to verify:
1022
+ - Success: ${result.verification.successMessage}
1023
+ - If not working: ${result.verification.failureHint}
1024
+
1025
+ ## Next Steps
1026
+ ${nextStepsOutput}`
1027
+ }
1028
+ ]
1029
+ };
1030
+ } catch (error) {
1031
+ return {
1032
+ content: [
1033
+ {
1034
+ type: "text",
1035
+ text: formatNetworkError(error)
1036
+ }
1037
+ ],
1038
+ isError: true
1039
+ };
1040
+ }
1041
+ }
1042
+ );
1043
+ }
1044
+ function registerUnconfiguredTools(server, configError = null) {
1045
+ const message = configError ? `FirstDistro configuration error:
1046
+
1047
+ ${configError}
1048
+
1049
+ To fix this, run:
1050
+ npx @firstdistro/mcp init --api-key YOUR_KEY
1051
+
1052
+ Then restart your IDE.` : `FirstDistro is not configured yet.
1053
+
1054
+ To get started:
1055
+
1056
+ 1. Sign up at https://firstdistro.com/auth/register
1057
+
1058
+ 2. Get your API key from Settings \u2192 API Keys
1059
+
1060
+ 3. Configure MCP:
1061
+ npx @firstdistro/mcp init --api-key YOUR_KEY
1062
+
1063
+ Then restart your IDE to use FirstDistro tools.`;
1064
+ const placeholderTools = [
1065
+ { name: "list_experiences", description: "List all user journeys/funnels you're tracking" },
1066
+ { name: "check_events_flowing", description: "Check if events are being received" },
1067
+ { name: "get_customer_health", description: "Get health score for a customer" },
1068
+ { name: "get_experience_stats", description: "Get funnel statistics for a user journey" },
1069
+ { name: "get_stuck_customers", description: "Find customers who stopped progressing in a journey" },
1070
+ { name: "list_at_risk_accounts", description: "List at-risk customer accounts" },
1071
+ { name: "get_sdk_config", description: "Get your installation token and SDK setup code" },
1072
+ { name: "setup_sdk", description: "Generate files to set up FirstDistro SDK in your project" }
1073
+ ];
1074
+ for (const tool of placeholderTools) {
1075
+ server.tool(tool.name, tool.description, async () => ({
1076
+ content: [
1077
+ {
1078
+ type: "text",
1079
+ text: message
1080
+ }
1081
+ ]
1082
+ }));
1083
+ }
496
1084
  }
497
1085
  export {
498
1086
  startServer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstdistro/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for FirstDistro - query and manage customer health from AI assistants",
5
5
  "type": "module",
6
6
  "bin": {