fullstory-mcp-server 1.0.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/.env.example +20 -0
- package/DEPLOYMENT_GUIDE.md +272 -0
- package/Dockerfile +28 -0
- package/README.md +181 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +63 -0
- package/dist/cli.js.map +1 -0
- package/dist/fullstory-client.d.ts +68 -0
- package/dist/fullstory-client.d.ts.map +1 -0
- package/dist/fullstory-client.js +172 -0
- package/dist/fullstory-client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +401 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
- package/src/cli.ts +75 -0
- package/src/fullstory-client.ts +231 -0
- package/src/index.ts +443 -0
- package/tsconfig.json +18 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
Tool,
|
|
10
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { createServer, IncomingMessage, ServerResponse } from 'http';
|
|
12
|
+
import { FullstoryClient } from './fullstory-client.js';
|
|
13
|
+
|
|
14
|
+
// Configuration from environment variables
|
|
15
|
+
const FULLSTORY_API_KEY = process.env.FULLSTORY_API_KEY;
|
|
16
|
+
const FULLSTORY_DATA_CENTER = (process.env.FULLSTORY_DATA_CENTER as 'na1' | 'eu1') || 'na1';
|
|
17
|
+
const PORT = parseInt(process.env.PORT || '3000', 10);
|
|
18
|
+
const TRANSPORT = process.env.TRANSPORT || 'stdio'; // 'stdio' or 'http'
|
|
19
|
+
|
|
20
|
+
if (!FULLSTORY_API_KEY) {
|
|
21
|
+
console.error('Error: FULLSTORY_API_KEY environment variable is required');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Initialize Fullstory client
|
|
26
|
+
const fullstory = new FullstoryClient({
|
|
27
|
+
apiKey: FULLSTORY_API_KEY,
|
|
28
|
+
dataCenter: FULLSTORY_DATA_CENTER,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Define available tools
|
|
32
|
+
const tools: Tool[] = [
|
|
33
|
+
{
|
|
34
|
+
name: 'fullstory_get_user_sessions',
|
|
35
|
+
description: 'Retrieve session replay data for a specific user. Returns list of sessions with replay URLs.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
userId: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'The user ID (uid) to look up sessions for',
|
|
42
|
+
},
|
|
43
|
+
limit: {
|
|
44
|
+
type: 'number',
|
|
45
|
+
description: 'Maximum number of sessions to return (default: 20)',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ['userId'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'fullstory_get_session_replay_url',
|
|
53
|
+
description: 'Get a direct URL to watch a specific session replay in Fullstory.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
sessionId: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'The session ID to get replay URL for',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ['sessionId'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'fullstory_get_user',
|
|
67
|
+
description: 'Get detailed information about a specific user including custom properties.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
userId: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'The user ID to look up',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ['userId'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'fullstory_search_users',
|
|
81
|
+
description: 'Search for users matching specific criteria.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
email: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Filter by email address',
|
|
88
|
+
},
|
|
89
|
+
displayName: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Filter by display name',
|
|
92
|
+
},
|
|
93
|
+
customProperty: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
description: 'Filter by custom property (key-value pair)',
|
|
96
|
+
properties: {
|
|
97
|
+
key: { type: 'string' },
|
|
98
|
+
value: { type: 'string' },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'fullstory_get_user_events',
|
|
106
|
+
description: 'Retrieve events recorded for a specific user within a time range.',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
userId: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description: 'The user ID to get events for',
|
|
113
|
+
},
|
|
114
|
+
startTime: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
description: 'Start time in ISO 8601 format (e.g., 2024-01-01T00:00:00Z)',
|
|
117
|
+
},
|
|
118
|
+
endTime: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: 'End time in ISO 8601 format',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ['userId'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'fullstory_list_segments',
|
|
128
|
+
description: 'List all available segments in your Fullstory account.',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'fullstory_export_segment',
|
|
136
|
+
description: 'Start an export job for a segment. Returns an operation ID to check status.',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
segmentId: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'The segment ID to export',
|
|
143
|
+
},
|
|
144
|
+
type: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
enum: ['individuals', 'events'],
|
|
147
|
+
description: 'Export type: "individuals" for user data or "events" for event data',
|
|
148
|
+
},
|
|
149
|
+
format: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
enum: ['FORMAT_CSV', 'FORMAT_JSON', 'FORMAT_NDJSON'],
|
|
152
|
+
description: 'Export format (default: FORMAT_JSON)',
|
|
153
|
+
},
|
|
154
|
+
startTime: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
description: 'Start time for the export range (ISO 8601)',
|
|
157
|
+
},
|
|
158
|
+
endTime: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'End time for the export range (ISO 8601)',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ['segmentId', 'type'],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'fullstory_get_export_status',
|
|
168
|
+
description: 'Check the status of a segment export job. Returns progress and download URL when complete.',
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
operationId: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: 'The operation ID from the export request',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: ['operationId'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'fullstory_download_export',
|
|
182
|
+
description: 'Download the results of a completed export job.',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
downloadUrl: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
description: 'The download URL from the completed export status',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
required: ['downloadUrl'],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'fullstory_send_event',
|
|
196
|
+
description: 'Send a custom event to Fullstory for a user.',
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
userId: {
|
|
201
|
+
type: 'string',
|
|
202
|
+
description: 'The user ID to associate the event with',
|
|
203
|
+
},
|
|
204
|
+
sessionId: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
description: 'Optional session ID to associate the event with',
|
|
207
|
+
},
|
|
208
|
+
eventName: {
|
|
209
|
+
type: 'string',
|
|
210
|
+
description: 'Name of the custom event',
|
|
211
|
+
},
|
|
212
|
+
eventData: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
description: 'Additional event properties (key-value pairs)',
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
required: ['userId', 'eventName'],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
// Tool execution handler
|
|
223
|
+
async function executeTool(name: string, args: Record<string, unknown> | undefined) {
|
|
224
|
+
switch (name) {
|
|
225
|
+
case 'fullstory_get_user_sessions': {
|
|
226
|
+
const sessions = await fullstory.getUserSessions(
|
|
227
|
+
args?.userId as string,
|
|
228
|
+
args?.limit as number | undefined
|
|
229
|
+
);
|
|
230
|
+
return JSON.stringify(sessions, null, 2);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case 'fullstory_get_session_replay_url': {
|
|
234
|
+
const url = await fullstory.getSessionReplayUrl(args?.sessionId as string);
|
|
235
|
+
return `Session replay URL: ${url}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case 'fullstory_get_user': {
|
|
239
|
+
const user = await fullstory.getUser(args?.userId as string);
|
|
240
|
+
return JSON.stringify(user, null, 2);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case 'fullstory_search_users': {
|
|
244
|
+
const query: Record<string, unknown> = {};
|
|
245
|
+
if (args?.email) query.email = args.email;
|
|
246
|
+
if (args?.displayName) query.displayName = args.displayName;
|
|
247
|
+
if (args?.customProperty) {
|
|
248
|
+
const prop = args.customProperty as { key: string; value: string };
|
|
249
|
+
query[prop.key] = prop.value;
|
|
250
|
+
}
|
|
251
|
+
const users = await fullstory.searchUsers(query);
|
|
252
|
+
return JSON.stringify(users, null, 2);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case 'fullstory_get_user_events': {
|
|
256
|
+
const events = await fullstory.getUserEvents(
|
|
257
|
+
args?.userId as string,
|
|
258
|
+
args?.startTime as string | undefined,
|
|
259
|
+
args?.endTime as string | undefined
|
|
260
|
+
);
|
|
261
|
+
return JSON.stringify(events, null, 2);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'fullstory_list_segments': {
|
|
265
|
+
const segments = await fullstory.listSegments();
|
|
266
|
+
return JSON.stringify(segments, null, 2);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'fullstory_export_segment': {
|
|
270
|
+
const exportResult = await fullstory.createSegmentExport({
|
|
271
|
+
segmentId: args?.segmentId as string,
|
|
272
|
+
type: args?.type as 'individuals' | 'events',
|
|
273
|
+
format: args?.format as 'FORMAT_CSV' | 'FORMAT_JSON' | 'FORMAT_NDJSON' | undefined,
|
|
274
|
+
startTime: args?.startTime as string | undefined,
|
|
275
|
+
endTime: args?.endTime as string | undefined,
|
|
276
|
+
});
|
|
277
|
+
return `Export started. Operation ID: ${exportResult.operationId}\nUse fullstory_get_export_status to check progress.`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
case 'fullstory_get_export_status': {
|
|
281
|
+
const status = await fullstory.getExportStatus(args?.operationId as string);
|
|
282
|
+
return JSON.stringify(status, null, 2);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'fullstory_download_export': {
|
|
286
|
+
const data = await fullstory.downloadExport(args?.downloadUrl as string);
|
|
287
|
+
return JSON.stringify(data, null, 2);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'fullstory_send_event': {
|
|
291
|
+
await fullstory.sendEvent({
|
|
292
|
+
userId: args?.userId as string,
|
|
293
|
+
sessionId: args?.sessionId as string | undefined,
|
|
294
|
+
eventName: args?.eventName as string,
|
|
295
|
+
eventData: args?.eventData as Record<string, unknown> | undefined,
|
|
296
|
+
});
|
|
297
|
+
return 'Event sent successfully.';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
default:
|
|
301
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Create server instance
|
|
306
|
+
function createMcpServer() {
|
|
307
|
+
const server = new Server(
|
|
308
|
+
{
|
|
309
|
+
name: 'fullstory-mcp-server',
|
|
310
|
+
version: '1.0.0',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
capabilities: {
|
|
314
|
+
tools: {},
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Handle tool listing
|
|
320
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
321
|
+
return { tools };
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Handle tool execution
|
|
325
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
326
|
+
const { name, arguments: args } = request.params;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const result = await executeTool(name, args);
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: 'text', text: result }],
|
|
332
|
+
};
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
|
337
|
+
isError: true,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return server;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Start with stdio transport (for local use)
|
|
346
|
+
async function startStdio() {
|
|
347
|
+
const server = createMcpServer();
|
|
348
|
+
const transport = new StdioServerTransport();
|
|
349
|
+
await server.connect(transport);
|
|
350
|
+
console.error('Fullstory MCP Server running on stdio');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Start with HTTP/SSE transport (for cloud deployment)
|
|
354
|
+
async function startHttp() {
|
|
355
|
+
const sessions = new Map<string, SSEServerTransport>();
|
|
356
|
+
|
|
357
|
+
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
358
|
+
// CORS headers
|
|
359
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
360
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
361
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
362
|
+
|
|
363
|
+
if (req.method === 'OPTIONS') {
|
|
364
|
+
res.writeHead(204);
|
|
365
|
+
res.end();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Health check endpoint
|
|
370
|
+
if (req.url === '/health') {
|
|
371
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
372
|
+
res.end(JSON.stringify({ status: 'ok', server: 'fullstory-mcp-server' }));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// SSE endpoint for MCP connection
|
|
377
|
+
if (req.url === '/sse' && req.method === 'GET') {
|
|
378
|
+
const server = createMcpServer();
|
|
379
|
+
const transport = new SSEServerTransport('/message', res);
|
|
380
|
+
const sessionId = Date.now().toString();
|
|
381
|
+
sessions.set(sessionId, transport);
|
|
382
|
+
|
|
383
|
+
res.on('close', () => {
|
|
384
|
+
sessions.delete(sessionId);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
await server.connect(transport);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Message endpoint for SSE
|
|
392
|
+
if (req.url === '/message' && req.method === 'POST') {
|
|
393
|
+
let body = '';
|
|
394
|
+
req.on('data', chunk => { body += chunk; });
|
|
395
|
+
req.on('end', async () => {
|
|
396
|
+
try {
|
|
397
|
+
// Find active session and forward message
|
|
398
|
+
for (const transport of sessions.values()) {
|
|
399
|
+
await transport.handlePostMessage(req, res, body);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
res.writeHead(404);
|
|
403
|
+
res.end('No active session');
|
|
404
|
+
} catch (error) {
|
|
405
|
+
res.writeHead(500);
|
|
406
|
+
res.end('Error processing message');
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Default response
|
|
413
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
414
|
+
res.end(JSON.stringify({
|
|
415
|
+
name: 'fullstory-mcp-server',
|
|
416
|
+
version: '1.0.0',
|
|
417
|
+
endpoints: {
|
|
418
|
+
health: '/health',
|
|
419
|
+
sse: '/sse',
|
|
420
|
+
message: '/message'
|
|
421
|
+
}
|
|
422
|
+
}));
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
httpServer.listen(PORT, () => {
|
|
426
|
+
console.log(`Fullstory MCP Server running on http://0.0.0.0:${PORT}`);
|
|
427
|
+
console.log(`SSE endpoint: http://0.0.0.0:${PORT}/sse`);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Main entry point
|
|
432
|
+
async function main() {
|
|
433
|
+
if (TRANSPORT === 'http') {
|
|
434
|
+
await startHttp();
|
|
435
|
+
} else {
|
|
436
|
+
await startStdio();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
main().catch((error) => {
|
|
441
|
+
console.error('Fatal error:', error);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|