@posthog/mcp 0.10.4 → 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 (70) hide show
  1. package/dist/extensions/capture.d.ts +2 -1
  2. package/dist/extensions/capture.d.ts.map +1 -1
  3. package/dist/extensions/capture.js +2 -3
  4. package/dist/extensions/capture.mjs +2 -3
  5. package/dist/extensions/compatibility.d.ts +3 -2
  6. package/dist/extensions/compatibility.d.ts.map +1 -1
  7. package/dist/extensions/compatibility.js +19 -20
  8. package/dist/extensions/compatibility.mjs +15 -16
  9. package/dist/extensions/context-parameters.d.ts +3 -2
  10. package/dist/extensions/context-parameters.d.ts.map +1 -1
  11. package/dist/extensions/context-parameters.js +4 -5
  12. package/dist/extensions/context-parameters.mjs +4 -4
  13. package/dist/extensions/conversation-id.d.ts +3 -2
  14. package/dist/extensions/conversation-id.d.ts.map +1 -1
  15. package/dist/extensions/conversation-id.js +4 -5
  16. package/dist/extensions/conversation-id.mjs +4 -4
  17. package/dist/extensions/instrument-highlevel.d.ts +2 -1
  18. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  19. package/dist/extensions/instrument-highlevel.js +24 -25
  20. package/dist/extensions/instrument-highlevel.mjs +24 -25
  21. package/dist/extensions/instrument-lowlevel.d.ts +2 -1
  22. package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
  23. package/dist/extensions/instrument-lowlevel.js +8 -9
  24. package/dist/extensions/instrument-lowlevel.mjs +8 -9
  25. package/dist/extensions/instrumentation.d.ts +3 -2
  26. package/dist/extensions/instrumentation.d.ts.map +1 -1
  27. package/dist/extensions/instrumentation.js +25 -26
  28. package/dist/extensions/instrumentation.mjs +25 -26
  29. package/dist/extensions/intent.d.ts.map +1 -1
  30. package/dist/extensions/intent.js +1 -2
  31. package/dist/extensions/intent.mjs +1 -2
  32. package/dist/extensions/internal.d.ts.map +1 -1
  33. package/dist/extensions/internal.js +5 -6
  34. package/dist/extensions/internal.mjs +5 -6
  35. package/dist/extensions/logger.d.ts +12 -4
  36. package/dist/extensions/logger.d.ts.map +1 -1
  37. package/dist/extensions/logger.js +12 -3
  38. package/dist/extensions/logger.mjs +10 -4
  39. package/dist/extensions/sink.d.ts +4 -2
  40. package/dist/extensions/sink.d.ts.map +1 -1
  41. package/dist/extensions/sink.js +11 -10
  42. package/dist/extensions/sink.mjs +11 -10
  43. package/dist/extensions/tools.d.ts +2 -1
  44. package/dist/extensions/tools.d.ts.map +1 -1
  45. package/dist/extensions/tools.js +2 -2
  46. package/dist/extensions/tools.mjs +2 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +18 -16
  49. package/dist/index.mjs +19 -17
  50. package/dist/types.d.ts +2 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/dist/version.mjs +1 -1
  55. package/package.json +1 -1
  56. package/src/extensions/capture.ts +3 -3
  57. package/src/extensions/compatibility.ts +16 -16
  58. package/src/extensions/context-parameters.ts +8 -6
  59. package/src/extensions/conversation-id.ts +12 -6
  60. package/src/extensions/instrument-highlevel.ts +37 -31
  61. package/src/extensions/instrument-lowlevel.ts +14 -10
  62. package/src/extensions/instrumentation.ts +30 -25
  63. package/src/extensions/intent.ts +1 -2
  64. package/src/extensions/internal.ts +7 -6
  65. package/src/extensions/logger.ts +22 -7
  66. package/src/extensions/sink.ts +17 -12
  67. package/src/extensions/tools.ts +3 -3
  68. package/src/index.ts +21 -19
  69. package/src/types.ts +2 -0
  70. package/src/version.ts +1 -1
