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/src/cli.ts ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'child_process';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ import * as readline from 'readline';
7
+
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout
11
+ });
12
+
13
+ function question(prompt: string): Promise<string> {
14
+ return new Promise((resolve) => {
15
+ rl.question(prompt, resolve);
16
+ });
17
+ }
18
+
19
+ async function setup() {
20
+ console.log('\n🔧 Fullstory MCP Server Setup\n');
21
+
22
+ const apiKey = await question('Enter your Fullstory API key: ');
23
+ const dataCenter = apiKey.startsWith('eu1.') ? 'eu1' : 'na1';
24
+
25
+ const configDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
26
+ const configPath = path.join(configDir, 'claude_desktop_config.json');
27
+
28
+ // Ensure directory exists
29
+ if (!fs.existsSync(configDir)) {
30
+ fs.mkdirSync(configDir, { recursive: true });
31
+ }
32
+
33
+ // Read existing config or create new
34
+ let config: any = {};
35
+ if (fs.existsSync(configPath)) {
36
+ try {
37
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
38
+ } catch (e) {
39
+ config = {};
40
+ }
41
+ }
42
+
43
+ // Find the installed package path
44
+ const packagePath = path.dirname(__dirname);
45
+ const serverPath = path.join(packagePath, 'dist', 'index.js');
46
+
47
+ // Add fullstory server config
48
+ config.mcpServers = config.mcpServers || {};
49
+ config.mcpServers.fullstory = {
50
+ command: 'node',
51
+ args: [serverPath],
52
+ env: {
53
+ FULLSTORY_API_KEY: apiKey,
54
+ FULLSTORY_DATA_CENTER: dataCenter,
55
+ TRANSPORT: 'stdio'
56
+ }
57
+ };
58
+
59
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
60
+
61
+ console.log('\n✅ Setup complete!');
62
+ console.log(` Data center: ${dataCenter}`);
63
+ console.log('\n👉 Please restart Claude Desktop (Cmd+Q, then reopen)\n');
64
+
65
+ rl.close();
66
+ }
67
+
68
+ const command = process.argv[2];
69
+
70
+ if (command === 'setup') {
71
+ setup().catch(console.error);
72
+ } else {
73
+ // Run as MCP server
74
+ import('./index.js');
75
+ }
@@ -0,0 +1,231 @@
1
+ import axios, { AxiosInstance, AxiosError } from 'axios';
2
+
3
+ export interface FullstoryConfig {
4
+ apiKey: string;
5
+ dataCenter?: 'na1' | 'eu1'; // na1 = North America, eu1 = Europe
6
+ }
7
+
8
+ export interface Session {
9
+ sessionId: string;
10
+ userId: string;
11
+ createdTime: string;
12
+ fsUrl: string;
13
+ deviceType?: string;
14
+ browser?: string;
15
+ location?: string;
16
+ }
17
+
18
+ export interface UserEvent {
19
+ eventId: string;
20
+ eventType: string;
21
+ eventTime: string;
22
+ eventData?: Record<string, unknown>;
23
+ sessionId?: string;
24
+ }
25
+
26
+ export interface User {
27
+ id: string;
28
+ uid?: string;
29
+ displayName?: string;
30
+ email?: string;
31
+ properties?: Record<string, unknown>;
32
+ }
33
+
34
+ export interface SegmentExport {
35
+ operationId: string;
36
+ status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
37
+ progress?: number;
38
+ downloadUrl?: string;
39
+ }
40
+
41
+ export interface ExportRequest {
42
+ segmentId: string;
43
+ type: 'individuals' | 'events';
44
+ format?: 'FORMAT_CSV' | 'FORMAT_JSON' | 'FORMAT_NDJSON';
45
+ startTime?: string;
46
+ endTime?: string;
47
+ }
48
+
49
+ export class FullstoryClient {
50
+ private client: AxiosInstance;
51
+ private dataCenter: string;
52
+
53
+ constructor(config: FullstoryConfig) {
54
+ this.dataCenter = config.dataCenter || 'na1';
55
+
56
+ const baseURL = this.dataCenter === 'eu1'
57
+ ? 'https://api.eu1.fullstory.com'
58
+ : 'https://api.fullstory.com';
59
+
60
+ this.client = axios.create({
61
+ baseURL,
62
+ headers: {
63
+ 'Authorization': `Basic ${config.apiKey}`,
64
+ 'Content-Type': 'application/json',
65
+ },
66
+ });
67
+ }
68
+
69
+ private handleError(error: unknown): never {
70
+ if (error instanceof AxiosError) {
71
+ const status = error.response?.status;
72
+ const message = error.response?.data?.message || error.message;
73
+
74
+ if (status === 401) {
75
+ throw new Error('Authentication failed. Check your API key.');
76
+ } else if (status === 403) {
77
+ throw new Error('Access forbidden. Your API key may not have sufficient permissions.');
78
+ } else if (status === 429) {
79
+ throw new Error('Rate limit exceeded. Please wait before making more requests.');
80
+ } else if (status === 404) {
81
+ throw new Error('Resource not found.');
82
+ }
83
+ throw new Error(`Fullstory API error: ${message}`);
84
+ }
85
+ throw error;
86
+ }
87
+
88
+ // ============ SESSIONS ============
89
+
90
+ async getUserSessions(userId: string, limit: number = 20): Promise<Session[]> {
91
+ try {
92
+ const response = await this.client.get('/v1/sessions', {
93
+ params: { uid: userId, limit },
94
+ });
95
+ return response.data.sessions || [];
96
+ } catch (error) {
97
+ this.handleError(error);
98
+ }
99
+ }
100
+
101
+ async getSessionReplayUrl(sessionId: string): Promise<string> {
102
+ try {
103
+ const response = await this.client.get(`/v1/sessions/${sessionId}`);
104
+ return response.data.fsUrl;
105
+ } catch (error) {
106
+ this.handleError(error);
107
+ }
108
+ }
109
+
110
+ // ============ USERS ============
111
+
112
+ async getUser(userId: string): Promise<User> {
113
+ try {
114
+ const response = await this.client.get(`/v2/users/${userId}`);
115
+ return response.data;
116
+ } catch (error) {
117
+ this.handleError(error);
118
+ }
119
+ }
120
+
121
+ async searchUsers(query: Record<string, unknown>): Promise<User[]> {
122
+ try {
123
+ const response = await this.client.post('/v2/users/search', query);
124
+ return response.data.users || [];
125
+ } catch (error) {
126
+ this.handleError(error);
127
+ }
128
+ }
129
+
130
+ // ============ EVENTS ============
131
+
132
+ async getUserEvents(userId: string, startTime?: string, endTime?: string): Promise<UserEvent[]> {
133
+ try {
134
+ const params: Record<string, string> = { uid: userId };
135
+ if (startTime) params.start = startTime;
136
+ if (endTime) params.end = endTime;
137
+
138
+ const response = await this.client.get('/v1/users/events', { params });
139
+ return response.data.events || [];
140
+ } catch (error) {
141
+ this.handleError(error);
142
+ }
143
+ }
144
+
145
+ async sendEvent(event: {
146
+ userId: string;
147
+ sessionId?: string;
148
+ eventName: string;
149
+ eventData?: Record<string, unknown>;
150
+ timestamp?: string;
151
+ }): Promise<void> {
152
+ try {
153
+ await this.client.post('/v2/events', {
154
+ user: { uid: event.userId },
155
+ session: event.sessionId ? { id: event.sessionId } : undefined,
156
+ name: event.eventName,
157
+ properties: event.eventData,
158
+ timestamp: event.timestamp || new Date().toISOString(),
159
+ });
160
+ } catch (error) {
161
+ this.handleError(error);
162
+ }
163
+ }
164
+
165
+ // ============ SEGMENT EXPORTS ============
166
+
167
+ async createSegmentExport(request: ExportRequest): Promise<SegmentExport> {
168
+ try {
169
+ const response = await this.client.post('/segments/v1/exports', {
170
+ segmentId: request.segmentId,
171
+ type: request.type.toUpperCase(),
172
+ format: request.format || 'FORMAT_JSON',
173
+ timeRange: {
174
+ start: request.startTime,
175
+ end: request.endTime,
176
+ },
177
+ });
178
+ return {
179
+ operationId: response.data.operationId,
180
+ status: 'PENDING',
181
+ };
182
+ } catch (error) {
183
+ this.handleError(error);
184
+ }
185
+ }
186
+
187
+ async getExportStatus(operationId: string): Promise<SegmentExport> {
188
+ try {
189
+ const response = await this.client.get(`/operations/v1/${operationId}`);
190
+ return {
191
+ operationId,
192
+ status: response.data.state,
193
+ progress: response.data.progress,
194
+ downloadUrl: response.data.results?.downloadUrl,
195
+ };
196
+ } catch (error) {
197
+ this.handleError(error);
198
+ }
199
+ }
200
+
201
+ async downloadExport(downloadUrl: string): Promise<unknown> {
202
+ try {
203
+ const response = await axios.get(downloadUrl);
204
+ return response.data;
205
+ } catch (error) {
206
+ this.handleError(error);
207
+ }
208
+ }
209
+
210
+ // ============ SEGMENTS ============
211
+
212
+ async listSegments(): Promise<Array<{ id: string; name: string; description?: string }>> {
213
+ try {
214
+ const response = await this.client.get('/segments/v1');
215
+ return response.data.segments || [];
216
+ } catch (error) {
217
+ this.handleError(error);
218
+ }
219
+ }
220
+
221
+ // ============ AUDIT / SETTINGS ============
222
+
223
+ async getAuditTrail(settingType: string): Promise<unknown> {
224
+ try {
225
+ const response = await this.client.get(`/settings/v1/audit/${settingType}`);
226
+ return response.data;
227
+ } catch (error) {
228
+ this.handleError(error);
229
+ }
230
+ }
231
+ }