@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.
- package/README.md +76 -21
- package/dist/server.js +641 -40
- 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
|
|
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:
|
|
@@ -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
|
-
|
|
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:
|
|
483
|
+
version: MCP_VERSION
|
|
87
484
|
});
|
|
88
|
-
|
|
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
|
|
494
|
+
function registerAuthenticatedTools(server, config) {
|
|
94
495
|
server.tool(
|
|
95
496
|
"list_experiences",
|
|
96
|
-
|
|
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
|
|
117
|
-
const
|
|
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
|
|
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
|
|
248
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "MCP server
|
|
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"
|