@@ -19,7 +19,7 @@ import {
19
19
  import { isContextEnabled } from './context-parameters'
20
20
  import { MCPAnalyticsEventType } from './event-types'
21
21
  import { getServerTrackingData } from './internal'
22
- import { log } from './logger'
22
+ import type { LoggerFn } from './logger'
23
23
  import { createWrappedTool, getToolFunction, hasToolFunction } from './mcp-sdk-compat'
24
24
  import { handleReportMissing, resolveMissingCapabilityToolName } from './tools'
25
25
  import {
@@ -28,6 +28,7 @@ import {
28
28
  patchRequestHandlers,
29
29
  captureToolCall,
30
30
  readToolMetaCategory,
31
+ type HandlerPatch,
31
32
  } from './instrumentation'
32
33
  import { getContextArgument } from './tracing-helpers'
33
34
 
@@ -51,18 +52,22 @@ function isCallbackUpdate(value: unknown): value is { callback: ToolCallback } {
51
52
 
52
53
  function addTracingToToolRegistry(
53
54
  tools: Record<string, RegisteredTool>,
54
- server: HighLevelMCPServerLike
55
+ server: HighLevelMCPServerLike,
56
+ logger: LoggerFn
55
57
  ): Record<string, RegisteredTool> {
56
58
  return Object.fromEntries(
57
- 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
+ ])
58
63
  )
59
64
  }
60
65
 
61
- function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
66
+ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike, logger: LoggerFn): void {
62
67
  try {
63
68
  const data = getServerTrackingData(server.server as MCPServerLike)
64
69
  if (!data) {
65
- log('Warning: Cannot setup listener - no tracking data found')
70
+ logger('Warning: Cannot setup listener - no tracking data found')
66
71
  return
67
72
  }
68
73
 
@@ -74,19 +79,19 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
74
79
  data.toolDescriptions.set(property, value.description)
75
80
  }
76
81
  if ((value as ProcessedRegisteredTool)[MCP_ANALYTICS_PROCESSED]) {
77
- log(`Tool ${String(property)} already processed, skipping proxy wrapping`)
82
+ logger(`Tool ${String(property)} already processed, skipping proxy wrapping`)
78
83
  return Reflect.set(target, property, value)
79
84
  }
80
85
 
81
86
  if (wrappedCallbacks.has(getToolFunction(value))) {
82
- log(`Tool ${String(property)} callback already wrapped, skipping proxy wrapping`)
87
+ logger(`Tool ${String(property)} callback already wrapped, skipping proxy wrapping`)
83
88
  return Reflect.set(target, property, value)
84
89
  }
85
90
 
86
91
  // The MCP SDK's registry copy is currently stale after update(), but keep this lookup lazy so
87
92
  // ownership follows the current schema once the registry reflects live tool state.
88
93
  const getCurrentInputSchema = () => value.inputSchema
89
- const nextValue = addTracingToToolCallbackInternal(value, property, server, getCurrentInputSchema)
94
+ const nextValue = addTracingToToolCallbackInternal(value, property, server, getCurrentInputSchema, logger)
90
95
 
91
96
  if (typeof nextValue.update === 'function') {
92
97
  const originalUpdate = nextValue.update
@@ -98,7 +103,8 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
98
103
  { callback: updateObj.callback },
99
104
  property,
100
105
  server,
101
- getCurrentInputSchema
106
+ getCurrentInputSchema,
107
+ logger
102
108
  )
103
109
  updateObj.callback = getToolFunction(wrappedTool)
104
110
  }
@@ -111,7 +117,7 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
111
117
 
112
118
  return Reflect.set(target, property, value)
