@posthog/mcp 0.10.3 → 0.10.5

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 (76) hide show
  1. package/dist/extensions/analytics-parameters.d.ts +5 -0
  2. package/dist/extensions/analytics-parameters.d.ts.map +1 -1
  3. package/dist/extensions/analytics-parameters.js +57 -3
  4. package/dist/extensions/analytics-parameters.mjs +47 -2
  5. package/dist/extensions/capture.d.ts +2 -1
  6. package/dist/extensions/capture.d.ts.map +1 -1
  7. package/dist/extensions/capture.js +2 -3
  8. package/dist/extensions/capture.mjs +2 -3
  9. package/dist/extensions/compatibility.d.ts +3 -2
  10. package/dist/extensions/compatibility.d.ts.map +1 -1
  11. package/dist/extensions/compatibility.js +19 -20
  12. package/dist/extensions/compatibility.mjs +15 -16
  13. package/dist/extensions/context-parameters.d.ts +4 -3
  14. package/dist/extensions/context-parameters.d.ts.map +1 -1
  15. package/dist/extensions/context-parameters.js +4 -5
  16. package/dist/extensions/context-parameters.mjs +4 -4
  17. package/dist/extensions/conversation-id.d.ts +3 -8
  18. package/dist/extensions/conversation-id.d.ts.map +1 -1
  19. package/dist/extensions/conversation-id.js +6 -22
  20. package/dist/extensions/conversation-id.mjs +5 -17
  21. package/dist/extensions/instrument-highlevel.d.ts +2 -1
  22. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  23. package/dist/extensions/instrument-highlevel.js +31 -51
  24. package/dist/extensions/instrument-highlevel.mjs +30 -50
  25. package/dist/extensions/instrument-lowlevel.d.ts +2 -1
  26. package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
  27. package/dist/extensions/instrument-lowlevel.js +8 -9
  28. package/dist/extensions/instrument-lowlevel.mjs +8 -9
  29. package/dist/extensions/instrumentation.d.ts +7 -8
  30. package/dist/extensions/instrumentation.d.ts.map +1 -1
  31. package/dist/extensions/instrumentation.js +57 -32
  32. package/dist/extensions/instrumentation.mjs +58 -33
  33. package/dist/extensions/intent.d.ts +1 -1
  34. package/dist/extensions/intent.d.ts.map +1 -1
  35. package/dist/extensions/intent.js +3 -4
  36. package/dist/extensions/intent.mjs +3 -4
  37. package/dist/extensions/internal.d.ts.map +1 -1
  38. package/dist/extensions/internal.js +5 -6
  39. package/dist/extensions/internal.mjs +5 -6
  40. package/dist/extensions/logger.d.ts +12 -4
  41. package/dist/extensions/logger.d.ts.map +1 -1
  42. package/dist/extensions/logger.js +12 -3
  43. package/dist/extensions/logger.mjs +10 -4
  44. package/dist/extensions/sink.d.ts +4 -2
  45. package/dist/extensions/sink.d.ts.map +1 -1
  46. package/dist/extensions/sink.js +11 -10
  47. package/dist/extensions/sink.mjs +11 -10
  48. package/dist/extensions/tools.d.ts +2 -1
  49. package/dist/extensions/tools.d.ts.map +1 -1
  50. package/dist/extensions/tools.js +2 -2
  51. package/dist/extensions/tools.mjs +2 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +19 -16
  54. package/dist/index.mjs +20 -17
  55. package/dist/types.d.ts +7 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/dist/version.mjs +1 -1
  60. package/package.json +4 -4
  61. package/src/extensions/analytics-parameters.ts +67 -1
  62. package/src/extensions/capture.ts +3 -3
  63. package/src/extensions/compatibility.ts +16 -16
  64. package/src/extensions/context-parameters.ts +9 -7
  65. package/src/extensions/conversation-id.ts +12 -25
  66. package/src/extensions/instrument-highlevel.ts +52 -75
  67. package/src/extensions/instrument-lowlevel.ts +14 -10
  68. package/src/extensions/instrumentation.ts +101 -36
  69. package/src/extensions/intent.ts +8 -3
  70. package/src/extensions/internal.ts +7 -6
  71. package/src/extensions/logger.ts +22 -7
  72. package/src/extensions/sink.ts +17 -12
  73. package/src/extensions/tools.ts +3 -3
  74. package/src/index.ts +22 -19
  75. package/src/types.ts +8 -0
  76. package/src/version.ts +1 -1
