@posthog/mcp 0.0.8 → 0.1.16

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.
Files changed (138) hide show
  1. package/LICENSE +307 -2
  2. package/README.md +186 -93
  3. package/dist/extensions/capture.d.ts +13 -0
  4. package/dist/extensions/capture.d.ts.map +1 -0
  5. package/dist/extensions/capture.js +80 -0
  6. package/dist/extensions/capture.mjs +46 -0
  7. package/dist/extensions/compatibility.d.ts +23 -0
  8. package/dist/extensions/compatibility.d.ts.map +1 -0
  9. package/dist/extensions/compatibility.js +113 -0
  10. package/dist/extensions/compatibility.mjs +67 -0
  11. package/dist/extensions/constants.d.ts +39 -0
  12. package/dist/extensions/constants.d.ts.map +1 -0
  13. package/dist/extensions/constants.js +86 -0
  14. package/dist/extensions/constants.mjs +37 -0
  15. package/dist/extensions/context-parameters.d.ts +31 -0
  16. package/dist/extensions/context-parameters.d.ts.map +1 -0
  17. package/dist/extensions/context-parameters.js +93 -0
  18. package/dist/extensions/context-parameters.mjs +50 -0
  19. package/dist/extensions/conversation-id.d.ts +52 -0
  20. package/dist/extensions/conversation-id.d.ts.map +1 -0
  21. package/dist/extensions/conversation-id.js +164 -0
  22. package/dist/extensions/conversation-id.mjs +103 -0
  23. package/dist/extensions/event-types.d.ts +21 -0
  24. package/dist/extensions/event-types.d.ts.map +1 -0
  25. package/dist/extensions/event-types.js +47 -0
  26. package/dist/extensions/event-types.mjs +13 -0
  27. package/dist/extensions/exceptions.d.ts +13 -0
  28. package/dist/extensions/exceptions.d.ts.map +1 -0
  29. package/dist/extensions/exceptions.js +58 -0
  30. package/dist/extensions/exceptions.mjs +24 -0
  31. package/dist/extensions/ids.d.ts +13 -0
  32. package/dist/extensions/ids.d.ts.map +1 -0
  33. package/dist/extensions/ids.js +55 -0
  34. package/dist/extensions/ids.mjs +18 -0
  35. package/dist/extensions/instrument-highlevel.d.ts +3 -0
  36. package/dist/extensions/instrument-highlevel.d.ts.map +1 -0
  37. package/dist/extensions/instrument-highlevel.js +229 -0
  38. package/dist/extensions/instrument-highlevel.mjs +195 -0
  39. package/dist/extensions/instrument-lowlevel.d.ts +8 -0
  40. package/dist/extensions/instrument-lowlevel.d.ts.map +1 -0
  41. package/dist/extensions/instrument-lowlevel.js +85 -0
  42. package/dist/extensions/instrument-lowlevel.mjs +51 -0
  43. package/dist/extensions/instrumentation.d.ts +59 -0
  44. package/dist/extensions/instrumentation.d.ts.map +1 -0
  45. package/dist/extensions/instrumentation.js +237 -0
  46. package/dist/extensions/instrumentation.mjs +194 -0
  47. package/dist/extensions/intent.d.ts +10 -0
  48. package/dist/extensions/intent.d.ts.map +1 -0
  49. package/dist/extensions/intent.js +84 -0
  50. package/dist/extensions/intent.mjs +44 -0
  51. package/dist/extensions/internal.d.ts +32 -0
  52. package/dist/extensions/internal.d.ts.map +1 -0
  53. package/dist/extensions/internal.js +158 -0
  54. package/dist/extensions/internal.mjs +106 -0
  55. package/dist/extensions/logger.d.ts +14 -0
  56. package/dist/extensions/logger.d.ts.map +1 -0
  57. package/dist/extensions/logger.js +47 -0
  58. package/dist/extensions/logger.mjs +10 -0
  59. package/dist/extensions/mcp-payloads.d.ts +5 -0
  60. package/dist/extensions/mcp-payloads.d.ts.map +1 -0
  61. package/dist/extensions/mcp-payloads.js +91 -0
  62. package/dist/extensions/mcp-payloads.mjs +54 -0
  63. package/dist/extensions/mcp-sdk-compat.d.ts +33 -0
  64. package/dist/extensions/mcp-sdk-compat.d.ts.map +1 -0
  65. package/dist/extensions/mcp-sdk-compat.js +112 -0
  66. package/dist/extensions/mcp-sdk-compat.mjs +60 -0
  67. package/dist/extensions/posthog-events.d.ts +14 -0
  68. package/dist/extensions/posthog-events.d.ts.map +1 -0
  69. package/dist/extensions/posthog-events.js +143 -0
  70. package/dist/extensions/posthog-events.mjs +109 -0
  71. package/dist/extensions/sanitization.d.ts +10 -0
  72. package/dist/extensions/sanitization.d.ts.map +1 -0
  73. package/dist/extensions/sanitization.js +96 -0
  74. package/dist/extensions/sanitization.mjs +62 -0
  75. package/dist/extensions/session.d.ts +20 -0
  76. package/dist/extensions/session.d.ts.map +1 -0
  77. package/dist/extensions/session.js +113 -0
  78. package/dist/extensions/session.mjs +67 -0
  79. package/dist/extensions/sink.d.ts +44 -0
  80. package/dist/extensions/sink.d.ts.map +1 -0
  81. package/dist/extensions/sink.js +102 -0
  82. package/dist/extensions/sink.mjs +65 -0
  83. package/dist/extensions/tools.d.ts +9 -0
  84. package/dist/extensions/tools.d.ts.map +1 -0
  85. package/dist/extensions/tools.js +79 -0
  86. package/dist/extensions/tools.mjs +39 -0
  87. package/dist/extensions/tracing-helpers.d.ts +29 -0
  88. package/dist/extensions/tracing-helpers.d.ts.map +1 -0
  89. package/dist/extensions/tracing-helpers.js +59 -0
  90. package/dist/extensions/tracing-helpers.mjs +16 -0
  91. package/dist/extensions/truncation.d.ts +25 -0
  92. package/dist/extensions/truncation.d.ts.map +1 -0
  93. package/dist/extensions/truncation.js +277 -0
  94. package/dist/extensions/truncation.mjs +228 -0
  95. package/dist/index.d.ts +36 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +132 -0
  98. package/dist/index.mjs +83 -2928
  99. package/dist/types.d.ts +220 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +18 -0
  102. package/dist/types.mjs +0 -0
  103. package/dist/version.d.ts +2 -0
  104. package/dist/version.d.ts.map +1 -0
  105. package/dist/version.js +36 -0
  106. package/dist/version.mjs +2 -0
  107. package/package.json +55 -87
  108. package/src/extensions/capture.ts +68 -0
  109. package/src/extensions/compatibility.ts +138 -0
  110. package/src/extensions/constants.ts +48 -0
  111. package/src/extensions/context-parameters.ts +119 -0
  112. package/src/extensions/conversation-id.ts +177 -0
  113. package/src/extensions/event-types.ts +21 -0
  114. package/src/extensions/exceptions.ts +83 -0
  115. package/src/extensions/ids.ts +32 -0
  116. package/src/extensions/instrument-highlevel.ts +297 -0
  117. package/src/extensions/instrument-lowlevel.ts +83 -0
  118. package/src/extensions/instrumentation.ts +415 -0
  119. package/src/extensions/intent.ts +78 -0
  120. package/src/extensions/internal.ts +193 -0
  121. package/src/extensions/logger.ts +27 -0
  122. package/src/extensions/mcp-payloads.ts +98 -0
  123. package/src/extensions/mcp-sdk-compat.ts +160 -0
  124. package/src/extensions/posthog-events.ts +212 -0
  125. package/src/extensions/sanitization.ts +121 -0
  126. package/src/extensions/session.ts +117 -0
  127. package/src/extensions/sink.ts +130 -0
  128. package/src/extensions/tools.ts +50 -0
  129. package/src/extensions/tracing-helpers.ts +51 -0
  130. package/src/extensions/truncation.ts +453 -0
  131. package/src/index.ts +174 -0
  132. package/src/types.ts +244 -0
  133. package/src/version.ts +1 -0
  134. package/dist/index.cjs +0 -2936
  135. package/dist/index.cjs.map +0 -1
  136. package/dist/index.d.cts +0 -264
  137. package/dist/index.d.mts +0 -263
  138. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,48 @@