113
119
  } catch (error) {
114
- 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}`)
115
121
  return Reflect.set(target, property, value)
116
122
  }
117
123
  },
@@ -132,9 +138,9 @@ function setupListenerToRegisteredTools(server: HighLevelMCPServerLike): void {
132
138
  const originalTools = server._registeredTools || {}
133
139
  server._registeredTools = new Proxy(originalTools, handler)
134
140
 
135
- log('Successfully set up listener for new tool registrations')
141
+ logger('Successfully set up listener for new tool registrations')
136
142
  } catch (error) {
137
- log(`Warning: Failed to setup listener for registered tools - ${error}`)
143
+ logger(`Warning: Failed to setup listener for registered tools - ${error}`)
138
144
  }
139
145
  }
140
146
 
@@ -151,19 +157,20 @@ function addTracingToToolCallbackInternal(
151
157
  tool: RegisteredTool,
152
158
  toolName: string,
153
159
  server: HighLevelMCPServerLike,
154
- getCurrentInputSchema: () => unknown = () => tool.inputSchema
160
+ getCurrentInputSchema: () => unknown,
161
+ logger: LoggerFn
155
162
  ): RegisteredTool {
156
163
  const originalCallback = getToolFunction(tool)
157
164
  const options = getServerTrackingData(server.server as MCPServerLike)?.options
158
165
  const missingToolName = resolveMissingCapabilityToolName(options)
159
166
 
160
167
  if (wrappedCallbacks.has(originalCallback)) {
161
- log(`Tool ${toolName} callback already wrapped, skipping re-wrap`)
168
+ logger(`Tool ${toolName} callback already wrapped, skipping re-wrap`)
162
169
  return tool
163
170
  }
164
171
 
165
172
  if ((tool as ProcessedRegisteredTool)[MCP_ANALYTICS_PROCESSED]) {
166
- log(`Tool ${toolName} already processed, skipping re-wrap`)
173
+ logger(`Tool ${toolName} already processed, skipping re-wrap`)
167
174
  return tool
168
175
  }
169
176
 
@@ -224,11 +231,12 @@ async function handleToolCallRequest(
224
231
  server: MCPServerLike,
225
232
  originalCallToolHandler: MCPRequestHandler,
226
233
  request: MCPRequest,
227
- extra: MCPRequestExtra
234
+ extra: MCPRequestExtra,
235
+ logger: LoggerFn
228
236
  ): Promise<unknown> {
229
237
  const data = getServerTrackingData(server)
230
238
  if (!data) {
231
- log(
239
+ logger(
232
240
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
233
241
  )
234
242
  return await originalCallToolHandler(request, extra)
@@ -243,7 +251,7 @@ async function handleToolCallRequest(
243
251
  extra,
244
252
  eventType: MCPAnalyticsEventType.mcpMissingCapability,
245
253
  explicitContextIntent: context,
246
- execute: async () => handleReportMissing({ context }),
254
+ execute: async () => handleReportMissing({ context }, data.logger),
247
255
  })
248
256
  }
249
257
 
@@ -270,34 +278,32 @@ async function handleToolCallRequest(
270
278
  })
271
279
  }
272
280
 
273
- export function instrumentHighLevelServer(server: HighLevelMCPServerLike): void {
281
+ export function instrumentHighLevelServer(server: HighLevelMCPServerLike, logger: LoggerFn): void {
274
282
  try {
275
283
  const lowLevelServer = server.server
276
284
  const mcpAnalyticsData = getServerTrackingData(lowLevelServer)
277
285
 
278
286
  // Patch already existing handlers, and patch setRequestHandler to capture dynamically created handlers.
279
- const handlers = {
280
- initialize: handleInitializeRequest,
281
- 'tools/list': handleListToolsRequest,
282
- 'tools/call': (
283
- trackedServer: MCPServerLike,
284
- originalHandler: MCPRequestHandler,
285
- request: MCPRequest,
286
- extra: MCPRequestExtra
287
- ) => handleToolCallRequest(server, trackedServer, originalHandler, request, extra),
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),
288
294
  }
289
295
  patchRequestHandlers(lowLevelServer, handlers)
290
296
 
291
- server._registeredTools = addTracingToToolRegistry(server._registeredTools, server)
297
+ server._registeredTools = addTracingToToolRegistry(server._registeredTools, server, logger)
292
298
 
293
299
  if (mcpAnalyticsData) {
294
300
  seedToolDescriptionsFromRegistry(mcpAnalyticsData.toolDescriptions, server._registeredTools)
295
301
  seedToolCategoriesFromRegistry(mcpAnalyticsData.toolCategories, server._registeredTools)
296
302
  }
297
303
 
298
- setupListenerToRegisteredTools(server)
304
+ setupListenerToRegisteredTools(server, logger)
299
305
  } catch (error) {
300
- log(`Warning: Failed to setup tool call instrumentation - ${error}`)
306
+ logger(`Warning: Failed to setup tool call instrumentation - ${error}`)
301
307
  }
302
308
  }
303
309
 
@@ -7,13 +7,14 @@ import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
7
7
  import type { CompatibleRequestHandlerExtra, MCPRequestLike, MCPServerLike } from '../types'
8
8
  import { MCPAnalyticsEventType } from './event-types'
9
9
  import { getServerTrackingData } from './internal'
10
- import { log } from './logger'
10
+ import type { LoggerFn } from './logger'
11
11
  import { handleReportMissing, resolveMissingCapabilityToolName } from './tools'
12
12
  import {
13
13
  handleInitializeRequest,
14
14
  handleListToolsRequest,
15
15
  patchRequestHandlers,
16
16
  captureToolCall,
17
+ type HandlerPatch,
17
18
  } from './instrumentation'
18
19
  import { getContextArgument } from './tracing-helpers'
19
20
 
@@ -28,22 +29,24 @@ type MCPRequestExtra = Parameters<MCPRequestHandler>[1]
28
29
  * `tools/call`. The tool-call lifecycle is delegated to {@link captureToolCall},
29
30
  * shared with the high-level wrapper.
30
31
  */
31
- export function instrumentLowLevelServer(server: MCPServerLike): void {
32
+ export function instrumentLowLevelServer(server: MCPServerLike, logger: LoggerFn): void {
32
33
  try {
33
34
  // Patch already existing handlers, and patch setRequestHandler to capture dynamically created handlers.
34
- const handlers = {
35
- initialize: handleInitializeRequest,
36
- 'tools/list': handleListToolsRequest,
35
+ const handlers: Record<string, HandlerPatch> = {
36
+ initialize: (server, originalHandler, request, extra) =>
37
+ handleInitializeRequest(server, originalHandler, request, extra, logger),
38
+ 'tools/list': (server, originalHandler, request, extra) =>
39
+ handleListToolsRequest(server, originalHandler, request, extra, logger),
37
40
  }
38
41
  patchRequestHandlers(server, handlers)
39
42
 
40
43
  const originalCallToolHandler = server._requestHandlers.get('tools/call')
41
44
  server.setRequestHandler(
42
45
  CallToolRequestSchema,
43
- async (request, extra) => await handleToolCallRequest(server, originalCallToolHandler, request, extra)
46
+ async (request, extra) => await handleToolCallRequest(server, originalCallToolHandler, request, extra, logger)
44
47
  )
45
48
  } catch (error) {
46
- log(`Warning: Failed to setup tool call instrumentation - ${error}`)
49
+ logger(`Warning: Failed to setup tool call instrumentation - ${error}`)
47
50
  throw error
48
51
  }
49
52
  }
@@ -52,11 +55,12 @@ async function handleToolCallRequest(
52
55
  server: MCPServerLike,
53
56
  originalCallToolHandler: MCPRequestHandler | undefined,
54
57
  request: MCPRequest,
55
- extra: MCPRequestExtra
58
+ extra: MCPRequestExtra,
59
+ logger: LoggerFn
56
60
  ): Promise<unknown> {
57
61
  const data = getServerTrackingData(server)
58
62
  if (!data) {
59
- log(
63
+ logger(
60
64
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
61
65
  )
62
66
  return await originalCallToolHandler?.(request, extra)
@@ -71,7 +75,7 @@ async function handleToolCallRequest(
71
75
  extra,
72
76
  eventType: MCPAnalyticsEventType.mcpMissingCapability,
73
77
  explicitContextIntent: context,
74
- execute: async () => handleReportMissing({ context }),
78
+ execute: async () => handleReportMissing({ context }, data.logger),
75
79
  })
76
80
  }
77
81
 
@@ -28,7 +28,7 @@ import { MCPAnalyticsEventType } from './event-types'
28
28
  import { captureException } from './exceptions'
29
29
  import { resolveToolCallIntent, setEventIntent, setExplicitContextIntent } from './intent'
30
30
  import { getServerTrackingData, handleIdentify, setServerTrackingData } from './internal'
31
- import { log } from './logger'
31
+ import type { LoggerFn } from './logger'
32
32
  import { buildCapturedMcpParameters } from './mcp-payloads'
33
33
  import { getLiteralValue, getObjectShape } from './mcp-sdk-compat'
34
34
  import { getSessionId, newSessionId } from './session'
@@ -130,12 +130,12 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
130
130
  try {
131
131
  result = await execute(downstreamRequest)
132
132
  } catch (error) {
133
- publishFailedToolEvent(server, event, error, startTime, conversation)
133
+ publishFailedToolEvent(server, event, error, startTime, conversation, data.logger)
134
134
  throw error
135
135
  }
136
136
 
137
137
  const finalResult = applyConversationPromptBack(event, result, conversation)
138
- publishSuccessfulToolEvent(server, event, finalResult, startTime, takeCapturedError)
138
+ publishSuccessfulToolEvent(server, event, finalResult, startTime, data.logger, takeCapturedError)
139
139
  return finalResult
140
140
  }
141
141
 
@@ -206,7 +206,7 @@ async function prepareToolCallEvent(
206
206
  setEventIntent(event, await resolveToolCallIntent(data, request, ownership.context, extra))
207
207
  return event
208
208
  } catch (error) {
209
- log(
209
+ data.logger(
210
210
  `Warning: PostHog MCP analytics instrumentation failed for tool ${request.params?.name}, the tool will still run - ${error}`
211
211
  )
212
212
  return null
@@ -240,6 +240,7 @@ function publishSuccessfulToolEvent(
240
240
  event: McpEvent | null,
241
241
  result: unknown,
242
242
  startTime: Date,
243
+ logger: LoggerFn,
243
244
  takeCapturedError?: () => unknown
244
245
  ): void {
245
246
  if (!event) {
@@ -255,9 +256,9 @@ function publishSuccessfulToolEvent(
255
256
  }
256
257
  event.response = result
257
258
  event.duration = Date.now() - startTime.getTime()
258
- captureEvent(server, event)
259
+ captureEvent(server, event, logger)
259
260
  } catch (error) {
260
- log(`Warning: PostHog MCP analytics failed to publish tool event - ${error}`)
261
+ logger(`Warning: PostHog MCP analytics failed to publish tool event - ${error}`)
261
262
  }
262
263
  }
263
264
 
@@ -266,7 +267,8 @@ function publishFailedToolEvent(
266
267
  event: McpEvent | null,
267
268
  error: unknown,
268
269
  startTime: Date,
269
- conversation: ConversationIdResolution
270
+ conversation: ConversationIdResolution,
271
+ logger: LoggerFn
270
272
  ): void {
271
273
  if (!event) {
272
274
  return
@@ -278,9 +280,9 @@ function publishFailedToolEvent(
278
280
  event.isError = true
279
281
  event.error = captureException(error)
280
282
  event.duration = Date.now() - startTime.getTime()
281
- captureEvent(server, event)
283
+ captureEvent(server, event, logger)
282
284
  } catch (publishError) {
283
- log(`Warning: PostHog MCP analytics failed to publish failed tool event - ${publishError}`)
285
+ logger(`Warning: PostHog MCP analytics failed to publish failed tool event - ${publishError}`)
284
286
  }
285
287
  }
286
288
 
@@ -336,7 +338,8 @@ export async function handleListToolsRequest(
336
338
  server: MCPServerLike,
337
339
  originalListToolsHandler: MCPRequestHandler,
338
340
  request: MCPRequestLike,
339
- extra?: CompatibleRequestHandlerExtra
341
+ extra: CompatibleRequestHandlerExtra | undefined,
342
+ logger: LoggerFn
340
343
  ): Promise<{ tools: ListToolsResult['tools'] }> {
341
344
  const data = getServerTrackingData(server)
342
345
  const startTime = new Date()
@@ -352,23 +355,23 @@ export async function handleListToolsRequest(
352
355
  await applyResolvedMetadata(event, data, request, extra)
353
356
  }
354
357
 
355
- const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event)
358
+ const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event, logger)
356
359
 
357
360
  if (!data) {
358
- log(
361
+ logger(
359
362
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
360
363
  )
361
364
  return { tools }
362
365
  }
363
366
 
364
367
  if (tools.length === 0) {
365
- log(
368
+ data.logger(
366
369
  'Warning: No tools found in the original list. This is likely due to the tools not being registered before PostHog MCP analytics.instrument().'
367
370
  )
368
371
  event.error = captureException('No tools were sent to MCP client.')
369
372
  event.isError = true
370
373
  event.duration = Date.now() - startTime.getTime()
371
- captureEvent(server, event)
374
+ captureEvent(server, event, data.logger)
372
375
  return { tools }
373
376
  }
374
377
 
@@ -376,7 +379,7 @@ export async function handleListToolsRequest(
376
379
  event.listedToolNames = collectListedToolNames(tools)
377
380
  event.isError = false
378
381
  event.duration = Date.now() - startTime.getTime()
379
- captureEvent(server, event)
382
+ captureEvent(server, event, data.logger)
380
383
  return { tools }
381
384
  }
382
385
 
@@ -405,7 +408,8 @@ async function getTracedToolsList(
405
408
  originalListToolsHandler: MCPRequestHandler,
406
409
  request: MCPRequestLike,
407
410
  extra: CompatibleRequestHandlerExtra | undefined,
408
- event: McpEvent
411
+ event: McpEvent,
412
+ logger: LoggerFn
409
413
  ): Promise<ListToolsResult['tools']> {
410
414
  try {
411
415
  const data = getServerTrackingData(server)
@@ -417,11 +421,11 @@ async function getTracedToolsList(
417
421
  }
418
422
 
419
423
  if (data && isContextEnabled(data.options.context)) {
420
- tools = addContextParameterToTools(tools, getContextDescription(data.options.context))
424
+ tools = addContextParameterToTools(tools, getContextDescription(data.options.context), data.logger)
421
425
  }
422
426
 
423
427
  if (data?.options.enableConversationId) {
424
- tools = addConversationIdToTools(tools, resolveMissingCapabilityToolName(data.options))
428
+ tools = addConversationIdToTools(tools, resolveMissingCapabilityToolName(data.options), data.logger)
425
429
  }
426
430
 
427
431
  if (data?.options.reportMissing) {
@@ -439,13 +443,13 @@ async function getTracedToolsList(
439
443
 
440
444
  return tools
441
445
  } catch (error) {
442
- log(
446
+ logger(
443
447
  `Warning: Original list tools handler failed, this suggests an error PostHog MCP analytics did not cause - ${error}`
444
448
  )
445
449
  event.error = captureException(error)
446
450
  event.isError = true
447
451
  event.duration = event.timestamp ? Date.now() - event.timestamp.getTime() : 0
448
- captureEvent(server, event)
452
+ captureEvent(server, event, logger)
449
453
  throw error
450
454
  }
451
455
  }
@@ -541,7 +545,7 @@ function mintStatelessSessionOnInitialize(
541
545
  setServerTrackingData(server, data)
542
546
  return sessionId
543
547
  } catch (error) {
544
- log(`Warning: PostHog MCP analytics failed to mint a stateless session id - ${error}`)
548
+ data.logger(`Warning: PostHog MCP analytics failed to mint a stateless session id - ${error}`)
545
549
  return undefined
546
550
  }
547
551
  }
@@ -571,7 +575,7 @@ function upgradeMintedTokenToNegotiated(
571
575
  })
572
576
  writeSessionIdToTransport(transport, token)
573
577
  } catch (error) {
574
- log(`Warning: PostHog MCP analytics failed to upgrade the stateless session token - ${error}`)
578
+ data.logger(`Warning: PostHog MCP analytics failed to upgrade the stateless session token - ${error}`)
575
579
  }
576
580
  }
577
581
 
@@ -599,11 +603,12 @@ export async function handleInitializeRequest(
599
603
  server: MCPServerLike,
600
604
  originalInitializeHandler: MCPRequestHandler,
601
605
  request: MCPRequestLike,
602
- extra?: CompatibleRequestHandlerExtra
606
+ extra: CompatibleRequestHandlerExtra | undefined,
607
+ logger: LoggerFn
603
608
  ): Promise<unknown> {
604
609
  const data = getServerTrackingData(server)
605
610
  if (!data) {
606
- log(
611
+ logger(
607
612
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
608
613
  )
609
614
  return await originalInitializeHandler(request, extra)
@@ -640,7 +645,7 @@ export async function handleInitializeRequest(
640
645
  if (mintedSessionId) {
641
646
  upgradeMintedTokenToNegotiated(server, data, mintedSessionId, negotiatedProtocolVersion)
642
647
  }
643
- captureEvent(server, event)
648
+ captureEvent(server, event, data.logger)
644
649
  return result
645
650
  }
646
651
 
@@ -11,7 +11,6 @@ import type {
11
11
  McpEvent,
12
12
  } from '../types'
13
13
  import { isContextEnabled } from './context-parameters'
14
- import { log } from './logger'
15
14
 
16
15
  interface ResolvedIntent {
17
16
  intent: string
@@ -45,7 +44,7 @@ async function runIntentFallback(
45
44
  const intent = normalizeIntent(await data.options.intentFallback(request, extra))
46
45
  return intent ? { intent, source: 'inferred' } : null
47
46
  } catch (error) {
48
- log(`intentFallback callback error: ${error}`)
47
+ data.logger(`intentFallback callback error: ${error}`)
49
48
  return null
50
49
  }
51
50
  }
@@ -12,7 +12,6 @@ import type {
12
12
  UserIdentity,
13
13
  } from '../types'
14
14
  import { MCPAnalyticsEventType } from './event-types'
15
- import { log } from './logger'
16
15
  import { captureEvent } from './capture'
17
16
  import { stampMetaClientInfo } from './client-identity'
18
17
 
@@ -169,14 +168,16 @@ export async function handleIdentify(
169
168
  data.identifiedSessions.set(sessionId, mergedIdentity)
170
169
 
171
170
  if (shouldPublish) {
172
- log(`Identified session ${sessionId} with identity: ${JSON.stringify(mergedIdentity)}`)
173
- captureEvent(server, identifyEvent)
171
+ data.logger(`Identified session ${sessionId}`)
172
+ captureEvent(server, identifyEvent, data.logger)
174
173
  }
175
174
  } else {
176
- log(`Warning: Supplied identify function returned null for session ${sessionId}`)
175
+ data.logger(`Warning: Supplied identify function returned null for session ${sessionId}`)
177
176
  }
178
177
  } catch (error) {
179
- log(`Error: User supplied identify function threw an error while identifying session ${sessionId} - ${error}`)
178
+ data.logger(
179
+ `Error: User supplied identify function threw an error while identifying session ${sessionId} - ${error}`
180
+ )
180
181
  }
181
182
  }
182
183
 
@@ -196,7 +197,7 @@ export async function resolveEventProperties(
196
197
  try {
197
198
  return (await data.options.eventProperties(request, extra)) ?? null
198
199
  } catch (e) {
199
- log(`eventProperties callback error: ${e}`)
200
+ data.logger(`eventProperties callback error: ${e}`)
200
201
  return null
201
202
  }
202
203
  }
@@ -8,10 +8,11 @@
8
8
  *
9
9
  * MCP servers running over the STDIO transport use stdout/stderr to exchange
10
10
  * protocol messages, so we cannot use the default `console.log`. We accept a
11
- * `logger` option on the public API; when omitted, log calls are silently
12
- * dropped. Errors that affect tracking should still be observable in apps that
13
- * want them, so the consumer can plug in any function (e.g. `console.error`
14
- * for non-STDIO transports, a file logger, etc.).
11
+ * `logger` option on the public API or the legacy logger configured through
12
+ * `setLogger`; when neither is configured, log calls are silently dropped.
13
+ * Errors that affect tracking should still be observable in apps that want
14
+ * them, so the consumer can plug in any function (e.g. `console.error` for
15
+ * non-STDIO transports, a file logger, etc.).
15
16
  */
16
17
  export type LoggerFn = (message: string) => void
17
18
 
@@ -21,12 +22,26 @@ export function setLogger(logger: LoggerFn | undefined): void {
21
22
  activeLogger = logger
22
23
  }
23
24
 
24
- export function log(message: string): void {
25
- if (activeLogger) {
25
+ function writeLog(logger: LoggerFn | undefined, message: string): void {
26
+ if (logger) {
26
27
  try {
27
- activeLogger(message)
28
+ logger(message)
28
29
  } catch {
29
30
  // never let logging blow up the tracking pipeline
30
31
  }
31
32
  }
32
33
  }
34
+
35
+ /**
36
+ * Creates a safe logger for one instrumented server. A server-specific logger
37
+ * remains isolated, while an omitted logger preserves the legacy `setLogger`
38
+ * fallback.
39
+ */
40
+ export function createLogger(logger: LoggerFn | undefined): LoggerFn {
41
+ return logger ? (message) => writeLog(logger, message) : log
42
+ }
43
+
44
+ /** Logs through the legacy module-level logger configured by {@link setLogger}. */
45
+ export function log(message: string): void {
46
+ writeLog(activeLogger, message)
47
+ }
@@ -7,7 +7,7 @@ import type { PostHog } from 'posthog-node'
7
7
  import { uuidv7 } from '@posthog/core'
8
8
 
9
9
  import type { BeforeSendFn, Event, McpEvent } from '../types'
10
- import { log } from './logger'
10
+ import { log, type LoggerFn } from './logger'
11
11
  import { type PostHogCaptureEvent, buildPostHogCaptureEvents } from './posthog-events'
12
12
  import { newPrefixedId } from './ids'
13
13
  import { sanitizeEvent } from './sanitization'
@@ -33,21 +33,22 @@ export interface McpCaptureOptions {
33
33
  */
34
34
  export async function processMcpEvent(
35
35
  event: McpEvent,
36
- options: McpCaptureOptions
36
+ options: McpCaptureOptions,
37
+ logger: LoggerFn = log
37
38
  ): Promise<{ event: Event; captures: PostHogCaptureEvent[] } | null> {
38
39
  let processed: McpEvent = event
39
40
 
40
41
  try {
41
42
  processed = sanitizeEvent(processed)
42
43
  } catch (err) {
43
- log(`Failed to sanitize event: ${err}`)
44
+ logger(`Failed to sanitize event: ${err}`)
44
45
  return null
45
46
  }
46
47
 
47
48
  try {
48
49
  processed = truncateEvent(processed)
49
50
  } catch (err) {
50
- log(`Failed to truncate event: ${err}`)
51
+ logger(`Failed to truncate event: ${err}`)
51
52
  return null
52
53
  }
53
54
 
@@ -58,7 +59,7 @@ export async function processMcpEvent(
58
59
  enableExceptionAutocapture: options.enableExceptionAutocapture,
59
60
  })
60
61
 
61
- const captures = await applyBeforeSend(built, options.beforeSend)
62
+ const captures = await applyBeforeSend(built, options.beforeSend, logger)
62
63
 
63
64
  return { event: fullEvent, captures }
64
65
  }
@@ -69,7 +70,8 @@ export async function processMcpEvent(
69
70
  */
70
71
  async function applyBeforeSend(
71
72
  captures: PostHogCaptureEvent[],
72
- beforeSend: BeforeSendFn | undefined
73
+ beforeSend: BeforeSendFn | undefined,
74
+ logger: LoggerFn
73
75
  ): Promise<PostHogCaptureEvent[]> {
74
76
  if (!beforeSend) {
75
77
  return captures
@@ -83,7 +85,7 @@ async function applyBeforeSend(
83
85
  kept.push(result)
84
86
  }
85
87
  } catch (err) {
86
- log(`beforeSend threw for event ${capture.event}; dropping it: ${err}`)
88
+ logger(`beforeSend threw for event ${capture.event}; dropping it: ${err}`)
87
89
  }
88
90
  }
89
91
  return kept
@@ -99,7 +101,10 @@ async function applyBeforeSend(
99
101
  * `@posthog/ai`).
100
102
  */
101
103
  export class McpEventSink {
102
- constructor(private readonly posthog: PostHog) {}
104
+ constructor(
105
+ private readonly posthog: PostHog,
106
+ private readonly logger: LoggerFn = log
107
+ ) {}
103
108
 
104
109
  /**
105
110
  * Push an MCP event through the pipeline (sanitize → truncate → fan out →
@@ -107,7 +112,7 @@ export class McpEventSink {
107
112
  * dropped, never re-thrown into tool code.
108
113
  */
109
114
  async capture(event: McpEvent, options: McpCaptureOptions): Promise<void> {
110
- const result = await processMcpEvent(event, options)
115
+ const result = await processMcpEvent(event, options, this.logger)
111
116
  if (!result) {
112
117
  return
113
118
  }
@@ -132,13 +137,13 @@ export class McpEventSink {
132
137
  uuid: uuidv7(),
133
138
  })
134
139
  }
135
- log(
140
+ this.logger(
136
141
  `Captured PostHog event ${fullEvent.id} | ${fullEvent.eventType} | ${fullEvent.duration} ms | ${
137
- fullEvent.identifyActorGivenId || 'anonymous'
142
+ fullEvent.identifyActorGivenId ? 'identified' : 'anonymous'
138
143
  }`
139
144
  )
140
145
  } catch (err) {
141
- log(`Failed to capture PostHog event ${fullEvent.id}: ${err}`)
146
+ this.logger(`Failed to capture PostHog event ${fullEvent.id}: ${err}`)
142
147
  }
143
148
  }
144
149
  }