@posthog/mcp 0.0.9 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +307 -3
- package/README.md +182 -90
- package/dist/extensions/capture.d.ts +13 -0
- package/dist/extensions/capture.d.ts.map +1 -0
- package/dist/extensions/capture.js +80 -0
- package/dist/extensions/capture.mjs +46 -0
- package/dist/extensions/compatibility.d.ts +23 -0
- package/dist/extensions/compatibility.d.ts.map +1 -0
- package/dist/extensions/compatibility.js +113 -0
- package/dist/extensions/compatibility.mjs +67 -0
- package/dist/extensions/constants.d.ts +39 -0
- package/dist/extensions/constants.d.ts.map +1 -0
- package/dist/extensions/constants.js +86 -0
- package/dist/extensions/constants.mjs +37 -0
- package/dist/extensions/context-parameters.d.ts +31 -0
- package/dist/extensions/context-parameters.d.ts.map +1 -0
- package/dist/extensions/context-parameters.js +93 -0
- package/dist/extensions/context-parameters.mjs +50 -0
- package/dist/extensions/conversation-id.d.ts +52 -0
- package/dist/extensions/conversation-id.d.ts.map +1 -0
- package/dist/extensions/conversation-id.js +164 -0
- package/dist/extensions/conversation-id.mjs +103 -0
- package/dist/extensions/event-types.d.ts +21 -0
- package/dist/extensions/event-types.d.ts.map +1 -0
- package/dist/extensions/event-types.js +47 -0
- package/dist/extensions/event-types.mjs +13 -0
- package/dist/extensions/exceptions.d.ts +13 -0
- package/dist/extensions/exceptions.d.ts.map +1 -0
- package/dist/extensions/exceptions.js +58 -0
- package/dist/extensions/exceptions.mjs +24 -0
- package/dist/extensions/ids.d.ts +13 -0
- package/dist/extensions/ids.d.ts.map +1 -0
- package/dist/extensions/ids.js +55 -0
- package/dist/extensions/ids.mjs +18 -0
- package/dist/extensions/instrument-highlevel.d.ts +3 -0
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -0
- package/dist/extensions/instrument-highlevel.js +229 -0
- package/dist/extensions/instrument-highlevel.mjs +195 -0
- package/dist/extensions/instrument-lowlevel.d.ts +8 -0
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -0
- package/dist/extensions/instrument-lowlevel.js +85 -0
- package/dist/extensions/instrument-lowlevel.mjs +51 -0
- package/dist/extensions/instrumentation.d.ts +59 -0
- package/dist/extensions/instrumentation.d.ts.map +1 -0
- package/dist/extensions/instrumentation.js +237 -0
- package/dist/extensions/instrumentation.mjs +194 -0
- package/dist/extensions/intent.d.ts +10 -0
- package/dist/extensions/intent.d.ts.map +1 -0
- package/dist/extensions/intent.js +84 -0
- package/dist/extensions/intent.mjs +44 -0
- package/dist/extensions/internal.d.ts +32 -0
- package/dist/extensions/internal.d.ts.map +1 -0
- package/dist/extensions/internal.js +158 -0
- package/dist/extensions/internal.mjs +106 -0
- package/dist/extensions/logger.d.ts +14 -0
- package/dist/extensions/logger.d.ts.map +1 -0
- package/dist/extensions/logger.js +47 -0
- package/dist/extensions/logger.mjs +10 -0
- package/dist/extensions/mcp-payloads.d.ts +5 -0
- package/dist/extensions/mcp-payloads.d.ts.map +1 -0
- package/dist/extensions/mcp-payloads.js +91 -0
- package/dist/extensions/mcp-payloads.mjs +54 -0
- package/dist/extensions/mcp-sdk-compat.d.ts +33 -0
- package/dist/extensions/mcp-sdk-compat.d.ts.map +1 -0
- package/dist/extensions/mcp-sdk-compat.js +112 -0
- package/dist/extensions/mcp-sdk-compat.mjs +60 -0
- package/dist/extensions/posthog-events.d.ts +14 -0
- package/dist/extensions/posthog-events.d.ts.map +1 -0
- package/dist/extensions/posthog-events.js +143 -0
- package/dist/extensions/posthog-events.mjs +109 -0
- package/dist/extensions/sanitization.d.ts +10 -0
- package/dist/extensions/sanitization.d.ts.map +1 -0
- package/dist/extensions/sanitization.js +96 -0
- package/dist/extensions/sanitization.mjs +62 -0
- package/dist/extensions/session.d.ts +20 -0
- package/dist/extensions/session.d.ts.map +1 -0
- package/dist/extensions/session.js +113 -0
- package/dist/extensions/session.mjs +67 -0
- package/dist/extensions/sink.d.ts +44 -0
- package/dist/extensions/sink.d.ts.map +1 -0
- package/dist/extensions/sink.js +102 -0
- package/dist/extensions/sink.mjs +65 -0
- package/dist/extensions/tools.d.ts +9 -0
- package/dist/extensions/tools.d.ts.map +1 -0
- package/dist/extensions/tools.js +79 -0
- package/dist/extensions/tools.mjs +39 -0
- package/dist/extensions/tracing-helpers.d.ts +29 -0
- package/dist/extensions/tracing-helpers.d.ts.map +1 -0
- package/dist/extensions/tracing-helpers.js +59 -0
- package/dist/extensions/tracing-helpers.mjs +16 -0
- package/dist/extensions/truncation.d.ts +25 -0
- package/dist/extensions/truncation.d.ts.map +1 -0
- package/dist/extensions/truncation.js +277 -0
- package/dist/extensions/truncation.mjs +228 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/dist/index.mjs +83 -2929
- package/dist/types.d.ts +220 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.mjs +0 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +36 -0
- package/dist/version.mjs +2 -0
- package/package.json +55 -87
- package/src/extensions/capture.ts +68 -0
- package/src/extensions/compatibility.ts +138 -0
- package/src/extensions/constants.ts +48 -0
- package/src/extensions/context-parameters.ts +119 -0
- package/src/extensions/conversation-id.ts +177 -0
- package/src/extensions/event-types.ts +21 -0
- package/src/extensions/exceptions.ts +83 -0
- package/src/extensions/ids.ts +32 -0
- package/src/extensions/instrument-highlevel.ts +297 -0
- package/src/extensions/instrument-lowlevel.ts +83 -0
- package/src/extensions/instrumentation.ts +415 -0
- package/src/extensions/intent.ts +78 -0
- package/src/extensions/internal.ts +193 -0
- package/src/extensions/logger.ts +27 -0
- package/src/extensions/mcp-payloads.ts +98 -0
- package/src/extensions/mcp-sdk-compat.ts +160 -0
- package/src/extensions/posthog-events.ts +212 -0
- package/src/extensions/sanitization.ts +121 -0
- package/src/extensions/session.ts +117 -0
- package/src/extensions/sink.ts +130 -0
- package/src/extensions/tools.ts +50 -0
- package/src/extensions/tracing-helpers.ts +51 -0
- package/src/extensions/truncation.ts +453 -0
- package/src/index.ts +174 -0
- package/src/types.ts +244 -0
- package/src/version.ts +1 -0
- package/dist/index.cjs +0 -2937
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -264
- package/dist/index.d.mts +0 -263
- 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
|
+
}
|