@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.
- package/README.md +76 -21
- package/dist/server.js +620 -32
- 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
|
|
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.
|
|
7
|
+
### 1. Create a FirstDistro Account
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
**
|
|
137
|
+
**Verify events are flowing:**
|
|
86
138
|
```
|
|
87
|
-
User: "
|
|
139
|
+
User: "Are events flowing from my app?"
|
|
88
140
|
|
|
89
|
-
Claude:
|
|
90
|
-
• Critical: 2
|
|
91
|
-
• At-Risk: 5
|
|
92
|
-
• Healthy: 43
|
|
141
|
+
Claude: ✓ Events are flowing!
|
|
93
142
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
477
|
+
version: MCP_VERSION
|
|
87
478
|
});
|
|
88
|
-
|
|
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
|
|
488
|
+
function registerAuthenticatedTools(server, config) {
|
|
94
489
|
server.tool(
|
|
95
490
|
"list_experiences",
|
|
96
|
-
|
|
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
|
|
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
|
|
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
|