1
+ export const INACTIVITY_TIMEOUT_IN_MINUTES = 30
2
+
3
+ export 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."`
4
+
5
+ export const DEFAULT_CONVERSATION_ID_DESCRIPTION =
6
+ "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."
7
+
8
+ export const POSTHOG_MCP_ANALYTICS_SOURCE = 'posthog_mcp_analytics'
9
+
10
+ // All PostHog-owned event names start with `$` per the PostHog convention.
11
+ // Non-`$` names would be treated as customer-defined events and confuse the schema.
12
+ export const PostHogMCPAnalyticsEvent = {
13
+ Custom: '$mcp_custom',
14
+ Exception: '$exception',
15
+ Identify: '$identify',
16
+ Initialize: '$mcp_initialize',
17
+ MissingCapability: '$mcp_missing_capability',
18
+ PromptGet: '$mcp_prompt_get',
19
+ PromptsList: '$mcp_prompts_list',
20
+ ResourceRead: '$mcp_resource_read',
21
+ ResourcesList: '$mcp_resources_list',
22
+ ToolCall: '$mcp_tool_call',
23
+ ToolsList: '$mcp_tools_list',
24
+ } as const
25
+
26
+ export type PostHogMCPAnalyticsEvent = (typeof PostHogMCPAnalyticsEvent)[keyof typeof PostHogMCPAnalyticsEvent]
27
+
28
+ export const PostHogMCPAnalyticsProperty = {
29
+ ClientName: '$mcp_client_name',
30
+ ClientVersion: '$mcp_client_version',
31
+ ConversationId: '$mcp_conversation_id',
32
+ DurationMs: '$mcp_duration_ms',
33
+ IsError: '$mcp_is_error',
34
+ Intent: '$mcp_intent',
35
+ IntentSource: '$mcp_intent_source',
36
+ ListedToolNames: '$mcp_listed_tool_names',
37
+ Parameters: '$mcp_parameters',
38
+ ResourceName: '$mcp_resource_name',
39
+ Response: '$mcp_response',
40
+ ServerName: '$mcp_server_name',
41
+ ServerVersion: '$mcp_server_version',
42
+ SessionId: '$session_id',
43
+ Source: '$mcp_source',
44
+ ToolDescription: '$mcp_tool_description',
45
+ ToolName: '$mcp_tool_name',
46
+ } as const
47
+
48
+ export type PostHogMCPAnalyticsProperty = (typeof PostHogMCPAnalyticsProperty)[keyof typeof PostHogMCPAnalyticsProperty]
@@ -0,0 +1,119 @@
1
+ import type { MCPAnalyticsOptions } from '../types'
2
+ import { DEFAULT_CONTEXT_PARAMETER_DESCRIPTION } from './constants'
3
+ import { log } from './logger'
4
+
5
+ interface JsonSchema {
6
+ additionalProperties?: boolean
7
+ allOf?: unknown
8
+ anyOf?: unknown
9
+ oneOf?: unknown
10
+ properties?: Record<string, unknown>
11
+ required?: string[]
12
+ type?: string
13
+ }
14
+
15
+ export interface ContextInjectableTool {
16
+ inputSchema?: JsonSchema
17
+ name?: string
18
+ [key: string]: unknown
19
+ }
20
+
21
+ export function isContextEnabled(context: MCPAnalyticsOptions['context']): boolean {
22
+ return context !== false
23
+ }
24
+
25
+ export function getContextDescription(context: MCPAnalyticsOptions['context']): string | undefined {
26
+ return typeof context === 'object' ? context.description : undefined
27
+ }
28
+
29
+ /**
30
+ * Adds a context parameter to a tool's JSON Schema.
31
+ * This function is called AFTER the MCP SDK has converted Zod schemas to JSON Schema,
32
+ * so we only need to handle JSON Schema format.
33
+ *
34
+ * Skips injection (with warning) for:
35
+ * - Tools that already have a 'context' parameter
36
+ * - Complex schemas (oneOf/allOf/anyOf) that can't safely have properties added
37
+ * - Schemas with additionalProperties: false
38
+ */
39
+ export function addContextParameterToTool<TTool extends ContextInjectableTool>(
40
+ tool: TTool,
41
+ contextDescriptionOverride?: string
42
+ ): TTool {
43
+ // Create a shallow copy of the tool to avoid modifying the original
44
+ const modifiedTool = { ...tool }
45
+ const toolName = tool.name || 'unknown'
46
+ const schema = modifiedTool.inputSchema as JsonSchema | undefined
47
+
48
+ // Check if tool already has context parameter - skip to avoid collision
49
+ if (schema?.properties?.context) {
50
+ log(`WARN: Tool "${toolName}" already has 'context' parameter. Skipping context injection.`)
51
+ return modifiedTool
52
+ }
53
+
54
+ // Skip complex schemas that can't safely have properties added at root level
55
+ if (schema?.oneOf || schema?.allOf || schema?.anyOf) {
56
+ log(`WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf). Skipping context injection.`)
57
+ return modifiedTool
58
+ }
59
+
60
+ // Note: If additionalProperties is false, we'll need to remove that constraint
61
+ // when adding context, otherwise the schema would be invalid. We handle this
62
+ // after the deep copy below.
63
+
64
+ if (!modifiedTool.inputSchema) {
65
+ modifiedTool.inputSchema = {
66
+ type: 'object',
67
+ properties: {},
68
+ required: [],
69
+ }
70
+ }
71
+
72
+ const contextDescription = contextDescriptionOverride || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION
73
+
74
+ // Deep copy the inputSchema to avoid mutations
75
+ modifiedTool.inputSchema = JSON.parse(JSON.stringify(modifiedTool.inputSchema)) as JsonSchema
76
+
77
+ const inputSchema = modifiedTool.inputSchema as JsonSchema
78
+
79
+ // Ensure properties object exists
80
+ if (!inputSchema.properties) {
81
+ inputSchema.properties = {}
82
+ }
83
+
84
+ // Handle additionalProperties: false - must remove this constraint since we're adding context
85
+ // The MCP SDK adds this constraint when converting Zod schemas to JSON Schema
86
+ if (inputSchema.additionalProperties === false) {
87
+ inputSchema.additionalProperties = undefined
88
+ }
89
+
90
+ // Add context property
91
+ inputSchema.properties.context = {
92
+ type: 'string',
93
+ description: contextDescription,
94
+ }
95
+
96
+ // Add context to required array
97
+ if (Array.isArray(inputSchema.required)) {
98
+ if (!inputSchema.required.includes('context')) {
99
+ inputSchema.required.push('context')
100
+ }
101
+ } else {
102
+ inputSchema.required = ['context']
103
+ }
104
+
105
+ return modifiedTool
106
+ }
107
+
108
+ export function addContextParameterToTools<TTool extends ContextInjectableTool>(
109
+ tools: TTool[],
110
+ contextDescriptionOverride?: string
111
+ ): TTool[] {
112
+ return tools.map((tool) => {
113
+ // Skip get_more_tools - it has its own special context parameter
114
+ if (tool.name === 'get_more_tools') {
115
+ return tool
116
+ }
117
+ return addContextParameterToTool(tool, contextDescriptionOverride)
118
+ })
119
+ }
@@ -0,0 +1,177 @@
1
+ import { uuidv7 } from '@posthog/core'
2
+ import { DEFAULT_CONVERSATION_ID_DESCRIPTION } from './constants'
3
+ import { log } from './logger'
4
+ import { GET_MORE_TOOLS_NAME } from './tools'
5
+
6
+ export const CONVERSATION_ID_PARAM_NAME = 'conversation_id'
7
+
8
+ interface JsonSchema {
9
+ additionalProperties?: boolean
10
+ allOf?: unknown
11
+ anyOf?: unknown
12
+ oneOf?: unknown
13
+ properties?: Record<string, unknown>
14
+ required?: string[]
15
+ type?: string
16
+ }
17
+
18
+ export interface ConversationIdInjectableTool {
19
+ inputSchema?: JsonSchema
20
+ name?: string
21
+ [key: string]: unknown
22
+ }
23
+
24
+ export function addConversationIdToTool<TTool extends ConversationIdInjectableTool>(tool: TTool): TTool {
25
+ const modifiedTool = { ...tool }
26
+ const toolName = tool.name || 'unknown'
27
+ const schema = modifiedTool.inputSchema as JsonSchema | undefined
28
+
29
+ if (schema?.properties?.[CONVERSATION_ID_PARAM_NAME]) {
30
+ log(
31
+ `WARN: Tool "${toolName}" already has '${CONVERSATION_ID_PARAM_NAME}' parameter. Skipping conversation_id injection.`
32
+ )
33
+ return modifiedTool
34
+ }
35
+
36
+ if (schema?.oneOf || schema?.allOf || schema?.anyOf) {
37
+ log(`WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf). Skipping conversation_id injection.`)
38
+ return modifiedTool
39
+ }
40
+
41
+ if (!modifiedTool.inputSchema) {
42
+ modifiedTool.inputSchema = {
43
+ type: 'object',
44
+ properties: {},
45
+ required: [],
46
+ }
47
+ }
48
+
49
+ modifiedTool.inputSchema = JSON.parse(JSON.stringify(modifiedTool.inputSchema)) as JsonSchema
50
+
51
+ const inputSchema = modifiedTool.inputSchema as JsonSchema
52
+
53
+ if (!inputSchema.properties) {
54
+ inputSchema.properties = {}
55
+ }
56
+
57
+ if (inputSchema.additionalProperties === false) {
58
+ inputSchema.additionalProperties = undefined
59
+ }
60
+
61
+ inputSchema.properties[CONVERSATION_ID_PARAM_NAME] = {
62
+ type: 'string',
63
+ description: DEFAULT_CONVERSATION_ID_DESCRIPTION,
64
+ }
65
+
66
+ return modifiedTool
67
+ }
68
+
69
+ export function addConversationIdToTools<TTool extends ConversationIdInjectableTool>(tools: TTool[]): TTool[] {
70
+ return tools.map((tool) => {
71
+ if (tool.name === GET_MORE_TOOLS_NAME) {
72
+ return tool
73
+ }
74
+ return addConversationIdToTool(tool)
75
+ })
76
+ }
77
+
78
+ export type ConversationIdResolution =
79
+ | { minted: false; conversationId: string | undefined }
80
+ | { minted: true; conversationId: string }
81
+
82
+ /**
83
+ * Decides which conversation_id to use for a tool call:
84
+ * - disabled or get_more_tools → none
85
+ * - agent supplied a value → use it
86
+ * - agent omitted → mint a UUID
87
+ */
88
+ export function resolveConversationId(
89
+ enabled: boolean,
90
+ args: unknown,
91
+ toolName: string | undefined
92
+ ): ConversationIdResolution {
93
+ if (!enabled || toolName === GET_MORE_TOOLS_NAME) {
94
+ return { minted: false, conversationId: undefined }
95
+ }
96
+ const supplied = extractConversationId(args)
97
+ if (supplied) {
98
+ return { minted: false, conversationId: supplied }
99
+ }
100
+ return { minted: true, conversationId: uuidv7() }
101
+ }
102
+
103
+ /**
104
+ * Predicate matching the same eligibility checks injectConversationIdPromptBack uses,
105
+ * exposed so callers can pre-decide whether the prompt-back will actually land
106
+ * (and clear event.conversationId if not, to avoid orphan ids in analytics).
107
+ */
108
+ export function canInjectConversationIdPromptBack(result: unknown): boolean {
109
+ if (!(result && typeof result === 'object')) {
110
+ return false
111
+ }
112
+ const resultObj = result as { content?: unknown; isError?: unknown }
113
+ if (resultObj.isError === true) {
114
+ return false
115
+ }
116
+ return Array.isArray(resultObj.content)
117
+ }
118
+
119
+ export function extractConversationId(args: unknown): string | undefined {
120
+ if (!(args && typeof args === 'object')) {
121
+ return
122
+ }
123
+ const value = (args as Record<string, unknown>)[CONVERSATION_ID_PARAM_NAME]
124
+ if (typeof value !== 'string') {
125
+ return
126
+ }
127
+ const trimmed = value.trim()
128
+ return trimmed.length > 0 ? trimmed : undefined
129
+ }
130
+
131
+ export function cloneRequestWithoutConversationId<
132
+ TRequest extends { params?: { arguments?: unknown; [k: string]: unknown } },
133
+ >(request: TRequest): TRequest {
134
+ if (!request.params || typeof request.params !== 'object') {
135
+ return request
136
+ }
137
+ const args = request.params.arguments
138
+ if (!(args && typeof args === 'object')) {
139
+ return request
140
+ }
141
+ return {
142
+ ...request,
143
+ params: {
144
+ ...request.params,
145
+ arguments: stripConversationId(args) as typeof request.params.arguments,
146
+ },
147
+ }
148
+ }
149
+
150
+ export function stripConversationId(args: unknown): unknown {
151
+ if (!args || typeof args !== 'object' || !(CONVERSATION_ID_PARAM_NAME in (args as Record<string, unknown>))) {
152
+ return args
153
+ }
154
+ const { [CONVERSATION_ID_PARAM_NAME]: _omit, ...rest } = args as Record<string, unknown>
155
+ return rest
156
+ }
157
+
158
+ export function buildConversationIdPromptBack(conversationId: string): {
159
+ type: 'text'
160
+ text: string
161
+ } {
162
+ return {
163
+ type: 'text',
164
+ 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.`,
165
+ }
166
+ }
167
+
168
+ export function injectConversationIdPromptBack(result: unknown, conversationId: string): unknown {
169
+ if (!canInjectConversationIdPromptBack(result)) {
170
+ return result
171
+ }
172
+ const resultObj = result as { content: unknown[] }
173
+ return {
174
+ ...resultObj,
175
+ content: [...resultObj.content, buildConversationIdPromptBack(conversationId)],
176
+ }
177
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Internal SDK event vocabulary.
3
+ *
4
+ * These values are not imported from `@modelcontextprotocol/sdk`; they are the
5
+ * protocol-shaped event types this SDK observes before mapping them to PostHog
6
+ * event names.
7
+ */
8
+ export const MCPAnalyticsEventType = {
9
+ identify: 'posthog:identify',
10
+ custom: 'posthog:custom',
11
+ mcpMissingCapability: 'mcp:missing_capability',
12
+ mcpInitialize: 'mcp:initialize',
13
+ mcpPromptsGet: 'mcp:prompts/get',
14
+ mcpPromptsList: 'mcp:prompts/list',
15
+ mcpResourcesList: 'mcp:resources/list',
16
+ mcpResourcesRead: 'mcp:resources/read',
17
+ mcpToolsCall: 'mcp:tools/call',
18
+ mcpToolsList: 'mcp:tools/list',
19
+ } as const
20
+
21
+ export type MCPAnalyticsEventType = (typeof MCPAnalyticsEventType)[keyof typeof MCPAnalyticsEventType]
@@ -0,0 +1,83 @@
1
+ import { ErrorTracking as CoreErrorTracking } from '@posthog/core'
2
+
3
+ /**
4
+ * Builds PostHog error-tracking properties (`$exception_list`) from arbitrary
5
+ * thrown values, reusing `@posthog/core`'s shared coercers + stack parser
6
+ * instead of a bespoke V8 parser. Mirrors how `posthog-node` constructs its
7
+ * builder, minus the Node-only frame modifiers (source context, relative
8
+ * paths) which live in the `posthog-node` package and require async fs access.
9
+ */
10
+ const errorPropertiesBuilder = new CoreErrorTracking.ErrorPropertiesBuilder(
11
+ [
12
+ new CoreErrorTracking.EventCoercer(),
13
+ new CoreErrorTracking.ErrorCoercer(),
14
+ new CoreErrorTracking.ObjectCoercer(),
15
+ new CoreErrorTracking.StringCoercer(),
16
+ new CoreErrorTracking.PrimitiveCoercer(),
17
+ ],
18
+ CoreErrorTracking.createStackParser('node:javascript', CoreErrorTracking.nodeStackLineParser)
19
+ )
20
+
21
+ interface CallToolContentPart {
22
+ text?: unknown
23
+ type?: unknown
24
+ }
25
+
26
+ interface CallToolResult {
27
+ content: unknown[]
28
+ isError: unknown
29
+ }
30
+
31
+ /**
32
+ * Captures structured exception properties from any thrown value.
33
+ *
34
+ * Returns the `$exception_list` shape PostHog error tracking expects, so MCP
35
+ * tool failures group and (with uploaded source maps) symbolicate the same way
36
+ * as exceptions from any other PostHog SDK.
37
+ *
38
+ * @param error - The thrown value (Error, string, object, CallToolResult, or anything).
39
+ * @returns Core `ErrorProperties` ready to spread onto a `$exception` event.
40
+ */
41
+ export function captureException(error: unknown): CoreErrorTracking.ErrorProperties {
42
+ // MCP SDK 1.21.0+ converts tool errors to CallToolResult, which the core
43
+ // coercers don't recognize. Extract the human-readable message first so the
44
+ // exception still carries something useful.
45
+ const normalized = isCallToolResult(error) ? extractCallToolResultMessage(error) : error
46
+ return errorPropertiesBuilder.buildFromUnknown(normalized)
47
+ }
48
+
49
+ /**
50
+ * Detects a CallToolResult error object (SDK 1.21.0+ format):
51
+ * `{ content: [{ type: "text", text: "..." }], isError: true }`.
52
+ */
53
+ function isCallToolResult(value: unknown): value is CallToolResult {
54
+ return (
55
+ value !== null &&
56
+ typeof value === 'object' &&
57
+ 'isError' in value &&
58
+ 'content' in value &&
59
+ Array.isArray((value as { content?: unknown }).content)
60
+ )
61
+ }
62
+
63
+ function isTextContentPart(value: unknown): value is { text: string } {
64
+ if (value === null || typeof value !== 'object') {
65
+ return false
66
+ }
67
+ const contentPart = value as CallToolContentPart
68
+ return contentPart.type === 'text' && typeof contentPart.text === 'string'
69
+ }
70
+
71
+ /**
72
+ * Extracts the concatenated text from a CallToolResult's content array.
73
+ * The SDK strips the original Error, so only the message survives.
74
+ */
75
+ function extractCallToolResultMessage(result: CallToolResult): string {
76
+ return (
77
+ result.content
78
+ .filter(isTextContentPart)
79
+ .map((contentPart) => contentPart.text)
80
+ .join(' ')
81
+ .trim() || 'Unknown error'
82
+ )
83
+ }
@@ -0,0 +1,32 @@
1
+ import { uuidv7 } from '@posthog/core'
2
+
3
+ export type MCPAnalyticsIDPrefix = 'evt' | 'ses'
4
+
5
+ export function newPrefixedId(prefix: MCPAnalyticsIDPrefix): string {
6
+ return `${prefix}_${uuidv7()}`
7
+ }
8
+
9
+ /**
10
+ * Deterministic id derived from an arbitrary string. Used to map MCP protocol
11
+ * session ids to SDK session ids so the same MCP session reuses the same
12
+ * `$session_id` across server restarts.
13
+ *
14
+ * Uses the FNV-1a 64-bit hash (mixed twice to fill 32 hex chars), which works
15
+ * everywhere — no `node:crypto`, no Web Crypto async API. Not cryptographic; we
16
+ * only need stable, low-collision input → output mapping.
17
+ */
18
+ export function deterministicPrefixedId(prefix: MCPAnalyticsIDPrefix, input: string): string {
19
+ return `${prefix}_${fnv1aHex(input)}${fnv1aHex(`${input}::salt`)}`
20
+ }
21
+
22
+ function fnv1aHex(input: string): string {
23
+ // 64-bit FNV-1a implemented with two 32-bit halves to stay within safe-integer range.
24
+ let h1 = 0x84222325
25
+ let h2 = 0xcbf29ce4
26
+ for (let i = 0; i < input.length; i++) {
27
+ const c = input.charCodeAt(i)
28
+ h1 = Math.imul(h1 ^ c, 0x000001b3)
29
+ h2 = Math.imul(h2 ^ c, 0x00000193)
30
+ }
31
+ return (h1 >>> 0).toString(16).padStart(8, '0') + (h2 >>> 0).toString(16).padStart(8, '0')
32
+ }