@posthog/mcp 0.0.2 → 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
@@ -34,9 +34,40 @@ track(server, {
34
34
  With `context: true`, the SDK adds a required `context` argument to every tool call, strips it before invoking your handler, and captures it as `$mcp_intent` on PostHog events.
35
35
  Captured tool parameters omit the duplicated `context` value and MCP transport internals.
36
36
 
37
+ Use `intentFallback` when you want a fallback for clients that call tools without the `context` argument.
38
+ This is a consumer-supplied callback — the SDK does no inference of its own; whatever string you return becomes `$mcp_intent`.
39
+ The explicit `context` argument always wins; fallback values are marked with `$mcp_intent_source = "inferred"`.
40
+
41
+ ```ts
42
+ track(server, {
43
+ apiKey: process.env.POSTHOG_API_KEY,
44
+ context: true,
45
+ intentFallback: (request) => {
46
+ const toolName = request.params?.name;
47
+ return toolName ? `Calling ${toolName}` : undefined;
48
+ },
49
+ });
50
+ ```
51
+
37
52
  With `enableAITracing: true`, tool calls also emit `$ai_span` events with `$ai_trace_id` and `$ai_span_id`.
38
53
  The regular `mcp_tool_call` event includes the same trace/span IDs so it can be joined back to the LLM analytics span.
39
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
+
40
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.
41
72
 
42
73
  If your application already owns a PostHog client, pass it in instead:
@@ -52,6 +83,36 @@ track(server, {
52
83
  });
53
84
  ```
54
85
 
86
+ ## Event schema
87
+
88
+ The SDK emits PostHog events using a stable MCP-specific schema.
89
+ The package exports `PostHogMCPAnalyticsEvent`, `PostHogMCPAnalyticsProperty`, and `POSTHOG_MCP_ANALYTICS_SOURCE` constants so product code can query the same contract without hard-coded strings.
90
+
91
+ Canonical event names:
92
+
93
+ - `mcp_tool_call` for each MCP tool call
94
+ - `mcp_tools_list` when clients list available tools
95
+ - `mcp_initialize` when clients initialize a session
96
+ - `$ai_span` for tool-call spans when `enableAITracing: true`
97
+ - `$exception` for captured tool errors
98
+
99
+ Important properties:
100
+
101
+ - `$mcp_source = "posthog_mcp_analytics"`
102
+ - `$mcp_intent` from the `context` argument, or from `intentFallback` when no context was provided
103
+ - `$mcp_intent_source` as `context_parameter` or `inferred`
104
+ - `$mcp_tool_name` and `$mcp_resource_name`
105
+ - `$mcp_parameters` and `$mcp_response`, after redaction and truncation
106
+ - `$mcp_duration_ms`
107
+ - `$mcp_is_error`
108
+ - `$session_id`
109
+ - `$ai_trace_id`, `$ai_span_id`, and `$ai_session_id` when AI tracing is enabled
110
+
111
+ The SDK does not emit `$mcp_context`.
112
+ Use `$mcp_intent` for agent/user intent.
113
+
114
+ See [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) for the full architecture, property catalog, sample queries, and migration notes vs the upstream mcpcat schema.
115
+
55
116
  ## Development
56
117
 
57
118
  ```bash
package/dist/index.cjs CHANGED
@@ -120,15 +120,14 @@ function getMCPCompatibleErrorMessage(error) {
120
120
  return "Unknown error";
121
121
  }
122
122
  //#endregion
123
- //#region src/modules/ids.ts
124
- function newPrefixedId(prefix) {
125
- return `${prefix}_${(0, node_crypto.randomUUID)()}`;
126
- }
127
- function deterministicPrefixedId(prefix, input) {
128
- return `${prefix}_${(0, node_crypto.createHash)("sha256").update(input).digest("hex").slice(0, 32)}`;
129
- }
130
- //#endregion
131
123
  //#region src/modules/event-types.ts
124
+ /**
125
+ * Internal SDK event vocabulary.
126
+ *
127
+ * These values are not imported from `@modelcontextprotocol/sdk`; they are the
128
+ * protocol-shaped event types this SDK observes before mapping them to PostHog
129
+ * event names.
130
+ */
132
131
  const MCPAnalyticsEventType = {
133
132
  identify: "posthog:identify",
134
133
  custom: "posthog:custom",
@@ -141,6 +140,14 @@ const MCPAnalyticsEventType = {
141
140
  mcpToolsList: "mcp:tools/list"
142
141
  };
143
142
  //#endregion
143
+ //#region src/modules/ids.ts
144
+ function newPrefixedId(prefix) {
145
+ return `${prefix}_${(0, node_crypto.randomUUID)()}`;
146
+ }
147
+ function deterministicPrefixedId(prefix, input) {
148
+ return `${prefix}_${(0, node_crypto.createHash)("sha256").update(input).digest("hex").slice(0, 32)}`;
149
+ }
150
+ //#endregion
144
151
  //#region src/modules/validation.ts
145
152
  const TAG_KEY_REGEX = /^[a-zA-Z0-9$_.:\- ]+$/;
146
153
  const MAX_TAG_KEY_LENGTH = 32;
@@ -343,7 +350,21 @@ function getRequestResourceName(request) {
343
350
  //#endregion
344
351
  //#region src/modules/constants.ts
345
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.";
346
354
  const POSTHOG_MCP_ANALYTICS_SOURCE = "posthog_mcp_analytics";
355
+ const PostHogMCPAnalyticsEvent = {
356
+ AiSpan: "$ai_span",
357
+ Custom: "mcp_custom",
358
+ Exception: "$exception",
359
+ Identify: "$identify",
360
+ Initialize: "mcp_initialize",
361
+ PromptGet: "mcp_prompt_get",
362
+ PromptsList: "mcp_prompts_list",
363
+ ResourceRead: "mcp_resource_read",
364
+ ResourcesList: "mcp_resources_list",
365
+ ToolCall: "mcp_tool_call",
366
+ ToolsList: "mcp_tools_list"
367
+ };
347
368
  const PostHogMCPAnalyticsProperty = {
348
369
  AiInputState: "$ai_input_state",
349
370
  AiIsError: "$ai_is_error",
@@ -356,9 +377,11 @@ const PostHogMCPAnalyticsProperty = {
356
377
  AiTraceId: "$ai_trace_id",
357
378
  ClientName: "$mcp_client_name",
358
379
  ClientVersion: "$mcp_client_version",
380
+ ConversationId: "$mcp_conversation_id",
359
381
  DurationMs: "$mcp_duration_ms",
360
382
  IsError: "$mcp_is_error",
361
383
  Intent: "$mcp_intent",
384
+ IntentSource: "$mcp_intent_source",
362
385
  Parameters: "$mcp_parameters",
363
386
  ResourceName: "$mcp_resource_name",
364
387
  Response: "$mcp_response",
@@ -370,8 +393,17 @@ const PostHogMCPAnalyticsProperty = {
370
393
  };
371
394
  //#endregion
372
395
  //#region src/modules/posthog-events.ts
373
- const MCP_EVENT_PREFIX_REGEX = /^mcp:/;
374
- const SLASH_REGEX = /\//g;
396
+ const BUILT_IN_EVENT_NAME_BY_TYPE = {
397
+ [MCPAnalyticsEventType.custom]: PostHogMCPAnalyticsEvent.Custom,
398
+ [MCPAnalyticsEventType.identify]: PostHogMCPAnalyticsEvent.Identify,
399
+ [MCPAnalyticsEventType.mcpInitialize]: PostHogMCPAnalyticsEvent.Initialize,
400
+ [MCPAnalyticsEventType.mcpPromptsGet]: PostHogMCPAnalyticsEvent.PromptGet,
401
+ [MCPAnalyticsEventType.mcpPromptsList]: PostHogMCPAnalyticsEvent.PromptsList,
402
+ [MCPAnalyticsEventType.mcpResourcesList]: PostHogMCPAnalyticsEvent.ResourcesList,
403
+ [MCPAnalyticsEventType.mcpResourcesRead]: PostHogMCPAnalyticsEvent.ResourceRead,
404
+ [MCPAnalyticsEventType.mcpToolsCall]: PostHogMCPAnalyticsEvent.ToolCall,
405
+ [MCPAnalyticsEventType.mcpToolsList]: PostHogMCPAnalyticsEvent.ToolsList
406
+ };
375
407
  function getDistinctId(event) {
376
408
  return event.identifyActorGivenId || event.sessionId || "anonymous";
377
409
  }
@@ -386,17 +418,17 @@ function buildPostHogCaptureEvents(event, options = {}) {
386
418
  }
387
419
  function buildCaptureEvent(event, options) {
388
420
  const distinctId = getDistinctId(event);
389
- const eventName = mapEventType(event.eventType);
390
421
  const timestamp = getTimestamp(event);
391
422
  const properties = {
392
423
  [PostHogMCPAnalyticsProperty.SessionId]: event.sessionId,
393
424
  [PostHogMCPAnalyticsProperty.Source]: POSTHOG_MCP_ANALYTICS_SOURCE
394
425
  };
426
+ addConversationIdProperty(event, properties);
395
427
  addCommonEventProperties(event, properties);
396
428
  addTraceReferenceProperties(event, properties, options);
397
429
  addCustomEventProperties(event, properties);
398
430
  return {
399
- event: eventName,
431
+ event: BUILT_IN_EVENT_NAME_BY_TYPE[event.eventType],
400
432
  distinct_id: distinctId,
401
433
  properties,
402
434
  timestamp,
@@ -417,6 +449,9 @@ function addTraceReferenceProperties(event, properties, options) {
417
449
  properties[PostHogMCPAnalyticsProperty.AiTraceId] = getAITraceId(event);
418
450
  properties[PostHogMCPAnalyticsProperty.AiSpanId] = getAISpanId(event);
419
451
  }
452
+ function addConversationIdProperty(event, properties) {
453
+ if (event.conversationId !== void 0 && event.conversationId !== "") properties[PostHogMCPAnalyticsProperty.ConversationId] = event.conversationId;
454
+ }
420
455
  function addCommonEventProperties(event, properties) {
421
456
  if (event.resourceName) {
422
457
  properties[PostHogMCPAnalyticsProperty.ResourceName] = event.resourceName;
@@ -428,6 +463,7 @@ function addCommonEventProperties(event, properties) {
428
463
  if (event.clientName) properties[PostHogMCPAnalyticsProperty.ClientName] = event.clientName;
429
464
  if (event.clientVersion) properties[PostHogMCPAnalyticsProperty.ClientVersion] = event.clientVersion;
430
465
  if (event.userIntent) properties[PostHogMCPAnalyticsProperty.Intent] = event.userIntent;
466
+ if (event.userIntentSource) properties[PostHogMCPAnalyticsProperty.IntentSource] = event.userIntentSource;
431
467
  if (event.isError !== void 0) properties[PostHogMCPAnalyticsProperty.IsError] = event.isError;
432
468
  if (event.parameters !== void 0) properties[PostHogMCPAnalyticsProperty.Parameters] = event.parameters;
433
469
  if (event.response !== void 0) properties[PostHogMCPAnalyticsProperty.Response] = event.response;
@@ -447,6 +483,7 @@ function buildExceptionEvent(event) {
447
483
  $exception_source: "backend",
448
484
  [PostHogMCPAnalyticsProperty.SessionId]: event.sessionId
449
485
  };
486
+ addConversationIdProperty(event, properties);
450
487
  if (event.error) {
451
488
  if (event.error.message) properties.$exception_message = event.error.message;
452
489
  if (event.error.type) properties.$exception_type = event.error.type;
@@ -461,7 +498,7 @@ function buildExceptionEvent(event) {
461
498
  if (event.clientName) properties[PostHogMCPAnalyticsProperty.ClientName] = event.clientName;
462
499
  if (event.clientVersion) properties[PostHogMCPAnalyticsProperty.ClientVersion] = event.clientVersion;
463
500
  return {
464
- event: "$exception",
501
+ event: PostHogMCPAnalyticsEvent.Exception,
465
502
  distinct_id: distinctId,
466
503
  properties,
467
504
  timestamp,
@@ -480,6 +517,7 @@ function buildAISpanEvent(event) {
480
517
  [PostHogMCPAnalyticsProperty.SessionId]: event.sessionId,
481
518
  [PostHogMCPAnalyticsProperty.Source]: POSTHOG_MCP_ANALYTICS_SOURCE
482
519
  };
520
+ addConversationIdProperty(event, properties);
483
521
  if (event.duration !== void 0) properties[PostHogMCPAnalyticsProperty.AiLatency] = event.duration / 1e3;
484
522
  if (event.isError && event.error) properties.$ai_error = event.error;
485
523
  if (event.parameters !== void 0) properties[PostHogMCPAnalyticsProperty.AiInputState] = event.parameters;
@@ -487,27 +525,17 @@ function buildAISpanEvent(event) {
487
525
  if (event.serverName) properties[PostHogMCPAnalyticsProperty.ServerName] = event.serverName;
488
526
  if (event.clientName) properties[PostHogMCPAnalyticsProperty.ClientName] = event.clientName;
489
527
  if (event.userIntent) properties[PostHogMCPAnalyticsProperty.Intent] = event.userIntent;
528
+ if (event.userIntentSource) properties[PostHogMCPAnalyticsProperty.IntentSource] = event.userIntentSource;
490
529
  if (event.tags) for (const [key, value] of Object.entries(event.tags)) properties[key] = value;
491
530
  if (event.properties) for (const [key, value] of Object.entries(event.properties)) properties[key] = value;
492
531
  return {
493
- event: "$ai_span",
532
+ event: PostHogMCPAnalyticsEvent.AiSpan,
494
533
  distinct_id: distinctId,
495
534
  properties,
496
535
  timestamp,
497
536
  type: "capture"
498
537
  };
499
538
  }
500
- function mapEventType(eventType) {
501
- return {
502
- [MCPAnalyticsEventType.mcpToolsCall]: "mcp_tool_call",
503
- [MCPAnalyticsEventType.mcpToolsList]: "mcp_tools_list",
504
- [MCPAnalyticsEventType.mcpInitialize]: "mcp_initialize",
505
- [MCPAnalyticsEventType.mcpResourcesRead]: "mcp_resource_read",
506
- [MCPAnalyticsEventType.mcpResourcesList]: "mcp_resources_list",
507
- [MCPAnalyticsEventType.mcpPromptsGet]: "mcp_prompt_get",
508
- [MCPAnalyticsEventType.mcpPromptsList]: "mcp_prompts_list"
509
- }[eventType] || `mcp_${eventType.replace(MCP_EVENT_PREFIX_REGEX, "").replace(SLASH_REGEX, "_")}`;
510
- }
511
539
  //#endregion
512
540
  //#region src/modules/redaction.ts
513
541
  /**
@@ -701,7 +729,7 @@ function sanitizeParameters(obj) {
701
729
  }
702
730
  //#endregion
703
731
  //#region package.json
704
- var version = "0.0.2";
732
+ var version = "0.0.4";
705
733
  //#endregion
706
734
  //#region src/modules/session.ts
707
735
  function newSessionId() {
@@ -1085,7 +1113,6 @@ var EventQueue = class {
1085
1113
  timestamp: new Date(captureEvent.timestamp)
1086
1114
  });
1087
1115
  writeToLog(`Queued PostHog event ${event.id} | ${event.eventType} | ${event.duration} ms | ${event.identifyActorGivenId || "anonymous"}`);
1088
- writeToLog(`Event details: ${JSON.stringify(event)}`);
1089
1116
  } catch (error) {
1090
1117
  writeToLog(`Failed to queue PostHog event ${event.id}: ${getMCPCompatibleErrorMessage(error)}`);
1091
1118
  throw error;
@@ -1148,7 +1175,7 @@ function publishEvent(server, eventInput) {
1148
1175
  id: eventInput.id || "",
1149
1176
  sessionId: eventInput.sessionId || data.sessionId,
1150
1177
  apiKey: data.apiKey,
1151
- eventType: eventInput.eventType || "",
1178
+ eventType: eventInput.eventType || MCPAnalyticsEventType.custom,
1152
1179
  timestamp: eventInput.timestamp || /* @__PURE__ */ new Date(),
1153
1180
  duration,
1154
1181
  ipAddress: sessionInfo.ipAddress,
@@ -1165,8 +1192,10 @@ function publishEvent(server, eventInput) {
1165
1192
  parameters: eventInput.parameters,
1166
1193
  response: eventInput.response,
1167
1194
  userIntent: eventInput.userIntent,
1195
+ userIntentSource: eventInput.userIntentSource,
1168
1196
  isError: eventInput.isError,
1169
1197
  error: eventInput.error,
1198
+ conversationId: eventInput.conversationId,
1170
1199
  redactionFn: eventInput.redactionFn,
1171
1200
  tags: eventInput.tags,
1172
1201
  properties: eventInput.properties
@@ -1903,6 +1932,155 @@ function setupMCPAnalyticsTools(server) {
1903
1932
  }
1904
1933
  }
1905
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
2041
+ //#region src/modules/intent.ts
2042
+ function getContextArgument$2(request) {
2043
+ const context = request.params?.arguments?.context;
2044
+ return typeof context === "string" && context.trim() ? context : void 0;
2045
+ }
2046
+ function normalizeIntent(intent) {
2047
+ if (typeof intent !== "string") return null;
2048
+ const trimmed = intent.trim();
2049
+ return trimmed ? trimmed : null;
2050
+ }
2051
+ async function runIntentFallback(data, request, extra) {
2052
+ if (!data.options.intentFallback) return null;
2053
+ try {
2054
+ const intent = normalizeIntent(await data.options.intentFallback(request, extra));
2055
+ return intent ? {
2056
+ intent,
2057
+ source: "inferred"
2058
+ } : null;
2059
+ } catch (error) {
2060
+ writeToLog(`intentFallback callback error: ${error}`);
2061
+ return null;
2062
+ }
2063
+ }
2064
+ async function resolveToolCallIntent(data, request, extra) {
2065
+ const contextArgument = getContextArgument$2(request);
2066
+ if (isContextEnabled(data.options.context) && request.params?.name !== "get_more_tools" && contextArgument) return {
2067
+ intent: contextArgument,
2068
+ source: "context_parameter"
2069
+ };
2070
+ return await runIntentFallback(data, request, extra);
2071
+ }
2072
+ function setEventIntent(event, resolvedIntent) {
2073
+ if (!resolvedIntent) return;
2074
+ event.userIntent = resolvedIntent.intent;
2075
+ event.userIntentSource = resolvedIntent.source;
2076
+ }
2077
+ function setExplicitContextIntent(event, context) {
2078
+ const intent = normalizeIntent(context);
2079
+ if (!intent) return;
2080
+ event.userIntent = intent;
2081
+ event.userIntentSource = "context_parameter";
2082
+ }
2083
+ //#endregion
1906
2084
  //#region src/modules/tracing.ts
1907
2085
  function isToolResultError$1(result) {
1908
2086
  return !!result && typeof result === "object" && "isError" in result && result.isError === true;
@@ -1955,6 +2133,7 @@ async function getTracedToolsList(server, originalListToolsHandler, request, ext
1955
2133
  const data = getServerTrackingData(server);
1956
2134
  let tools = (await originalListToolsHandler(request, extra)).tools || [];
1957
2135
  if (data && isContextEnabled(data.options.context)) tools = addContextParameterToTools(tools, getContextDescription(data.options.context));
2136
+ if (data?.options.enableConversationId) tools = addConversationIdToTools(tools);
1958
2137
  if (data?.options.reportMissing) {
1959
2138
  if (!tools.some((tool) => tool?.name === "get_more_tools")) tools.push(getReportMissingToolDescriptor());
1960
2139
  }
@@ -2039,10 +2218,13 @@ async function handleToolCallRequest(server, originalCallToolHandler, request, e
2039
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.");
2040
2219
  return await originalCallToolHandler?.(request, extra);
2041
2220
  }
2221
+ const conversation = resolveConversationId(data.options.enableConversationId ?? false, request.params?.arguments, request.params?.name);
2222
+ const downstreamRequest = conversation.conversationId ? cloneRequestWithoutConversationId(request) : request;
2042
2223
  const event = {
2043
2224
  sessionId: getServerSessionId(server, extra),
2225
+ conversationId: conversation.conversationId,
2044
2226
  resourceName: request.params?.name || "Unknown Tool Name",
2045
- parameters: buildCapturedMcpParameters(request),
2227
+ parameters: buildCapturedMcpParameters(downstreamRequest),
2046
2228
  eventType: MCPAnalyticsEventType.mcpToolsCall,
2047
2229
  timestamp: /* @__PURE__ */ new Date(),
2048
2230
  redactionFn: data.options.redactSensitiveInformation
@@ -2050,18 +2232,24 @@ async function handleToolCallRequest(server, originalCallToolHandler, request, e
2050
2232
  try {
2051
2233
  await handleIdentify(server, data, request, extra);
2052
2234
  await applyResolvedMetadata$1(event, data, request, extra);
2053
- setToolCallContext(event, data.options.context, request);
2054
- const result = await executeToolCall(server, originalCallToolHandler, request, extra, event);
2235
+ setEventIntent(event, await resolveToolCallIntent(data, request, extra));
2236
+ const result = await executeToolCall(server, originalCallToolHandler, downstreamRequest, extra, event);
2055
2237
  if (isToolResultError$1(result)) {
2056
2238
  event.isError = true;
2057
2239
  event.error = captureException(result);
2058
- }
2059
- event.response = result;
2240
+ } else event.isError = false;
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;
2245
+ event.duration = getEventDuration(event);
2060
2246
  publishEvent(server, event);
2061
- return result;
2247
+ return finalResult;
2062
2248
  } catch (error) {
2063
2249
  event.isError = true;
2064
2250
  event.error = captureException(error);
2251
+ if (conversation.minted) event.conversationId = void 0;
2252
+ event.duration = getEventDuration(event);
2065
2253
  publishEvent(server, event);
2066
2254
  throw error;
2067
2255
  }
@@ -2069,7 +2257,7 @@ async function handleToolCallRequest(server, originalCallToolHandler, request, e
2069
2257
  async function executeToolCall(server, originalCallToolHandler, request, extra, event) {
2070
2258
  if (request.params?.name === "get_more_tools") {
2071
2259
  const context = getContextArgument$1(request) || "";
2072
- event.userIntent = context;
2260
+ setExplicitContextIntent(event, context);
2073
2261
  return handleReportMissing({ context });
2074
2262
  }
2075
2263
  if (originalCallToolHandler) return await originalCallToolHandler(request, extra);
@@ -2085,11 +2273,6 @@ async function applyResolvedMetadata$1(event, data, request, extra) {
2085
2273
  const resolvedProperties = await resolveEventProperties(data, request, extra);
2086
2274
  if (resolvedProperties) event.properties = resolvedProperties;
2087
2275
  }
2088
- function setToolCallContext(event, context, request) {
2089
- if (!(isContextEnabled(context) && request.params?.name !== "get_more_tools")) return;
2090
- const contextArgument = getContextArgument$1(request);
2091
- if (contextArgument) event.userIntent = contextArgument;
2092
- }
2093
2276
  function getContextArgument$1(request) {
2094
2277
  const context = request.params?.arguments?.context;
2095
2278
  return typeof context === "string" ? context : void 0;
@@ -2257,14 +2440,14 @@ function addTracingToToolCallbackInternal(tool, toolName, _server) {
2257
2440
  args = void 0;
2258
2441
  extra = params[0];
2259
2442
  }
2260
- const removeContextFromArgs = (args) => {
2261
- if (args && typeof args === "object" && "context" in args) {
2262
- const { context: _context, ...argsWithoutContext } = args;
2263
- return argsWithoutContext;
2443
+ const removeContextFromArgs = (input) => {
2444
+ if (input && typeof input === "object" && "context" in input) {
2445
+ const { context: _context, ...rest } = input;
2446
+ return rest;
2264
2447
  }
2265
- return args;
2448
+ return input;
2266
2449
  };
2267
- const cleanedArgs = toolName === "get_more_tools" ? args : removeContextFromArgs(args);
2450
+ const cleanedArgs = toolName === "get_more_tools" ? args : stripConversationId(removeContextFromArgs(args));
2268
2451
  try {
2269
2452
  if (cleanedArgs === void 0) return await originalCallback(extra);
2270
2453
  return await originalCallback(cleanedArgs, extra);
@@ -2298,24 +2481,27 @@ function createToolsCallWrapper(originalHandler, server) {
2298
2481
  }
2299
2482
  async function handleWrappedToolsCall(originalHandler, server, request, extra) {
2300
2483
  const startTime = /* @__PURE__ */ new Date();
2301
- 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);
2302
2486
  if (request?.params?.name === "get_more_tools") return await executeReportMissingTool(server, request, tracing, startTime);
2303
2487
  return await executeOriginalTool(originalHandler, server, request, extra, tracing, startTime);
2304
2488
  }
2305
- async function initializeToolCallEvent(server, request, extra, startTime) {
2489
+ async function initializeToolCallEvent(server, request, downstreamRequest, extra, startTime, conversation) {
2306
2490
  try {
2307
2491
  const data = getServerTrackingData(server);
2308
2492
  if (!data) {
2309
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.");
2310
2494
  return {
2311
2495
  event: null,
2496
+ mintedConversationId: void 0,
2312
2497
  shouldPublishEvent: false
2313
2498
  };
2314
2499
  }
2315
2500
  const event = {
2316
2501
  sessionId: getServerSessionId(server, extra),
2502
+ conversationId: conversation.conversationId,
2317
2503
  resourceName: request.params?.name || "Unknown Tool",
2318
- parameters: buildCapturedMcpParameters(request),
2504
+ parameters: buildCapturedMcpParameters(downstreamRequest),
2319
2505
  eventType: MCPAnalyticsEventType.mcpToolsCall,
2320
2506
  timestamp: startTime,
2321
2507
  redactionFn: data.options.redactSensitiveInformation
@@ -2323,16 +2509,17 @@ async function initializeToolCallEvent(server, request, extra, startTime) {
2323
2509
  await handleIdentify(server, data, request, extra);
2324
2510
  event.sessionId = data.sessionId;
2325
2511
  await applyResolvedMetadata(event, data, request, extra);
2326
- const contextArgument = getContextArgument(request);
2327
- if (isContextEnabled(data.options.context) && contextArgument) event.userIntent = contextArgument;
2512
+ setEventIntent(event, await resolveToolCallIntent(data, request, extra));
2328
2513
  return {
2329
2514
  event,
2515
+ mintedConversationId: conversation.minted ? conversation.conversationId : void 0,
2330
2516
  shouldPublishEvent: true
2331
2517
  };
2332
2518
  } catch (error) {
2333
2519
  writeToLog(`Warning: PostHog MCP analytics tracing failed for tool ${request.params?.name}, falling back to original handler - ${error}`);
2334
2520
  return {
2335
2521
  event: null,
2522
+ mintedConversationId: void 0,
2336
2523
  shouldPublishEvent: false
2337
2524
  };
2338
2525
  }
@@ -2347,7 +2534,10 @@ async function executeReportMissingTool(server, request, tracing, startTime) {
2347
2534
  try {
2348
2535
  const context = getContextArgument(request) || "";
2349
2536
  const result = await handleReportMissing({ context });
2350
- publishSuccessfulToolEvent(server, tracing, result, startTime, { userIntent: context });
2537
+ publishSuccessfulToolEvent(server, tracing, result, startTime, {
2538
+ userIntent: context,
2539
+ userIntentSource: "context_parameter"
2540
+ });
2351
2541
  return result;
2352
2542
  } catch (error) {
2353
2543
  publishFailedToolEvent(server, tracing, error, startTime);
@@ -2357,14 +2547,20 @@ async function executeReportMissingTool(server, request, tracing, startTime) {
2357
2547
  async function executeOriginalTool(originalHandler, server, request, extra, tracing, startTime) {
2358
2548
  try {
2359
2549
  const result = await originalHandler(request, extra);
2360
- 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, {
2361
2556
  capturedError: extra?.__mcp_analytics_error,
2362
2557
  clearCapturedError: () => {
2363
2558
  if (extra) extra.__mcp_analytics_error = void 0;
2364
2559
  }
2365
2560
  });
2366
- return result;
2561
+ return finalResult;
2367
2562
  } catch (error) {
2563
+ if (tracing.mintedConversationId && tracing.event) tracing.event.conversationId = void 0;
2368
2564
  publishFailedToolEvent(server, tracing, error, startTime);
2369
2565
  throw error;
2370
2566
  }
@@ -2375,12 +2571,15 @@ function getContextArgument(request) {
2375
2571
  }
2376
2572
  function publishSuccessfulToolEvent(server, tracing, result, startTime, options = {}) {
2377
2573
  if (!(tracing.event && tracing.shouldPublishEvent)) return;
2378
- if (options.userIntent) tracing.event.userIntent = options.userIntent;
2574
+ if (options.userIntent) {
2575
+ setExplicitContextIntent(tracing.event, options.userIntent);
2576
+ if (options.userIntentSource) tracing.event.userIntentSource = options.userIntentSource;
2577
+ }
2379
2578
  if (isToolResultError(result)) {
2380
2579
  tracing.event.isError = true;
2381
2580
  tracing.event.error = captureException(options.capturedError || result);
2382
2581
  options.clearCapturedError?.();
2383
- }
2582
+ } else tracing.event.isError = false;
2384
2583
  tracing.event.response = result;
2385
2584
  tracing.event.duration = Date.now() - startTime.getTime();
2386
2585
  publishEvent(server, tracing.event);
@@ -2417,6 +2616,7 @@ function setupTracking(server) {
2417
2616
  * @param options.enableAITracing - Emits `$ai_span` events for tool calls so MCP activity appears in PostHog LLM analytics. Defaults to false.
2418
2617
  * @param options.enableTracing - Enables tracking of tool calls and usage patterns.
2419
2618
  * @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.
2619
+ * @param options.intentFallback - Optional consumer-supplied callback invoked when a tool call has no explicit `context` argument. Return a short user intent string to capture as `$mcp_intent` (with `$mcp_intent_source = "inferred"`). The SDK does not infer anything on its own — this is purely a slot for your own derivation logic (e.g. a switch on `request.params.name`, an LLM call, etc.). Runs on the hot path of every uncontextualized tool call.
2420
2620
  * @param options.identify - Async function to identify users and attach custom data to their sessions.
2421
2621
  * @param options.redactSensitiveInformation - Function to redact sensitive data before sending to PostHog.
2422
2622
  * @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`.
@@ -2544,13 +2744,15 @@ function buildTrackingData(lowLevelServer, options) {
2544
2744
  enableAITracing: options.enableAITracing ?? false,
2545
2745
  enableTracing: options.enableTracing ?? true,
2546
2746
  context: options.context,
2747
+ intentFallback: options.intentFallback,
2547
2748
  identify: options.identify,
2548
2749
  redactSensitiveInformation: options.redactSensitiveInformation,
2549
2750
  eventTags: options.eventTags,
2550
2751
  eventProperties: options.eventProperties,
2551
2752
  host: options.host,
2552
2753
  posthogClient: options.posthogClient,
2553
- posthogOptions: options.posthogOptions
2754
+ posthogOptions: options.posthogOptions,
2755
+ enableConversationId: options.enableConversationId ?? false
2554
2756
  },
2555
2757
  sessionSource: "generated"
2556
2758
  };
@@ -2688,6 +2890,8 @@ function publishResolvedCustomEvent(target, event) {
2688
2890
  eventQueue.add(event);
2689
2891
  }
2690
2892
  //#endregion
2893
+ exports.POSTHOG_MCP_ANALYTICS_SOURCE = POSTHOG_MCP_ANALYTICS_SOURCE;
2894
+ exports.PostHogMCPAnalyticsEvent = PostHogMCPAnalyticsEvent;
2691
2895
  exports.PostHogMCPAnalyticsProperty = PostHogMCPAnalyticsProperty;
2692
2896
  exports.publishCustomEvent = publishCustomEvent;
2693
2897
  exports.track = track;