@@ -1,4 +1,21 @@
1
+ import type { AnalyticsParameterOwnership } from '../types'
2
+ import { getObjectShape, isZodRawShapeCompat } from './mcp-sdk-compat'
3
+
4
+ const JSON_SCHEMA_KEYWORDS = [
5
+ '$defs',
6
+ '$ref',
7
+ 'additionalProperties',
8
+ 'allOf',
9
+ 'anyOf',
10
+ 'definitions',
11
+ 'oneOf',
12
+ 'properties',
13
+ 'required',
14
+ 'type',
15
+ ]
16
+
1
17
  export interface AnalyticsInjectableJsonSchema {
18
+ $ref?: unknown
2
19
  additionalProperties?: boolean
3
20
  allOf?: unknown
4
21
  anyOf?: unknown
@@ -19,5 +36,54 @@ export function canInjectAnalyticsParameter(
19
36
  schema: AnalyticsInjectableJsonSchema | undefined,
20
37
  parameterName: string
21
38
  ): boolean {
22
- return !hasAnalyticsParameter(schema, parameterName) && !schema?.oneOf && !schema?.allOf && !schema?.anyOf
39
+ return (
40
+ !hasAnalyticsParameter(schema, parameterName) && !schema?.$ref && !schema?.oneOf && !schema?.allOf && !schema?.anyOf
41
+ )
42
+ }
43
+
44
+ export function analyticsOwnsParameter(inputSchema: unknown, parameterName: string): boolean {
45
+ if (!inputSchema || typeof inputSchema !== 'object') {
46
+ return canInjectAnalyticsParameter(undefined, parameterName)
47
+ }
48
+
49
+ if (isZodRawShapeCompat(inputSchema)) {
50
+ return canInjectAnalyticsParameter({ properties: inputSchema }, parameterName)
51
+ }
52
+
53
+ const shape = getObjectShape(inputSchema)
54
+ if (shape) {
55
+ return canInjectAnalyticsParameter({ properties: shape }, parameterName)
56
+ }
57
+
58
+ const schema = inputSchema as Record<string, unknown>
59
+ if (JSON_SCHEMA_KEYWORDS.some((keyword) => Object.prototype.hasOwnProperty.call(schema, keyword))) {
60
+ return canInjectAnalyticsParameter(schema, parameterName)
61
+ }
62
+
63
+ if ('_def' in schema || '_zod' in schema || '~standard' in schema) {
64
+ // The MCP SDK advertises non-object schemas as an empty object schema.
65
+ return canInjectAnalyticsParameter(undefined, parameterName)
66
+ }
67
+
68
+ return canInjectAnalyticsParameter(schema, parameterName)
69
+ }
70
+
71
+ export function getAnalyticsParameterOwnership(inputSchema: unknown): AnalyticsParameterOwnership {
72
+ return {
73
+ context: analyticsOwnsParameter(inputSchema, 'context'),
74
+ conversationId: analyticsOwnsParameter(inputSchema, 'conversation_id'),
75
+ }
76
+ }
77
+
78
+ export function stripOwnedAnalyticsArguments(args: unknown, ownership: AnalyticsParameterOwnership): unknown {
79
+ let cleanedArgs = args
80
+ if (ownership.context && cleanedArgs && typeof cleanedArgs === 'object' && 'context' in cleanedArgs) {
81
+ const { context: _context, ...rest } = cleanedArgs
82
+ cleanedArgs = rest
83
+ }
84
+ if (ownership.conversationId && cleanedArgs && typeof cleanedArgs === 'object' && 'conversation_id' in cleanedArgs) {
85
+ const { conversation_id: _conversationId, ...rest } = cleanedArgs
86
+ cleanedArgs = rest
87
+ }
88
+ return cleanedArgs
23
89
  }
@@ -6,7 +6,7 @@
6
6
  import type { MCPServerLike, McpEvent } from '../types'
7
7
  import { MCPAnalyticsEventType } from './event-types'
8
8
  import { getServerTrackingData } from './internal'
9
- import { log } from './logger'
9
+ import type { LoggerFn } from './logger'
10
10
  import { getSessionInfo } from './session'
11
11
 
12
12
  /**
@@ -19,10 +19,10 @@ import { getSessionInfo } from './session'
19
19
  * Auto-capture callers (tool calls, listings, identify) intentionally ignore the
20
20
  * return value, keeping the tool path isolated from analytics latency/errors.
21
21
  */
22
- export function captureEvent(server: MCPServerLike, eventInput: McpEvent): Promise<void> | undefined {
22
+ export function captureEvent(server: MCPServerLike, eventInput: McpEvent, logger: LoggerFn): Promise<void> | undefined {
23
23
  const data = getServerTrackingData(server)
24
24
  if (!data) {
25
- log('Warning: Server tracking data not found. Event will not be published.')
25
+ logger('Warning: Server tracking data not found. Event will not be published.')
26
26
  return
27
27
  }
28
28
 
@@ -4,7 +4,7 @@
4
4
  // Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
5
5
 
6
6
  import type { HighLevelMCPServerLike, MCPServerLike } from '../types'
7
- import { log } from './logger'
7
+ import type { LoggerFn } from './logger'
8
8
 
9
9
  type ServerRecord = Record<string, unknown>
10
10
 
@@ -22,8 +22,8 @@ type ServerRecord = Record<string, unknown>
22
22
  */
23
23
 
24
24
  // Function to log compatibility information
25
- export function logCompatibilityWarning(): void {
26
- log(
25
+ export function logCompatibilityWarning(logger: LoggerFn): void {
26
+ logger(
27
27
  'PostHog MCP analytics SDK Compatibility: This version only supports Model Context Protocol TypeScript SDK v1.11 and above. Please upgrade if using an older version.'
28
28
  )
29
29
  }
@@ -41,9 +41,9 @@ export function isLowLevelServer(server: unknown): server is ServerRecord {
41
41
  }
42
42
 
43
43
  // Type guard function that validates server compatibility and returns typed server
44
- export function isCompatibleServerType(server: unknown): MCPServerLike | HighLevelMCPServerLike {
44
+ export function isCompatibleServerType(server: unknown, logger: LoggerFn): MCPServerLike | HighLevelMCPServerLike {
45
45
  if (!server || typeof server !== 'object') {
46
- logCompatibilityWarning()
46
+ logCompatibilityWarning(logger)
47
47
  throw new Error(
48
48
  "PostHog MCP analytics SDK compatibility error: Server must be an object. Ensure you're using MCP SDK v1.11 or higher."
49
49
  )
@@ -52,13 +52,13 @@ export function isCompatibleServerType(server: unknown): MCPServerLike | HighLev
52
52
  if (isHighLevelServer(server)) {
53
53
  // Validate high-level server requirements
54
54
  if (!server._registeredTools || typeof server._registeredTools !== 'object') {
55
- logCompatibilityWarning()
55
+ logCompatibilityWarning(logger)
56
56
  throw new Error(
57
57
  'PostHog MCP analytics SDK compatibility error: High-level server must have _registeredTools object. This requires MCP SDK v1.11 or higher.'
58
58
  )
59
59
  }
60
60
  if (typeof server.tool !== 'function') {
61
- logCompatibilityWarning()
61
+ logCompatibilityWarning(logger)
62
62
  throw new Error(
63
63
  'PostHog MCP analytics SDK compatibility error: High-level server must have tool() method. This requires MCP SDK v1.11 or higher.'
64
64
  )
@@ -66,19 +66,19 @@ export function isCompatibleServerType(server: unknown): MCPServerLike | HighLev
66
66
 
67
67
  // Validate the underlying low-level server
68
68
  const targetServer = server.server
69
- validateLowLevelServer(targetServer)
69
+ validateLowLevelServer(targetServer, logger)
70
70
 
71
71
  return server as unknown as HighLevelMCPServerLike
72
72
  }
73
73
  // Direct low-level server validation
74
- validateLowLevelServer(server)
74
+ validateLowLevelServer(server, logger)
75
75
  return server as MCPServerLike
76
76
  }
77
77
 
78
78
  // Helper function to validate low-level server requirements
79
- function validateLowLevelServer(server: unknown): void {
79
+ function validateLowLevelServer(server: unknown, logger: LoggerFn): void {
80
80
  if (!server || typeof server !== 'object') {
81
- logCompatibilityWarning()
81
+ logCompatibilityWarning(logger)
82
82
  throw new Error(
83
83
  "PostHog MCP analytics SDK compatibility error: Server must be an object. Ensure you're using MCP SDK v1.11 or higher."
84
84
  )
@@ -87,14 +87,14 @@ function validateLowLevelServer(server: unknown): void {
87
87
  const serverRecord = server as ServerRecord
88
88
 
89
89
  if (typeof serverRecord.setRequestHandler !== 'function') {
90
- logCompatibilityWarning()
90
+ logCompatibilityWarning(logger)
91
91
  throw new Error(
92
92
  'PostHog MCP analytics SDK compatibility error: Server must have a setRequestHandler method. This requires MCP SDK v1.11 or higher.'
93
93
  )
94
94
  }
95
95
 
96
96
  if (!(serverRecord._requestHandlers && serverRecord._requestHandlers instanceof Map)) {
97
- logCompatibilityWarning()
97
+ logCompatibilityWarning(logger)
98
98
  throw new Error(
99
99
  'PostHog MCP analytics SDK compatibility error: Server._requestHandlers is not accessible. This requires MCP SDK v1.11 or higher.'
100
100
  )
@@ -102,14 +102,14 @@ function validateLowLevelServer(server: unknown): void {
102
102
 
103
103
  // Validate that _requestHandlers contains functions with compatible signatures
104
104
  if (typeof serverRecord._requestHandlers.get !== 'function') {
105
- logCompatibilityWarning()
105
+ logCompatibilityWarning(logger)
106
106
  throw new Error(
107
107
  'PostHog MCP analytics SDK compatibility error: Server._requestHandlers must be a Map with a get method. This requires MCP SDK v1.11 or higher.'
108
108
  )
109
109
  }
110
110
 
111
111
  if (typeof serverRecord.getClientVersion !== 'function') {
112
- logCompatibilityWarning()
112
+ logCompatibilityWarning(logger)
113
113
  throw new Error(
114
114
  'PostHog MCP analytics SDK compatibility error: Server.getClientVersion must be a function. This requires MCP SDK v1.11 or higher.'
115
115
  )
@@ -120,7 +120,7 @@ function validateLowLevelServer(server: unknown): void {
120
120
  typeof serverRecord._serverInfo !== 'object' ||
121
121
  !('name' in serverRecord._serverInfo)
122
122
  ) {
123
- logCompatibilityWarning()
123
+ logCompatibilityWarning(logger)
124
124
  throw new Error(
125
125
  'PostHog MCP analytics SDK compatibility error: Server._serverInfo is not accessible or missing name. This requires MCP SDK v1.11 or higher.'
126
126
  )
@@ -10,7 +10,7 @@ import {
10
10
  type AnalyticsInjectableJsonSchema,
11
11
  } from './analytics-parameters'
12
12
  import { DEFAULT_CONTEXT_PARAMETER_DESCRIPTION } from './constants'
13
- import { log } from './logger'
13
+ import { log, type LoggerFn } from './logger'
14
14
 
15
15
  export interface ContextInjectableTool {
16
16
  inputSchema?: AnalyticsInjectableJsonSchema
@@ -33,12 +33,13 @@ export function getContextDescription(context: MCPAnalyticsOptions['context']):
33
33
  *
34
34
  * Skips injection (with warning) for:
35
35
  * - Tools that already have a 'context' parameter
36
- * - Complex schemas (oneOf/allOf/anyOf) that can't safely have properties added
36
+ * - Complex schemas (oneOf/allOf/anyOf/$ref) that can't safely have properties added
37
37
  * - Schemas with additionalProperties: false
38
38
  */
39
39
  export function addContextParameterToTool<TTool extends ContextInjectableTool>(
40
40
  tool: TTool,
41
- contextDescriptionOverride?: string
41
+ contextDescriptionOverride?: string,
42
+ logger: LoggerFn = log
42
43
  ): TTool {
43
44
  // Create a shallow copy of the tool to avoid modifying the original
44
45
  const modifiedTool = { ...tool }
@@ -47,9 +48,9 @@ export function addContextParameterToTool<TTool extends ContextInjectableTool>(
47
48
 
48
49
  if (!canInjectAnalyticsParameter(schema, 'context')) {
49
50
  if (hasAnalyticsParameter(schema, 'context')) {
50
- log(`WARN: Tool "${toolName}" already has 'context' parameter. Skipping context injection.`)
51
+ logger(`WARN: Tool "${toolName}" already has 'context' parameter. Skipping context injection.`)
51
52
  } else {
52
- log(`WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf). Skipping context injection.`)
53
+ logger(`WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf/$ref). Skipping context injection.`)
53
54
  }
54
55
  return modifiedTool
55
56
  }
@@ -104,13 +105,14 @@ export function addContextParameterToTool<TTool extends ContextInjectableTool>(
104
105
 
105
106
  export function addContextParameterToTools<TTool extends ContextInjectableTool>(
106
107
  tools: TTool[],
107
- contextDescriptionOverride?: string
108
+ contextDescriptionOverride?: string,
109
+ logger: LoggerFn = log
108
110
  ): TTool[] {
109
111
  return tools.map((tool) => {
110
112
  // Skip get_more_tools - it has its own special context parameter
111
113
  if (tool.name === 'get_more_tools') {
112
114
  return tool
113
115
  }
114
- return addContextParameterToTool(tool, contextDescriptionOverride)
116
+ return addContextParameterToTool(tool, contextDescriptionOverride, logger)
115
117
  })
116
118
  }
@@ -10,7 +10,7 @@ import {
10
10
  type AnalyticsInjectableJsonSchema,
11
11
  } from './analytics-parameters'
12
12
  import { DEFAULT_CONVERSATION_ID_DESCRIPTION } from './constants'
13
- import { log } from './logger'
13
+ import { log, type LoggerFn } from './logger'
14
14
  import { GET_MORE_TOOLS_NAME } from './tools'
15
15
 
16
16
  export const CONVERSATION_ID_PARAM_NAME = 'conversation_id'
@@ -21,18 +21,23 @@ export interface ConversationIdInjectableTool {
21
21
  [key: string]: unknown
22
22
  }
23
23
 
24
- export function addConversationIdToTool<TTool extends ConversationIdInjectableTool>(tool: TTool): TTool {
24
+ export function addConversationIdToTool<TTool extends ConversationIdInjectableTool>(
25
+ tool: TTool,
26
+ logger: LoggerFn = log
27
+ ): TTool {
25
28
  const modifiedTool = { ...tool }
26
29
  const toolName = tool.name || 'unknown'
27
30
  const schema = modifiedTool.inputSchema as AnalyticsInjectableJsonSchema | undefined
28
31
 
29
32
  if (!canInjectAnalyticsParameter(schema, CONVERSATION_ID_PARAM_NAME)) {
30
33
  if (hasAnalyticsParameter(schema, CONVERSATION_ID_PARAM_NAME)) {
31
- log(
34
+ logger(
32
35
  `WARN: Tool "${toolName}" already has '${CONVERSATION_ID_PARAM_NAME}' parameter. Skipping conversation_id injection.`
33
36
  )
34
37
  } else {
35
- log(`WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf). Skipping conversation_id injection.`)
38
+ logger(
39
+ `WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf/$ref). Skipping conversation_id injection.`
40
+ )
36
41
  }
37
42
  return modifiedTool
38
43
  }
@@ -67,13 +72,14 @@ export function addConversationIdToTool<TTool extends ConversationIdInjectableTo
67
72
 
68
73
  export function addConversationIdToTools<TTool extends ConversationIdInjectableTool>(
69
74
  tools: TTool[],
70
- missingCapabilityToolName: string = GET_MORE_TOOLS_NAME
75
+ missingCapabilityToolName: string = GET_MORE_TOOLS_NAME,
76
+ logger: LoggerFn = log
71
77
  ): TTool[] {
72
78
  return tools.map((tool) => {
73
79
  if (tool.name === missingCapabilityToolName) {
74
80
  return tool
75
81
  }
76
- return addConversationIdToTool(tool)
82
+ return addConversationIdToTool(tool, logger)
77
83
  })
78
84
  }
79
85
 
@@ -131,25 +137,6 @@ export function extractConversationId(args: unknown): string | undefined {
131
137
  return trimmed.length > 0 ? trimmed : undefined
132
138
  }
133
139
 
134
- export function cloneRequestWithoutConversationId<
135
- TRequest extends { params?: { arguments?: unknown; [k: string]: unknown } },
136
- >(request: TRequest): TRequest {
137
- if (!request.params || typeof request.params !== 'object') {
138
- return request
139
- }
140
- const args = request.params.arguments
141
- if (!(args && typeof args === 'object')) {
142
- return request
143
- }
144
- return {
145
- ...request,
146
- params: {
147
- ...request.params,
148
- arguments: stripConversationId(args) as typeof request.params.arguments,
149
- },
150
- }
151
- }
152
-
153
140
  export function stripConversationId(args: unknown): unknown {
154
141
  if (!args || typeof args !== 'object' || !(CONVERSATION_ID_PARAM_NAME in (args as Record<string, unknown>))) {
155
142
  return args
@@ -11,19 +11,16 @@ import type {
11
11
  RegisteredTool,
12
12
  ToolCallback,
13
13
  } from '../types'
14
- import { canInjectAnalyticsParameter } from './analytics-parameters'
15
- import { stripConversationId } from './conversation-id'
14
+ import {
15
+ analyticsOwnsParameter,
16
+ getAnalyticsParameterOwnership,
17
+ stripOwnedAnalyticsArguments,
18
+ } from './analytics-parameters'
16
19
  import { isContextEnabled } from './context-parameters'
17
20
  import { MCPAnalyticsEventType } from './event-types'
18
21
  import { getServerTrackingData } from './internal'
19
- import { log } from './logger'
20
- import {
21
- createWrappedTool,
22
- getObjectShape,
23
- getToolFunction,
24
- hasToolFunction,
25
- isZodRawShapeCompat,
26
- } from './mcp-sdk-compat'
22
+ import type { LoggerFn } from './logger'
23
+ import { createWrappedTool, getToolFunction, hasToolFunction } from './mcp-sdk-compat'
27
24
  import { handleReportMissing, resolveMissingCapabilityToolName } from './tools'
28
25
  import {
29
26
  handleInitializeRequest,
@@ -31,6 +28,7 @@ import {
31
28
  patchRequestHandlers,
32
29
  captureToolCall,
33
30
  readToolMetaCategory,
31
+ type HandlerPatch,
34
32
  } from './instrumentation'
35
33
  import { getContextArgument } from './tracing-helpers'
36
34
 
@@ -52,55 +50,24 @@ function isCallbackUpdate(value: unknown): value is { callback: ToolCallback } {
52
50
  return !!value && typeof value === 'object' && 'callback' in value && typeof value.callback === 'function'
53
51
  }
54
52
 
55
- function analyticsOwnsParameter(inputSchema: unknown, parameterName: string): boolean {
56
- if (!inputSchema || typeof inputSchema !== 'object') {
57
- return canInjectAnalyticsParameter(undefined, parameterName)
58
- }
59
-
60
- if (isZodRawShapeCompat(inputSchema)) {
61
- return canInjectAnalyticsParameter({ properties: inputSchema }, parameterName)
62
- }
63
-
64
- const shape = getObjectShape(inputSchema)
65
- if (shape) {
66
- return canInjectAnalyticsParameter({ properties: shape }, parameterName)
67
- }
68
-
69
- const schema = inputSchema as Record<string, unknown>
70
- if ('_def' in schema || '_zod' in schema || '~standard' in schema) {
71
- // The MCP SDK advertises non-object schemas as an empty object schema.
72
- return canInjectAnalyticsParameter(undefined, parameterName)
73
- }
74
-
75
- return canInjectAnalyticsParameter(schema, parameterName)
76
- }
77
-
78
- function stripOwnedAnalyticsArguments(
79
- args: unknown,
80
- ownership: { context: boolean; conversationId: boolean }
81
- ): unknown {
82
- let cleanedArgs = args
83
- if (ownership.context && cleanedArgs && typeof cleanedArgs === 'object' && 'context' in cleanedArgs) {
84
- const { context: _context, ...rest } = cleanedArgs
85
- cleanedArgs = rest
86
- }
87
- return ownership.conversationId ? stripConversationId(cleanedArgs) : cleanedArgs
88
- }
89
-
90
53
  function addTracingToToolRegistry(
91
54
  tools: Record<string, RegisteredTool>,
92
- server: HighLevelMCPServerLike
55
+ server: HighLevelMCPServerLike,
56
+ logger: LoggerFn
93
57
  ): Record<string, RegisteredTool> {
94
58
  return Object.fromEntries(
95
- Object.entries(tools).map(([name, tool]) => [name, addTracingToToolCallbackInternal(tool, name, server)])
59
+ Object.entries(tools).map(([name, tool]) => [
60
+ name,
61
+ addTracingToToolCallbackInternal(tool, name, server, () => tool.inputSchema, logger),
62
+ ])
96
63
  )
97
64
  }
98
65
 
99
- function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
66
+ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike, logger: LoggerFn): void {
100
67
  try {
101
68
  const data = getServerTrackingData(server.server as MCPServerLike)
102
69
  if (!data) {
103
- log('Warning: Cannot setup listener - no tracking data found')
70
+ logger('Warning: Cannot setup listener - no tracking data found')
104
71
  return
105
72
  }
106
73
 
@@ -112,19 +79,19 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
112
79
  data.toolDescriptions.set(property, value.description)
113
80
  }
114
81
  if ((value as ProcessedRegisteredTool)[MCP_ANALYTICS_PROCESSED]) {
115
- log(`Tool ${String(property)} already processed, skipping proxy wrapping`)
82
+ logger(`Tool ${String(property)} already processed, skipping proxy wrapping`)
116
83
  return Reflect.set(target, property, value)
117
84
  }
118
85
 
119
86
  if (wrappedCallbacks.has(getToolFunction(value))) {
120
- log(`Tool ${String(property)} callback already wrapped, skipping proxy wrapping`)
87
+ logger(`Tool ${String(property)} callback already wrapped, skipping proxy wrapping`)
121
88
  return Reflect.set(target, property, value)
122
89
  }
123
90
 
124
91
  // The MCP SDK's registry copy is currently stale after update(), but keep this lookup lazy so
125
92
  // ownership follows the current schema once the registry reflects live tool state.
126
93
  const getCurrentInputSchema = () => value.inputSchema
127
- const nextValue = addTracingToToolCallbackInternal(value, property, server, getCurrentInputSchema)
94
+ const nextValue = addTracingToToolCallbackInternal(value, property, server, getCurrentInputSchema, logger)
128
95
 
129
96
  if (typeof nextValue.update === 'function') {
130
97
  const originalUpdate = nextValue.update
@@ -136,7 +103,8 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
136
103
  { callback: updateObj.callback },
137
104
  property,
138
105
  server,
139
- getCurrentInputSchema
106
+ getCurrentInputSchema,
107
+ logger
140
108
  )
141
109
  updateObj.callback = getToolFunction(wrappedTool)
142
110
  }
@@ -149,7 +117,7 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
149
117
 
150
118
  return Reflect.set(target, property, value)
151
119
  } catch (error) {
152
- log(`Warning: Error in proxy set handler for tool ${String(property)} - ${error}`)
120
+ logger(`Warning: Error in proxy set handler for tool ${String(property)} - ${error}`)
153
121
  return Reflect.set(target, property, value)
154
122
  }
155
123
  },
@@ -170,9 +138,9 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
170
138
  const originalTools = server._registeredTools || {}
171
139
  server._registeredTools = new Proxy(originalTools, handler)
172
140
 
173
- log('Successfully set up listener for new tool registrations')
141
+ logger('Successfully set up listener for new tool registrations')
174
142
  } catch (error) {
175
- log(`Warning: Failed to setup listener for registered tools - ${error}`)
143
+ logger(`Warning: Failed to setup listener for registered tools - ${error}`)
176
144
  }
177
145
  }
178
146
 
@@ -189,19 +157,20 @@ function addTracingToToolCallbackInternal(
189
157
  tool: RegisteredTool,
190
158
  toolName: string,
191
159
  server: HighLevelMCPServerLike,
192
- getCurrentInputSchema: () => unknown = () => tool.inputSchema
160
+ getCurrentInputSchema: () => unknown,
161
+ logger: LoggerFn
193
162
  ): RegisteredTool {
194
163
  const originalCallback = getToolFunction(tool)
195
164
  const options = getServerTrackingData(server.server as MCPServerLike)?.options
196
165
  const missingToolName = resolveMissingCapabilityToolName(options)
197
166
 
198
167
  if (wrappedCallbacks.has(originalCallback)) {
199
- log(`Tool ${toolName} callback already wrapped, skipping re-wrap`)
168
+ logger(`Tool ${toolName} callback already wrapped, skipping re-wrap`)
200
169
  return tool
201
170
  }
202
171
 
203
172
  if ((tool as ProcessedRegisteredTool)[MCP_ANALYTICS_PROCESSED]) {
204
- log(`Tool ${toolName} already processed, skipping re-wrap`)
173
+ logger(`Tool ${toolName} already processed, skipping re-wrap`)
205
174
  return tool
206
175
  }
207
176
 
@@ -258,14 +227,16 @@ function addTracingToToolCallbackInternal(
258
227
  }
259
228
 
260
229
  async function handleToolCallRequest(
230
+ highLevelServer: HighLevelMCPServerLike,
261
231
  server: MCPServerLike,
262
232
  originalCallToolHandler: MCPRequestHandler,
263
233
  request: MCPRequest,
264
- extra: MCPRequestExtra
234
+ extra: MCPRequestExtra,
235
+ logger: LoggerFn
265
236
  ): Promise<unknown> {
266
237
  const data = getServerTrackingData(server)
267
238
  if (!data) {
268
- log(
239
+ logger(
269
240
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
270
241
  )
271
242
  return await originalCallToolHandler(request, extra)
@@ -280,20 +251,23 @@ async function handleToolCallRequest(
280
251
  extra,
281
252
  eventType: MCPAnalyticsEventType.mcpMissingCapability,
282
253
  explicitContextIntent: context,
283
- execute: async () => handleReportMissing({ context }),
254
+ execute: async () => handleReportMissing({ context }, data.logger),
284
255
  })
285
256
  }
286
257
 
287
- // The high-level handler re-derives arguments from the original request and
288
- // strips the injected params inside the wrapped callback, so we hand it the
289
- // original request rather than the conversation-stripped one. Errors thrown
290
- // by the tool are stashed on `extra` by the callback wrapper; surface them.
258
+ const toolName = request.params?.name
259
+ const registeredTool = toolName ? highLevelServer._registeredTools[toolName] : undefined
260
+
261
+ // Strip SDK-owned arguments before the MCP SDK validates the registered Zod
262
+ // schema. The callback wrapper repeats the ownership-aware cleanup as a
263
+ // defensive fallback for direct callback invocation.
291
264
  return await captureToolCall({
292
265
  server,
293
266
  data,
294
267
  request,
295
268
  extra,
296
- execute: () => originalCallToolHandler(request, extra),
269
+ execute: (downstreamRequest) => originalCallToolHandler(downstreamRequest as MCPRequest, extra),
270
+ parameterOwnership: registeredTool ? getAnalyticsParameterOwnership(registeredTool.inputSchema) : undefined,
297
271
  takeCapturedError: () => {
298
272
  const captured = extra?.__mcp_analytics_error
299
273
  if (extra) {
@@ -304,29 +278,32 @@ async function handleToolCallRequest(
304
278
  })
305
279
  }
306
280
 
307
- export function instrumentHighLevelServer(server: HighLevelMCPServerLike): void {
281
+ export function instrumentHighLevelServer(server: HighLevelMCPServerLike, logger: LoggerFn): void {
308
282
  try {
309
283
  const lowLevelServer = server.server
310
284
  const mcpAnalyticsData = getServerTrackingData(lowLevelServer)
311
285
 
312
286
  // Patch already existing handlers, and patch setRequestHandler to capture dynamically created handlers.
313
- const handlers = {
314
- initialize: handleInitializeRequest,
315
- 'tools/list': handleListToolsRequest,
316
- 'tools/call': handleToolCallRequest,
287
+ const handlers: Record<string, HandlerPatch> = {
288
+ initialize: (trackedServer, originalHandler, request, extra) =>
289
+ handleInitializeRequest(trackedServer, originalHandler, request, extra, logger),
290
+ 'tools/list': (trackedServer, originalHandler, request, extra) =>
291
+ handleListToolsRequest(trackedServer, originalHandler, request, extra, logger),
292
+ 'tools/call': (trackedServer, originalHandler, request, extra) =>
293
+ handleToolCallRequest(server, trackedServer, originalHandler, request, extra, logger),
317
294
  }
318
295
  patchRequestHandlers(lowLevelServer, handlers)
319
296
 
320
- server._registeredTools = addTracingToToolRegistry(server._registeredTools, server)
297
+ server._registeredTools = addTracingToToolRegistry(server._registeredTools, server, logger)
321
298
 
322
299
  if (mcpAnalyticsData) {
323
300
  seedToolDescriptionsFromRegistry(mcpAnalyticsData.toolDescriptions, server._registeredTools)
324
301
  seedToolCategoriesFromRegistry(mcpAnalyticsData.toolCategories, server._registeredTools)
325
302
  }
326
303
 
327
- setupListenerToRegisteredTools(server)
304
+ setupListenerToRegisteredTools(server, logger)
328
305
  } catch (error) {
329
- log(`Warning: Failed to setup tool call instrumentation - ${error}`)
306
+ logger(`Warning: Failed to setup tool call instrumentation - ${error}`)
330
307
  }
331
308
  }
332
309