@posthog/mcp 0.0.9 → 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 -3
  2. package/README.md +182 -90
  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 -2929
  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 -2937
  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,98 @@
1
+ const CONTEXT_ARGUMENT_NAME = 'context'
2
+ const REDACTED_VALUE = '[redacted]'
3
+ const BASE64_PATTERN = /^[A-Za-z0-9+/\n\r]+=*$/
4
+ const SIZE_GATE = 10_240
5
+ const POSTHOG_TOKEN_PATTERN = /\bph[a-z]_[A-Za-z0-9_-]{20,}\b/g
6
+ const SENSITIVE_KEY_PATTERN =
7
+ /^(authorization|cookie|set-cookie|x-api-key|api[-_]?key|api[-_]?token|access[-_]?token|refresh[-_]?token|token|password|secret|client[-_]?secret|private[-_]?key)$/i
8
+
9
+ type JsonRecord = Record<string, unknown>
10
+
11
+ function isRecord(value: unknown): value is JsonRecord {
12
+ return !!value && typeof value === 'object' && !Array.isArray(value)
13
+ }
14
+
15
+ function shouldRedactKey(key: string): boolean {
16
+ return SENSITIVE_KEY_PATTERN.test(key)
17
+ }
18
+
19
+ function sanitizeString(value: string): string {
20
+ if (value.length >= SIZE_GATE && BASE64_PATTERN.test(value)) {
21
+ return '[binary data redacted - not supported by PostHog MCP analytics]'
22
+ }
23
+ return value.replace(POSTHOG_TOKEN_PATTERN, REDACTED_VALUE)
24
+ }
25
+
26
+ export function sanitizeCapturedValue(value: unknown): unknown {
27
+ if (value == null) {
28
+ return value
29
+ }
30
+
31
+ if (typeof value === 'string') {
32
+ return sanitizeString(value)
33
+ }
34
+
35
+ if (Array.isArray(value)) {
36
+ return value.map(sanitizeCapturedValue)
37
+ }
38
+
39
+ if (value instanceof Date) {
40
+ return value
41
+ }
42
+
43
+ if (typeof value !== 'object') {
44
+ return value
45
+ }
46
+
47
+ const result: JsonRecord = {}
48
+ for (const [key, nestedValue] of Object.entries(value)) {
49
+ result[key] = shouldRedactKey(key) ? REDACTED_VALUE : sanitizeCapturedValue(nestedValue)
50
+ }
51
+ return result
52
+ }
53
+
54
+ function buildCapturedMcpArguments(argumentsValue: unknown): unknown {
55
+ if (!isRecord(argumentsValue)) {
56
+ return sanitizeCapturedValue(argumentsValue)
57
+ }
58
+
59
+ const capturedArguments: JsonRecord = {}
60
+ for (const [key, value] of Object.entries(argumentsValue)) {
61
+ if (key === CONTEXT_ARGUMENT_NAME) {
62
+ continue
63
+ }
64
+ capturedArguments[key] = sanitizeCapturedValue(value)
65
+ }
66
+ return capturedArguments
67
+ }
68
+
69
+ function buildCapturedMcpParams(params: unknown): unknown {
70
+ if (!isRecord(params)) {
71
+ return sanitizeCapturedValue(params)
72
+ }
73
+
74
+ const capturedParams: JsonRecord = {}
75
+ for (const [key, value] of Object.entries(params)) {
76
+ capturedParams[key] = key === 'arguments' ? buildCapturedMcpArguments(value) : sanitizeCapturedValue(value)
77
+ }
78
+ return capturedParams
79
+ }
80
+
81
+ export function buildCapturedMcpParameters(request: unknown): JsonRecord {
82
+ if (!isRecord(request)) {
83
+ return { request: sanitizeCapturedValue(request) }
84
+ }
85
+
86
+ const capturedRequest: JsonRecord = {}
87
+ for (const key of ['id', 'jsonrpc', 'method'] as const) {
88
+ if (key in request) {
89
+ capturedRequest[key] = sanitizeCapturedValue(request[key])
90
+ }
91
+ }
92
+
93
+ if ('params' in request) {
94
+ capturedRequest.params = buildCapturedMcpParams(request.params)
95
+ }
96
+
97
+ return { request: capturedRequest }
98
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * MCP SDK Compatibility Helpers
3
+ *
4
+ * Internal utilities for handling differences between MCP SDK versions.
5
+ * These helpers abstract away SDK-internal details like:
6
+ * - Tool callback/handler property names (changed in SDK 1.24)
7
+ * - Zod schema internal structures (v3 vs v4)
8
+ */
9
+
10
+ import type { RegisteredTool, ToolCallback } from '../types'
11
+
12
+ // --- Tool function property utilities for MCP SDK version compatibility ---
13
+ // MCP SDK 1.23 and earlier use "callback", 1.24+ uses "handler"
14
+
15
+ export type ToolFunctionKey = 'callback' | 'handler'
16
+
17
+ /**
18
+ * Returns the tool function (callback/handler) from a RegisteredTool.
19
+ * Supports both MCP SDK 1.23- (callback) and 1.24+ (handler).
20
+ */
21
+ export function getToolFunction(tool: RegisteredTool): ToolCallback {
22
+ if ('handler' in tool && typeof tool.handler === 'function') {
23
+ return tool.handler
24
+ }
25
+ if ('callback' in tool && typeof tool.callback === 'function') {
26
+ return tool.callback
27
+ }
28
+ throw new Error('Tool has neither callback nor handler property')
29
+ }
30
+
31
+ /**
32
+ * Returns the property key name used for the tool function ("callback" or "handler").
33
+ * This preserves the original property name when wrapping tools.
34
+ */
35
+ export function getToolFunctionKey(tool: RegisteredTool): ToolFunctionKey {
36
+ if ('handler' in tool && typeof tool.handler === 'function') {
37
+ return 'handler'
38
+ }
39
+ return 'callback'
40
+ }
41
+
42
+ /**
43
+ * Returns true if the tool has a callback or handler property.
44
+ */
45
+ export function hasToolFunction(tool: unknown): tool is RegisteredTool {
46
+ if (!tool || typeof tool !== 'object') {
47
+ return false
48
+ }
49
+ const t = tool as Record<string, unknown>
50
+ return ('handler' in t && typeof t.handler === 'function') || ('callback' in t && typeof t.callback === 'function')
51
+ }
52
+
53
+ /**
54
+ * Creates a new tool object with the wrapped function, preserving the original property name.
55
+ * This ensures MCP SDK 1.24+ gets back a tool with "handler" and 1.23- gets "callback".
56
+ */
57
+ export function createWrappedTool(originalTool: RegisteredTool, wrappedFunction: ToolCallback): RegisteredTool {
58
+ const key = getToolFunctionKey(originalTool)
59
+ return {
60
+ ...originalTool,
61
+ [key]: wrappedFunction,
62
+ } as RegisteredTool
63
+ }
64
+
65
+ // --- Zod schema internal property helpers ---
66
+ // These access internal properties to extract method names from MCP SDK schemas
67
+ // No Zod import needed - we introspect the internal structure directly
68
+
69
+ interface ZodV3Internal {
70
+ _def?: {
71
+ value?: unknown
72
+ values?: unknown[] // For enums - some Zod versions store literal values here
73
+ shape?: Record<string, unknown> | (() => Record<string, unknown>)
74
+ }
75
+ shape?: Record<string, unknown> | (() => Record<string, unknown>)
76
+ }
77
+
78
+ interface ZodV4Internal {
79
+ _zod?: {
80
+ def?: {
81
+ value?: unknown
82
+ values?: unknown[] // For enums - some Zod versions store literal values here
83
+ shape?: Record<string, unknown> | (() => Record<string, unknown>)
84
+ }
85
+ }
86
+ }
87
+
88
+ export function isZ4Schema(schema: unknown): boolean {
89
+ if (!schema || typeof schema !== 'object') {
90
+ return false
91
+ }
92
+ return !!(schema as ZodV4Internal)._zod
93
+ }
94
+
95
+ export function getObjectShape(schema: unknown): Record<string, unknown> | undefined {
96
+ if (!schema || typeof schema !== 'object') {
97
+ return
98
+ }
99
+
100
+ let rawShape: Record<string, unknown> | (() => Record<string, unknown>) | undefined
101
+
102
+ if (isZ4Schema(schema)) {
103
+ const v4Schema = schema as ZodV4Internal
104
+ rawShape = v4Schema._zod?.def?.shape
105
+ } else {
106
+ const v3Schema = schema as ZodV3Internal
107
+ // Try .shape first, then fall back to _def.shape (some v3 schema types store it there)
108
+ rawShape = v3Schema.shape ?? v3Schema._def?.shape
109
+ }
110
+
111
+ if (!rawShape) {
112
+ return
113
+ }
114
+
115
+ if (typeof rawShape === 'function') {
116
+ try {
117
+ return rawShape()
118
+ } catch {
119
+ return
120
+ }
121
+ }
122
+
123
+ return rawShape
124
+ }
125
+
126
+ export function getLiteralValue(schema: unknown): unknown {
127
+ if (!schema || typeof schema !== 'object') {
128
+ return
129
+ }
130
+
131
+ if (isZ4Schema(schema)) {
132
+ const v4Schema = schema as ZodV4Internal
133
+ const def = v4Schema._zod?.def
134
+ if (def?.value !== undefined) {
135
+ return def.value
136
+ }
137
+ // Fallback: values array (for enums)
138
+ if (Array.isArray(def?.values) && def.values.length > 0) {
139
+ return def.values[0]
140
+ }
141
+ } else {
142
+ const v3Schema = schema as ZodV3Internal
143
+ const def = v3Schema._def
144
+ if (def?.value !== undefined) {
145
+ return def.value
146
+ }
147
+ // Fallback: values array (for enums)
148
+ if (Array.isArray(def?.values) && def.values.length > 0) {
149
+ return def.values[0]
150
+ }
151
+ }
152
+
153
+ // Final fallback: direct .value property (some Zod versions)
154
+ const directValue = (schema as { value?: unknown }).value
155
+ if (directValue !== undefined) {
156
+ return directValue
157
+ }
158
+
159
+ return
160
+ }
@@ -0,0 +1,212 @@
1
+ import type { Event } from '../types'
2
+ import { POSTHOG_MCP_ANALYTICS_SOURCE, PostHogMCPAnalyticsEvent, PostHogMCPAnalyticsProperty } from './constants'
3
+ import { MCPAnalyticsEventType } from './event-types'
4
+
5
+ const BUILT_IN_EVENT_NAME_BY_TYPE = {
6
+ [MCPAnalyticsEventType.custom]: PostHogMCPAnalyticsEvent.Custom,
7
+ [MCPAnalyticsEventType.identify]: PostHogMCPAnalyticsEvent.Identify,
8
+ [MCPAnalyticsEventType.mcpMissingCapability]: PostHogMCPAnalyticsEvent.MissingCapability,
9
+ [MCPAnalyticsEventType.mcpInitialize]: PostHogMCPAnalyticsEvent.Initialize,
10
+ [MCPAnalyticsEventType.mcpPromptsGet]: PostHogMCPAnalyticsEvent.PromptGet,
11
+ [MCPAnalyticsEventType.mcpPromptsList]: PostHogMCPAnalyticsEvent.PromptsList,
12
+ [MCPAnalyticsEventType.mcpResourcesList]: PostHogMCPAnalyticsEvent.ResourcesList,
13
+ [MCPAnalyticsEventType.mcpResourcesRead]: PostHogMCPAnalyticsEvent.ResourceRead,
14
+ [MCPAnalyticsEventType.mcpToolsCall]: PostHogMCPAnalyticsEvent.ToolCall,
15
+ [MCPAnalyticsEventType.mcpToolsList]: PostHogMCPAnalyticsEvent.ToolsList,
16
+ } satisfies Record<MCPAnalyticsEventType, PostHogMCPAnalyticsEvent>
17
+
18
+ function getDistinctId(event: Event): string {
19
+ return event.identifyActorGivenId || event.sessionId || 'anonymous'
20
+ }
21
+
22
+ function getTimestamp(event: Event): string {
23
+ return event.timestamp ? event.timestamp.toISOString() : new Date().toISOString()
24
+ }
25
+
26
+ export interface PostHogCaptureEvent {
27
+ distinct_id: string
28
+ event: string
29
+ properties: Record<string, unknown>
30
+ timestamp: string
31
+ type: 'capture'
32
+ }
33
+
34
+ export interface BuildPostHogCaptureEventsOptions {
35
+ /** Whether to emit a `$exception` sibling alongside errored events. Defaults to `true`. */
36
+ enableExceptionAutocapture?: boolean
37
+ }
38
+
39
+ export function buildPostHogCaptureEvents(
40
+ event: Event,
41
+ options: BuildPostHogCaptureEventsOptions = {}
42
+ ): PostHogCaptureEvent[] {
43
+ const batch = [buildCaptureEvent(event)]
44
+
45
+ if (event.isError && event.error && options.enableExceptionAutocapture !== false) {
46
+ batch.push(buildExceptionEvent(event))
47
+ }
48
+
49
+ return batch
50
+ }
51
+
52
+ function buildCaptureEvent(event: Event): PostHogCaptureEvent {
53
+ const distinctId = getDistinctId(event)
54
+ const timestamp = getTimestamp(event)
55
+
56
+ const properties: Record<string, unknown> = {
57
+ [PostHogMCPAnalyticsProperty.SessionId]: event.sessionId,
58
+ [PostHogMCPAnalyticsProperty.Source]: POSTHOG_MCP_ANALYTICS_SOURCE,
59
+ }
60
+ addConversationIdProperty(event, properties)
61
+ addPersonProcessingProperty(event, properties)
62
+ addGroupsProperty(event, properties)
63
+
64
+ addCommonEventProperties(event, properties)
65
+ addCustomEventProperties(event, properties)
66
+
67
+ return {
68
+ event: event.eventName ?? BUILT_IN_EVENT_NAME_BY_TYPE[event.eventType],
69
+ distinct_id: distinctId,
70
+ properties,
71
+ timestamp,
72
+ type: 'capture',
73
+ }
74
+ }
75
+
76
+ function addConversationIdProperty(event: Event, properties: Record<string, unknown>): void {
77
+ if (event.conversationId !== undefined && event.conversationId !== '') {
78
+ properties[PostHogMCPAnalyticsProperty.ConversationId] = event.conversationId
79
+ }
80
+ }
81
+
82
+ function addGroupsProperty(event: Event, properties: Record<string, unknown>): void {
83
+ if (event.groups && Object.keys(event.groups).length > 0) {
84
+ properties.$groups = event.groups
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Without a resolved identity the distinct id is just the session id, so
90
+ * processing a person profile would mint one anonymous person per session and
91
+ * inflate person counts. Opt out of person processing in that case (matching
92
+ * `@posthog/ai` / posthog-node). When an identity is present we keep person
93
+ * processing so `$set` lands on a real person.
94
+ */
95
+ function addPersonProcessingProperty(event: Event, properties: Record<string, unknown>): void {
96
+ if (!event.identifyActorGivenId) {
97
+ properties.$process_person_profile = false
98
+ }
99
+ }
100
+
101
+ function addCommonEventProperties(event: Event, properties: Record<string, unknown>): void {
102
+ if (event.resourceName) {
103
+ properties[PostHogMCPAnalyticsProperty.ResourceName] = event.resourceName
104
+ if (event.eventType === MCPAnalyticsEventType.mcpToolsCall) {
105
+ properties[PostHogMCPAnalyticsProperty.ToolName] = event.resourceName
106
+ }
107
+ }
108
+ if (event.toolDescription && event.eventType === MCPAnalyticsEventType.mcpToolsCall) {
109
+ properties[PostHogMCPAnalyticsProperty.ToolDescription] = event.toolDescription
110
+ }
111
+ if (
112
+ event.listedToolNames &&
113
+ event.listedToolNames.length > 0 &&
114
+ event.eventType === MCPAnalyticsEventType.mcpToolsList
115
+ ) {
116
+ properties[PostHogMCPAnalyticsProperty.ListedToolNames] = event.listedToolNames
117
+ }
118
+ if (event.duration !== undefined) {
119
+ properties[PostHogMCPAnalyticsProperty.DurationMs] = event.duration
120
+ }
121
+ if (event.serverName) {
122
+ properties[PostHogMCPAnalyticsProperty.ServerName] = event.serverName
123
+ }
124
+ if (event.serverVersion) {
125
+ properties[PostHogMCPAnalyticsProperty.ServerVersion] = event.serverVersion
126
+ }
127
+ if (event.clientName) {
128
+ properties[PostHogMCPAnalyticsProperty.ClientName] = event.clientName
129
+ }
130
+ if (event.clientVersion) {
131
+ properties[PostHogMCPAnalyticsProperty.ClientVersion] = event.clientVersion
132
+ }
133
+ if (event.userIntent) {
134
+ properties[PostHogMCPAnalyticsProperty.Intent] = event.userIntent
135
+ }
136
+ if (event.userIntentSource) {
137
+ properties[PostHogMCPAnalyticsProperty.IntentSource] = event.userIntentSource
138
+ }
139
+ if (event.isError !== undefined) {
140
+ properties[PostHogMCPAnalyticsProperty.IsError] = event.isError
141
+ }
142
+
143
+ if (event.parameters !== undefined) {
144
+ properties[PostHogMCPAnalyticsProperty.Parameters] = event.parameters
145
+ }
146
+ if (event.response !== undefined) {
147
+ properties[PostHogMCPAnalyticsProperty.Response] = event.response
148
+ }
149
+
150
+ if (event.identifyActorData && Object.keys(event.identifyActorData).length > 0) {
151
+ // Person properties from `identify().properties` go straight to `$set`.
152
+ properties.$set = { ...event.identifyActorData }
153
+ }
154
+ }
155
+
156
+ function addCustomEventProperties(event: Event, properties: Record<string, unknown>): void {
157
+ if (event.properties) {
158
+ for (const [key, value] of Object.entries(event.properties)) {
159
+ properties[key] = value
160
+ }
161
+ }
162
+ }
163
+
164
+ function buildExceptionEvent(event: Event): PostHogCaptureEvent {
165
+ const distinctId = getDistinctId(event)
166
+ const timestamp = getTimestamp(event)
167
+
168
+ const properties: Record<string, unknown> = {
169
+ [PostHogMCPAnalyticsProperty.SessionId]: event.sessionId,
170
+ }
171
+ addConversationIdProperty(event, properties)
172
+ addPersonProcessingProperty(event, properties)
173
+ addGroupsProperty(event, properties)
174
+
175
+ if (event.error) {
176
+ // Spread the core `$exception_list` / `$exception_level` properties so MCP
177
+ // tool failures use the same error-tracking contract as every other SDK.
178
+ Object.assign(properties, event.error)
179
+ }
180
+
181
+ if (event.resourceName) {
182
+ properties[PostHogMCPAnalyticsProperty.ResourceName] = event.resourceName
183
+ if (event.eventType === MCPAnalyticsEventType.mcpToolsCall) {
184
+ properties[PostHogMCPAnalyticsProperty.ToolName] = event.resourceName
185
+ }
186
+ }
187
+ if (event.toolDescription && event.eventType === MCPAnalyticsEventType.mcpToolsCall) {
188
+ properties[PostHogMCPAnalyticsProperty.ToolDescription] = event.toolDescription
189
+ }
190
+ if (event.serverName) {
191
+ properties[PostHogMCPAnalyticsProperty.ServerName] = event.serverName
192
+ }
193
+ if (event.serverVersion) {
194
+ properties[PostHogMCPAnalyticsProperty.ServerVersion] = event.serverVersion
195
+ }
196
+ if (event.clientName) {
197
+ properties[PostHogMCPAnalyticsProperty.ClientName] = event.clientName
198
+ }
199
+ if (event.clientVersion) {
200
+ properties[PostHogMCPAnalyticsProperty.ClientVersion] = event.clientVersion
201
+ }
202
+
203
+ addCustomEventProperties(event, properties)
204
+
205
+ return {
206
+ event: PostHogMCPAnalyticsEvent.Exception,
207
+ distinct_id: distinctId,
208
+ properties,
209
+ timestamp,
210
+ type: 'capture',
211
+ }
212
+ }
@@ -0,0 +1,121 @@
1
+ import type { Event, McpEvent } from '../types'
2
+ import { sanitizeCapturedValue } from './mcp-payloads'
3
+
4
+ type SanitizedRecord = Record<string, unknown>
5
+
6
+ function isRecord(value: unknown): value is SanitizedRecord {
7
+ return !!value && typeof value === 'object' && !Array.isArray(value)
8
+ }
9
+
10
+ /**
11
+ * Sanitizes an event by redacting non-text content blocks from responses
12
+ * and large base64-encoded strings from parameters.
13
+ *
14
+ * This is a synchronous operation that returns a new object without mutating the original.
15
+ * It should run after customer redaction in the event pipeline.
16
+ */
17
+ export function sanitizeEvent<T extends Event | McpEvent>(event: T): T {
18
+ const result = { ...event }
19
+
20
+ if (result.response != null) {
21
+ result.response = sanitizeResponse(result.response)
22
+ }
23
+
24
+ if (result.parameters != null) {
25
+ result.parameters = sanitizeParameters(result.parameters)
26
+ }
27
+
28
+ return result
29
+ }
30
+
31
+ /**
32
+ * Sanitizes response content blocks by replacing non-text content types
33
+ * with informative redaction messages.
34
+ */
35
+ function sanitizeResponse(response: unknown): unknown {
36
+ if (response == null || typeof response !== 'object') {
37
+ return sanitizeCapturedValue(response)
38
+ }
39
+
40
+ const sanitized = sanitizeCapturedValue(response)
41
+ if (!isRecord(sanitized)) {
42
+ return sanitized
43
+ }
44
+
45
+ const result: SanitizedRecord = { ...sanitized }
46
+ const content = result.content
47
+ if (Array.isArray(content)) {
48
+ result.content = content.map(sanitizeContentBlock)
49
+ }
50
+
51
+ if (result.structuredContent != null && typeof result.structuredContent === 'object') {
52
+ result.structuredContent = sanitizeCapturedValue(result.structuredContent)
53
+ }
54
+
55
+ return result
56
+ }
57
+
58
+ /**
59
+ * Sanitizes a single content block based on its type discriminator.
60
+ */
61
+ function sanitizeContentBlock(block: unknown): unknown {
62
+ if (block == null || typeof block !== 'object') {
63
+ return block
64
+ }
65
+
66
+ if (!isRecord(block)) {
67
+ return block
68
+ }
69
+
70
+ switch (block.type) {
71
+ case 'text':
72
+ return sanitizeCapturedValue(block)
73
+
74
+ case 'image':
75
+ return {
76
+ type: 'text',
77
+ text: '[image content redacted - not supported by PostHog MCP analytics]',
78
+ }
79
+
80
+ case 'audio':
81
+ return {
82
+ type: 'text',
83
+ text: '[audio content redacted - not supported by PostHog MCP analytics]',
84
+ }
85
+
86
+ case 'resource':
87
+ return sanitizeResourceBlock(block)
88
+
89
+ case 'resource_link':
90
+ return sanitizeCapturedValue(block)
91
+
92
+ default:
93
+ return {
94
+ type: 'text',
95
+ text: `[unsupported content type "${block.type}" redacted - not supported by PostHog MCP analytics]`,
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Sanitizes an embedded resource content block.
102
+ * BlobResourceContents (has `blob` field) are redacted.
103
+ * TextResourceContents (has `text` field) pass through.
104
+ */
105
+ function sanitizeResourceBlock(block: SanitizedRecord): unknown {
106
+ if (isRecord(block.resource) && 'blob' in block.resource) {
107
+ return {
108
+ type: 'text',
109
+ text: '[binary resource content redacted - not supported by PostHog MCP analytics]',
110
+ }
111
+ }
112
+ return sanitizeCapturedValue(block)
113
+ }
114
+
115
+ /**
116
+ * Recursively scans parameters for large base64-encoded strings and replaces them.
117
+ * Uses a size gate (10KB) to avoid regex testing on small strings.
118
+ */
119
+ function sanitizeParameters(obj: unknown): unknown {
120
+ return sanitizeCapturedValue(obj)
121
+ }
@@ -0,0 +1,117 @@
1
+ import { version } from '../version'
2
+ import type {
3
+ CompatibleRequestHandlerExtra,
4
+ MCPAnalyticsData,
5
+ MCPServerLike,
6
+ ServerClientInfoLike,
7
+ SessionInfo,
8
+ } from '../types'
9
+ import { INACTIVITY_TIMEOUT_IN_MINUTES } from './constants'
10
+ import { deterministicPrefixedId, newPrefixedId } from './ids'
11
+ import { getServerTrackingData, setServerTrackingData } from './internal'
12
+
13
+ export function newSessionId(): string {
14
+ return newPrefixedId('ses')
15
+ }
16
+
17
+ /**
18
+ * Derives the SDK session id deterministically from the MCP sessionId, so the
19
+ * same MCP session correlates to one SDK session across server restarts.
20
+ */
21
+ export function deriveSessionIdFromMCPSession(mcpSessionId: string): string {
22
+ return deterministicPrefixedId('ses', mcpSessionId)
23
+ }
24
+
25
+ /**
26
+ * Resolves the session id for a request, preferring the MCP protocol sessionId
27
+ * over an SDK-generated one so a transport-supplied session wins.
28
+ */
29
+ export function getServerSessionId(server: MCPServerLike, extra?: CompatibleRequestHandlerExtra): string {
30
+ const data = getServerTrackingData(server)
31
+
32
+ if (!data) {
33
+ throw new Error('Server tracking data not found')
34
+ }
35
+
36
+ const mcpSessionId = extra?.sessionId
37
+
38
+ if (mcpSessionId) {
39
+ data.sessionId = deriveSessionIdFromMCPSession(mcpSessionId)
40
+ data.lastMcpSessionId = mcpSessionId
41
+ data.sessionSource = 'mcp'
42
+ setServerTrackingData(server, data)
43
+ setLastActivity(server)
44
+ return data.sessionId
45
+ }
46
+
47
+ // Once a session has been MCP-derived, keep that id even if a later request
48
+ // arrives without the MCP sessionId, so the session doesn't fragment.
49
+ if (data.sessionSource === 'mcp' && data.lastMcpSessionId) {
50
+ setLastActivity(server)
51
+ return data.sessionId
52
+ }
53
+
54
+ // SDK-generated sessions roll over after an inactivity timeout.
55
+ const now = Date.now()
56
+ const timeoutMs = INACTIVITY_TIMEOUT_IN_MINUTES * 60 * 1000
57
+ if (now - data.lastActivity.getTime() > timeoutMs) {
58
+ data.sessionId = newSessionId()
59
+ data.sessionSource = 'generated'
60
+ setServerTrackingData(server, data)
61
+ }
62
+ setLastActivity(server)
63
+
64
+ return data.sessionId
65
+ }
66
+
67
+ export function setLastActivity(server: MCPServerLike): void {
68
+ const data = getServerTrackingData(server)
69
+
70
+ if (!data) {
71
+ throw new Error('Server tracking data not found')
72
+ }
73
+
74
+ data.lastActivity = new Date()
75
+ setServerTrackingData(server, data)
76
+ }
77
+
78
+ /**
79
+ * Builds the session metadata stamped onto an event. The caller passes the
80
+ * session id resolved for *this* request so identity attribution can't be
81
+ * clobbered by a concurrent request mutating shared `data.sessionId`.
82
+ */
83
+ export function getSessionInfo(
84
+ server: MCPServerLike,
85
+ data: MCPAnalyticsData | undefined,
86
+ sessionId?: string
87
+ ): SessionInfo {
88
+ let clientInfo: ServerClientInfoLike | undefined = {
89
+ name: undefined,
90
+ version: undefined,
91
+ }
92
+ if (!data?.sessionInfo.clientName) {
93
+ clientInfo = server.getClientVersion()
94
+ }
95
+ const actorInfo = data?.identifiedSessions.get(sessionId ?? data.sessionId)
96
+
97
+ const sessionInfo: SessionInfo = {
98
+ ipAddress: undefined, // grab from django
99
+ sdkLanguage: 'TypeScript', // hardcoded for now
100
+ sdkVersion: version,
101
+ serverName: server._serverInfo?.name,
102
+ serverVersion: server._serverInfo?.version,
103
+ clientName: clientInfo?.name,
104
+ clientVersion: clientInfo?.version,
105
+ identifyActorGivenId: actorInfo?.distinctId,
106
+ identifyActorData: actorInfo?.properties || {},
107
+ identifyActorGroups: actorInfo?.groups,
108
+ }
109
+
110
+ if (!data) {
111
+ return sessionInfo
112
+ }
113
+
114
+ data.sessionInfo = sessionInfo
115
+ setServerTrackingData(server, data)
116
+ return data.sessionInfo
117
+ }