@posthog/mcp 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,6 +52,22 @@ track(server, {
52
52
  With `enableAITracing: true`, tool calls also emit `$ai_span` events with `$ai_trace_id` and `$ai_span_id`.
53
53
  The regular `mcp_tool_call` event includes the same trace/span IDs so it can be joined back to the LLM analytics span.
54
54
 
55
+ ### Conversation ID (opt-in)
56
+
57
+ When `enableConversationId: true` is passed to `track()`, the SDK injects an optional `conversation_id` argument into every tool's input schema and tells the agent to reuse the value the server returns. When the agent omits it on a call, the SDK mints a UUID and appends a short text block to the tool's response asking the agent to echo the same `conversation_id` on subsequent calls. The value the agent supplies (or the SDK mints) is captured on PostHog events as `$mcp_conversation_id` — separate from `$session_id`, which is unchanged. Off by default.
58
+
59
+ ```ts
60
+ track(server, {
61
+ apiKey: process.env.POSTHOG_API_KEY,
62
+ enableConversationId: true,
63
+ });
64
+ ```
65
+
66
+ **Caveats:**
67
+
68
+ - The prompt-back is appended as a regular `text` content block (the only channel MCP currently offers for server→agent communication). Consumers that surface raw tool-call content to end users will see `[SERVER]: Reuse conversation_id=…` in their UI. If/when the MCP spec grows a server-directives channel (e.g. on `_meta`), the SDK will move there.
69
+ - The captured `$mcp_conversation_id` value is agent-controlled when the agent supplies one — the SDK accepts any non-empty string. Customers wanting to bind it to their own session scheme (chat ID, JWT `jti`, request ID) can do so freely; the trade-off is that nothing prevents a misbehaving agent from sending arbitrary values.
70
+
55
71
  The SDK sends events through `posthog-node`, so it uses the same PostHog ingestion client, batching, retry, flush, and shutdown behavior as the existing Node SDK.
56
72
 
57
73
  If your application already owns a PostHog client, pass it in instead:
package/dist/index.cjs CHANGED
@@ -350,6 +350,7 @@ function getRequestResourceName(request) {
350
350
  //#endregion
351
351
  //#region src/modules/constants.ts
352
352
  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."`;
353
+ const DEFAULT_CONVERSATION_ID_DESCRIPTION = "Echo the conversation_id from the server's previous response. The server provides it on the first call — never invent one, and do not issue parallel tool calls until you have it.";
353
354
  const POSTHOG_MCP_ANALYTICS_SOURCE = "posthog_mcp_analytics";
354
355
  const PostHogMCPAnalyticsEvent = {
355
356
  AiSpan: "$ai_span",
@@ -376,6 +377,7 @@ const PostHogMCPAnalyticsProperty = {
376
377
  AiTraceId: "$ai_trace_id",
377
378
  ClientName: "$mcp_client_name",
378
379
  ClientVersion: "$mcp_client_version",
380
+ ConversationId: "$mcp_conversation_id",
379
381
  DurationMs: "$mcp_duration_ms",
380
382
  IsError: "$mcp_is_error",
381
383
  Intent: "$mcp_intent",
@@ -421,6 +423,7 @@ function buildCaptureEvent(event, options) {
421
423
  [PostHogMCPAnalyticsProperty.SessionId]: event.sessionId,
422
424
  [PostHogMCPAnalyticsProperty.Source]: POSTHOG_MCP_ANALYTICS_SOURCE
423
425
  };
426
+ addConversationIdProperty(event, properties);
424
427
  addCommonEventProperties(event, properties);
425
428
  addTraceReferenceProperties(event, properties, options);
426
429
  addCustomEventProperties(event, properties);
@@ -446,6 +449,9 @@ function addTraceReferenceProperties(event, properties, options) {
446
449
  properties[PostHogMCPAnalyticsProperty.AiTraceId] = getAITraceId(event);
447
450
  properties[PostHogMCPAnalyticsProperty.AiSpanId] = getAISpanId(event);
448
451
  }
452
+ function addConversationIdProperty(event, properties) {
453
+ if (event.conversationId !== void 0 && event.conversationId !== "") properties[PostHogMCPAnalyticsProperty.ConversationId] = event.conversationId;
454
+ }
449
455
  function addCommonEventProperties(event, properties) {
450
456
  if (event.resourceName) {
451
457
  properties[PostHogMCPAnalyticsProperty.ResourceName] = event.resourceName;
@@ -477,6 +483,7 @@ function buildExceptionEvent(event) {
477
483
  $exception_source: "backend",
478
484
  [PostHogMCPAnalyticsProperty.SessionId]: event.sessionId
479
485
  };
486
+ addConversationIdProperty(event, properties);
480
487
  if (event.error) {
481
488
  if (event.error.message) properties.$exception_message = event.error.message;
482
489
  if (event.error.type) properties.$exception_type = event.error.type;
@@ -510,6 +517,7 @@ function buildAISpanEvent(event) {
510
517
  [PostHogMCPAnalyticsProperty.SessionId]: event.sessionId,
511
518
  [PostHogMCPAnalyticsProperty.Source]: POSTHOG_MCP_ANALYTICS_SOURCE
512
519
  };
520
+ addConversationIdProperty(event, properties);
513
521
  if (event.duration !== void 0) properties[PostHogMCPAnalyticsProperty.AiLatency] = event.duration / 1e3;
514
522
  if (event.isError && event.error) properties.$ai_error = event.error;
515
523
  if (event.parameters !== void 0) properties[PostHogMCPAnalyticsProperty.AiInputState] = event.parameters;
@@ -721,7 +729,7 @@ function sanitizeParameters(obj) {
721
729
  }
722
730
  //#endregion
723
731
  //#region package.json
724
- var version = "0.0.3";
732
+ var version = "0.0.4";
725
733
  //#endregion
726
734
  //#region src/modules/session.ts
727
735
  function newSessionId() {
@@ -1187,6 +1195,7 @@ function publishEvent(server, eventInput) {
1187
1195
  userIntentSource: eventInput.userIntentSource,
1188
1196
  isError: eventInput.isError,
1189
1197
  error: eventInput.error,
1198
+ conversationId: eventInput.conversationId,
1190
1199
  redactionFn: eventInput.redactionFn,
1191
1200
  tags: eventInput.tags,
1192
1201
  properties: eventInput.properties
@@ -1923,6 +1932,112 @@ function setupMCPAnalyticsTools(server) {
1923
1932
  }
1924
1933
  }
1925
1934
  //#endregion
1935
+ //#region src/modules/conversation-id.ts
1936
+ const CONVERSATION_ID_PARAM_NAME = "conversation_id";
1937
+ function addConversationIdToTool(tool) {
1938
+ const modifiedTool = { ...tool };
1939
+ const toolName = tool.name || "unknown";
1940
+ const schema = modifiedTool.inputSchema;
1941
+ if (schema?.properties?.["conversation_id"]) {
1942
+ writeToLog(`WARN: Tool "${toolName}" already has '${CONVERSATION_ID_PARAM_NAME}' parameter. Skipping conversation_id injection.`);
1943
+ return modifiedTool;
1944
+ }
1945
+ if (schema?.oneOf || schema?.allOf || schema?.anyOf) {
1946
+ writeToLog(`WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf). Skipping conversation_id injection.`);
1947
+ return modifiedTool;
1948
+ }
1949
+ if (!modifiedTool.inputSchema) modifiedTool.inputSchema = {
1950
+ type: "object",
1951
+ properties: {},
1952
+ required: []
1953
+ };
1954
+ modifiedTool.inputSchema = JSON.parse(JSON.stringify(modifiedTool.inputSchema));
1955
+ const inputSchema = modifiedTool.inputSchema;
1956
+ if (!inputSchema.properties) inputSchema.properties = {};
1957
+ if (inputSchema.additionalProperties === false) inputSchema.additionalProperties = void 0;
1958
+ inputSchema.properties[CONVERSATION_ID_PARAM_NAME] = {
1959
+ type: "string",
1960
+ description: DEFAULT_CONVERSATION_ID_DESCRIPTION
1961
+ };
1962
+ return modifiedTool;
1963
+ }
1964
+ function addConversationIdToTools(tools) {
1965
+ return tools.map((tool) => {
1966
+ if (tool.name === "get_more_tools") return tool;
1967
+ return addConversationIdToTool(tool);
1968
+ });
1969
+ }
1970
+ /**
1971
+ * Decides which conversation_id to use for a tool call:
1972
+ * - disabled or get_more_tools → none
1973
+ * - agent supplied a value → use it
1974
+ * - agent omitted → mint a UUID
1975
+ */
1976
+ function resolveConversationId(enabled, args, toolName) {
1977
+ if (!enabled || toolName === "get_more_tools") return {
1978
+ minted: false,
1979
+ conversationId: void 0
1980
+ };
1981
+ const supplied = extractConversationId(args);
1982
+ if (supplied) return {
1983
+ minted: false,
1984
+ conversationId: supplied
1985
+ };
1986
+ return {
1987
+ minted: true,
1988
+ conversationId: (0, node_crypto.randomUUID)()
1989
+ };
1990
+ }
1991
+ /**
1992
+ * Predicate matching the same eligibility checks injectConversationIdPromptBack uses,
1993
+ * exposed so callers can pre-decide whether the prompt-back will actually land
1994
+ * (and clear event.conversationId if not, to avoid orphan ids in analytics).
1995
+ */
1996
+ function canInjectConversationIdPromptBack(result) {
1997
+ if (!(result && typeof result === "object")) return false;
1998
+ const resultObj = result;
1999
+ if (resultObj.isError === true) return false;
2000
+ return Array.isArray(resultObj.content);
2001
+ }
2002
+ function extractConversationId(args) {
2003
+ if (!(args && typeof args === "object")) return;
2004
+ const value = args[CONVERSATION_ID_PARAM_NAME];
2005
+ if (typeof value !== "string") return;
2006
+ const trimmed = value.trim();
2007
+ return trimmed.length > 0 ? trimmed : void 0;
2008
+ }
2009
+ function cloneRequestWithoutConversationId(request) {
2010
+ if (!request.params || typeof request.params !== "object") return request;
2011
+ const args = request.params.arguments;
2012
+ if (!(args && typeof args === "object")) return request;
2013
+ return {
2014
+ ...request,
2015
+ params: {
2016
+ ...request.params,
2017
+ arguments: stripConversationId(args)
2018
+ }
2019
+ };
2020
+ }
2021
+ function stripConversationId(args) {
2022
+ if (!args || typeof args !== "object" || !("conversation_id" in args)) return args;
2023
+ const { [CONVERSATION_ID_PARAM_NAME]: _omit, ...rest } = args;
2024
+ return rest;
2025
+ }
2026
+ function buildConversationIdPromptBack(conversationId) {
2027
+ return {
2028
+ type: "text",
2029
+ text: `[SERVER]: Reuse conversation_id=${conversationId} on every subsequent tool call in this conversation. Required for the server to correlate calls and provide context-aware results.`
2030
+ };
2031
+ }
2032
+ function injectConversationIdPromptBack(result, conversationId) {
2033
+ if (!canInjectConversationIdPromptBack(result)) return result;
2034
+ const resultObj = result;
2035
+ return {
2036
+ ...resultObj,
2037
+ content: [...resultObj.content, buildConversationIdPromptBack(conversationId)]
2038
+ };
2039
+ }
2040
+ //#endregion
1926
2041
  //#region src/modules/intent.ts
1927
2042
  function getContextArgument$2(request) {
1928
2043
  const context = request.params?.arguments?.context;
@@ -2018,6 +2133,7 @@ async function getTracedToolsList(server, originalListToolsHandler, request, ext
2018
2133
  const data = getServerTrackingData(server);
2019
2134
  let tools = (await originalListToolsHandler(request, extra)).tools || [];
2020
2135
  if (data && isContextEnabled(data.options.context)) tools = addContextParameterToTools(tools, getContextDescription(data.options.context));
2136
+ if (data?.options.enableConversationId) tools = addConversationIdToTools(tools);
2021
2137
  if (data?.options.reportMissing) {
2022
2138
  if (!tools.some((tool) => tool?.name === "get_more_tools")) tools.push(getReportMissingToolDescriptor());
2023
2139
  }
@@ -2102,10 +2218,13 @@ async function handleToolCallRequest(server, originalCallToolHandler, request, e
2102
2218
  writeToLog("Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
2103
2219
  return await originalCallToolHandler?.(request, extra);
2104
2220
  }
2221
+ const conversation = resolveConversationId(data.options.enableConversationId ?? false, request.params?.arguments, request.params?.name);
2222
+ const downstreamRequest = conversation.conversationId ? cloneRequestWithoutConversationId(request) : request;
2105
2223
  const event = {
2106
2224
  sessionId: getServerSessionId(server, extra),
2225
+ conversationId: conversation.conversationId,
2107
2226
  resourceName: request.params?.name || "Unknown Tool Name",
2108
- parameters: buildCapturedMcpParameters(request),
2227
+ parameters: buildCapturedMcpParameters(downstreamRequest),
2109
2228
  eventType: MCPAnalyticsEventType.mcpToolsCall,
2110
2229
  timestamp: /* @__PURE__ */ new Date(),
2111
2230
  redactionFn: data.options.redactSensitiveInformation
@@ -2114,18 +2233,22 @@ async function handleToolCallRequest(server, originalCallToolHandler, request, e
2114
2233
  await handleIdentify(server, data, request, extra);
2115
2234
  await applyResolvedMetadata$1(event, data, request, extra);
2116
2235
  setEventIntent(event, await resolveToolCallIntent(data, request, extra));
2117
- const result = await executeToolCall(server, originalCallToolHandler, request, extra, event);
2236
+ const result = await executeToolCall(server, originalCallToolHandler, downstreamRequest, extra, event);
2118
2237
  if (isToolResultError$1(result)) {
2119
2238
  event.isError = true;
2120
2239
  event.error = captureException(result);
2121
2240
  } else event.isError = false;
2122
- event.response = result;
2241
+ let finalResult = result;
2242
+ if (conversation.minted) if (canInjectConversationIdPromptBack(result)) finalResult = injectConversationIdPromptBack(result, conversation.conversationId);
2243
+ else event.conversationId = void 0;
2244
+ event.response = finalResult;
2123
2245
  event.duration = getEventDuration(event);
2124
2246
  publishEvent(server, event);
2125
- return result;
2247
+ return finalResult;
2126
2248
  } catch (error) {
2127
2249
  event.isError = true;
2128
2250
  event.error = captureException(error);
2251
+ if (conversation.minted) event.conversationId = void 0;
2129
2252
  event.duration = getEventDuration(event);
2130
2253
  publishEvent(server, event);
2131
2254
  throw error;
@@ -2317,14 +2440,14 @@ function addTracingToToolCallbackInternal(tool, toolName, _server) {
2317
2440
  args = void 0;
2318
2441
  extra = params[0];
2319
2442
  }
2320
- const removeContextFromArgs = (args) => {
2321
- if (args && typeof args === "object" && "context" in args) {
2322
- const { context: _context, ...argsWithoutContext } = args;
2323
- return argsWithoutContext;
2443
+ const removeContextFromArgs = (input) => {
2444
+ if (input && typeof input === "object" && "context" in input) {
2445
+ const { context: _context, ...rest } = input;
2446
+ return rest;
2324
2447
  }
2325
- return args;
2448
+ return input;
2326
2449
  };
2327
- const cleanedArgs = toolName === "get_more_tools" ? args : removeContextFromArgs(args);
2450
+ const cleanedArgs = toolName === "get_more_tools" ? args : stripConversationId(removeContextFromArgs(args));
2328
2451
  try {
2329
2452
  if (cleanedArgs === void 0) return await originalCallback(extra);
2330
2453
  return await originalCallback(cleanedArgs, extra);
@@ -2358,24 +2481,27 @@ function createToolsCallWrapper(originalHandler, server) {
2358
2481
  }
2359
2482
  async function handleWrappedToolsCall(originalHandler, server, request, extra) {
2360
2483
  const startTime = /* @__PURE__ */ new Date();
2361
- const tracing = await initializeToolCallEvent(server, request, extra, startTime);
2484
+ const conversation = resolveConversationId(getServerTrackingData(server)?.options.enableConversationId ?? false, request.params?.arguments, request.params?.name);
2485
+ const tracing = await initializeToolCallEvent(server, request, conversation.conversationId ? cloneRequestWithoutConversationId(request) : request, extra, startTime, conversation);
2362
2486
  if (request?.params?.name === "get_more_tools") return await executeReportMissingTool(server, request, tracing, startTime);
2363
2487
  return await executeOriginalTool(originalHandler, server, request, extra, tracing, startTime);
2364
2488
  }
2365
- async function initializeToolCallEvent(server, request, extra, startTime) {
2489
+ async function initializeToolCallEvent(server, request, downstreamRequest, extra, startTime, conversation) {
2366
2490
  try {
2367
2491
  const data = getServerTrackingData(server);
2368
2492
  if (!data) {
2369
2493
  writeToLog("Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
2370
2494
  return {
2371
2495
  event: null,
2496
+ mintedConversationId: void 0,
2372
2497
  shouldPublishEvent: false
2373
2498
  };
2374
2499
  }
2375
2500
  const event = {
2376
2501
  sessionId: getServerSessionId(server, extra),
2502
+ conversationId: conversation.conversationId,
2377
2503
  resourceName: request.params?.name || "Unknown Tool",
2378
- parameters: buildCapturedMcpParameters(request),
2504
+ parameters: buildCapturedMcpParameters(downstreamRequest),
2379
2505
  eventType: MCPAnalyticsEventType.mcpToolsCall,
2380
2506
  timestamp: startTime,
2381
2507
  redactionFn: data.options.redactSensitiveInformation
@@ -2386,12 +2512,14 @@ async function initializeToolCallEvent(server, request, extra, startTime) {
2386
2512
  setEventIntent(event, await resolveToolCallIntent(data, request, extra));
2387
2513
  return {
2388
2514
  event,
2515
+ mintedConversationId: conversation.minted ? conversation.conversationId : void 0,
2389
2516
  shouldPublishEvent: true
2390
2517
  };
2391
2518
  } catch (error) {
2392
2519
  writeToLog(`Warning: PostHog MCP analytics tracing failed for tool ${request.params?.name}, falling back to original handler - ${error}`);
2393
2520
  return {
2394
2521
  event: null,
2522
+ mintedConversationId: void 0,
2395
2523
  shouldPublishEvent: false
2396
2524
  };
2397
2525
  }
@@ -2419,14 +2547,20 @@ async function executeReportMissingTool(server, request, tracing, startTime) {
2419
2547
  async function executeOriginalTool(originalHandler, server, request, extra, tracing, startTime) {
2420
2548
  try {
2421
2549
  const result = await originalHandler(request, extra);
2422
- publishSuccessfulToolEvent(server, tracing, result, startTime, {
2550
+ let finalResult = result;
2551
+ if (tracing.mintedConversationId) {
2552
+ if (canInjectConversationIdPromptBack(result)) finalResult = injectConversationIdPromptBack(result, tracing.mintedConversationId);
2553
+ else if (tracing.event) tracing.event.conversationId = void 0;
2554
+ }
2555
+ publishSuccessfulToolEvent(server, tracing, finalResult, startTime, {
2423
2556
  capturedError: extra?.__mcp_analytics_error,
2424
2557
  clearCapturedError: () => {
2425
2558
  if (extra) extra.__mcp_analytics_error = void 0;
2426
2559
  }
2427
2560
  });
2428
- return result;
2561
+ return finalResult;
2429
2562
  } catch (error) {
2563
+ if (tracing.mintedConversationId && tracing.event) tracing.event.conversationId = void 0;
2430
2564
  publishFailedToolEvent(server, tracing, error, startTime);
2431
2565
  throw error;
2432
2566
  }
@@ -2617,7 +2751,8 @@ function buildTrackingData(lowLevelServer, options) {
2617
2751
  eventProperties: options.eventProperties,
2618
2752
  host: options.host,
2619
2753
  posthogClient: options.posthogClient,
2620
- posthogOptions: options.posthogOptions
2754
+ posthogOptions: options.posthogOptions,
2755
+ enableConversationId: options.enableConversationId ?? false
2621
2756
  },
2622
2757
  sessionSource: "generated"
2623
2758
  };