@posthog/mcp 0.0.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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/index.cjs +2695 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +240 -0
- package/dist/index.d.mts +239 -0
- package/dist/index.mjs +2692 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +103 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2692 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { PostHog } from "posthog-node";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { CallToolRequestSchema, InitializeRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
//#region src/modules/logging.ts
|
|
6
|
+
let fsModule$1 = null;
|
|
7
|
+
let logFilePath = null;
|
|
8
|
+
let initAttempted = false;
|
|
9
|
+
let useConsoleFallback = false;
|
|
10
|
+
/**
|
|
11
|
+
* Attempts to initialize Node.js file logging.
|
|
12
|
+
* Falls back to console.log in edge environments where fs/os modules are unavailable.
|
|
13
|
+
*/
|
|
14
|
+
function tryInitSync() {
|
|
15
|
+
if (initAttempted) return;
|
|
16
|
+
initAttempted = true;
|
|
17
|
+
try {
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const fs = require("node:fs");
|
|
20
|
+
const os = require("node:os");
|
|
21
|
+
const path = require("node:path");
|
|
22
|
+
const home = os.homedir?.();
|
|
23
|
+
if (home) {
|
|
24
|
+
fsModule$1 = fs;
|
|
25
|
+
logFilePath = path.join(home, "posthog-mcp-analytics.log");
|
|
26
|
+
} else useConsoleFallback = true;
|
|
27
|
+
} catch {
|
|
28
|
+
useConsoleFallback = true;
|
|
29
|
+
fsModule$1 = null;
|
|
30
|
+
logFilePath = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function writeToLog(message) {
|
|
34
|
+
tryInitSync();
|
|
35
|
+
const logEntry = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}`;
|
|
36
|
+
if (useConsoleFallback) return;
|
|
37
|
+
if (!(logFilePath && fsModule$1)) return;
|
|
38
|
+
try {
|
|
39
|
+
if (fsModule$1.existsSync(logFilePath)) fsModule$1.appendFileSync(logFilePath, `${logEntry}\n`);
|
|
40
|
+
else fsModule$1.writeFileSync(logFilePath, `${logEntry}\n`);
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/modules/compatibility.ts
|
|
45
|
+
/**
|
|
46
|
+
* PostHog MCP analytics Compatibility Module
|
|
47
|
+
*
|
|
48
|
+
* This module ensures compatibility with Model Context Protocol TypeScript SDK.
|
|
49
|
+
* PostHog MCP analytics only supports MCP SDK version 1.11 and above.
|
|
50
|
+
*
|
|
51
|
+
* Version 1.11+ is required because it introduced stable APIs for:
|
|
52
|
+
* - Tool registration and handling
|
|
53
|
+
* - Request handler access patterns
|
|
54
|
+
* - Client version detection
|
|
55
|
+
* - Server info structure
|
|
56
|
+
*/
|
|
57
|
+
function logCompatibilityWarning() {
|
|
58
|
+
writeToLog("PostHog MCP analytics SDK Compatibility: This version only supports Model Context Protocol TypeScript SDK v1.11 and above. Please upgrade if using an older version.");
|
|
59
|
+
}
|
|
60
|
+
function isHighLevelServer(server) {
|
|
61
|
+
return !!server && typeof server === "object" && "server" in server && !!server.server && typeof server.server === "object";
|
|
62
|
+
}
|
|
63
|
+
function isCompatibleServerType(server) {
|
|
64
|
+
if (!server || typeof server !== "object") {
|
|
65
|
+
logCompatibilityWarning();
|
|
66
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: Server must be an object. Ensure you're using MCP SDK v1.11 or higher.");
|
|
67
|
+
}
|
|
68
|
+
if (isHighLevelServer(server)) {
|
|
69
|
+
if (!server._registeredTools || typeof server._registeredTools !== "object") {
|
|
70
|
+
logCompatibilityWarning();
|
|
71
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: High-level server must have _registeredTools object. This requires MCP SDK v1.11 or higher.");
|
|
72
|
+
}
|
|
73
|
+
if (typeof server.tool !== "function") {
|
|
74
|
+
logCompatibilityWarning();
|
|
75
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: High-level server must have tool() method. This requires MCP SDK v1.11 or higher.");
|
|
76
|
+
}
|
|
77
|
+
const targetServer = server.server;
|
|
78
|
+
validateLowLevelServer(targetServer);
|
|
79
|
+
return server;
|
|
80
|
+
}
|
|
81
|
+
validateLowLevelServer(server);
|
|
82
|
+
return server;
|
|
83
|
+
}
|
|
84
|
+
function validateLowLevelServer(server) {
|
|
85
|
+
if (!server || typeof server !== "object") {
|
|
86
|
+
logCompatibilityWarning();
|
|
87
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: Server must be an object. Ensure you're using MCP SDK v1.11 or higher.");
|
|
88
|
+
}
|
|
89
|
+
const serverRecord = server;
|
|
90
|
+
if (typeof serverRecord.setRequestHandler !== "function") {
|
|
91
|
+
logCompatibilityWarning();
|
|
92
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: Server must have a setRequestHandler method. This requires MCP SDK v1.11 or higher.");
|
|
93
|
+
}
|
|
94
|
+
if (!(serverRecord._requestHandlers && serverRecord._requestHandlers instanceof Map)) {
|
|
95
|
+
logCompatibilityWarning();
|
|
96
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: Server._requestHandlers is not accessible. This requires MCP SDK v1.11 or higher.");
|
|
97
|
+
}
|
|
98
|
+
if (typeof serverRecord._requestHandlers.get !== "function") {
|
|
99
|
+
logCompatibilityWarning();
|
|
100
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: Server._requestHandlers must be a Map with a get method. This requires MCP SDK v1.11 or higher.");
|
|
101
|
+
}
|
|
102
|
+
if (typeof serverRecord.getClientVersion !== "function") {
|
|
103
|
+
logCompatibilityWarning();
|
|
104
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: Server.getClientVersion must be a function. This requires MCP SDK v1.11 or higher.");
|
|
105
|
+
}
|
|
106
|
+
if (!serverRecord._serverInfo || typeof serverRecord._serverInfo !== "object" || !("name" in serverRecord._serverInfo)) {
|
|
107
|
+
logCompatibilityWarning();
|
|
108
|
+
throw new Error("PostHog MCP analytics SDK compatibility error: Server._serverInfo is not accessible or missing name. This requires MCP SDK v1.11 or higher.");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function getMCPCompatibleErrorMessage(error) {
|
|
112
|
+
if (error instanceof Error) try {
|
|
113
|
+
return JSON.stringify(error, Object.getOwnPropertyNames(error));
|
|
114
|
+
} catch {
|
|
115
|
+
return "Unknown error";
|
|
116
|
+
}
|
|
117
|
+
else if (typeof error === "string") return error;
|
|
118
|
+
else if (typeof error === "object" && error !== null) return JSON.stringify(error);
|
|
119
|
+
return "Unknown error";
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/modules/ids.ts
|
|
123
|
+
function newPrefixedId(prefix) {
|
|
124
|
+
return `${prefix}_${randomUUID()}`;
|
|
125
|
+
}
|
|
126
|
+
function deterministicPrefixedId(prefix, input) {
|
|
127
|
+
return `${prefix}_${createHash("sha256").update(input).digest("hex").slice(0, 32)}`;
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/modules/event-types.ts
|
|
131
|
+
const MCPAnalyticsEventType = {
|
|
132
|
+
identify: "posthog:identify",
|
|
133
|
+
custom: "posthog:custom",
|
|
134
|
+
mcpInitialize: "mcp:initialize",
|
|
135
|
+
mcpPromptsGet: "mcp:prompts/get",
|
|
136
|
+
mcpPromptsList: "mcp:prompts/list",
|
|
137
|
+
mcpResourcesList: "mcp:resources/list",
|
|
138
|
+
mcpResourcesRead: "mcp:resources/read",
|
|
139
|
+
mcpToolsCall: "mcp:tools/call",
|
|
140
|
+
mcpToolsList: "mcp:tools/list"
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/modules/validation.ts
|
|
144
|
+
const TAG_KEY_REGEX = /^[a-zA-Z0-9$_.:\- ]+$/;
|
|
145
|
+
const MAX_TAG_KEY_LENGTH = 32;
|
|
146
|
+
const MAX_TAG_VALUE_LENGTH = 200;
|
|
147
|
+
const MAX_TAG_ENTRIES = 50;
|
|
148
|
+
/**
|
|
149
|
+
* Validates and filters a tags object against PostHog MCP analytics tag constraints.
|
|
150
|
+
* Invalid entries are logged as warnings and dropped.
|
|
151
|
+
* Returns null if no valid entries remain.
|
|
152
|
+
*/
|
|
153
|
+
function validateTags(tags) {
|
|
154
|
+
const entries = Object.entries(tags);
|
|
155
|
+
if (entries.length === 0) return null;
|
|
156
|
+
const valid = [];
|
|
157
|
+
for (const [key, value] of entries) {
|
|
158
|
+
if (typeof key !== "string" || !TAG_KEY_REGEX.test(key)) {
|
|
159
|
+
writeToLog(`Dropping invalid tag: "${String(key)}" — key contains invalid characters or is empty`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (key.length > MAX_TAG_KEY_LENGTH) {
|
|
163
|
+
writeToLog(`Dropping invalid tag: "${key}" — key exceeds max length of ${MAX_TAG_KEY_LENGTH}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (typeof value !== "string") {
|
|
167
|
+
writeToLog(`Dropping invalid tag: "${key}" — non-string value (got ${typeof value})`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (value.length > MAX_TAG_VALUE_LENGTH) {
|
|
171
|
+
writeToLog(`Dropping invalid tag: "${key}" — value exceeds max length of ${MAX_TAG_VALUE_LENGTH}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (value.includes("\n")) {
|
|
175
|
+
writeToLog(`Dropping invalid tag: "${key}" — value contains newline character`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
valid.push([key, value]);
|
|
179
|
+
}
|
|
180
|
+
if (valid.length === 0) return null;
|
|
181
|
+
if (valid.length > MAX_TAG_ENTRIES) {
|
|
182
|
+
writeToLog(`Dropping ${valid.length - MAX_TAG_ENTRIES} tag(s) — exceeds maximum of ${MAX_TAG_ENTRIES} entries per event`);
|
|
183
|
+
valid.length = MAX_TAG_ENTRIES;
|
|
184
|
+
}
|
|
185
|
+
return Object.fromEntries(valid);
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/modules/internal.ts
|
|
189
|
+
/**
|
|
190
|
+
* Simple LRU cache for session identities.
|
|
191
|
+
* Prevents memory leaks by capping at maxSize entries.
|
|
192
|
+
* This cache persists across server instance restarts.
|
|
193
|
+
*/
|
|
194
|
+
var IdentityCache = class {
|
|
195
|
+
constructor(maxSize = 1e3) {
|
|
196
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
197
|
+
this.maxSize = maxSize;
|
|
198
|
+
}
|
|
199
|
+
get(sessionId) {
|
|
200
|
+
const entry = this.cache.get(sessionId);
|
|
201
|
+
if (entry) {
|
|
202
|
+
entry.timestamp = Date.now();
|
|
203
|
+
this.cache.delete(sessionId);
|
|
204
|
+
this.cache.set(sessionId, entry);
|
|
205
|
+
return entry.identity;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
set(sessionId, identity) {
|
|
209
|
+
this.cache.delete(sessionId);
|
|
210
|
+
if (this.cache.size >= this.maxSize) {
|
|
211
|
+
const oldestKey = this.cache.keys().next().value;
|
|
212
|
+
if (oldestKey !== void 0) this.cache.delete(oldestKey);
|
|
213
|
+
}
|
|
214
|
+
this.cache.set(sessionId, {
|
|
215
|
+
identity,
|
|
216
|
+
timestamp: Date.now()
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
has(sessionId) {
|
|
220
|
+
return this.cache.has(sessionId);
|
|
221
|
+
}
|
|
222
|
+
size() {
|
|
223
|
+
return this.cache.size;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const _globalIdentityCache = new IdentityCache(1e3);
|
|
227
|
+
const _serverTracking = /* @__PURE__ */ new WeakMap();
|
|
228
|
+
function getServerTrackingData(server) {
|
|
229
|
+
return _serverTracking.get(server);
|
|
230
|
+
}
|
|
231
|
+
function setServerTrackingData(server, data) {
|
|
232
|
+
_serverTracking.set(server, data);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Deep comparison of two UserIdentity objects
|
|
236
|
+
*/
|
|
237
|
+
function areIdentitiesEqual(a, b) {
|
|
238
|
+
if (a.userId !== b.userId) return false;
|
|
239
|
+
if (a.userName !== b.userName) return false;
|
|
240
|
+
const aData = a.userData || {};
|
|
241
|
+
const bData = b.userData || {};
|
|
242
|
+
const aKeys = Object.keys(aData);
|
|
243
|
+
const bKeys = Object.keys(bData);
|
|
244
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
245
|
+
for (const key of aKeys) {
|
|
246
|
+
if (!(key in bData)) return false;
|
|
247
|
+
if (JSON.stringify(aData[key]) !== JSON.stringify(bData[key])) return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Merges two UserIdentity objects, overwriting userId and userName,
|
|
253
|
+
* but merging userData fields
|
|
254
|
+
*/
|
|
255
|
+
function mergeIdentities(previous, next) {
|
|
256
|
+
if (!previous) return next;
|
|
257
|
+
return {
|
|
258
|
+
userId: next.userId,
|
|
259
|
+
userName: next.userName,
|
|
260
|
+
userData: {
|
|
261
|
+
...previous.userData || {},
|
|
262
|
+
...next.userData || {}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Handles user identification for a request.
|
|
268
|
+
* Calls the identify function if configured, compares with previous identity,
|
|
269
|
+
* and publishes an identify event only if the identity has changed.
|
|
270
|
+
*
|
|
271
|
+
* @param server - The MCP server instance
|
|
272
|
+
* @param data - The server tracking data
|
|
273
|
+
* @param request - The request object to pass to identify function
|
|
274
|
+
* @param extra - Optional extra parameters containing headers, sessionId, etc.
|
|
275
|
+
*/
|
|
276
|
+
async function handleIdentify(server, data, request, extra) {
|
|
277
|
+
if (!data.options.identify) return;
|
|
278
|
+
const sessionId = data.sessionId;
|
|
279
|
+
const identifyEvent = {
|
|
280
|
+
sessionId,
|
|
281
|
+
resourceName: getRequestResourceName(request),
|
|
282
|
+
eventType: MCPAnalyticsEventType.identify,
|
|
283
|
+
parameters: {
|
|
284
|
+
request,
|
|
285
|
+
extra
|
|
286
|
+
},
|
|
287
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
288
|
+
redactionFn: data.options.redactSensitiveInformation
|
|
289
|
+
};
|
|
290
|
+
try {
|
|
291
|
+
const identityResult = await data.options.identify(request, extra);
|
|
292
|
+
if (identityResult) {
|
|
293
|
+
const currentSessionId = data.sessionId;
|
|
294
|
+
const previousIdentity = _globalIdentityCache.get(currentSessionId);
|
|
295
|
+
const mergedIdentity = mergeIdentities(previousIdentity, identityResult);
|
|
296
|
+
const hasChanged = !(previousIdentity && areIdentitiesEqual(previousIdentity, mergedIdentity));
|
|
297
|
+
_globalIdentityCache.set(currentSessionId, mergedIdentity);
|
|
298
|
+
data.identifiedSessions.set(data.sessionId, mergedIdentity);
|
|
299
|
+
if (hasChanged) {
|
|
300
|
+
writeToLog(`Identified session ${currentSessionId} with identity: ${JSON.stringify(mergedIdentity)}`);
|
|
301
|
+
publishEvent(server, identifyEvent);
|
|
302
|
+
}
|
|
303
|
+
} else writeToLog(`Warning: Supplied identify function returned null for session ${sessionId}`);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
writeToLog(`Error: User supplied identify function threw an error while identifying session ${sessionId} - ${error}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Resolves the eventTags callback, validates the result, and returns validated tags.
|
|
310
|
+
* Returns null if no callback configured, callback returns nullish, or callback throws.
|
|
311
|
+
*/
|
|
312
|
+
async function resolveEventTags(data, request, extra) {
|
|
313
|
+
if (!data.options.eventTags) return null;
|
|
314
|
+
try {
|
|
315
|
+
const raw = await data.options.eventTags(request, extra) ?? null;
|
|
316
|
+
if (!raw) return null;
|
|
317
|
+
return validateTags(raw);
|
|
318
|
+
} catch (e) {
|
|
319
|
+
writeToLog(`eventTags callback error: ${e}`);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Resolves the eventProperties callback and returns the result.
|
|
325
|
+
* Returns null if no callback configured, callback returns nullish, or callback throws.
|
|
326
|
+
*/
|
|
327
|
+
async function resolveEventProperties(data, request, extra) {
|
|
328
|
+
if (!data.options.eventProperties) return null;
|
|
329
|
+
try {
|
|
330
|
+
return await data.options.eventProperties(request, extra) ?? null;
|
|
331
|
+
} catch (e) {
|
|
332
|
+
writeToLog(`eventProperties callback error: ${e}`);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function getRequestResourceName(request) {
|
|
337
|
+
if (!request || typeof request !== "object" || !("params" in request)) return "Unknown";
|
|
338
|
+
const params = request.params;
|
|
339
|
+
if (!params || typeof params !== "object" || !("name" in params)) return "Unknown";
|
|
340
|
+
return typeof params.name === "string" ? params.name : "Unknown";
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/modules/constants.ts
|
|
344
|
+
const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = `Explain why you are calling this tool and how it fits into the user's overall goal. This parameter is used for analytics and user intent tracking. YOU MUST provide 15-25 words (count carefully). NEVER use first person ('I', 'we', 'you') - maintain third-person perspective. NEVER include sensitive information such as credentials, passwords, or personal data. Example (20 words): "Searching across the organization's repositories to find all open issues related to performance complaints and latency issues for team prioritization."`;
|
|
345
|
+
const POSTHOG_MCP_ANALYTICS_SOURCE = "posthog_mcp_analytics";
|
|
346
|
+
const PostHogMCPAnalyticsProperty = {
|
|
347
|
+
AiInputState: "$ai_input_state",
|
|
348
|
+
AiIsError: "$ai_is_error",
|
|
349
|
+
AiLatency: "$ai_latency",
|
|
350
|
+
AiOutputState: "$ai_output_state",
|
|
351
|
+
AiProduct: "$ai_product",
|
|
352
|
+
AiSessionId: "$ai_session_id",
|
|
353
|
+
AiSpanId: "$ai_span_id",
|
|
354
|
+
AiSpanName: "$ai_span_name",
|
|
355
|
+
AiTraceId: "$ai_trace_id",
|
|
356
|
+
ClientName: "$mcp_client_name",
|
|
357
|
+
ClientVersion: "$mcp_client_version",
|
|
358
|
+
DurationMs: "$mcp_duration_ms",
|
|
359
|
+
IsError: "$mcp_is_error",
|
|
360
|
+
Intent: "$mcp_intent",
|
|
361
|
+
Parameters: "$mcp_parameters",
|
|
362
|
+
ResourceName: "$mcp_resource_name",
|
|
363
|
+
Response: "$mcp_response",
|
|
364
|
+
ServerName: "$mcp_server_name",
|
|
365
|
+
ServerVersion: "$mcp_server_version",
|
|
366
|
+
SessionId: "$session_id",
|
|
367
|
+
Source: "$mcp_source",
|
|
368
|
+
ToolName: "$mcp_tool_name"
|
|
369
|
+
};
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/modules/posthog-events.ts
|
|
372
|
+
const MCP_EVENT_PREFIX_REGEX = /^mcp:/;
|
|
373
|
+
const SLASH_REGEX = /\//g;
|
|
374
|
+
function getDistinctId(event) {
|
|
375
|
+
return event.identifyActorGivenId || event.sessionId || "anonymous";
|
|
376
|
+
}
|
|
377
|
+
function getTimestamp(event) {
|
|
378
|
+
return event.timestamp ? event.timestamp.toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
379
|
+
}
|
|
380
|
+
function buildPostHogCaptureEvents(event, options = {}) {
|
|
381
|
+
const batch = [buildCaptureEvent(event, options)];
|
|
382
|
+
if (event.isError && event.error) batch.push(buildExceptionEvent(event));
|
|
383
|
+
if (shouldBuildAISpan(event, options)) batch.push(buildAISpanEvent(event));
|
|
384
|
+
return batch;
|
|
385
|
+
}
|
|
386
|
+
function buildCaptureEvent(event, options) {
|
|
387
|
+
const distinctId = getDistinctId(event);
|
|
388
|
+
const eventName = mapEventType(event.eventType);
|
|
389
|
+
const timestamp = getTimestamp(event);
|
|
390
|
+
const properties = {
|
|
391
|
+
[PostHogMCPAnalyticsProperty.SessionId]: event.sessionId,
|
|
392
|
+
[PostHogMCPAnalyticsProperty.Source]: POSTHOG_MCP_ANALYTICS_SOURCE
|
|
393
|
+
};
|
|
394
|
+
addCommonEventProperties(event, properties);
|
|
395
|
+
addTraceReferenceProperties(event, properties, options);
|
|
396
|
+
addCustomEventProperties(event, properties);
|
|
397
|
+
return {
|
|
398
|
+
event: eventName,
|
|
399
|
+
distinct_id: distinctId,
|
|
400
|
+
properties,
|
|
401
|
+
timestamp,
|
|
402
|
+
type: "capture"
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function shouldBuildAISpan(event, options) {
|
|
406
|
+
return options.enableAITracing === true && event.eventType === MCPAnalyticsEventType.mcpToolsCall;
|
|
407
|
+
}
|
|
408
|
+
function getAITraceId(event) {
|
|
409
|
+
return event.sessionId;
|
|
410
|
+
}
|
|
411
|
+
function getAISpanId(event) {
|
|
412
|
+
return event.id;
|
|
413
|
+
}
|
|
414
|
+
function addTraceReferenceProperties(event, properties, options) {
|
|
415
|
+
if (!shouldBuildAISpan(event, options)) return;
|
|
416
|
+
properties[PostHogMCPAnalyticsProperty.AiTraceId] = getAITraceId(event);
|
|
417
|
+
properties[PostHogMCPAnalyticsProperty.AiSpanId] = getAISpanId(event);
|
|
418
|
+
}
|
|
419
|
+
function addCommonEventProperties(event, properties) {
|
|
420
|
+
if (event.resourceName) {
|
|
421
|
+
properties[PostHogMCPAnalyticsProperty.ResourceName] = event.resourceName;
|
|
422
|
+
if (event.eventType === MCPAnalyticsEventType.mcpToolsCall) properties[PostHogMCPAnalyticsProperty.ToolName] = event.resourceName;
|
|
423
|
+
}
|
|
424
|
+
if (event.duration !== void 0) properties[PostHogMCPAnalyticsProperty.DurationMs] = event.duration;
|
|
425
|
+
if (event.serverName) properties[PostHogMCPAnalyticsProperty.ServerName] = event.serverName;
|
|
426
|
+
if (event.serverVersion) properties[PostHogMCPAnalyticsProperty.ServerVersion] = event.serverVersion;
|
|
427
|
+
if (event.clientName) properties[PostHogMCPAnalyticsProperty.ClientName] = event.clientName;
|
|
428
|
+
if (event.clientVersion) properties[PostHogMCPAnalyticsProperty.ClientVersion] = event.clientVersion;
|
|
429
|
+
if (event.userIntent) properties[PostHogMCPAnalyticsProperty.Intent] = event.userIntent;
|
|
430
|
+
if (event.isError !== void 0) properties[PostHogMCPAnalyticsProperty.IsError] = event.isError;
|
|
431
|
+
if (event.parameters !== void 0) properties[PostHogMCPAnalyticsProperty.Parameters] = event.parameters;
|
|
432
|
+
if (event.response !== void 0) properties[PostHogMCPAnalyticsProperty.Response] = event.response;
|
|
433
|
+
const $set = {};
|
|
434
|
+
if (event.identifyActorName) $set.name = event.identifyActorName;
|
|
435
|
+
if (event.identifyActorData) Object.assign($set, event.identifyActorData);
|
|
436
|
+
if (Object.keys($set).length > 0) properties.$set = $set;
|
|
437
|
+
}
|
|
438
|
+
function addCustomEventProperties(event, properties) {
|
|
439
|
+
if (event.tags) for (const [key, value] of Object.entries(event.tags)) properties[key] = value;
|
|
440
|
+
if (event.properties) for (const [key, value] of Object.entries(event.properties)) properties[key] = value;
|
|
441
|
+
}
|
|
442
|
+
function buildExceptionEvent(event) {
|
|
443
|
+
const distinctId = getDistinctId(event);
|
|
444
|
+
const timestamp = getTimestamp(event);
|
|
445
|
+
const properties = {
|
|
446
|
+
$exception_source: "backend",
|
|
447
|
+
[PostHogMCPAnalyticsProperty.SessionId]: event.sessionId
|
|
448
|
+
};
|
|
449
|
+
if (event.error) {
|
|
450
|
+
if (event.error.message) properties.$exception_message = event.error.message;
|
|
451
|
+
if (event.error.type) properties.$exception_type = event.error.type;
|
|
452
|
+
if (event.error.stack) properties.$exception_stacktrace = event.error.stack;
|
|
453
|
+
}
|
|
454
|
+
if (event.resourceName) {
|
|
455
|
+
properties[PostHogMCPAnalyticsProperty.ResourceName] = event.resourceName;
|
|
456
|
+
if (event.eventType === MCPAnalyticsEventType.mcpToolsCall) properties[PostHogMCPAnalyticsProperty.ToolName] = event.resourceName;
|
|
457
|
+
}
|
|
458
|
+
if (event.serverName) properties[PostHogMCPAnalyticsProperty.ServerName] = event.serverName;
|
|
459
|
+
if (event.serverVersion) properties[PostHogMCPAnalyticsProperty.ServerVersion] = event.serverVersion;
|
|
460
|
+
if (event.clientName) properties[PostHogMCPAnalyticsProperty.ClientName] = event.clientName;
|
|
461
|
+
if (event.clientVersion) properties[PostHogMCPAnalyticsProperty.ClientVersion] = event.clientVersion;
|
|
462
|
+
return {
|
|
463
|
+
event: "$exception",
|
|
464
|
+
distinct_id: distinctId,
|
|
465
|
+
properties,
|
|
466
|
+
timestamp,
|
|
467
|
+
type: "capture"
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function buildAISpanEvent(event) {
|
|
471
|
+
const distinctId = getDistinctId(event);
|
|
472
|
+
const timestamp = getTimestamp(event);
|
|
473
|
+
const properties = {
|
|
474
|
+
[PostHogMCPAnalyticsProperty.AiSessionId]: `posthog_mcp_analytics_${event.sessionId}`,
|
|
475
|
+
[PostHogMCPAnalyticsProperty.AiTraceId]: getAITraceId(event),
|
|
476
|
+
[PostHogMCPAnalyticsProperty.AiSpanId]: getAISpanId(event),
|
|
477
|
+
[PostHogMCPAnalyticsProperty.AiSpanName]: event.resourceName || "unknown_tool",
|
|
478
|
+
[PostHogMCPAnalyticsProperty.AiIsError]: event.isError,
|
|
479
|
+
[PostHogMCPAnalyticsProperty.SessionId]: event.sessionId,
|
|
480
|
+
[PostHogMCPAnalyticsProperty.Source]: POSTHOG_MCP_ANALYTICS_SOURCE
|
|
481
|
+
};
|
|
482
|
+
if (event.duration !== void 0) properties[PostHogMCPAnalyticsProperty.AiLatency] = event.duration / 1e3;
|
|
483
|
+
if (event.isError && event.error) properties.$ai_error = event.error;
|
|
484
|
+
if (event.parameters !== void 0) properties[PostHogMCPAnalyticsProperty.AiInputState] = event.parameters;
|
|
485
|
+
if (event.response !== void 0) properties[PostHogMCPAnalyticsProperty.AiOutputState] = event.response;
|
|
486
|
+
if (event.serverName) properties[PostHogMCPAnalyticsProperty.ServerName] = event.serverName;
|
|
487
|
+
if (event.clientName) properties[PostHogMCPAnalyticsProperty.ClientName] = event.clientName;
|
|
488
|
+
if (event.userIntent) properties[PostHogMCPAnalyticsProperty.Intent] = event.userIntent;
|
|
489
|
+
if (event.tags) for (const [key, value] of Object.entries(event.tags)) properties[key] = value;
|
|
490
|
+
if (event.properties) for (const [key, value] of Object.entries(event.properties)) properties[key] = value;
|
|
491
|
+
return {
|
|
492
|
+
event: "$ai_span",
|
|
493
|
+
distinct_id: distinctId,
|
|
494
|
+
properties,
|
|
495
|
+
timestamp,
|
|
496
|
+
type: "capture"
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function mapEventType(eventType) {
|
|
500
|
+
return {
|
|
501
|
+
[MCPAnalyticsEventType.mcpToolsCall]: "mcp_tool_call",
|
|
502
|
+
[MCPAnalyticsEventType.mcpToolsList]: "mcp_tools_list",
|
|
503
|
+
[MCPAnalyticsEventType.mcpInitialize]: "mcp_initialize",
|
|
504
|
+
[MCPAnalyticsEventType.mcpResourcesRead]: "mcp_resource_read",
|
|
505
|
+
[MCPAnalyticsEventType.mcpResourcesList]: "mcp_resources_list",
|
|
506
|
+
[MCPAnalyticsEventType.mcpPromptsGet]: "mcp_prompt_get",
|
|
507
|
+
[MCPAnalyticsEventType.mcpPromptsList]: "mcp_prompts_list"
|
|
508
|
+
}[eventType] || `mcp_${eventType.replace(MCP_EVENT_PREFIX_REGEX, "").replace(SLASH_REGEX, "_")}`;
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/modules/redaction.ts
|
|
512
|
+
/**
|
|
513
|
+
* Set of field names that should be protected from redaction.
|
|
514
|
+
* These fields contain system-level identifiers and metadata that
|
|
515
|
+
* need to be preserved for analytics tracking.
|
|
516
|
+
*/
|
|
517
|
+
const PROTECTED_FIELDS = new Set([
|
|
518
|
+
"sessionId",
|
|
519
|
+
"id",
|
|
520
|
+
"apiKey",
|
|
521
|
+
"server",
|
|
522
|
+
"identifyActorGivenId",
|
|
523
|
+
"identifyActorName",
|
|
524
|
+
"identifyData",
|
|
525
|
+
"resourceName",
|
|
526
|
+
"eventType",
|
|
527
|
+
"actorId",
|
|
528
|
+
"tags",
|
|
529
|
+
"properties"
|
|
530
|
+
]);
|
|
531
|
+
/**
|
|
532
|
+
* Recursively applies a redaction function to all string values in an object.
|
|
533
|
+
* This ensures that sensitive information is removed from all string fields
|
|
534
|
+
* before events are sent to the analytics service.
|
|
535
|
+
*
|
|
536
|
+
* @param obj - The object to redact strings from
|
|
537
|
+
* @param redactFn - The redaction function to apply to each string
|
|
538
|
+
* @param path - The current path in the object tree (used to check protected fields)
|
|
539
|
+
* @param isProtected - Whether the current object/value is within a protected field
|
|
540
|
+
* @returns A new object with all strings redacted
|
|
541
|
+
*/
|
|
542
|
+
async function redactStringsInObject(obj, redactFn, path = "", isProtected = false) {
|
|
543
|
+
if (obj === null || obj === void 0) return obj;
|
|
544
|
+
if (typeof obj === "string") {
|
|
545
|
+
if (isProtected) return obj;
|
|
546
|
+
return await redactFn(obj);
|
|
547
|
+
}
|
|
548
|
+
if (Array.isArray(obj)) return Promise.all(obj.map((item, index) => redactStringsInObject(item, redactFn, `${path}[${index}]`, isProtected)));
|
|
549
|
+
if (obj instanceof Date) return obj;
|
|
550
|
+
if (typeof obj === "object") {
|
|
551
|
+
const redactedObj = {};
|
|
552
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
553
|
+
if (typeof value === "function" || value === void 0) continue;
|
|
554
|
+
redactedObj[key] = await redactStringsInObject(value, redactFn, path ? `${path}.${key}` : key, isProtected || path === "" && PROTECTED_FIELDS.has(key));
|
|
555
|
+
}
|
|
556
|
+
return redactedObj;
|
|
557
|
+
}
|
|
558
|
+
return obj;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Applies the customer's redaction function to all string fields in an Event object.
|
|
562
|
+
* This is the main entry point for redacting sensitive information from events
|
|
563
|
+
* before they are sent to the analytics service.
|
|
564
|
+
*
|
|
565
|
+
* @param event - The event to redact
|
|
566
|
+
* @param redactFn - The customer's redaction function
|
|
567
|
+
* @returns A new event object with all strings redacted
|
|
568
|
+
*/
|
|
569
|
+
function redactEvent(event, redactFn) {
|
|
570
|
+
return redactStringsInObject(event, redactFn, "", false);
|
|
571
|
+
}
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/modules/mcp-payloads.ts
|
|
574
|
+
const CONTEXT_ARGUMENT_NAME = "context";
|
|
575
|
+
const REDACTED_VALUE = "[redacted]";
|
|
576
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/\n\r]+=*$/;
|
|
577
|
+
const SIZE_GATE = 10240;
|
|
578
|
+
const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g;
|
|
579
|
+
const SENSITIVE_KEY_PATTERN = /^(authorization|cookie|set-cookie|x-api-key|api[-_]?key|api[-_]?token|access[-_]?token|refresh[-_]?token|token|password|secret|client[-_]?secret|private[-_]?key)$/i;
|
|
580
|
+
function isRecord$1(value) {
|
|
581
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
582
|
+
}
|
|
583
|
+
function shouldRedactKey(key) {
|
|
584
|
+
return SENSITIVE_KEY_PATTERN.test(key);
|
|
585
|
+
}
|
|
586
|
+
function sanitizeString(value) {
|
|
587
|
+
if (value.length >= SIZE_GATE && BASE64_PATTERN.test(value)) return "[binary data redacted - not supported by PostHog MCP analytics]";
|
|
588
|
+
return value.replace(POSTHOG_TOKEN_PATTERN, REDACTED_VALUE);
|
|
589
|
+
}
|
|
590
|
+
function sanitizeCapturedValue(value) {
|
|
591
|
+
if (value == null) return value;
|
|
592
|
+
if (typeof value === "string") return sanitizeString(value);
|
|
593
|
+
if (Array.isArray(value)) return value.map(sanitizeCapturedValue);
|
|
594
|
+
if (value instanceof Date) return value;
|
|
595
|
+
if (typeof value !== "object") return value;
|
|
596
|
+
const result = {};
|
|
597
|
+
for (const [key, nestedValue] of Object.entries(value)) result[key] = shouldRedactKey(key) ? REDACTED_VALUE : sanitizeCapturedValue(nestedValue);
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
function buildCapturedMcpArguments(argumentsValue) {
|
|
601
|
+
if (!isRecord$1(argumentsValue)) return sanitizeCapturedValue(argumentsValue);
|
|
602
|
+
const capturedArguments = {};
|
|
603
|
+
for (const [key, value] of Object.entries(argumentsValue)) {
|
|
604
|
+
if (key === CONTEXT_ARGUMENT_NAME) continue;
|
|
605
|
+
capturedArguments[key] = sanitizeCapturedValue(value);
|
|
606
|
+
}
|
|
607
|
+
return capturedArguments;
|
|
608
|
+
}
|
|
609
|
+
function buildCapturedMcpParams(params) {
|
|
610
|
+
if (!isRecord$1(params)) return sanitizeCapturedValue(params);
|
|
611
|
+
const capturedParams = {};
|
|
612
|
+
for (const [key, value] of Object.entries(params)) capturedParams[key] = key === "arguments" ? buildCapturedMcpArguments(value) : sanitizeCapturedValue(value);
|
|
613
|
+
return capturedParams;
|
|
614
|
+
}
|
|
615
|
+
function buildCapturedMcpParameters(request) {
|
|
616
|
+
if (!isRecord$1(request)) return { request: sanitizeCapturedValue(request) };
|
|
617
|
+
const capturedRequest = {};
|
|
618
|
+
for (const key of [
|
|
619
|
+
"id",
|
|
620
|
+
"jsonrpc",
|
|
621
|
+
"method"
|
|
622
|
+
]) if (key in request) capturedRequest[key] = sanitizeCapturedValue(request[key]);
|
|
623
|
+
if ("params" in request) capturedRequest.params = buildCapturedMcpParams(request.params);
|
|
624
|
+
return { request: capturedRequest };
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/modules/sanitization.ts
|
|
628
|
+
function isRecord(value) {
|
|
629
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Sanitizes an event by redacting non-text content blocks from responses
|
|
633
|
+
* and large base64-encoded strings from parameters.
|
|
634
|
+
*
|
|
635
|
+
* This is a synchronous operation that returns a new object without mutating the original.
|
|
636
|
+
* It should run after customer redaction in the event pipeline.
|
|
637
|
+
*/
|
|
638
|
+
function sanitizeEvent(event) {
|
|
639
|
+
const result = { ...event };
|
|
640
|
+
if (result.response != null) result.response = sanitizeResponse(result.response);
|
|
641
|
+
if (result.parameters != null) result.parameters = sanitizeParameters(result.parameters);
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Sanitizes response content blocks by replacing non-text content types
|
|
646
|
+
* with informative redaction messages.
|
|
647
|
+
*/
|
|
648
|
+
function sanitizeResponse(response) {
|
|
649
|
+
if (response == null || typeof response !== "object") return sanitizeCapturedValue(response);
|
|
650
|
+
const sanitized = sanitizeCapturedValue(response);
|
|
651
|
+
if (!isRecord(sanitized)) return sanitized;
|
|
652
|
+
const result = { ...sanitized };
|
|
653
|
+
const content = result.content;
|
|
654
|
+
if (Array.isArray(content)) result.content = content.map(sanitizeContentBlock);
|
|
655
|
+
if (result.structuredContent != null && typeof result.structuredContent === "object") result.structuredContent = sanitizeCapturedValue(result.structuredContent);
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Sanitizes a single content block based on its type discriminator.
|
|
660
|
+
*/
|
|
661
|
+
function sanitizeContentBlock(block) {
|
|
662
|
+
if (block == null || typeof block !== "object") return block;
|
|
663
|
+
if (!isRecord(block)) return block;
|
|
664
|
+
switch (block.type) {
|
|
665
|
+
case "text": return sanitizeCapturedValue(block);
|
|
666
|
+
case "image": return {
|
|
667
|
+
type: "text",
|
|
668
|
+
text: "[image content redacted - not supported by PostHog MCP analytics]"
|
|
669
|
+
};
|
|
670
|
+
case "audio": return {
|
|
671
|
+
type: "text",
|
|
672
|
+
text: "[audio content redacted - not supported by PostHog MCP analytics]"
|
|
673
|
+
};
|
|
674
|
+
case "resource": return sanitizeResourceBlock(block);
|
|
675
|
+
case "resource_link": return sanitizeCapturedValue(block);
|
|
676
|
+
default: return {
|
|
677
|
+
type: "text",
|
|
678
|
+
text: `[unsupported content type "${block.type}" redacted - not supported by PostHog MCP analytics]`
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Sanitizes an embedded resource content block.
|
|
684
|
+
* BlobResourceContents (has `blob` field) are redacted.
|
|
685
|
+
* TextResourceContents (has `text` field) pass through.
|
|
686
|
+
*/
|
|
687
|
+
function sanitizeResourceBlock(block) {
|
|
688
|
+
if (isRecord(block.resource) && "blob" in block.resource) return {
|
|
689
|
+
type: "text",
|
|
690
|
+
text: "[binary resource content redacted - not supported by PostHog MCP analytics]"
|
|
691
|
+
};
|
|
692
|
+
return sanitizeCapturedValue(block);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Recursively scans parameters for large base64-encoded strings and replaces them.
|
|
696
|
+
* Uses a size gate (10KB) to avoid regex testing on small strings.
|
|
697
|
+
*/
|
|
698
|
+
function sanitizeParameters(obj) {
|
|
699
|
+
return sanitizeCapturedValue(obj);
|
|
700
|
+
}
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region package.json
|
|
703
|
+
var version = "0.0.0";
|
|
704
|
+
//#endregion
|
|
705
|
+
//#region src/modules/session.ts
|
|
706
|
+
function newSessionId() {
|
|
707
|
+
return newPrefixedId("ses");
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Creates a deterministic SDK session ID from an MCP sessionId.
|
|
711
|
+
* The same inputs will always produce the same session ID, enabling correlation across server restarts.
|
|
712
|
+
*
|
|
713
|
+
* @param mcpSessionId - The session ID from the MCP protocol
|
|
714
|
+
* @returns An SDK session ID with "ses" prefix derived deterministically from the inputs
|
|
715
|
+
*/
|
|
716
|
+
function deriveSessionIdFromMCPSession(mcpSessionId) {
|
|
717
|
+
return deterministicPrefixedId("ses", mcpSessionId);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Gets or generates a session ID for the server.
|
|
721
|
+
* Prioritizes MCP protocol sessionId over PostHog MCP analytics-generated sessionId.
|
|
722
|
+
*
|
|
723
|
+
* @param server - The MCP server instance
|
|
724
|
+
* @param extra - Optional extra data containing MCP sessionId
|
|
725
|
+
* @returns The session ID to use for events
|
|
726
|
+
*/
|
|
727
|
+
function getServerSessionId(server, extra) {
|
|
728
|
+
const data = getServerTrackingData(server);
|
|
729
|
+
if (!data) throw new Error("Server tracking data not found");
|
|
730
|
+
const mcpSessionId = extra?.sessionId;
|
|
731
|
+
if (mcpSessionId) {
|
|
732
|
+
data.sessionId = deriveSessionIdFromMCPSession(mcpSessionId);
|
|
733
|
+
data.lastMcpSessionId = mcpSessionId;
|
|
734
|
+
data.sessionSource = "mcp";
|
|
735
|
+
setServerTrackingData(server, data);
|
|
736
|
+
setLastActivity(server);
|
|
737
|
+
return data.sessionId;
|
|
738
|
+
}
|
|
739
|
+
if (data.sessionSource === "mcp" && data.lastMcpSessionId) {
|
|
740
|
+
setLastActivity(server);
|
|
741
|
+
return data.sessionId;
|
|
742
|
+
}
|
|
743
|
+
if (Date.now() - data.lastActivity.getTime() > 1800 * 1e3) {
|
|
744
|
+
data.sessionId = newSessionId();
|
|
745
|
+
data.sessionSource = "generated";
|
|
746
|
+
setServerTrackingData(server, data);
|
|
747
|
+
}
|
|
748
|
+
setLastActivity(server);
|
|
749
|
+
return data.sessionId;
|
|
750
|
+
}
|
|
751
|
+
function setLastActivity(server) {
|
|
752
|
+
const data = getServerTrackingData(server);
|
|
753
|
+
if (!data) throw new Error("Server tracking data not found");
|
|
754
|
+
data.lastActivity = /* @__PURE__ */ new Date();
|
|
755
|
+
setServerTrackingData(server, data);
|
|
756
|
+
}
|
|
757
|
+
function getSessionInfo(server, data) {
|
|
758
|
+
let clientInfo = {
|
|
759
|
+
name: void 0,
|
|
760
|
+
version: void 0
|
|
761
|
+
};
|
|
762
|
+
if (!data?.sessionInfo.clientName) clientInfo = server.getClientVersion();
|
|
763
|
+
const actorInfo = data?.identifiedSessions.get(data.sessionId);
|
|
764
|
+
const sessionInfo = {
|
|
765
|
+
ipAddress: void 0,
|
|
766
|
+
sdkLanguage: "TypeScript",
|
|
767
|
+
sdkVersion: version,
|
|
768
|
+
serverName: server._serverInfo?.name,
|
|
769
|
+
serverVersion: server._serverInfo?.version,
|
|
770
|
+
clientName: clientInfo?.name,
|
|
771
|
+
clientVersion: clientInfo?.version,
|
|
772
|
+
identifyActorGivenId: actorInfo?.userId,
|
|
773
|
+
identifyActorName: actorInfo?.userName,
|
|
774
|
+
identifyActorData: actorInfo?.userData || {}
|
|
775
|
+
};
|
|
776
|
+
if (!data) return sessionInfo;
|
|
777
|
+
data.sessionInfo = sessionInfo;
|
|
778
|
+
setServerTrackingData(server, data);
|
|
779
|
+
return data.sessionInfo;
|
|
780
|
+
}
|
|
781
|
+
const MAX_STRING_LENGTH = 32768;
|
|
782
|
+
const MAX_EVENT_BYTES = 102400;
|
|
783
|
+
const MAX_USER_INTENT_LENGTH = 2048;
|
|
784
|
+
const MAX_ERROR_MESSAGE_LENGTH = 2048;
|
|
785
|
+
const MAX_RESOURCE_NAME_LENGTH = 256;
|
|
786
|
+
const MAX_METADATA_LENGTH = 256;
|
|
787
|
+
const MAX_STACK_FRAMES$1 = 50;
|
|
788
|
+
const MAX_CONTENT_TEXT_LENGTH = 32768;
|
|
789
|
+
const TRUNCATION_SUFFIX = "...";
|
|
790
|
+
/**
|
|
791
|
+
* Recursively normalizes a value, handling:
|
|
792
|
+
* - String truncation (> MAX_STRING_LENGTH)
|
|
793
|
+
* - Non-serializable values (functions, symbols, undefined, BigInt, NaN, Infinity)
|
|
794
|
+
* - Date objects -> ISO string
|
|
795
|
+
* - Circular reference detection
|
|
796
|
+
* - Depth limiting
|
|
797
|
+
* - Breadth limiting
|
|
798
|
+
*/
|
|
799
|
+
function normalize(input, depth = 10, maxBreadth = 100, maxStringLength = MAX_STRING_LENGTH) {
|
|
800
|
+
return visit(input, depth, maxBreadth, maxStringLength, /* @__PURE__ */ new WeakSet());
|
|
801
|
+
}
|
|
802
|
+
function visit(value, remainingDepth, maxBreadth, maxStringLength, memo) {
|
|
803
|
+
if (value === null) return null;
|
|
804
|
+
if (value === void 0) return "[undefined]";
|
|
805
|
+
if (typeof value === "boolean") return value;
|
|
806
|
+
if (typeof value === "number") {
|
|
807
|
+
if (Number.isNaN(value)) return "[NaN]";
|
|
808
|
+
if (!Number.isFinite(value)) return value > 0 ? "[Infinity]" : "[-Infinity]";
|
|
809
|
+
return value;
|
|
810
|
+
}
|
|
811
|
+
if (typeof value === "bigint") return `[BigInt: ${value}]`;
|
|
812
|
+
if (typeof value === "string") {
|
|
813
|
+
if (value.length > maxStringLength) return value.slice(0, maxStringLength) + TRUNCATION_SUFFIX;
|
|
814
|
+
return value;
|
|
815
|
+
}
|
|
816
|
+
if (typeof value === "symbol") {
|
|
817
|
+
const desc = value.description;
|
|
818
|
+
return desc ? `[Symbol(${desc})]` : "[Symbol()]";
|
|
819
|
+
}
|
|
820
|
+
if (typeof value === "function") return `[Function: ${value.name || "<anonymous>"}]`;
|
|
821
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "[Invalid Date]" : value.toISOString();
|
|
822
|
+
if (typeof value === "object") {
|
|
823
|
+
if (memo.has(value)) return "[Circular ~]";
|
|
824
|
+
if (remainingDepth <= 0) return Array.isArray(value) ? "[Array]" : "[Object]";
|
|
825
|
+
memo.add(value);
|
|
826
|
+
let result;
|
|
827
|
+
if (Array.isArray(value)) result = visitArray(value, remainingDepth - 1, maxBreadth, maxStringLength, memo);
|
|
828
|
+
else result = visitObject(value, remainingDepth - 1, maxBreadth, maxStringLength, memo);
|
|
829
|
+
memo.delete(value);
|
|
830
|
+
return result;
|
|
831
|
+
}
|
|
832
|
+
return String(value);
|
|
833
|
+
}
|
|
834
|
+
function visitArray(arr, remainingDepth, maxBreadth, maxStringLength, memo) {
|
|
835
|
+
const result = [];
|
|
836
|
+
for (let i = 0; i < arr.length; i++) {
|
|
837
|
+
if (i >= maxBreadth) {
|
|
838
|
+
result.push("[MaxProperties ~]");
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
result.push(visit(arr[i], remainingDepth, maxBreadth, maxStringLength, memo));
|
|
842
|
+
}
|
|
843
|
+
return result;
|
|
844
|
+
}
|
|
845
|
+
function visitObject(obj, remainingDepth, maxBreadth, maxStringLength, memo) {
|
|
846
|
+
const result = {};
|
|
847
|
+
const keys = Object.keys(obj);
|
|
848
|
+
let count = 0;
|
|
849
|
+
for (const key of keys) {
|
|
850
|
+
if (count >= maxBreadth) {
|
|
851
|
+
result["..."] = "[MaxProperties ~]";
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
if (obj[key] === void 0) continue;
|
|
855
|
+
result[key] = visit(obj[key], remainingDepth, maxBreadth, maxStringLength, memo);
|
|
856
|
+
count++;
|
|
857
|
+
}
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
function truncateString(str, maxLength) {
|
|
861
|
+
if (str == null) return str;
|
|
862
|
+
if (str.length <= maxLength) return str;
|
|
863
|
+
return str.slice(0, maxLength) + TRUNCATION_SUFFIX;
|
|
864
|
+
}
|
|
865
|
+
function truncateStackFrames(frames) {
|
|
866
|
+
if (!frames || frames.length <= MAX_STACK_FRAMES$1) return frames;
|
|
867
|
+
const half = Math.floor(MAX_STACK_FRAMES$1 / 2);
|
|
868
|
+
return [...frames.slice(0, half), ...frames.slice(-half)];
|
|
869
|
+
}
|
|
870
|
+
function truncateResponseContent(response) {
|
|
871
|
+
if (response == null || typeof response !== "object") return response;
|
|
872
|
+
const result = { ...response };
|
|
873
|
+
if (Array.isArray(result.content)) result.content = result.content.map((block) => {
|
|
874
|
+
if (block != null && typeof block === "object" && "type" in block && "text" in block && block?.type === "text" && typeof block.text === "string" && block.text.length > MAX_CONTENT_TEXT_LENGTH) return {
|
|
875
|
+
...block,
|
|
876
|
+
text: block.text.slice(0, MAX_CONTENT_TEXT_LENGTH) + TRUNCATION_SUFFIX
|
|
877
|
+
};
|
|
878
|
+
return block;
|
|
879
|
+
});
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Calculates the UTF-8 byte size of a JSON-serialized value.
|
|
884
|
+
*/
|
|
885
|
+
const textEncoder = new TextEncoder();
|
|
886
|
+
function jsonByteSize(value) {
|
|
887
|
+
return textEncoder.encode(JSON.stringify(value)).length;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Finds and truncates the largest string values in an object to fit within a byte budget.
|
|
891
|
+
* Last-resort mechanism when depth reduction alone isn't enough.
|
|
892
|
+
* Iterates until the result fits or no further reduction is possible.
|
|
893
|
+
*/
|
|
894
|
+
function truncateLargestFields(obj, maxBytes) {
|
|
895
|
+
const result = structuredClone(obj);
|
|
896
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
897
|
+
const currentSize = jsonByteSize(result);
|
|
898
|
+
if (currentSize <= maxBytes) return result;
|
|
899
|
+
const excess = currentSize - maxBytes;
|
|
900
|
+
const stringPaths = [];
|
|
901
|
+
collectStringPaths(result, [], stringPaths);
|
|
902
|
+
stringPaths.sort((a, b) => b.length - a.length);
|
|
903
|
+
if (stringPaths.length === 0) break;
|
|
904
|
+
let remaining = excess + 200;
|
|
905
|
+
let truncated = false;
|
|
906
|
+
for (const { path, length } of stringPaths) {
|
|
907
|
+
if (remaining <= 0) break;
|
|
908
|
+
const reduction = Math.min(remaining, Math.floor(length * .5));
|
|
909
|
+
if (reduction < 10) continue;
|
|
910
|
+
const newLength = length - reduction;
|
|
911
|
+
const currentValue = getNestedValue(result, path);
|
|
912
|
+
if (typeof currentValue !== "string") continue;
|
|
913
|
+
setNestedValue(result, path, currentValue.slice(0, newLength) + TRUNCATION_SUFFIX);
|
|
914
|
+
remaining -= reduction;
|
|
915
|
+
truncated = true;
|
|
916
|
+
}
|
|
917
|
+
if (!truncated) break;
|
|
918
|
+
}
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
function collectStringPaths(obj, currentPath, results) {
|
|
922
|
+
if (typeof obj === "string" && obj.length > 100) {
|
|
923
|
+
results.push({
|
|
924
|
+
path: [...currentPath],
|
|
925
|
+
length: obj.length
|
|
926
|
+
});
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (Array.isArray(obj)) {
|
|
930
|
+
for (const [i, item] of obj.entries()) collectStringPaths(item, [...currentPath, String(i)], results);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (obj != null && typeof obj === "object") for (const [key, value] of Object.entries(obj)) collectStringPaths(value, [...currentPath, key], results);
|
|
934
|
+
}
|
|
935
|
+
function getNestedValue(obj, path) {
|
|
936
|
+
let current = obj;
|
|
937
|
+
for (const key of path) {
|
|
938
|
+
if (current == null || typeof current !== "object") return;
|
|
939
|
+
current = current[key];
|
|
940
|
+
}
|
|
941
|
+
return current;
|
|
942
|
+
}
|
|
943
|
+
function setNestedValue(obj, path, value) {
|
|
944
|
+
let current = obj;
|
|
945
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
946
|
+
if (current == null || typeof current !== "object") return;
|
|
947
|
+
current = current[path[i]];
|
|
948
|
+
}
|
|
949
|
+
const finalKey = path.at(-1);
|
|
950
|
+
if (finalKey !== void 0 && current != null && typeof current === "object") current[finalKey] = value;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Ensures an event fits within MAX_EVENT_BYTES by progressively reducing
|
|
954
|
+
* normalization depth, then truncating largest string fields as a last resort.
|
|
955
|
+
*/
|
|
956
|
+
function truncateToSize(event) {
|
|
957
|
+
if (jsonByteSize(event) <= 102400) return event;
|
|
958
|
+
for (let depth = 9; depth >= 1; depth--) {
|
|
959
|
+
const reduced = { ...event };
|
|
960
|
+
if (reduced.parameters != null) reduced.parameters = normalize(reduced.parameters, depth);
|
|
961
|
+
if (reduced.response != null) reduced.response = normalize(reduced.response, depth);
|
|
962
|
+
if (reduced.identifyActorData != null) reduced.identifyActorData = normalize(reduced.identifyActorData, depth);
|
|
963
|
+
if (reduced.error != null) reduced.error = normalize(reduced.error, depth);
|
|
964
|
+
if (jsonByteSize(reduced) <= 102400) return reduced;
|
|
965
|
+
}
|
|
966
|
+
const minimal = { ...event };
|
|
967
|
+
if (minimal.parameters != null) minimal.parameters = normalize(minimal.parameters, 1);
|
|
968
|
+
if (minimal.response != null) minimal.response = normalize(minimal.response, 1);
|
|
969
|
+
if (minimal.identifyActorData != null) minimal.identifyActorData = normalize(minimal.identifyActorData, 1);
|
|
970
|
+
if (minimal.error != null) minimal.error = normalize(minimal.error, 1);
|
|
971
|
+
return truncateLargestFields(minimal, MAX_EVENT_BYTES);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Applies layered truncation to an event:
|
|
975
|
+
* 1. Field-level string limits (userIntent, resourceName, metadata fields, error.message)
|
|
976
|
+
* 2. Error frame limiting (first 25 + last 25 if > 50)
|
|
977
|
+
* 3. Response content text limits (32KB per text block)
|
|
978
|
+
* 4. Recursive normalization on user-controlled fields
|
|
979
|
+
* 5. Size-targeted truncation (progressive depth reduction + last-resort string truncation)
|
|
980
|
+
*/
|
|
981
|
+
function truncateEvent(event) {
|
|
982
|
+
const result = { ...event };
|
|
983
|
+
result.userIntent = truncateString(result.userIntent, MAX_USER_INTENT_LENGTH);
|
|
984
|
+
result.resourceName = truncateString(result.resourceName, MAX_RESOURCE_NAME_LENGTH);
|
|
985
|
+
result.serverName = truncateString(result.serverName, MAX_METADATA_LENGTH);
|
|
986
|
+
result.serverVersion = truncateString(result.serverVersion, MAX_METADATA_LENGTH);
|
|
987
|
+
result.clientName = truncateString(result.clientName, MAX_METADATA_LENGTH);
|
|
988
|
+
result.clientVersion = truncateString(result.clientVersion, MAX_METADATA_LENGTH);
|
|
989
|
+
if (result.error != null && typeof result.error === "object") {
|
|
990
|
+
const error = { ...result.error };
|
|
991
|
+
error.message = truncateString(error.message, MAX_ERROR_MESSAGE_LENGTH);
|
|
992
|
+
if (error.frames !== void 0) error.frames = truncateStackFrames(error.frames);
|
|
993
|
+
result.error = error;
|
|
994
|
+
}
|
|
995
|
+
result.response = truncateResponseContent(result.response);
|
|
996
|
+
if (result.parameters != null) result.parameters = normalize(result.parameters);
|
|
997
|
+
if (result.response != null) result.response = normalize(result.response);
|
|
998
|
+
if (result.identifyActorData != null) result.identifyActorData = normalize(result.identifyActorData);
|
|
999
|
+
if (result.error != null) result.error = normalize(result.error);
|
|
1000
|
+
return truncateToSize(result);
|
|
1001
|
+
}
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/modules/event-queue.ts
|
|
1004
|
+
var EventQueue = class {
|
|
1005
|
+
constructor() {
|
|
1006
|
+
this.queue = [];
|
|
1007
|
+
this.processing = false;
|
|
1008
|
+
this.maxQueueSize = 1e4;
|
|
1009
|
+
this.concurrency = 5;
|
|
1010
|
+
this.activeRequests = 0;
|
|
1011
|
+
this.host = "https://us.i.posthog.com";
|
|
1012
|
+
this.posthogOptions = {};
|
|
1013
|
+
this.posthogClients = /* @__PURE__ */ new Map();
|
|
1014
|
+
}
|
|
1015
|
+
configure(host) {
|
|
1016
|
+
this.host = host;
|
|
1017
|
+
this.posthogClients.clear();
|
|
1018
|
+
}
|
|
1019
|
+
configurePostHogOptions(posthogOptions) {
|
|
1020
|
+
this.posthogOptions = {
|
|
1021
|
+
...this.posthogOptions,
|
|
1022
|
+
...posthogOptions
|
|
1023
|
+
};
|
|
1024
|
+
if (posthogOptions.host) this.host = posthogOptions.host;
|
|
1025
|
+
this.posthogClients.clear();
|
|
1026
|
+
}
|
|
1027
|
+
add(event, posthogClient, enableAITracing = false) {
|
|
1028
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
1029
|
+
writeToLog("Event queue full, dropping oldest event");
|
|
1030
|
+
this.queue.shift();
|
|
1031
|
+
}
|
|
1032
|
+
this.queue.push({
|
|
1033
|
+
enableAITracing,
|
|
1034
|
+
event,
|
|
1035
|
+
posthogClient
|
|
1036
|
+
});
|
|
1037
|
+
this.process();
|
|
1038
|
+
}
|
|
1039
|
+
async process() {
|
|
1040
|
+
if (this.processing) return;
|
|
1041
|
+
this.processing = true;
|
|
1042
|
+
while (this.queue.length > 0 && this.activeRequests < this.concurrency) {
|
|
1043
|
+
const queuedEvent = this.queue.shift();
|
|
1044
|
+
if (!queuedEvent) continue;
|
|
1045
|
+
const { enableAITracing, event, posthogClient } = queuedEvent;
|
|
1046
|
+
if (event.redactionFn) try {
|
|
1047
|
+
const redactedEvent = await redactEvent(event, event.redactionFn);
|
|
1048
|
+
event.redactionFn = void 0;
|
|
1049
|
+
Object.assign(event, redactedEvent);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
writeToLog(`Failed to redact event: ${error}`);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
Object.assign(event, sanitizeEvent(event));
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
writeToLog(`Failed to sanitize event: ${error}`);
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
try {
|
|
1061
|
+
Object.assign(event, truncateEvent(event));
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
writeToLog(`Failed to truncate event: ${error}`);
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
event.id = event.id || newPrefixedId("evt");
|
|
1067
|
+
this.activeRequests++;
|
|
1068
|
+
try {
|
|
1069
|
+
this.sendEvent(event, posthogClient, enableAITracing);
|
|
1070
|
+
} finally {
|
|
1071
|
+
this.activeRequests--;
|
|
1072
|
+
this.process();
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
this.processing = false;
|
|
1076
|
+
}
|
|
1077
|
+
sendEvent(event, posthogClientOverride, enableAITracing = false) {
|
|
1078
|
+
const posthogClient = this.getPostHogClient(event.apiKey, posthogClientOverride);
|
|
1079
|
+
if (posthogClient) try {
|
|
1080
|
+
for (const captureEvent of buildPostHogCaptureEvents(event, { enableAITracing })) posthogClient.capture({
|
|
1081
|
+
distinctId: captureEvent.distinct_id,
|
|
1082
|
+
event: captureEvent.event,
|
|
1083
|
+
properties: captureEvent.properties,
|
|
1084
|
+
timestamp: new Date(captureEvent.timestamp)
|
|
1085
|
+
});
|
|
1086
|
+
writeToLog(`Queued PostHog event ${event.id} | ${event.eventType} | ${event.duration} ms | ${event.identifyActorGivenId || "anonymous"}`);
|
|
1087
|
+
writeToLog(`Event details: ${JSON.stringify(event)}`);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
writeToLog(`Failed to queue PostHog event ${event.id}: ${getMCPCompatibleErrorMessage(error)}`);
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
getPostHogClient(apiKey, posthogClient) {
|
|
1094
|
+
if (posthogClient) return posthogClient;
|
|
1095
|
+
if (!apiKey) return;
|
|
1096
|
+
const existingClient = this.posthogClients.get(apiKey);
|
|
1097
|
+
if (existingClient) return existingClient;
|
|
1098
|
+
const client = new PostHog(apiKey, {
|
|
1099
|
+
...this.posthogOptions,
|
|
1100
|
+
host: this.host
|
|
1101
|
+
});
|
|
1102
|
+
this.posthogClients.set(apiKey, client);
|
|
1103
|
+
return client;
|
|
1104
|
+
}
|
|
1105
|
+
delay(ms) {
|
|
1106
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1107
|
+
}
|
|
1108
|
+
getStats() {
|
|
1109
|
+
return {
|
|
1110
|
+
queueLength: this.queue.length,
|
|
1111
|
+
activeRequests: this.activeRequests,
|
|
1112
|
+
isProcessing: this.processing
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
async destroy() {
|
|
1116
|
+
this.add = () => {
|
|
1117
|
+
writeToLog("Queue is shutting down, event dropped");
|
|
1118
|
+
};
|
|
1119
|
+
const timeout = 5e3;
|
|
1120
|
+
const start = Date.now();
|
|
1121
|
+
while ((this.queue.length > 0 || this.activeRequests > 0) && Date.now() - start < timeout) await this.delay(100);
|
|
1122
|
+
if (this.queue.length > 0) writeToLog(`Shutting down with ${this.queue.length} events still in queue`);
|
|
1123
|
+
const shutdowns = [];
|
|
1124
|
+
for (const client of this.posthogClients.values()) if (client.shutdown) shutdowns.push(client.shutdown(timeout));
|
|
1125
|
+
else if (client.flush) shutdowns.push(client.flush());
|
|
1126
|
+
await Promise.allSettled(shutdowns);
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
const eventQueue = new EventQueue();
|
|
1130
|
+
try {
|
|
1131
|
+
if (typeof process !== "undefined" && typeof process.once === "function") {
|
|
1132
|
+
process.once("SIGINT", () => eventQueue.destroy());
|
|
1133
|
+
process.once("SIGTERM", () => eventQueue.destroy());
|
|
1134
|
+
process.once("beforeExit", () => eventQueue.destroy());
|
|
1135
|
+
}
|
|
1136
|
+
} catch {}
|
|
1137
|
+
function publishEvent(server, eventInput) {
|
|
1138
|
+
const data = getServerTrackingData(server);
|
|
1139
|
+
if (!data) {
|
|
1140
|
+
writeToLog("Warning: Server tracking data not found. Event will not be published.");
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (!data.options.enableTracing) return;
|
|
1144
|
+
const sessionInfo = getSessionInfo(server, data);
|
|
1145
|
+
const duration = eventInput.duration || (eventInput.timestamp ? Date.now() - eventInput.timestamp.getTime() : void 0);
|
|
1146
|
+
const fullEvent = {
|
|
1147
|
+
id: eventInput.id || "",
|
|
1148
|
+
sessionId: eventInput.sessionId || data.sessionId,
|
|
1149
|
+
apiKey: data.apiKey,
|
|
1150
|
+
eventType: eventInput.eventType || "",
|
|
1151
|
+
timestamp: eventInput.timestamp || /* @__PURE__ */ new Date(),
|
|
1152
|
+
duration,
|
|
1153
|
+
ipAddress: sessionInfo.ipAddress,
|
|
1154
|
+
sdkLanguage: sessionInfo.sdkLanguage,
|
|
1155
|
+
sdkVersion: sessionInfo.sdkVersion,
|
|
1156
|
+
serverName: sessionInfo.serverName,
|
|
1157
|
+
serverVersion: sessionInfo.serverVersion,
|
|
1158
|
+
clientName: sessionInfo.clientName,
|
|
1159
|
+
clientVersion: sessionInfo.clientVersion,
|
|
1160
|
+
identifyActorGivenId: sessionInfo.identifyActorGivenId,
|
|
1161
|
+
identifyActorName: sessionInfo.identifyActorName,
|
|
1162
|
+
identifyActorData: sessionInfo.identifyActorData,
|
|
1163
|
+
resourceName: eventInput.resourceName,
|
|
1164
|
+
parameters: eventInput.parameters,
|
|
1165
|
+
response: eventInput.response,
|
|
1166
|
+
userIntent: eventInput.userIntent,
|
|
1167
|
+
isError: eventInput.isError,
|
|
1168
|
+
error: eventInput.error,
|
|
1169
|
+
redactionFn: eventInput.redactionFn,
|
|
1170
|
+
tags: eventInput.tags,
|
|
1171
|
+
properties: eventInput.properties
|
|
1172
|
+
};
|
|
1173
|
+
if (data.options.posthogClient) eventQueue.add(fullEvent, data.options.posthogClient, data.options.enableAITracing);
|
|
1174
|
+
else eventQueue.add(fullEvent, void 0, data.options.enableAITracing);
|
|
1175
|
+
}
|
|
1176
|
+
//#endregion
|
|
1177
|
+
//#region src/modules/exceptions.ts
|
|
1178
|
+
let fsModule = null;
|
|
1179
|
+
let fsInitAttempted = false;
|
|
1180
|
+
function getFsSync() {
|
|
1181
|
+
if (!fsInitAttempted) {
|
|
1182
|
+
fsInitAttempted = true;
|
|
1183
|
+
try {
|
|
1184
|
+
fsModule = createRequire(import.meta.url)("node:fs");
|
|
1185
|
+
} catch {
|
|
1186
|
+
fsModule = null;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return fsModule;
|
|
1190
|
+
}
|
|
1191
|
+
const MAX_EXCEPTION_CHAIN_DEPTH = 10;
|
|
1192
|
+
const MAX_STACK_FRAMES = 50;
|
|
1193
|
+
const LOCATION_WITH_LINE_COLUMN_REGEX = /^(.+):(\d+):(\d+)$/;
|
|
1194
|
+
const WINDOWS_DRIVE_PREFIX_REGEX = /^[A-Za-z]:/;
|
|
1195
|
+
const WINDOWS_ABSOLUTE_PATH_REGEX = /^[A-Za-z]:\\/;
|
|
1196
|
+
const WINDOWS_ABSOLUTE_SLASH_PATH_REGEX = /^[A-Za-z]:[/]/;
|
|
1197
|
+
const UNIX_USER_HOME_REGEX = /^\/Users\/[^/]+\//;
|
|
1198
|
+
const LINUX_USER_HOME_REGEX = /^\/home\/[^/]+\//;
|
|
1199
|
+
const WINDOWS_USER_HOME_REGEX = /^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]/;
|
|
1200
|
+
const DEPLOYMENT_PREFIX_REGEXES = [
|
|
1201
|
+
/^\/var\/www\/[^/]+\//,
|
|
1202
|
+
/^\/var\/task\//,
|
|
1203
|
+
/^\/usr\/src\/app\//,
|
|
1204
|
+
/^\/app\//,
|
|
1205
|
+
/^\/opt\/[^/]+\//,
|
|
1206
|
+
/^\/srv\/[^/]+\//
|
|
1207
|
+
];
|
|
1208
|
+
/**
|
|
1209
|
+
* Captures detailed exception information including stack traces and cause chains.
|
|
1210
|
+
*
|
|
1211
|
+
* This function extracts error metadata (type, message, stack trace) and recursively
|
|
1212
|
+
* unwraps Error.cause chains. It parses V8 stack traces into structured frames and
|
|
1213
|
+
* detects whether each frame is user code (in_app: true) or library code (in_app: false).
|
|
1214
|
+
*
|
|
1215
|
+
* @param error - The error to capture (can be Error, string, object, or any value)
|
|
1216
|
+
* @param contextStack - Optional Error object to use for stack context (for validation errors)
|
|
1217
|
+
* @returns ErrorData object with structured error information
|
|
1218
|
+
*/
|
|
1219
|
+
function captureException(error, contextStack) {
|
|
1220
|
+
if (isCallToolResult(error)) return captureCallToolResultError(error, contextStack);
|
|
1221
|
+
if (!(error instanceof Error)) return {
|
|
1222
|
+
message: stringifyNonError(error),
|
|
1223
|
+
type: void 0,
|
|
1224
|
+
platform: "javascript"
|
|
1225
|
+
};
|
|
1226
|
+
const errorData = {
|
|
1227
|
+
message: error.message || "",
|
|
1228
|
+
type: error.name || error.constructor?.name || void 0,
|
|
1229
|
+
platform: "javascript"
|
|
1230
|
+
};
|
|
1231
|
+
if (error.stack) {
|
|
1232
|
+
errorData.stack = error.stack;
|
|
1233
|
+
errorData.frames = parseV8StackTrace(error.stack);
|
|
1234
|
+
}
|
|
1235
|
+
const chainedErrors = unwrapErrorCauses(error);
|
|
1236
|
+
if (chainedErrors.length > 0) errorData.chained_errors = chainedErrors;
|
|
1237
|
+
return errorData;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Parses V8 stack trace string into structured StackFrame array.
|
|
1241
|
+
*
|
|
1242
|
+
* V8 stack traces have the format:
|
|
1243
|
+
* Error: message
|
|
1244
|
+
* at functionName (filename:line:col)
|
|
1245
|
+
* at Object.method (filename:line:col)
|
|
1246
|
+
* ...
|
|
1247
|
+
*
|
|
1248
|
+
* This function handles various V8 format variations including:
|
|
1249
|
+
* - Regular functions: "at functionName (file:10:5)"
|
|
1250
|
+
* - Anonymous functions: "at file:10:5"
|
|
1251
|
+
* - Async functions: "at async functionName (file:10:5)"
|
|
1252
|
+
* - Object methods: "at Object.method (file:10:5)"
|
|
1253
|
+
* - Native code: "at Array.map (native)"
|
|
1254
|
+
*
|
|
1255
|
+
* @param stackTrace - Raw V8 stack trace string from Error.stack
|
|
1256
|
+
* @returns Array of parsed StackFrame objects (limited to MAX_STACK_FRAMES)
|
|
1257
|
+
*/
|
|
1258
|
+
function parseV8StackTrace(stackTrace) {
|
|
1259
|
+
const frames = [];
|
|
1260
|
+
const lines = stackTrace.split("\n");
|
|
1261
|
+
for (const line of lines) {
|
|
1262
|
+
if (!line.trim().startsWith("at ")) continue;
|
|
1263
|
+
const frame = parseV8StackFrame(line.trim());
|
|
1264
|
+
if (frame) {
|
|
1265
|
+
addContextToFrame(frame);
|
|
1266
|
+
frames.push(frame);
|
|
1267
|
+
}
|
|
1268
|
+
if (frames.length >= MAX_STACK_FRAMES) break;
|
|
1269
|
+
}
|
|
1270
|
+
return frames;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Adds context_line to a stack frame by reading the source file.
|
|
1274
|
+
*
|
|
1275
|
+
* This function extracts the line of code where the error occurred by:
|
|
1276
|
+
* 1. Reading the source file using abs_path
|
|
1277
|
+
* 2. Extracting the line at the specified line number
|
|
1278
|
+
* 3. Setting the context_line field on the frame
|
|
1279
|
+
*
|
|
1280
|
+
* Only extracts context for user code (in_app: true)
|
|
1281
|
+
* If the file cannot be read or the line number is invalid, context_line remains undefined.
|
|
1282
|
+
*
|
|
1283
|
+
* @param frame - The StackFrame to add context to (modified in place)
|
|
1284
|
+
* @returns The modified StackFrame
|
|
1285
|
+
*/
|
|
1286
|
+
function addContextToFrame(frame) {
|
|
1287
|
+
if (!(frame.in_app && frame.abs_path && frame.lineno)) return frame;
|
|
1288
|
+
const fs = getFsSync();
|
|
1289
|
+
if (!fs) return frame;
|
|
1290
|
+
try {
|
|
1291
|
+
const lines = fs.readFileSync(frame.abs_path, "utf8").split("\n");
|
|
1292
|
+
const lineIndex = frame.lineno - 1;
|
|
1293
|
+
if (lineIndex >= 0 && lineIndex < lines.length) frame.context_line = lines[lineIndex];
|
|
1294
|
+
} catch {}
|
|
1295
|
+
return frame;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Parses a location string from a V8 stack frame.
|
|
1299
|
+
*
|
|
1300
|
+
* Handles different location formats:
|
|
1301
|
+
* - "fileName:lineNumber:columnNumber" - normal file location
|
|
1302
|
+
* - "eval at functionName (location)" - eval'd code (recursively unwraps)
|
|
1303
|
+
* - "native" - V8 internal code
|
|
1304
|
+
* - "unknown location" - location unavailable
|
|
1305
|
+
*
|
|
1306
|
+
* @param location - Location string from stack frame
|
|
1307
|
+
* @returns Object with filename, abs_path, and optional lineno/colno, or null if unparseable
|
|
1308
|
+
*/
|
|
1309
|
+
function parseLocation(location) {
|
|
1310
|
+
if (location === "native") return {
|
|
1311
|
+
filename: "native",
|
|
1312
|
+
abs_path: "native"
|
|
1313
|
+
};
|
|
1314
|
+
if (location === "unknown location") return {
|
|
1315
|
+
filename: "<unknown>",
|
|
1316
|
+
abs_path: "<unknown>"
|
|
1317
|
+
};
|
|
1318
|
+
if (location.startsWith("eval at ")) return parseEvalOrigin(location);
|
|
1319
|
+
const match = location.match(LOCATION_WITH_LINE_COLUMN_REGEX);
|
|
1320
|
+
if (match) {
|
|
1321
|
+
const [, filename, lineStr, colStr] = match;
|
|
1322
|
+
return {
|
|
1323
|
+
filename: makeRelativePath(filename),
|
|
1324
|
+
abs_path: filename,
|
|
1325
|
+
lineno: Number.parseInt(lineStr, 10),
|
|
1326
|
+
colno: Number.parseInt(colStr, 10)
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Recursively unwraps eval location chains to extract the underlying file location.
|
|
1333
|
+
*
|
|
1334
|
+
* Eval locations have the format: "eval at functionName (location), <anonymous>:line:col"
|
|
1335
|
+
* where location can be another eval or a file location.
|
|
1336
|
+
*
|
|
1337
|
+
* V8 formats:
|
|
1338
|
+
* - "eval at Bar.z (myscript.js:10:3)" → extract myscript.js:10:3
|
|
1339
|
+
* - "eval at Foo (eval at Bar (file.js:10:3)), <anonymous>:5:2" → extract file.js:10:3
|
|
1340
|
+
*
|
|
1341
|
+
* @param evalLocation - Eval location string starting with "eval at "
|
|
1342
|
+
* @returns Object with extracted file location, or null if unparseable
|
|
1343
|
+
*/
|
|
1344
|
+
function parseEvalOrigin(evalLocation) {
|
|
1345
|
+
let evalChainPart = evalLocation;
|
|
1346
|
+
const commaIndex = findCommaAfterBalancedParens(evalLocation);
|
|
1347
|
+
if (commaIndex !== -1) evalChainPart = evalLocation.slice(0, commaIndex);
|
|
1348
|
+
const innerLocation = extractTrailingParenthesizedContent(evalChainPart);
|
|
1349
|
+
if (!(innerLocation && evalChainPart.startsWith("eval at "))) return null;
|
|
1350
|
+
if (innerLocation.startsWith("eval at ")) return parseEvalOrigin(innerLocation);
|
|
1351
|
+
const locationMatch = innerLocation.match(LOCATION_WITH_LINE_COLUMN_REGEX);
|
|
1352
|
+
if (locationMatch) {
|
|
1353
|
+
const [, filename, lineStr, colStr] = locationMatch;
|
|
1354
|
+
return {
|
|
1355
|
+
filename: makeRelativePath(filename),
|
|
1356
|
+
abs_path: filename,
|
|
1357
|
+
lineno: Number.parseInt(lineStr, 10),
|
|
1358
|
+
colno: Number.parseInt(colStr, 10)
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Finds the index of the comma that appears after balanced parentheses.
|
|
1365
|
+
*
|
|
1366
|
+
* For "eval at f (eval at g (x)), <anonymous>:1:2", returns the index of the comma
|
|
1367
|
+
* after the closing ")" and before "<anonymous>".
|
|
1368
|
+
*
|
|
1369
|
+
* @param str - String to search
|
|
1370
|
+
* @returns Index of comma, or -1 if not found
|
|
1371
|
+
*/
|
|
1372
|
+
function findCommaAfterBalancedParens(str) {
|
|
1373
|
+
let depth = 0;
|
|
1374
|
+
let foundOpenParen = false;
|
|
1375
|
+
for (let i = 0; i < str.length; i++) if (str[i] === "(") {
|
|
1376
|
+
depth++;
|
|
1377
|
+
foundOpenParen = true;
|
|
1378
|
+
} else if (str[i] === ")") {
|
|
1379
|
+
depth--;
|
|
1380
|
+
if (depth === 0 && foundOpenParen) {
|
|
1381
|
+
for (let j = i + 1; j < str.length; j++) {
|
|
1382
|
+
if (str[j] === ",") return j;
|
|
1383
|
+
if (str[j] !== " ") return -1;
|
|
1384
|
+
}
|
|
1385
|
+
return -1;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return -1;
|
|
1389
|
+
}
|
|
1390
|
+
function extractTrailingParenthesizedContent(value) {
|
|
1391
|
+
const trimmedValue = value.trim();
|
|
1392
|
+
if (!trimmedValue.endsWith(")")) return null;
|
|
1393
|
+
const openingParenIndex = findMatchingOpeningParen(trimmedValue);
|
|
1394
|
+
if (openingParenIndex === -1) return null;
|
|
1395
|
+
return trimmedValue.slice(openingParenIndex + 1, -1);
|
|
1396
|
+
}
|
|
1397
|
+
function findMatchingOpeningParen(value) {
|
|
1398
|
+
let depth = 0;
|
|
1399
|
+
for (let index = value.length - 1; index >= 0; index--) {
|
|
1400
|
+
const char = value[index];
|
|
1401
|
+
if (char === ")") depth++;
|
|
1402
|
+
else if (char === "(") {
|
|
1403
|
+
depth--;
|
|
1404
|
+
if (depth === 0) return index;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return -1;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Parses a single V8 stack frame line into a StackFrame object.
|
|
1411
|
+
*
|
|
1412
|
+
* Handles multiple V8 stack frame formats:
|
|
1413
|
+
* - "at functionName (filename:line:col)"
|
|
1414
|
+
* - "at filename:line:col" (top-level code)
|
|
1415
|
+
* - "at async functionName (filename:line:col)"
|
|
1416
|
+
* - "at Object.method (filename:line:col)"
|
|
1417
|
+
* - "at Module._compile (node:internal/...)" (internal modules)
|
|
1418
|
+
* - "at functionName (eval at ...)" (eval'd code)
|
|
1419
|
+
* - "at functionName (native)" (native code)
|
|
1420
|
+
*
|
|
1421
|
+
* @param line - Single line from V8 stack trace (trimmed, starts with "at ")
|
|
1422
|
+
* @returns Parsed StackFrame or null if line cannot be parsed
|
|
1423
|
+
*/
|
|
1424
|
+
function parseV8StackFrame(line) {
|
|
1425
|
+
const withoutAt = line.slice(3);
|
|
1426
|
+
const location = extractTrailingParenthesizedContent(withoutAt);
|
|
1427
|
+
if (location) {
|
|
1428
|
+
const openingParenIndex = findMatchingOpeningParen(withoutAt.trim());
|
|
1429
|
+
const functionName = withoutAt.slice(0, openingParenIndex).trim();
|
|
1430
|
+
const parsedLocation = parseLocation(location);
|
|
1431
|
+
if (functionName && parsedLocation) return {
|
|
1432
|
+
function: functionName,
|
|
1433
|
+
filename: parsedLocation.filename,
|
|
1434
|
+
abs_path: parsedLocation.abs_path,
|
|
1435
|
+
lineno: parsedLocation.lineno,
|
|
1436
|
+
colno: parsedLocation.colno,
|
|
1437
|
+
in_app: isInApp(parsedLocation.abs_path)
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
const parsedLocation = parseLocation(withoutAt);
|
|
1441
|
+
if (parsedLocation) return {
|
|
1442
|
+
function: "<anonymous>",
|
|
1443
|
+
filename: parsedLocation.filename,
|
|
1444
|
+
abs_path: parsedLocation.abs_path,
|
|
1445
|
+
lineno: parsedLocation.lineno,
|
|
1446
|
+
colno: parsedLocation.colno,
|
|
1447
|
+
in_app: isInApp(parsedLocation.abs_path)
|
|
1448
|
+
};
|
|
1449
|
+
return {
|
|
1450
|
+
function: withoutAt,
|
|
1451
|
+
filename: "<unknown>",
|
|
1452
|
+
in_app: false
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Determines if a file path represents user code (in_app: true) or library code (in_app: false).
|
|
1457
|
+
*
|
|
1458
|
+
* Library code is identified by:
|
|
1459
|
+
* - Paths containing "/node_modules/"
|
|
1460
|
+
* - Node.js internal modules (e.g., "node:internal/...")
|
|
1461
|
+
* - Native code
|
|
1462
|
+
*
|
|
1463
|
+
* @param filename - File path from stack frame
|
|
1464
|
+
* @returns true if user code, false if library code
|
|
1465
|
+
*/
|
|
1466
|
+
function isInApp(filename) {
|
|
1467
|
+
if (filename.includes("/node_modules/") || filename.includes("\\node_modules\\")) return false;
|
|
1468
|
+
if (filename.startsWith("node:")) return false;
|
|
1469
|
+
if (filename === "native" || filename === "<unknown>") return false;
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Normalizes URL schemes to regular file paths.
|
|
1474
|
+
*
|
|
1475
|
+
* Handles file:// URLs commonly seen in ESM modules and local testing:
|
|
1476
|
+
* - "file:///Users/john/project/src/index.ts" → "/Users/john/project/src/index.ts"
|
|
1477
|
+
* - "file:///C:/projects/app/src/index.ts" → "C:/projects/app/src/index.ts"
|
|
1478
|
+
*
|
|
1479
|
+
* @param filename - File path that may be a file:// URL
|
|
1480
|
+
* @returns Clean file path without URL scheme
|
|
1481
|
+
*/
|
|
1482
|
+
function normalizeUrl(filename) {
|
|
1483
|
+
if (filename.startsWith("file://")) {
|
|
1484
|
+
let result = filename.slice(7);
|
|
1485
|
+
if (!(result.startsWith("/") || WINDOWS_DRIVE_PREFIX_REGEX.test(result))) result = `/${result}`;
|
|
1486
|
+
return result;
|
|
1487
|
+
}
|
|
1488
|
+
return filename;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Normalizes Node.js internal module paths for consistent error grouping.
|
|
1492
|
+
*
|
|
1493
|
+
* Examples:
|
|
1494
|
+
* - "node:internal/modules/cjs/loader" → "node:internal"
|
|
1495
|
+
* - "node:fs/promises" → "node:fs"
|
|
1496
|
+
* - "node:fs" → "node:fs" (unchanged)
|
|
1497
|
+
*
|
|
1498
|
+
* @param filename - File path that may be a Node.js internal module
|
|
1499
|
+
* @returns Simplified module path or original filename
|
|
1500
|
+
*/
|
|
1501
|
+
function normalizeNodeInternals(filename) {
|
|
1502
|
+
if (filename.startsWith("node:internal")) return "node:internal";
|
|
1503
|
+
if (filename.startsWith("node:")) return filename.split("/")[0];
|
|
1504
|
+
return filename;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Strips user-specific and system path prefixes.
|
|
1508
|
+
*
|
|
1509
|
+
* Removes prefixes like:
|
|
1510
|
+
* - /Users/username/ → ~/
|
|
1511
|
+
* - /home/username/ → ~/
|
|
1512
|
+
* - C:\Users\username\ → ~\
|
|
1513
|
+
* - C:/Users/username/ → ~/ (mixed separators)
|
|
1514
|
+
*
|
|
1515
|
+
* @param path - File path to normalize
|
|
1516
|
+
* @returns Path with system prefixes removed
|
|
1517
|
+
*/
|
|
1518
|
+
function stripSystemPrefixes(path) {
|
|
1519
|
+
let result = path;
|
|
1520
|
+
result = result.replace(UNIX_USER_HOME_REGEX, "~/");
|
|
1521
|
+
result = result.replace(LINUX_USER_HOME_REGEX, "~/");
|
|
1522
|
+
result = result.replace(WINDOWS_USER_HOME_REGEX, "~/");
|
|
1523
|
+
return result;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Normalizes node_modules paths to be consistent across deployments.
|
|
1527
|
+
*
|
|
1528
|
+
* Extracts only the package-relative portion of the path:
|
|
1529
|
+
* - /Users/john/project/node_modules/express/lib/router.js → node_modules/express/lib/router.js
|
|
1530
|
+
* - /app/node_modules/@scope/pkg/index.js → node_modules/@scope/pkg/index.js
|
|
1531
|
+
*
|
|
1532
|
+
* @param path - File path that may contain node_modules
|
|
1533
|
+
* @returns Normalized node_modules path or original path
|
|
1534
|
+
*/
|
|
1535
|
+
function normalizeNodeModules(path) {
|
|
1536
|
+
const unixIndex = path.lastIndexOf("/node_modules/");
|
|
1537
|
+
const winIndex = path.lastIndexOf("\\node_modules\\");
|
|
1538
|
+
if (unixIndex !== -1) return path.slice(unixIndex + 1);
|
|
1539
|
+
if (winIndex !== -1) return path.slice(winIndex + 1).replace(/\\/g, "/");
|
|
1540
|
+
return path;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Strips common deployment-specific path prefixes.
|
|
1544
|
+
*
|
|
1545
|
+
* Removes prefixes like:
|
|
1546
|
+
* - /var/www/app/ → ""
|
|
1547
|
+
* - /app/ → ""
|
|
1548
|
+
* - /opt/project/ → ""
|
|
1549
|
+
* - /var/task/ → "" (AWS Lambda)
|
|
1550
|
+
* - /usr/src/app/ → "" (Docker)
|
|
1551
|
+
*
|
|
1552
|
+
* @param path - File path to normalize
|
|
1553
|
+
* @returns Path with deployment prefixes removed
|
|
1554
|
+
*/
|
|
1555
|
+
function stripDeploymentPaths(path) {
|
|
1556
|
+
let result = path;
|
|
1557
|
+
for (const prefix of DEPLOYMENT_PREFIX_REGEXES) result = result.replace(prefix, "");
|
|
1558
|
+
return result;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Finds project-relative path using common project boundary markers.
|
|
1562
|
+
*
|
|
1563
|
+
* Looks for markers like /src/, /lib/, /dist/, /build/ and extracts the path
|
|
1564
|
+
* from that marker onwards:
|
|
1565
|
+
* - /Users/john/project/src/components/Button.tsx → src/components/Button.tsx
|
|
1566
|
+
* - /app/dist/index.js → dist/index.js
|
|
1567
|
+
*
|
|
1568
|
+
* Priority order: looks for primary markers first (src, lib, dist, build),
|
|
1569
|
+
* then secondary markers. Uses the highest-priority marker found.
|
|
1570
|
+
*
|
|
1571
|
+
* @param path - File path to search for project boundaries
|
|
1572
|
+
* @returns Project-relative path or original path if no marker found
|
|
1573
|
+
*/
|
|
1574
|
+
function findProjectPath(path) {
|
|
1575
|
+
const primaryMarkers = [
|
|
1576
|
+
"/src/",
|
|
1577
|
+
"/lib/",
|
|
1578
|
+
"/dist/",
|
|
1579
|
+
"/build/"
|
|
1580
|
+
];
|
|
1581
|
+
const secondaryMarkers = [
|
|
1582
|
+
"/app/",
|
|
1583
|
+
"/components/",
|
|
1584
|
+
"/pages/",
|
|
1585
|
+
"/api/",
|
|
1586
|
+
"/utils/",
|
|
1587
|
+
"/services/",
|
|
1588
|
+
"/modules/"
|
|
1589
|
+
];
|
|
1590
|
+
for (const marker of primaryMarkers) {
|
|
1591
|
+
const index = path.lastIndexOf(marker);
|
|
1592
|
+
if (index !== -1) return path.slice(index + 1);
|
|
1593
|
+
}
|
|
1594
|
+
for (const marker of secondaryMarkers) {
|
|
1595
|
+
const index = path.lastIndexOf(marker);
|
|
1596
|
+
if (index !== -1) return path.slice(index + 1);
|
|
1597
|
+
}
|
|
1598
|
+
return path;
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Converts absolute file paths to normalized relative paths for consistent error grouping.
|
|
1602
|
+
*
|
|
1603
|
+
* This function performs comprehensive path normalization to ensure errors from the same
|
|
1604
|
+
* code location group together regardless of deployment environment, user directories,
|
|
1605
|
+
* or system-specific paths. The original absolute path is always preserved in abs_path.
|
|
1606
|
+
*
|
|
1607
|
+
* Normalization steps:
|
|
1608
|
+
* 1. Normalize URL schemes (file://, etc.) - must be first to strip URL prefixes
|
|
1609
|
+
* 2. Preserve special paths (already relative, Node internals, etc.)
|
|
1610
|
+
* 3. Normalize path separators to forward slashes (for consistent processing)
|
|
1611
|
+
* 4. Normalize Node.js internal modules (node:internal/*, node:fs/*)
|
|
1612
|
+
* 5. Normalize node_modules paths to package-relative format
|
|
1613
|
+
* 6. Strip user home directories (/Users/*, /home/*, C:\Users\*)
|
|
1614
|
+
* 7. Strip deployment-specific paths (/var/www/*, /app/, AWS Lambda, Docker)
|
|
1615
|
+
* 8. Strip current working directory
|
|
1616
|
+
* 9. Find project boundaries (/src/, /lib/, /dist/, etc.)
|
|
1617
|
+
* 10. Remove leading slashes for clean relative paths
|
|
1618
|
+
*
|
|
1619
|
+
* @param filename - Absolute or relative file path from stack trace
|
|
1620
|
+
* @returns Normalized relative path for error grouping
|
|
1621
|
+
*
|
|
1622
|
+
* @example
|
|
1623
|
+
* makeRelativePath('/Users/john/project/src/index.ts')
|
|
1624
|
+
* // Returns: 'src/index.ts'
|
|
1625
|
+
*
|
|
1626
|
+
* @example
|
|
1627
|
+
* makeRelativePath('/home/ubuntu/app/node_modules/express/lib/router.js')
|
|
1628
|
+
* // Returns: 'node_modules/express/lib/router.js'
|
|
1629
|
+
*
|
|
1630
|
+
* @example
|
|
1631
|
+
* makeRelativePath('/var/www/myapp/dist/server.js')
|
|
1632
|
+
* // Returns: 'dist/server.js'
|
|
1633
|
+
*
|
|
1634
|
+
* @example
|
|
1635
|
+
* makeRelativePath('node:internal/modules/cjs/loader')
|
|
1636
|
+
* // Returns: 'node:internal'
|
|
1637
|
+
*
|
|
1638
|
+
* @example
|
|
1639
|
+
* makeRelativePath('C:\\Users\\John\\projects\\myapp\\src\\index.ts')
|
|
1640
|
+
* // Returns: 'src/index.ts'
|
|
1641
|
+
*/
|
|
1642
|
+
function makeRelativePath(filename) {
|
|
1643
|
+
let result = filename;
|
|
1644
|
+
result = normalizeUrl(result);
|
|
1645
|
+
if (!(result.startsWith("/") || WINDOWS_ABSOLUTE_PATH_REGEX.test(result))) {
|
|
1646
|
+
if (result.startsWith("node:")) return normalizeNodeInternals(result);
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1649
|
+
result = result.replace(/\\/g, "/");
|
|
1650
|
+
if (result.startsWith("node:")) return normalizeNodeInternals(result);
|
|
1651
|
+
if (result.includes("/node_modules/")) return normalizeNodeModules(result);
|
|
1652
|
+
result = stripSystemPrefixes(result);
|
|
1653
|
+
result = stripDeploymentPaths(result);
|
|
1654
|
+
let cwd = null;
|
|
1655
|
+
try {
|
|
1656
|
+
if (typeof process !== "undefined" && typeof process.cwd === "function") cwd = process.cwd();
|
|
1657
|
+
} catch {}
|
|
1658
|
+
if (cwd && result.startsWith(cwd)) result = result.slice(cwd.length + 1);
|
|
1659
|
+
if (result.startsWith("/") || WINDOWS_ABSOLUTE_SLASH_PATH_REGEX.test(result)) result = findProjectPath(result);
|
|
1660
|
+
else if (result.startsWith("~")) {
|
|
1661
|
+
const absoluteWithoutTilde = `/${result.slice(2)}`;
|
|
1662
|
+
const projectPath = findProjectPath(absoluteWithoutTilde);
|
|
1663
|
+
if (projectPath !== absoluteWithoutTilde) result = projectPath;
|
|
1664
|
+
}
|
|
1665
|
+
if (result.startsWith("/")) result = result.slice(1);
|
|
1666
|
+
return result;
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Recursively unwraps Error.cause chain and returns array of chained errors.
|
|
1670
|
+
*
|
|
1671
|
+
* Error.cause is a standard JavaScript feature that allows chaining errors:
|
|
1672
|
+
* const cause = new Error("Root cause");
|
|
1673
|
+
* const error = new Error("Wrapper error", { cause });
|
|
1674
|
+
*
|
|
1675
|
+
* This function extracts all errors in the cause chain up to MAX_EXCEPTION_CHAIN_DEPTH.
|
|
1676
|
+
*
|
|
1677
|
+
* @param error - Error object to unwrap
|
|
1678
|
+
* @returns Array of ChainedErrorData objects representing the error chain
|
|
1679
|
+
*/
|
|
1680
|
+
function unwrapErrorCauses(error) {
|
|
1681
|
+
const chainedErrors = [];
|
|
1682
|
+
const seenErrors = /* @__PURE__ */ new Set();
|
|
1683
|
+
let currentError = error.cause;
|
|
1684
|
+
let depth = 0;
|
|
1685
|
+
while (currentError && depth < MAX_EXCEPTION_CHAIN_DEPTH) {
|
|
1686
|
+
if (!(currentError instanceof Error)) {
|
|
1687
|
+
chainedErrors.push({
|
|
1688
|
+
message: stringifyNonError(currentError),
|
|
1689
|
+
type: void 0
|
|
1690
|
+
});
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
if (seenErrors.has(currentError)) break;
|
|
1694
|
+
seenErrors.add(currentError);
|
|
1695
|
+
const chainedErrorData = {
|
|
1696
|
+
message: currentError.message || "",
|
|
1697
|
+
type: currentError.name || currentError.constructor?.name || "Error"
|
|
1698
|
+
};
|
|
1699
|
+
if (currentError.stack) {
|
|
1700
|
+
chainedErrorData.stack = currentError.stack;
|
|
1701
|
+
chainedErrorData.frames = parseV8StackTrace(currentError.stack);
|
|
1702
|
+
}
|
|
1703
|
+
chainedErrors.push(chainedErrorData);
|
|
1704
|
+
currentError = currentError.cause;
|
|
1705
|
+
depth++;
|
|
1706
|
+
}
|
|
1707
|
+
return chainedErrors;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Detects if a value is a CallToolResult object (SDK 1.21.0+ error format).
|
|
1711
|
+
*
|
|
1712
|
+
* SDK 1.21.0+ converts errors to CallToolResult format:
|
|
1713
|
+
* { content: [{ type: "text", text: "error message" }], isError: true }
|
|
1714
|
+
*
|
|
1715
|
+
* @param value - Value to check
|
|
1716
|
+
* @returns True if value is a CallToolResult object
|
|
1717
|
+
*/
|
|
1718
|
+
function isCallToolResult(value) {
|
|
1719
|
+
return value !== null && typeof value === "object" && "isError" in value && "content" in value && Array.isArray(value.content);
|
|
1720
|
+
}
|
|
1721
|
+
function isTextContentPart(value) {
|
|
1722
|
+
if (value === null || typeof value !== "object") return false;
|
|
1723
|
+
const contentPart = value;
|
|
1724
|
+
return contentPart.type === "text" && typeof contentPart.text === "string";
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Extracts error information from CallToolResult objects.
|
|
1728
|
+
*
|
|
1729
|
+
* SDK 1.21.0+ converts errors to CallToolResult, losing original stack traces.
|
|
1730
|
+
* This extracts the error message from the content array.
|
|
1731
|
+
*
|
|
1732
|
+
* @param result - CallToolResult object with error
|
|
1733
|
+
* @param _contextStack - Optional Error object for stack context (unused, kept for compatibility)
|
|
1734
|
+
* @returns ErrorData with extracted message (no stack trace)
|
|
1735
|
+
*/
|
|
1736
|
+
function captureCallToolResultError(result, _contextStack) {
|
|
1737
|
+
return {
|
|
1738
|
+
message: result.content.filter(isTextContentPart).map((contentPart) => contentPart.text).join(" ").trim() || "Unknown error",
|
|
1739
|
+
type: void 0,
|
|
1740
|
+
platform: "javascript"
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Converts non-Error objects to string representation for error messages.
|
|
1745
|
+
*
|
|
1746
|
+
* In JavaScript, anything can be thrown (not just Error objects):
|
|
1747
|
+
* throw "string error";
|
|
1748
|
+
* throw { code: 404 };
|
|
1749
|
+
* throw null;
|
|
1750
|
+
*
|
|
1751
|
+
* This function handles these cases by converting them to meaningful strings.
|
|
1752
|
+
*
|
|
1753
|
+
* @param value - Non-Error value that was thrown
|
|
1754
|
+
* @returns String representation of the value
|
|
1755
|
+
*/
|
|
1756
|
+
function stringifyNonError(value) {
|
|
1757
|
+
if (value === null) return "null";
|
|
1758
|
+
if (value === void 0) return "undefined";
|
|
1759
|
+
if (typeof value === "string") return value;
|
|
1760
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1761
|
+
try {
|
|
1762
|
+
return JSON.stringify(value);
|
|
1763
|
+
} catch {
|
|
1764
|
+
return String(value);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/modules/context-parameters.ts
|
|
1769
|
+
function isContextEnabled(context) {
|
|
1770
|
+
return context !== false;
|
|
1771
|
+
}
|
|
1772
|
+
function getContextDescription(context) {
|
|
1773
|
+
return typeof context === "object" ? context.description : void 0;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Adds a context parameter to a tool's JSON Schema.
|
|
1777
|
+
* This function is called AFTER the MCP SDK has converted Zod schemas to JSON Schema,
|
|
1778
|
+
* so we only need to handle JSON Schema format.
|
|
1779
|
+
*
|
|
1780
|
+
* Skips injection (with warning) for:
|
|
1781
|
+
* - Tools that already have a 'context' parameter
|
|
1782
|
+
* - Complex schemas (oneOf/allOf/anyOf) that can't safely have properties added
|
|
1783
|
+
* - Schemas with additionalProperties: false
|
|
1784
|
+
*/
|
|
1785
|
+
function addContextParameterToTool(tool, contextDescriptionOverride) {
|
|
1786
|
+
const modifiedTool = { ...tool };
|
|
1787
|
+
const toolName = tool.name || "unknown";
|
|
1788
|
+
const schema = modifiedTool.inputSchema;
|
|
1789
|
+
if (schema?.properties?.context) {
|
|
1790
|
+
writeToLog(`WARN: Tool "${toolName}" already has 'context' parameter. Skipping context injection.`);
|
|
1791
|
+
return modifiedTool;
|
|
1792
|
+
}
|
|
1793
|
+
if (schema?.oneOf || schema?.allOf || schema?.anyOf) {
|
|
1794
|
+
writeToLog(`WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf). Skipping context injection.`);
|
|
1795
|
+
return modifiedTool;
|
|
1796
|
+
}
|
|
1797
|
+
if (!modifiedTool.inputSchema) modifiedTool.inputSchema = {
|
|
1798
|
+
type: "object",
|
|
1799
|
+
properties: {},
|
|
1800
|
+
required: []
|
|
1801
|
+
};
|
|
1802
|
+
const contextDescription = contextDescriptionOverride || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION;
|
|
1803
|
+
modifiedTool.inputSchema = JSON.parse(JSON.stringify(modifiedTool.inputSchema));
|
|
1804
|
+
const inputSchema = modifiedTool.inputSchema;
|
|
1805
|
+
if (!inputSchema.properties) inputSchema.properties = {};
|
|
1806
|
+
if (inputSchema.additionalProperties === false) inputSchema.additionalProperties = void 0;
|
|
1807
|
+
inputSchema.properties.context = {
|
|
1808
|
+
type: "string",
|
|
1809
|
+
description: contextDescription
|
|
1810
|
+
};
|
|
1811
|
+
if (Array.isArray(inputSchema.required)) {
|
|
1812
|
+
if (!inputSchema.required.includes("context")) inputSchema.required.push("context");
|
|
1813
|
+
} else inputSchema.required = ["context"];
|
|
1814
|
+
return modifiedTool;
|
|
1815
|
+
}
|
|
1816
|
+
function addContextParameterToTools(tools, contextDescriptionOverride) {
|
|
1817
|
+
return tools.map((tool) => {
|
|
1818
|
+
if (tool.name === "get_more_tools") return tool;
|
|
1819
|
+
return addContextParameterToTool(tool, contextDescriptionOverride);
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
//#endregion
|
|
1823
|
+
//#region src/modules/tools.ts
|
|
1824
|
+
const GET_MORE_TOOLS_NAME = "get_more_tools";
|
|
1825
|
+
function getReportMissingToolDescriptor() {
|
|
1826
|
+
return {
|
|
1827
|
+
name: GET_MORE_TOOLS_NAME,
|
|
1828
|
+
description: "Check for additional tools whenever your task might benefit from specialized capabilities - even if existing tools could work as a fallback.",
|
|
1829
|
+
inputSchema: {
|
|
1830
|
+
type: "object",
|
|
1831
|
+
properties: { context: {
|
|
1832
|
+
type: "string",
|
|
1833
|
+
description: "A description of your goal and what kind of tool would help accomplish it."
|
|
1834
|
+
} },
|
|
1835
|
+
required: ["context"]
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
function handleReportMissing(args) {
|
|
1840
|
+
writeToLog(`Missing tool reported: ${JSON.stringify(args)}`);
|
|
1841
|
+
return { content: [{
|
|
1842
|
+
type: "text",
|
|
1843
|
+
text: "Unfortunately, we have shown you the full tool list. We have noted your feedback and will work to improve the tool list in the future."
|
|
1844
|
+
}] };
|
|
1845
|
+
}
|
|
1846
|
+
function setupMCPAnalyticsTools(server) {
|
|
1847
|
+
const handlers = server._requestHandlers;
|
|
1848
|
+
const originalListToolsHandler = handlers.get("tools/list");
|
|
1849
|
+
const originalCallToolHandler = handlers.get("tools/call");
|
|
1850
|
+
if (!(originalListToolsHandler && originalCallToolHandler)) {
|
|
1851
|
+
writeToLog("Warning: Original tool handlers not found. Your tools may not be setup before PostHog MCP analytics .track().");
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
|
|
1856
|
+
let tools = [];
|
|
1857
|
+
const data = getServerTrackingData(server);
|
|
1858
|
+
const event = {
|
|
1859
|
+
sessionId: getServerSessionId(server, extra),
|
|
1860
|
+
parameters: {
|
|
1861
|
+
request,
|
|
1862
|
+
extra
|
|
1863
|
+
},
|
|
1864
|
+
eventType: MCPAnalyticsEventType.mcpToolsList,
|
|
1865
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1866
|
+
redactionFn: data?.options.redactSensitiveInformation
|
|
1867
|
+
};
|
|
1868
|
+
try {
|
|
1869
|
+
tools = (await originalListToolsHandler(request, extra)).tools || [];
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
writeToLog(`Warning: Original list tools handler failed, this suggests an error PostHog MCP analytics did not cause - ${error}`);
|
|
1872
|
+
event.error = { message: getMCPCompatibleErrorMessage(error) };
|
|
1873
|
+
event.isError = true;
|
|
1874
|
+
event.duration = event.timestamp && Date.now() - event.timestamp.getTime() || 0;
|
|
1875
|
+
publishEvent(server, event);
|
|
1876
|
+
throw error;
|
|
1877
|
+
}
|
|
1878
|
+
if (!data) {
|
|
1879
|
+
writeToLog("Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
|
|
1880
|
+
return { tools };
|
|
1881
|
+
}
|
|
1882
|
+
if (tools.length === 0) {
|
|
1883
|
+
writeToLog("Warning: No tools found in the original list. This is likely due to the tools not being registered before PostHog MCP analytics.track().");
|
|
1884
|
+
event.error = { message: "No tools were sent to MCP client." };
|
|
1885
|
+
event.isError = true;
|
|
1886
|
+
event.duration = event.timestamp && Date.now() - event.timestamp.getTime() || 0;
|
|
1887
|
+
publishEvent(server, event);
|
|
1888
|
+
return { tools };
|
|
1889
|
+
}
|
|
1890
|
+
if (isContextEnabled(data.options.context)) tools = addContextParameterToTools(tools, getContextDescription(data.options.context));
|
|
1891
|
+
if (data.options.reportMissing) {
|
|
1892
|
+
if (!tools.some((tool) => tool?.name === "get_more_tools")) tools.push(getReportMissingToolDescriptor());
|
|
1893
|
+
}
|
|
1894
|
+
event.response = { tools };
|
|
1895
|
+
event.isError = false;
|
|
1896
|
+
event.duration = event.timestamp && Date.now() - event.timestamp.getTime() || 0;
|
|
1897
|
+
publishEvent(server, event);
|
|
1898
|
+
return { tools };
|
|
1899
|
+
});
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
writeToLog(`Warning: Failed to override list tools handler - ${error}`);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
//#endregion
|
|
1905
|
+
//#region src/modules/tracing.ts
|
|
1906
|
+
function isToolResultError$1(result) {
|
|
1907
|
+
return !!result && typeof result === "object" && "isError" in result && result.isError === true;
|
|
1908
|
+
}
|
|
1909
|
+
const listToolsTracingSetup = /* @__PURE__ */ new WeakMap();
|
|
1910
|
+
function setupListToolsTracing(highLevelServer) {
|
|
1911
|
+
const server = highLevelServer.server;
|
|
1912
|
+
if (!server._capabilities?.tools) return;
|
|
1913
|
+
if (listToolsTracingSetup.get(server)) return;
|
|
1914
|
+
const originalListToolsHandler = server._requestHandlers.get("tools/list");
|
|
1915
|
+
if (!originalListToolsHandler) return;
|
|
1916
|
+
try {
|
|
1917
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => await handleListToolsRequest(server, originalListToolsHandler, request, extra));
|
|
1918
|
+
listToolsTracingSetup.set(server, true);
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
writeToLog(`Warning: Failed to override list tools handler - ${error}`);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
async function handleListToolsRequest(server, originalListToolsHandler, request, extra) {
|
|
1924
|
+
const data = getServerTrackingData(server);
|
|
1925
|
+
const event = {
|
|
1926
|
+
sessionId: getServerSessionId(server, extra),
|
|
1927
|
+
parameters: buildCapturedMcpParameters(request),
|
|
1928
|
+
eventType: MCPAnalyticsEventType.mcpToolsList,
|
|
1929
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1930
|
+
redactionFn: data?.options.redactSensitiveInformation
|
|
1931
|
+
};
|
|
1932
|
+
if (data) await applyResolvedMetadata$1(event, data, request, extra);
|
|
1933
|
+
const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event);
|
|
1934
|
+
if (!data) {
|
|
1935
|
+
writeToLog("Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
|
|
1936
|
+
return { tools };
|
|
1937
|
+
}
|
|
1938
|
+
if (tools.length === 0) {
|
|
1939
|
+
writeToLog("Warning: No tools found in the original list. This is likely due to the tools not being registered before PostHog MCP analytics.track().");
|
|
1940
|
+
event.error = { message: "No tools were sent to MCP client." };
|
|
1941
|
+
event.isError = true;
|
|
1942
|
+
event.duration = getEventDuration(event);
|
|
1943
|
+
publishEvent(server, event);
|
|
1944
|
+
return { tools };
|
|
1945
|
+
}
|
|
1946
|
+
event.response = { tools };
|
|
1947
|
+
event.isError = false;
|
|
1948
|
+
event.duration = getEventDuration(event);
|
|
1949
|
+
publishEvent(server, event);
|
|
1950
|
+
return { tools };
|
|
1951
|
+
}
|
|
1952
|
+
async function getTracedToolsList(server, originalListToolsHandler, request, extra, event) {
|
|
1953
|
+
try {
|
|
1954
|
+
const data = getServerTrackingData(server);
|
|
1955
|
+
let tools = (await originalListToolsHandler(request, extra)).tools || [];
|
|
1956
|
+
if (data && isContextEnabled(data.options.context)) tools = addContextParameterToTools(tools, getContextDescription(data.options.context));
|
|
1957
|
+
if (data?.options.reportMissing) {
|
|
1958
|
+
if (!tools.some((tool) => tool?.name === "get_more_tools")) tools.push(getReportMissingToolDescriptor());
|
|
1959
|
+
}
|
|
1960
|
+
return tools;
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
writeToLog(`Warning: Original list tools handler failed, this suggests an error PostHog MCP analytics did not cause - ${error}`);
|
|
1963
|
+
event.error = { message: getMCPCompatibleErrorMessage(error) };
|
|
1964
|
+
event.isError = true;
|
|
1965
|
+
event.duration = getEventDuration(event);
|
|
1966
|
+
publishEvent(server, event);
|
|
1967
|
+
throw error;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
function setupInitializeTracing(highLevelServer) {
|
|
1971
|
+
const server = highLevelServer.server;
|
|
1972
|
+
const originalInitializeHandler = server._requestHandlers.get("initialize");
|
|
1973
|
+
if (originalInitializeHandler) server.setRequestHandler(InitializeRequestSchema, async (request, extra) => {
|
|
1974
|
+
const data = getServerTrackingData(server);
|
|
1975
|
+
if (!data) {
|
|
1976
|
+
writeToLog("Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
|
|
1977
|
+
return await originalInitializeHandler(request, extra);
|
|
1978
|
+
}
|
|
1979
|
+
const sessionId = getServerSessionId(server, extra);
|
|
1980
|
+
await handleIdentify(server, data, request, extra);
|
|
1981
|
+
const event = {
|
|
1982
|
+
sessionId,
|
|
1983
|
+
resourceName: request.params?.name || "Unknown Tool Name",
|
|
1984
|
+
eventType: MCPAnalyticsEventType.mcpInitialize,
|
|
1985
|
+
parameters: buildCapturedMcpParameters(request),
|
|
1986
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1987
|
+
redactionFn: data.options.redactSensitiveInformation
|
|
1988
|
+
};
|
|
1989
|
+
const resolvedTags = await resolveEventTags(data, request, extra);
|
|
1990
|
+
if (resolvedTags) event.tags = resolvedTags;
|
|
1991
|
+
const resolvedProperties = await resolveEventProperties(data, request, extra);
|
|
1992
|
+
if (resolvedProperties) event.properties = resolvedProperties;
|
|
1993
|
+
const result = await originalInitializeHandler(request, extra);
|
|
1994
|
+
event.response = result;
|
|
1995
|
+
publishEvent(server, event);
|
|
1996
|
+
return result;
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
function setupToolCallTracing(server) {
|
|
2000
|
+
try {
|
|
2001
|
+
const handlers = server._requestHandlers;
|
|
2002
|
+
const originalCallToolHandler = handlers.get("tools/call");
|
|
2003
|
+
const originalInitializeHandler = handlers.get("initialize");
|
|
2004
|
+
if (originalInitializeHandler) server.setRequestHandler(InitializeRequestSchema, async (request, extra) => {
|
|
2005
|
+
const data = getServerTrackingData(server);
|
|
2006
|
+
if (!data) {
|
|
2007
|
+
writeToLog("Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
|
|
2008
|
+
return await originalInitializeHandler(request, extra);
|
|
2009
|
+
}
|
|
2010
|
+
const sessionId = getServerSessionId(server, extra);
|
|
2011
|
+
await handleIdentify(server, data, request, extra);
|
|
2012
|
+
const event = {
|
|
2013
|
+
sessionId,
|
|
2014
|
+
resourceName: request.params?.name || "Unknown Tool Name",
|
|
2015
|
+
eventType: MCPAnalyticsEventType.mcpInitialize,
|
|
2016
|
+
parameters: buildCapturedMcpParameters(request),
|
|
2017
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2018
|
+
redactionFn: data.options.redactSensitiveInformation
|
|
2019
|
+
};
|
|
2020
|
+
const resolvedTags = await resolveEventTags(data, request, extra);
|
|
2021
|
+
if (resolvedTags) event.tags = resolvedTags;
|
|
2022
|
+
const resolvedProperties = await resolveEventProperties(data, request, extra);
|
|
2023
|
+
if (resolvedProperties) event.properties = resolvedProperties;
|
|
2024
|
+
const result = await originalInitializeHandler(request, extra);
|
|
2025
|
+
event.response = result;
|
|
2026
|
+
publishEvent(server, event);
|
|
2027
|
+
return result;
|
|
2028
|
+
});
|
|
2029
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => await handleToolCallRequest(server, originalCallToolHandler, request, extra));
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
writeToLog(`Warning: Failed to setup tool call tracing - ${error}`);
|
|
2032
|
+
throw error;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
async function handleToolCallRequest(server, originalCallToolHandler, request, extra) {
|
|
2036
|
+
const data = getServerTrackingData(server);
|
|
2037
|
+
if (!data) {
|
|
2038
|
+
writeToLog("Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
|
|
2039
|
+
return await originalCallToolHandler?.(request, extra);
|
|
2040
|
+
}
|
|
2041
|
+
const event = {
|
|
2042
|
+
sessionId: getServerSessionId(server, extra),
|
|
2043
|
+
resourceName: request.params?.name || "Unknown Tool Name",
|
|
2044
|
+
parameters: buildCapturedMcpParameters(request),
|
|
2045
|
+
eventType: MCPAnalyticsEventType.mcpToolsCall,
|
|
2046
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2047
|
+
redactionFn: data.options.redactSensitiveInformation
|
|
2048
|
+
};
|
|
2049
|
+
try {
|
|
2050
|
+
await handleIdentify(server, data, request, extra);
|
|
2051
|
+
await applyResolvedMetadata$1(event, data, request, extra);
|
|
2052
|
+
setToolCallContext(event, data.options.context, request);
|
|
2053
|
+
const result = await executeToolCall(server, originalCallToolHandler, request, extra, event);
|
|
2054
|
+
if (isToolResultError$1(result)) {
|
|
2055
|
+
event.isError = true;
|
|
2056
|
+
event.error = captureException(result);
|
|
2057
|
+
}
|
|
2058
|
+
event.response = result;
|
|
2059
|
+
publishEvent(server, event);
|
|
2060
|
+
return result;
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
event.isError = true;
|
|
2063
|
+
event.error = captureException(error);
|
|
2064
|
+
publishEvent(server, event);
|
|
2065
|
+
throw error;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
async function executeToolCall(server, originalCallToolHandler, request, extra, event) {
|
|
2069
|
+
if (request.params?.name === "get_more_tools") {
|
|
2070
|
+
const context = getContextArgument$1(request) || "";
|
|
2071
|
+
event.userIntent = context;
|
|
2072
|
+
return handleReportMissing({ context });
|
|
2073
|
+
}
|
|
2074
|
+
if (originalCallToolHandler) return await originalCallToolHandler(request, extra);
|
|
2075
|
+
event.isError = true;
|
|
2076
|
+
event.error = { message: `Tool call handler not found for ${request.params?.name || "unknown"}` };
|
|
2077
|
+
event.duration = getEventDuration(event) || void 0;
|
|
2078
|
+
publishEvent(server, event);
|
|
2079
|
+
throw new Error(`Unknown tool: ${request.params?.name || "unknown"}`);
|
|
2080
|
+
}
|
|
2081
|
+
async function applyResolvedMetadata$1(event, data, request, extra) {
|
|
2082
|
+
const resolvedTags = await resolveEventTags(data, request, extra);
|
|
2083
|
+
if (resolvedTags) event.tags = resolvedTags;
|
|
2084
|
+
const resolvedProperties = await resolveEventProperties(data, request, extra);
|
|
2085
|
+
if (resolvedProperties) event.properties = resolvedProperties;
|
|
2086
|
+
}
|
|
2087
|
+
function setToolCallContext(event, context, request) {
|
|
2088
|
+
if (!(isContextEnabled(context) && request.params?.name !== "get_more_tools")) return;
|
|
2089
|
+
const contextArgument = getContextArgument$1(request);
|
|
2090
|
+
if (contextArgument) event.userIntent = contextArgument;
|
|
2091
|
+
}
|
|
2092
|
+
function getContextArgument$1(request) {
|
|
2093
|
+
const context = request.params?.arguments?.context;
|
|
2094
|
+
return typeof context === "string" ? context : void 0;
|
|
2095
|
+
}
|
|
2096
|
+
function getEventDuration(event) {
|
|
2097
|
+
return event.timestamp ? Date.now() - event.timestamp.getTime() : 0;
|
|
2098
|
+
}
|
|
2099
|
+
//#endregion
|
|
2100
|
+
//#region src/modules/mcp-sdk-compat.ts
|
|
2101
|
+
/**
|
|
2102
|
+
* Returns the tool function (callback/handler) from a RegisteredTool.
|
|
2103
|
+
* Supports both MCP SDK 1.23- (callback) and 1.24+ (handler).
|
|
2104
|
+
*/
|
|
2105
|
+
function getToolFunction(tool) {
|
|
2106
|
+
if ("handler" in tool && typeof tool.handler === "function") return tool.handler;
|
|
2107
|
+
if ("callback" in tool && typeof tool.callback === "function") return tool.callback;
|
|
2108
|
+
throw new Error("Tool has neither callback nor handler property");
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Returns the property key name used for the tool function ("callback" or "handler").
|
|
2112
|
+
* This preserves the original property name when wrapping tools.
|
|
2113
|
+
*/
|
|
2114
|
+
function getToolFunctionKey(tool) {
|
|
2115
|
+
if ("handler" in tool && typeof tool.handler === "function") return "handler";
|
|
2116
|
+
return "callback";
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Returns true if the tool has a callback or handler property.
|
|
2120
|
+
*/
|
|
2121
|
+
function hasToolFunction(tool) {
|
|
2122
|
+
if (!tool || typeof tool !== "object") return false;
|
|
2123
|
+
const t = tool;
|
|
2124
|
+
return "handler" in t && typeof t.handler === "function" || "callback" in t && typeof t.callback === "function";
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Creates a new tool object with the wrapped function, preserving the original property name.
|
|
2128
|
+
* This ensures MCP SDK 1.24+ gets back a tool with "handler" and 1.23- gets "callback".
|
|
2129
|
+
*/
|
|
2130
|
+
function createWrappedTool(originalTool, wrappedFunction) {
|
|
2131
|
+
const key = getToolFunctionKey(originalTool);
|
|
2132
|
+
return {
|
|
2133
|
+
...originalTool,
|
|
2134
|
+
[key]: wrappedFunction
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function isZ4Schema(schema) {
|
|
2138
|
+
if (!schema || typeof schema !== "object") return false;
|
|
2139
|
+
return !!schema._zod;
|
|
2140
|
+
}
|
|
2141
|
+
function getObjectShape(schema) {
|
|
2142
|
+
if (!schema || typeof schema !== "object") return;
|
|
2143
|
+
let rawShape;
|
|
2144
|
+
if (isZ4Schema(schema)) rawShape = schema._zod?.def?.shape;
|
|
2145
|
+
else {
|
|
2146
|
+
const v3Schema = schema;
|
|
2147
|
+
rawShape = v3Schema.shape ?? v3Schema._def?.shape;
|
|
2148
|
+
}
|
|
2149
|
+
if (!rawShape) return;
|
|
2150
|
+
if (typeof rawShape === "function") try {
|
|
2151
|
+
return rawShape();
|
|
2152
|
+
} catch {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
return rawShape;
|
|
2156
|
+
}
|
|
2157
|
+
function getLiteralValue(schema) {
|
|
2158
|
+
if (!schema || typeof schema !== "object") return;
|
|
2159
|
+
if (isZ4Schema(schema)) {
|
|
2160
|
+
const def = schema._zod?.def;
|
|
2161
|
+
if (def?.value !== void 0) return def.value;
|
|
2162
|
+
if (Array.isArray(def?.values) && def.values.length > 0) return def.values[0];
|
|
2163
|
+
} else {
|
|
2164
|
+
const def = schema._def;
|
|
2165
|
+
if (def?.value !== void 0) return def.value;
|
|
2166
|
+
if (Array.isArray(def?.values) && def.values.length > 0) return def.values[0];
|
|
2167
|
+
}
|
|
2168
|
+
const directValue = schema.value;
|
|
2169
|
+
if (directValue !== void 0) return directValue;
|
|
2170
|
+
}
|
|
2171
|
+
//#endregion
|
|
2172
|
+
//#region src/modules/tracing-v2.ts
|
|
2173
|
+
const wrappedCallbacks = /* @__PURE__ */ new WeakMap();
|
|
2174
|
+
const MCP_ANALYTICS_PROCESSED = Symbol("__posthog_mcp_analytics_processed__");
|
|
2175
|
+
function isToolResultError(result) {
|
|
2176
|
+
return !!result && typeof result === "object" && "isError" in result && result.isError === true;
|
|
2177
|
+
}
|
|
2178
|
+
function isCallbackUpdate(value) {
|
|
2179
|
+
return !!value && typeof value === "object" && "callback" in value && typeof value.callback === "function";
|
|
2180
|
+
}
|
|
2181
|
+
function addTracingToToolRegistry(tools, server) {
|
|
2182
|
+
return Object.fromEntries(Object.entries(tools).map(([name, tool]) => [name, addTracingToToolCallbackInternal(tool, name, server)]));
|
|
2183
|
+
}
|
|
2184
|
+
function setupListenerToRegisteredTools(server) {
|
|
2185
|
+
try {
|
|
2186
|
+
if (!getServerTrackingData(server.server)) {
|
|
2187
|
+
writeToLog("Warning: Cannot setup listener - no tracking data found");
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const handler = {
|
|
2191
|
+
set(target, property, value) {
|
|
2192
|
+
try {
|
|
2193
|
+
if (typeof property === "string" && value && typeof value === "object" && hasToolFunction(value)) {
|
|
2194
|
+
if (value[MCP_ANALYTICS_PROCESSED]) {
|
|
2195
|
+
writeToLog(`Tool ${String(property)} already processed, skipping proxy wrapping`);
|
|
2196
|
+
return Reflect.set(target, property, value);
|
|
2197
|
+
}
|
|
2198
|
+
if (wrappedCallbacks.has(getToolFunction(value))) {
|
|
2199
|
+
writeToLog(`Tool ${String(property)} callback already wrapped, skipping proxy wrapping`);
|
|
2200
|
+
return Reflect.set(target, property, value);
|
|
2201
|
+
}
|
|
2202
|
+
const nextValue = addTracingToToolCallbackInternal(value, property, server);
|
|
2203
|
+
setupListToolsTracing(server);
|
|
2204
|
+
if (typeof nextValue.update === "function") {
|
|
2205
|
+
const originalUpdate = nextValue.update;
|
|
2206
|
+
nextValue.update = function(...updateArgs) {
|
|
2207
|
+
if (updateArgs[0]) {
|
|
2208
|
+
const updateObj = updateArgs[0];
|
|
2209
|
+
if (isCallbackUpdate(updateObj)) updateObj.callback = getToolFunction(addTracingToToolCallbackInternal({ callback: updateObj.callback }, property, server));
|
|
2210
|
+
}
|
|
2211
|
+
return originalUpdate.apply(this, updateArgs);
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
return Reflect.set(target, property, nextValue);
|
|
2215
|
+
}
|
|
2216
|
+
return Reflect.set(target, property, value);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
writeToLog(`Warning: Error in proxy set handler for tool ${String(property)} - ${error}`);
|
|
2219
|
+
return Reflect.set(target, property, value);
|
|
2220
|
+
}
|
|
2221
|
+
},
|
|
2222
|
+
get(target, property) {
|
|
2223
|
+
return Reflect.get(target, property);
|
|
2224
|
+
},
|
|
2225
|
+
deleteProperty(target, property) {
|
|
2226
|
+
return Reflect.deleteProperty(target, property);
|
|
2227
|
+
},
|
|
2228
|
+
has(target, property) {
|
|
2229
|
+
return Reflect.has(target, property);
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
const originalTools = server._registeredTools || {};
|
|
2233
|
+
server._registeredTools = new Proxy(originalTools, handler);
|
|
2234
|
+
writeToLog("Successfully set up listener for new tool registrations");
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
writeToLog(`Warning: Failed to setup listener for registered tools - ${error}`);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function addTracingToToolCallbackInternal(tool, toolName, _server) {
|
|
2240
|
+
const originalCallback = getToolFunction(tool);
|
|
2241
|
+
if (wrappedCallbacks.has(originalCallback)) {
|
|
2242
|
+
writeToLog(`Tool ${toolName} callback already wrapped, skipping re-wrap`);
|
|
2243
|
+
return tool;
|
|
2244
|
+
}
|
|
2245
|
+
if (tool[MCP_ANALYTICS_PROCESSED]) {
|
|
2246
|
+
writeToLog(`Tool ${toolName} already processed, skipping re-wrap`);
|
|
2247
|
+
return tool;
|
|
2248
|
+
}
|
|
2249
|
+
const wrappedCallback = async (...params) => {
|
|
2250
|
+
let args;
|
|
2251
|
+
let extra;
|
|
2252
|
+
if (params.length === 2) {
|
|
2253
|
+
args = params[0];
|
|
2254
|
+
extra = params[1];
|
|
2255
|
+
} else {
|
|
2256
|
+
args = void 0;
|
|
2257
|
+
extra = params[0];
|
|
2258
|
+
}
|
|
2259
|
+
const removeContextFromArgs = (args) => {
|
|
2260
|
+
if (args && typeof args === "object" && "context" in args) {
|
|
2261
|
+
const { context: _context, ...argsWithoutContext } = args;
|
|
2262
|
+
return argsWithoutContext;
|
|
2263
|
+
}
|
|
2264
|
+
return args;
|
|
2265
|
+
};
|
|
2266
|
+
const cleanedArgs = toolName === "get_more_tools" ? args : removeContextFromArgs(args);
|
|
2267
|
+
try {
|
|
2268
|
+
if (cleanedArgs === void 0) return await originalCallback(extra);
|
|
2269
|
+
return await originalCallback(cleanedArgs, extra);
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
if (error instanceof Error) extra.__mcp_analytics_error = error;
|
|
2272
|
+
throw error;
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
wrappedCallbacks.set(originalCallback, true);
|
|
2276
|
+
wrappedCallbacks.set(wrappedCallback, true);
|
|
2277
|
+
const wrappedTool = createWrappedTool(tool, wrappedCallback);
|
|
2278
|
+
wrappedTool[MCP_ANALYTICS_PROCESSED] = true;
|
|
2279
|
+
return wrappedTool;
|
|
2280
|
+
}
|
|
2281
|
+
function setupToolsCallHandlerWrapping(server) {
|
|
2282
|
+
const lowLevelServer = server.server;
|
|
2283
|
+
const existingHandler = lowLevelServer._requestHandlers.get("tools/call");
|
|
2284
|
+
if (existingHandler) {
|
|
2285
|
+
const wrappedHandler = createToolsCallWrapper(existingHandler, lowLevelServer);
|
|
2286
|
+
lowLevelServer._requestHandlers.set("tools/call", wrappedHandler);
|
|
2287
|
+
}
|
|
2288
|
+
const originalSetRequestHandler = lowLevelServer.setRequestHandler.bind(lowLevelServer);
|
|
2289
|
+
lowLevelServer.setRequestHandler = ((requestSchema, handler) => {
|
|
2290
|
+
const shape = getObjectShape(requestSchema);
|
|
2291
|
+
if ((shape?.method ? getLiteralValue(shape.method) : void 0) === "tools/call") return originalSetRequestHandler(requestSchema, createToolsCallWrapper(handler, lowLevelServer));
|
|
2292
|
+
return originalSetRequestHandler(requestSchema, handler);
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
function createToolsCallWrapper(originalHandler, server) {
|
|
2296
|
+
return async (request, extra) => await handleWrappedToolsCall(originalHandler, server, request, extra);
|
|
2297
|
+
}
|
|
2298
|
+
async function handleWrappedToolsCall(originalHandler, server, request, extra) {
|
|
2299
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
2300
|
+
const tracing = await initializeToolCallEvent(server, request, extra, startTime);
|
|
2301
|
+
if (request?.params?.name === "get_more_tools") return await executeReportMissingTool(server, request, tracing, startTime);
|
|
2302
|
+
return await executeOriginalTool(originalHandler, server, request, extra, tracing, startTime);
|
|
2303
|
+
}
|
|
2304
|
+
async function initializeToolCallEvent(server, request, extra, startTime) {
|
|
2305
|
+
try {
|
|
2306
|
+
const data = getServerTrackingData(server);
|
|
2307
|
+
if (!data) {
|
|
2308
|
+
writeToLog("Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
|
|
2309
|
+
return {
|
|
2310
|
+
event: null,
|
|
2311
|
+
shouldPublishEvent: false
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
const event = {
|
|
2315
|
+
sessionId: getServerSessionId(server, extra),
|
|
2316
|
+
resourceName: request.params?.name || "Unknown Tool",
|
|
2317
|
+
parameters: buildCapturedMcpParameters(request),
|
|
2318
|
+
eventType: MCPAnalyticsEventType.mcpToolsCall,
|
|
2319
|
+
timestamp: startTime,
|
|
2320
|
+
redactionFn: data.options.redactSensitiveInformation
|
|
2321
|
+
};
|
|
2322
|
+
await handleIdentify(server, data, request, extra);
|
|
2323
|
+
event.sessionId = data.sessionId;
|
|
2324
|
+
await applyResolvedMetadata(event, data, request, extra);
|
|
2325
|
+
const contextArgument = getContextArgument(request);
|
|
2326
|
+
if (isContextEnabled(data.options.context) && contextArgument) event.userIntent = contextArgument;
|
|
2327
|
+
return {
|
|
2328
|
+
event,
|
|
2329
|
+
shouldPublishEvent: true
|
|
2330
|
+
};
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
writeToLog(`Warning: PostHog MCP analytics tracing failed for tool ${request.params?.name}, falling back to original handler - ${error}`);
|
|
2333
|
+
return {
|
|
2334
|
+
event: null,
|
|
2335
|
+
shouldPublishEvent: false
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
async function applyResolvedMetadata(event, data, request, extra) {
|
|
2340
|
+
const resolvedTags = await resolveEventTags(data, request, extra);
|
|
2341
|
+
if (resolvedTags) event.tags = resolvedTags;
|
|
2342
|
+
const resolvedProperties = await resolveEventProperties(data, request, extra);
|
|
2343
|
+
if (resolvedProperties) event.properties = resolvedProperties;
|
|
2344
|
+
}
|
|
2345
|
+
async function executeReportMissingTool(server, request, tracing, startTime) {
|
|
2346
|
+
try {
|
|
2347
|
+
const context = getContextArgument(request) || "";
|
|
2348
|
+
const result = await handleReportMissing({ context });
|
|
2349
|
+
publishSuccessfulToolEvent(server, tracing, result, startTime, { userIntent: context });
|
|
2350
|
+
return result;
|
|
2351
|
+
} catch (error) {
|
|
2352
|
+
publishFailedToolEvent(server, tracing, error, startTime);
|
|
2353
|
+
throw error;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
async function executeOriginalTool(originalHandler, server, request, extra, tracing, startTime) {
|
|
2357
|
+
try {
|
|
2358
|
+
const result = await originalHandler(request, extra);
|
|
2359
|
+
publishSuccessfulToolEvent(server, tracing, result, startTime, {
|
|
2360
|
+
capturedError: extra?.__mcp_analytics_error,
|
|
2361
|
+
clearCapturedError: () => {
|
|
2362
|
+
if (extra) extra.__mcp_analytics_error = void 0;
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
return result;
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
publishFailedToolEvent(server, tracing, error, startTime);
|
|
2368
|
+
throw error;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
function getContextArgument(request) {
|
|
2372
|
+
const context = request.params?.arguments?.context;
|
|
2373
|
+
return typeof context === "string" ? context : void 0;
|
|
2374
|
+
}
|
|
2375
|
+
function publishSuccessfulToolEvent(server, tracing, result, startTime, options = {}) {
|
|
2376
|
+
if (!(tracing.event && tracing.shouldPublishEvent)) return;
|
|
2377
|
+
if (options.userIntent) tracing.event.userIntent = options.userIntent;
|
|
2378
|
+
if (isToolResultError(result)) {
|
|
2379
|
+
tracing.event.isError = true;
|
|
2380
|
+
tracing.event.error = captureException(options.capturedError || result);
|
|
2381
|
+
options.clearCapturedError?.();
|
|
2382
|
+
}
|
|
2383
|
+
tracing.event.response = result;
|
|
2384
|
+
tracing.event.duration = Date.now() - startTime.getTime();
|
|
2385
|
+
publishEvent(server, tracing.event);
|
|
2386
|
+
}
|
|
2387
|
+
function publishFailedToolEvent(server, tracing, error, startTime) {
|
|
2388
|
+
if (!(tracing.event && tracing.shouldPublishEvent)) return;
|
|
2389
|
+
tracing.event.isError = true;
|
|
2390
|
+
tracing.event.error = captureException(error);
|
|
2391
|
+
tracing.event.duration = Date.now() - startTime.getTime();
|
|
2392
|
+
publishEvent(server, tracing.event);
|
|
2393
|
+
}
|
|
2394
|
+
function setupTracking(server) {
|
|
2395
|
+
try {
|
|
2396
|
+
getServerTrackingData(server.server);
|
|
2397
|
+
setupToolsCallHandlerWrapping(server);
|
|
2398
|
+
setupInitializeTracing(server);
|
|
2399
|
+
server._registeredTools = addTracingToToolRegistry(server._registeredTools, server);
|
|
2400
|
+
setupListToolsTracing(server);
|
|
2401
|
+
setupListenerToRegisteredTools(server);
|
|
2402
|
+
} catch (error) {
|
|
2403
|
+
writeToLog(`Warning: Failed to setup tool call tracing - ${error}`);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/index.ts
|
|
2408
|
+
/**
|
|
2409
|
+
* Integrates PostHog MCP into an MCP server to track tool usage patterns and user interactions.
|
|
2410
|
+
*
|
|
2411
|
+
* @param server - The MCP server instance to track. Must be a compatible MCP server implementation.
|
|
2412
|
+
* @param options - Configuration to customize tracking behavior.
|
|
2413
|
+
* @param options.apiKey - PostHog project API key (`phc_...`). Optional when using an injected `posthogClient`.
|
|
2414
|
+
* @param options.host - Custom PostHog ingestion host. Defaults to `https://us.i.posthog.com`.
|
|
2415
|
+
* @param options.reportMissing - Adds a "get_more_tools" tool that allows LLMs to automatically report missing functionality. Defaults to false.
|
|
2416
|
+
* @param options.enableAITracing - Emits `$ai_span` events for tool calls so MCP activity appears in PostHog LLM analytics. Defaults to false.
|
|
2417
|
+
* @param options.enableTracing - Enables tracking of tool calls and usage patterns.
|
|
2418
|
+
* @param options.context - Enables the required "context" parameter on tools to capture user intent. Pass false to disable, or an object with a custom description.
|
|
2419
|
+
* @param options.identify - Async function to identify users and attach custom data to their sessions.
|
|
2420
|
+
* @param options.redactSensitiveInformation - Function to redact sensitive data before sending to PostHog.
|
|
2421
|
+
* @param options.eventTags - Callback invoked on every auto-captured event (tool calls, tool lists, initialize) to attach string key-value tags. Tags are intended to be indexed and queryable in PostHog — use them for structured metadata you'll want to filter or group by (e.g., trace IDs, environments, regions). Tags are validated client-side: keys must be ≤32 chars matching `[a-zA-Z0-9$_.:\- ]`, values must be strings ≤200 chars with no newlines, max 50 entries per event. Invalid entries are silently dropped with a warning logged to `~/posthog-mcp-analytics.log`. If the callback throws or returns null, tags are omitted. Receives the same `(request, extra)` arguments as `identify`.
|
|
2422
|
+
* @param options.eventProperties - Callback invoked on every auto-captured event to attach flexible JSON metadata (device info, feature flags, nested context). No constraints beyond standard JSON types. If the callback throws or returns null, properties are omitted. Receives the same `(request, extra)` arguments as `identify`.
|
|
2423
|
+
* @param options.posthogClient - Optional existing posthog-node compatible client. If provided, MCP events are captured with that client instead of creating a new one.
|
|
2424
|
+
* @param options.posthogOptions - Optional posthog-node options used when the SDK creates its own client.
|
|
2425
|
+
*
|
|
2426
|
+
* @returns The tracked server instance.
|
|
2427
|
+
*
|
|
2428
|
+
* @remarks
|
|
2429
|
+
* Analytics data and debug information are logged to `~/posthog-mcp-analytics.log` since console logs interfere
|
|
2430
|
+
* with STDIO-based MCP servers.
|
|
2431
|
+
*
|
|
2432
|
+
* Do not call `track()` multiple times on the same server instance as this will cause unexpected behavior.
|
|
2433
|
+
*
|
|
2434
|
+
* @example
|
|
2435
|
+
* ```typescript
|
|
2436
|
+
* import { track } from "@posthog/mcp";
|
|
2437
|
+
*
|
|
2438
|
+
* const mcpServer = new Server({ name: "my-mcp-server", version: "1.0.0" });
|
|
2439
|
+
*
|
|
2440
|
+
* // Track the server with PostHog MCP
|
|
2441
|
+
* track(mcpServer, { apiKey: "phc_abc123xyz" });
|
|
2442
|
+
*
|
|
2443
|
+
* // Register your tools
|
|
2444
|
+
* mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2445
|
+
* tools: [{ name: "my_tool", description: "Does something useful" }]
|
|
2446
|
+
* }));
|
|
2447
|
+
* ```
|
|
2448
|
+
*
|
|
2449
|
+
* @example
|
|
2450
|
+
* ```typescript
|
|
2451
|
+
* // With user identification
|
|
2452
|
+
* track(mcpServer, {
|
|
2453
|
+
* apiKey: "phc_abc123xyz",
|
|
2454
|
+
* identify: async (request, extra) => {
|
|
2455
|
+
* const user = await getUserFromToken(request.params.arguments.token);
|
|
2456
|
+
* return {
|
|
2457
|
+
* userId: user.id,
|
|
2458
|
+
* userData: { plan: user.plan, company: user.company }
|
|
2459
|
+
* };
|
|
2460
|
+
* }
|
|
2461
|
+
* });
|
|
2462
|
+
* ```
|
|
2463
|
+
*
|
|
2464
|
+
* @example
|
|
2465
|
+
* ```typescript
|
|
2466
|
+
* // With custom context description
|
|
2467
|
+
* track(mcpServer, {
|
|
2468
|
+
* apiKey: "phc_abc123xyz",
|
|
2469
|
+
* context: {
|
|
2470
|
+
* description: "Explain why you're calling this tool and what business objective it helps achieve"
|
|
2471
|
+
* }
|
|
2472
|
+
* });
|
|
2473
|
+
* ```
|
|
2474
|
+
*
|
|
2475
|
+
* @example
|
|
2476
|
+
* ```typescript
|
|
2477
|
+
* // With sensitive data redaction
|
|
2478
|
+
* track(mcpServer, {
|
|
2479
|
+
* apiKey: "phc_abc123xyz",
|
|
2480
|
+
* redactSensitiveInformation: async (text) => {
|
|
2481
|
+
* return text.replace(/api_key_\w+/g, "[REDACTED]");
|
|
2482
|
+
* }
|
|
2483
|
+
* });
|
|
2484
|
+
* ```
|
|
2485
|
+
*
|
|
2486
|
+
* @example
|
|
2487
|
+
* ```typescript
|
|
2488
|
+
* // With event tags and properties
|
|
2489
|
+
* track(mcpServer, {
|
|
2490
|
+
* apiKey: "phc_abc123xyz",
|
|
2491
|
+
* eventTags: async (request, extra) => ({
|
|
2492
|
+
* trace_id: extra?.requestContext?.traceId,
|
|
2493
|
+
* env: process.env.NODE_ENV,
|
|
2494
|
+
* region: "us-east-1",
|
|
2495
|
+
* }),
|
|
2496
|
+
* eventProperties: async (request, extra) => ({
|
|
2497
|
+
* device: "desktop",
|
|
2498
|
+
* app_version: "2.1.0",
|
|
2499
|
+
* feature_flags: ["dark_mode", "beta_ui"],
|
|
2500
|
+
* }),
|
|
2501
|
+
* });
|
|
2502
|
+
* ```
|
|
2503
|
+
*
|
|
2504
|
+
* @example
|
|
2505
|
+
* ```typescript
|
|
2506
|
+
*/
|
|
2507
|
+
function track(server, options = {}) {
|
|
2508
|
+
try {
|
|
2509
|
+
const validatedServer = isCompatibleServerType(server);
|
|
2510
|
+
const lowLevelServer = getLowLevelServer(validatedServer);
|
|
2511
|
+
configureIngestion(options);
|
|
2512
|
+
if (getServerTrackingData(lowLevelServer)) {
|
|
2513
|
+
writeToLog("[SESSION DEBUG] track() - Server already being tracked, skipping initialization");
|
|
2514
|
+
return validatedServer;
|
|
2515
|
+
}
|
|
2516
|
+
if (!(options.apiKey || options.posthogClient)) writeToLog("Warning: No PostHog API key or PostHog client configured. Events will not be sent anywhere.");
|
|
2517
|
+
const mcpAnalyticsData = buildTrackingData(lowLevelServer, options);
|
|
2518
|
+
setServerTrackingData(lowLevelServer, mcpAnalyticsData);
|
|
2519
|
+
setupTrackedServer(validatedServer, lowLevelServer, mcpAnalyticsData);
|
|
2520
|
+
return validatedServer;
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
writeToLog(`Warning: Failed to track server - ${error}`);
|
|
2523
|
+
return server;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function getLowLevelServer(server) {
|
|
2527
|
+
return isHighLevelServer(server) ? server.server : server;
|
|
2528
|
+
}
|
|
2529
|
+
function configureIngestion(options) {
|
|
2530
|
+
const host = options.host || process.env.POSTHOG_MCP_ANALYTICS_HOST;
|
|
2531
|
+
if (options.posthogOptions) eventQueue.configurePostHogOptions(options.posthogOptions);
|
|
2532
|
+
if (host) eventQueue.configure(host);
|
|
2533
|
+
}
|
|
2534
|
+
function buildTrackingData(lowLevelServer, options) {
|
|
2535
|
+
return {
|
|
2536
|
+
apiKey: options.apiKey || "",
|
|
2537
|
+
sessionId: newSessionId(),
|
|
2538
|
+
lastActivity: /* @__PURE__ */ new Date(),
|
|
2539
|
+
identifiedSessions: /* @__PURE__ */ new Map(),
|
|
2540
|
+
sessionInfo: getSessionInfo(lowLevelServer, void 0),
|
|
2541
|
+
options: {
|
|
2542
|
+
reportMissing: options.reportMissing ?? false,
|
|
2543
|
+
enableAITracing: options.enableAITracing ?? false,
|
|
2544
|
+
enableTracing: options.enableTracing ?? true,
|
|
2545
|
+
context: options.context,
|
|
2546
|
+
identify: options.identify,
|
|
2547
|
+
redactSensitiveInformation: options.redactSensitiveInformation,
|
|
2548
|
+
eventTags: options.eventTags,
|
|
2549
|
+
eventProperties: options.eventProperties,
|
|
2550
|
+
host: options.host,
|
|
2551
|
+
posthogClient: options.posthogClient,
|
|
2552
|
+
posthogOptions: options.posthogOptions
|
|
2553
|
+
},
|
|
2554
|
+
sessionSource: "generated"
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
function setupTrackedServer(validatedServer, lowLevelServer, mcpAnalyticsData) {
|
|
2558
|
+
if (isHighLevelServer(validatedServer)) setupTracking(validatedServer);
|
|
2559
|
+
else {
|
|
2560
|
+
if (mcpAnalyticsData.options.reportMissing) try {
|
|
2561
|
+
setupMCPAnalyticsTools(lowLevelServer);
|
|
2562
|
+
} catch (error) {
|
|
2563
|
+
writeToLog(`Warning: Failed to setup report missing tool - ${error}`);
|
|
2564
|
+
}
|
|
2565
|
+
if (mcpAnalyticsData.options.enableTracing) try {
|
|
2566
|
+
setupToolCallTracing(lowLevelServer);
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
writeToLog(`Warning: Failed to setup tool call tracing - ${error}`);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Publishes a custom event to PostHog MCP with flexible session management.
|
|
2574
|
+
*
|
|
2575
|
+
* @param serverOrSessionId - Either a tracked MCP server instance or a MCP session ID string
|
|
2576
|
+
* @param eventData - Event data to include with the custom event. `apiKey` is required when publishing against a raw session ID.
|
|
2577
|
+
*
|
|
2578
|
+
* @returns Promise that resolves when the event is queued for publishing
|
|
2579
|
+
*
|
|
2580
|
+
* @example
|
|
2581
|
+
* ```typescript
|
|
2582
|
+
* // With a tracked server
|
|
2583
|
+
* await publishCustomEvent(
|
|
2584
|
+
* server,
|
|
2585
|
+
* {
|
|
2586
|
+
* resourceName: "custom-action",
|
|
2587
|
+
* parameters: { action: "user-feedback", rating: 5 },
|
|
2588
|
+
* message: "User provided feedback"
|
|
2589
|
+
* }
|
|
2590
|
+
* );
|
|
2591
|
+
* ```
|
|
2592
|
+
*
|
|
2593
|
+
* @example
|
|
2594
|
+
* ```typescript
|
|
2595
|
+
* // With a MCP session ID
|
|
2596
|
+
* await publishCustomEvent(
|
|
2597
|
+
* "user-session-12345",
|
|
2598
|
+
* {
|
|
2599
|
+
* apiKey: "phc_abc123xyz",
|
|
2600
|
+
* isError: true,
|
|
2601
|
+
* error: { message: "Custom error occurred", code: "ERR_001" }
|
|
2602
|
+
* }
|
|
2603
|
+
* );
|
|
2604
|
+
* ```
|
|
2605
|
+
*
|
|
2606
|
+
* @example
|
|
2607
|
+
* ```typescript
|
|
2608
|
+
* await publishCustomEvent(
|
|
2609
|
+
* server,
|
|
2610
|
+
* {
|
|
2611
|
+
* resourceName: "feature-usage",
|
|
2612
|
+
* }
|
|
2613
|
+
* );
|
|
2614
|
+
* ```
|
|
2615
|
+
*/
|
|
2616
|
+
function publishCustomEvent(serverOrSessionId, eventData = {}) {
|
|
2617
|
+
try {
|
|
2618
|
+
publishCustomEventSync(serverOrSessionId, eventData);
|
|
2619
|
+
return Promise.resolve();
|
|
2620
|
+
} catch (error) {
|
|
2621
|
+
return Promise.reject(error);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
function publishCustomEventSync(serverOrSessionId, eventData) {
|
|
2625
|
+
const target = resolveCustomEventTarget(serverOrSessionId, eventData);
|
|
2626
|
+
const event = {
|
|
2627
|
+
sessionId: target.sessionId,
|
|
2628
|
+
apiKey: target.apiKey,
|
|
2629
|
+
eventType: MCPAnalyticsEventType.custom,
|
|
2630
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2631
|
+
resourceName: eventData?.resourceName,
|
|
2632
|
+
parameters: eventData?.parameters,
|
|
2633
|
+
response: eventData?.response,
|
|
2634
|
+
userIntent: eventData?.message,
|
|
2635
|
+
duration: eventData?.duration,
|
|
2636
|
+
isError: eventData?.isError,
|
|
2637
|
+
error: resolveCustomEventError(eventData?.error)
|
|
2638
|
+
};
|
|
2639
|
+
if (eventData?.tags) event.tags = validateTags(eventData.tags);
|
|
2640
|
+
if (eventData?.properties && Object.keys(eventData.properties).length > 0) event.properties = eventData.properties;
|
|
2641
|
+
publishResolvedCustomEvent(target, event);
|
|
2642
|
+
writeToLog(`Published custom event for session ${target.sessionId} with type '${MCPAnalyticsEventType.custom}'`);
|
|
2643
|
+
}
|
|
2644
|
+
function resolveCustomEventError(error) {
|
|
2645
|
+
if (error === void 0 || error === null) return error;
|
|
2646
|
+
if (typeof error === "object" && "message" in error && typeof error.message === "string") return error;
|
|
2647
|
+
return captureException(error);
|
|
2648
|
+
}
|
|
2649
|
+
function resolveCustomEventTarget(serverOrSessionId, eventData) {
|
|
2650
|
+
if (typeof serverOrSessionId === "string") return resolveSessionIdTarget(serverOrSessionId, eventData);
|
|
2651
|
+
if (serverOrSessionId && typeof serverOrSessionId === "object") return resolveTrackedServerTarget(serverOrSessionId);
|
|
2652
|
+
throw new Error("First parameter must be either an MCP server object or a session ID string");
|
|
2653
|
+
}
|
|
2654
|
+
function resolveSessionIdTarget(sessionIdInput, eventData) {
|
|
2655
|
+
const apiKey = eventData.apiKey || "";
|
|
2656
|
+
if (!(apiKey || eventData.posthogClient)) throw new Error("apiKey or posthogClient is required when publishing with a session ID");
|
|
2657
|
+
return {
|
|
2658
|
+
apiKey,
|
|
2659
|
+
lowLevelServer: null,
|
|
2660
|
+
posthogClient: eventData.posthogClient,
|
|
2661
|
+
sessionId: deriveSessionIdFromMCPSession(sessionIdInput)
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
function resolveTrackedServerTarget(server) {
|
|
2665
|
+
const lowLevelServer = getLowLevelServerFromUnknownObject(server);
|
|
2666
|
+
const trackingData = getServerTrackingData(lowLevelServer);
|
|
2667
|
+
if (!trackingData) throw new Error("Server is not tracked. Please call track() first or provide a session ID string.");
|
|
2668
|
+
return {
|
|
2669
|
+
apiKey: trackingData.apiKey,
|
|
2670
|
+
lowLevelServer,
|
|
2671
|
+
posthogClient: trackingData.options.posthogClient,
|
|
2672
|
+
sessionId: trackingData.sessionId
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
function getLowLevelServerFromUnknownObject(server) {
|
|
2676
|
+
return "server" in server && server.server && typeof server.server === "object" ? server.server : server;
|
|
2677
|
+
}
|
|
2678
|
+
function publishResolvedCustomEvent(target, event) {
|
|
2679
|
+
if (target.lowLevelServer && getServerTrackingData(target.lowLevelServer)) {
|
|
2680
|
+
publishEvent(target.lowLevelServer, event);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
if (target.posthogClient) {
|
|
2684
|
+
eventQueue.add(event, target.posthogClient);
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
eventQueue.add(event);
|
|
2688
|
+
}
|
|
2689
|
+
//#endregion
|
|
2690
|
+
export { PostHogMCPAnalyticsProperty, publishCustomEvent, track };
|
|
2691
|
+
|
|
2692
|
+
//# sourceMappingURL=index.mjs.map
|