@posthog/mcp 0.10.4 → 0.10.6

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 -8
  12. package/dist/extensions/context-parameters.mjs +4 -7
  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 +5 -6
  16. package/dist/extensions/conversation-id.mjs +5 -5
  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 +29 -30
  20. package/dist/extensions/instrument-highlevel.mjs +30 -31
  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 +15 -11
  24. package/dist/extensions/instrument-lowlevel.mjs +16 -12
  25. package/dist/extensions/instrumentation.d.ts +9 -8
  26. package/dist/extensions/instrumentation.d.ts.map +1 -1
  27. package/dist/extensions/instrumentation.js +84 -40
  28. package/dist/extensions/instrumentation.mjs +80 -39
  29. package/dist/extensions/intent.d.ts.map +1 -1
  30. package/dist/extensions/intent.js +2 -3
  31. package/dist/extensions/intent.mjs +2 -3
  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 +3 -3
  56. package/src/extensions/capture.ts +3 -3
  57. package/src/extensions/compatibility.ts +16 -16
  58. package/src/extensions/context-parameters.ts +8 -12
  59. package/src/extensions/conversation-id.ts +14 -7
  60. package/src/extensions/instrument-highlevel.ts +45 -44
  61. package/src/extensions/instrument-lowlevel.ts +30 -15
  62. package/src/extensions/instrumentation.ts +119 -42
  63. package/src/extensions/intent.ts +2 -8
  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
@@ -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'
@@ -98,13 +98,20 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
98
98
  explicitContextIntent,
99
99
  takeCapturedError,
100
100
  } = params
101
-
102
- const ownership = getActiveAnalyticsParameterOwnership(data, request.params?.name, parameterOwnership)
101
+ const resolvedEventType = eventType ?? MCPAnalyticsEventType.mcpToolsCall
102
+ const ownership = getActiveAnalyticsParameterOwnership(
103
+ data,
104
+ request.params?.name,
105
+ parameterOwnership,
106
+ resolvedEventType === MCPAnalyticsEventType.mcpMissingCapability
107
+ )
103
108
  const conversation = resolveConversationId(
104
109
  ownership.conversationId,
105
110
  request.params?.arguments,
106
111
  request.params?.name,
107
- resolveMissingCapabilityToolName(data.options)
112
+ resolvedEventType === MCPAnalyticsEventType.mcpMissingCapability
113
+ ? resolveMissingCapabilityToolName(data.options)
114
+ : ''
108
115
  )
109
116
  const downstreamRequest = cloneRequestWithoutOwnedAnalyticsArguments(request, ownership)
110
117
 
@@ -120,7 +127,7 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
120
127
  startTime,
121
128
  conversation,
122
129
  ownership,
123
- eventType ?? MCPAnalyticsEventType.mcpToolsCall
130
+ resolvedEventType
124
131
  )
125
132
  if (event && explicitContextIntent) {
126
133
  setExplicitContextIntent(event, explicitContextIntent)
@@ -130,22 +137,22 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
130
137
  try {
131
138
  result = await execute(downstreamRequest)
132
139
  } catch (error) {
133
- publishFailedToolEvent(server, event, error, startTime, conversation)
140
+ publishFailedToolEvent(server, event, error, startTime, conversation, data.logger)
134
141
  throw error
135
142
  }
136
143
 
137
144
  const finalResult = applyConversationPromptBack(event, result, conversation)
138
- publishSuccessfulToolEvent(server, event, finalResult, startTime, takeCapturedError)
145
+ publishSuccessfulToolEvent(server, event, finalResult, startTime, data.logger, takeCapturedError)
139
146
  return finalResult
140
147
  }
141
148
 
142
149
  function getActiveAnalyticsParameterOwnership(
143
150
  data: MCPAnalyticsData,
144
151
  toolName: string | undefined,
145
- override?: AnalyticsParameterOwnership
152
+ override: AnalyticsParameterOwnership | undefined,
153
+ isMissingCapabilityTool: boolean
146
154
  ): AnalyticsParameterOwnership {
147
155
  const ownership = override ?? (toolName ? data.toolAnalyticsParameterOwnership.get(toolName) : undefined)
148
- const isMissingCapabilityTool = toolName === resolveMissingCapabilityToolName(data.options)
149
156
  return {
150
157
  context: !isMissingCapabilityTool && isContextEnabled(data.options.context) && ownership?.context === true,
151
158
  conversationId:
@@ -206,7 +213,7 @@ async function prepareToolCallEvent(
206
213
  setEventIntent(event, await resolveToolCallIntent(data, request, ownership.context, extra))
207
214
  return event
208
215
  } catch (error) {
209
- log(
216
+ data.logger(
210
217
  `Warning: PostHog MCP analytics instrumentation failed for tool ${request.params?.name}, the tool will still run - ${error}`
211
218
  )
212
219
  return null
@@ -240,6 +247,7 @@ function publishSuccessfulToolEvent(
240
247
  event: McpEvent | null,
241
248
  result: unknown,
242
249
  startTime: Date,
250
+ logger: LoggerFn,
243
251
  takeCapturedError?: () => unknown
244
252
  ): void {
245
253
  if (!event) {
@@ -255,9 +263,9 @@ function publishSuccessfulToolEvent(
255
263
  }
256
264
  event.response = result
257
265
  event.duration = Date.now() - startTime.getTime()
258
- captureEvent(server, event)
266
+ captureEvent(server, event, logger)
259
267
  } catch (error) {
260
- log(`Warning: PostHog MCP analytics failed to publish tool event - ${error}`)
268
+ logger(`Warning: PostHog MCP analytics failed to publish tool event - ${error}`)
261
269
  }
262
270
  }
263
271
 
@@ -266,7 +274,8 @@ function publishFailedToolEvent(
266
274
  event: McpEvent | null,
267
275
  error: unknown,
268
276
  startTime: Date,
269
- conversation: ConversationIdResolution
277
+ conversation: ConversationIdResolution,
278
+ logger: LoggerFn
270
279
  ): void {
271
280
  if (!event) {
272
281
  return
@@ -278,9 +287,9 @@ function publishFailedToolEvent(
278
287
  event.isError = true
279
288
  event.error = captureException(error)
280
289
  event.duration = Date.now() - startTime.getTime()
281
- captureEvent(server, event)
290
+ captureEvent(server, event, logger)
282
291
  } catch (publishError) {
283
- log(`Warning: PostHog MCP analytics failed to publish failed tool event - ${publishError}`)
292
+ logger(`Warning: PostHog MCP analytics failed to publish failed tool event - ${publishError}`)
284
293
  }
285
294
  }
286
295
 
@@ -305,11 +314,27 @@ export type HandlerPatch = (
305
314
  * that register handlers post-construction work — e.g. `@rekog/mcp-nest` hands a
306
315
  * bare server to instrument() and only then registers its handlers.
307
316
  */
317
+ const originalRequestHandlers = new WeakMap<MCPServerLike, Map<string, MCPRequestHandler>>()
318
+
319
+ function rememberOriginalRequestHandler(
320
+ server: MCPServerLike,
321
+ handlerName: string,
322
+ originalHandler: MCPRequestHandler
323
+ ): void {
324
+ let handlers = originalRequestHandlers.get(server)
325
+ if (!handlers) {
326
+ handlers = new Map()
327
+ originalRequestHandlers.set(server, handlers)
328
+ }
329
+ handlers.set(handlerName, originalHandler)
330
+ }
331
+
308
332
  export function patchRequestHandlers(server: MCPServerLike, patches: Record<string, HandlerPatch>): void {
309
333
  // Monkey patch existing handlers.
310
334
  for (const [handlerName, patch] of Object.entries(patches)) {
311
335
  const originalHandler = server._requestHandlers.get(handlerName)
312
336
  if (originalHandler) {
337
+ rememberOriginalRequestHandler(server, handlerName, originalHandler)
313
338
  server._requestHandlers.set(handlerName, (request, extra) => patch(server, originalHandler, request, extra))
314
339
  }
315
340
  }
@@ -320,14 +345,55 @@ export function patchRequestHandlers(server: MCPServerLike, patches: Record<stri
320
345
  const shape = getObjectShape(requestSchema)
321
346
  const handlerName = shape?.method ? getLiteralValue(shape.method) : undefined
322
347
  const patch = typeof handlerName === 'string' ? patches[handlerName] : undefined
323
- if (!patch) {
348
+ if (!patch || typeof handlerName !== 'string') {
324
349
  return originalSetRequestHandler(requestSchema, originalHandler)
325
350
  }
326
351
 
327
- return originalSetRequestHandler(requestSchema, (request, extra) => patch(server, originalHandler, request, extra))
352
+ // Register first so the MCP SDK's request/result validation stays inside
353
+ // our analytics wrapper, matching handlers that existed before instrument().
354
+ const result = originalSetRequestHandler(requestSchema, originalHandler)
355
+ const registeredHandler = server._requestHandlers.get(handlerName)
356
+ if (registeredHandler) {
357
+ rememberOriginalRequestHandler(server, handlerName, registeredHandler)
358
+ server._requestHandlers.set(handlerName, (request, extra) => patch(server, registeredHandler, request, extra))
359
+ }
360
+ return result
328
361
  }) as MCPServerLike['setRequestHandler']
329
362
  }
330
363
 
364
+ /**
365
+ * Checks the server's raw listing for a real owner of a candidate virtual tool.
366
+ * This does not depend on a previous client request and does not call the
367
+ * instrumented list wrapper, so it neither injects PostHog tools nor captures a
368
+ * synthetic tools/list event. `undefined` fails open to the real dispatcher.
369
+ */
370
+ export async function isToolAdvertised(
371
+ server: MCPServerLike,
372
+ toolName: string,
373
+ extra: CompatibleRequestHandlerExtra | undefined,
374
+ logger: LoggerFn
375
+ ): Promise<boolean | undefined> {
376
+ const listHandler = originalRequestHandlers.get(server)?.get('tools/list')
377
+ if (!listHandler || !server._requestHandlers.has('tools/list')) {
378
+ return undefined
379
+ }
380
+
381
+ try {
382
+ const response = (await listHandler({ method: 'tools/list', params: {} }, extra)) as ListToolsResult
383
+ if (!response || !Array.isArray(response.tools)) {
384
+ return undefined
385
+ }
386
+ // Match the page the current list instrumentation can expose. Pagination
387
+ // passthrough is handled separately from missing-capability ownership.
388
+ return response.tools.some((tool) => tool?.name === toolName)
389
+ } catch (error) {
390
+ logger(
391
+ `Warning: PostHog MCP analytics could not determine whether "${toolName}" is advertised; delegating to the server - ${error}`
392
+ )
393
+ return undefined
394
+ }
395
+ }
396
+
331
397
  /**
332
398
  * Captures each `tools/list` and injects the SDK-managed tools (context
333
399
  * parameter, conversation id, `get_more_tools`) into the returned list.
@@ -336,7 +402,8 @@ export async function handleListToolsRequest(
336
402
  server: MCPServerLike,
337
403
  originalListToolsHandler: MCPRequestHandler,
338
404
  request: MCPRequestLike,
339
- extra?: CompatibleRequestHandlerExtra
405
+ extra: CompatibleRequestHandlerExtra | undefined,
406
+ logger: LoggerFn
340
407
  ): Promise<{ tools: ListToolsResult['tools'] }> {
341
408
  const data = getServerTrackingData(server)
342
409
  const startTime = new Date()
@@ -352,23 +419,23 @@ export async function handleListToolsRequest(
352
419
  await applyResolvedMetadata(event, data, request, extra)
353
420
  }
354
421
 
355
- const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event)
422
+ const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event, logger)
356
423
 
357
424
  if (!data) {
358
- log(
425
+ logger(
359
426
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
360
427
  )
361
428
  return { tools }
362
429
  }
363
430
 
364
431
  if (tools.length === 0) {
365
- log(
432
+ data.logger(
366
433
  'Warning: No tools found in the original list. This is likely due to the tools not being registered before PostHog MCP analytics.instrument().'
367
434
  )
368
435
  event.error = captureException('No tools were sent to MCP client.')
369
436
  event.isError = true
370
437
  event.duration = Date.now() - startTime.getTime()
371
- captureEvent(server, event)
438
+ captureEvent(server, event, data.logger)
372
439
  return { tools }
373
440
  }
374
441
 
@@ -376,7 +443,7 @@ export async function handleListToolsRequest(
376
443
  event.listedToolNames = collectListedToolNames(tools)
377
444
  event.isError = false
378
445
  event.duration = Date.now() - startTime.getTime()
379
- captureEvent(server, event)
446
+ captureEvent(server, event, data.logger)
380
447
  return { tools }
381
448
  }
382
449
 
@@ -405,30 +472,39 @@ async function getTracedToolsList(
405
472
  originalListToolsHandler: MCPRequestHandler,
406
473
  request: MCPRequestLike,
407
474
  extra: CompatibleRequestHandlerExtra | undefined,
408
- event: McpEvent
475
+ event: McpEvent,
476
+ logger: LoggerFn
409
477
  ): Promise<ListToolsResult['tools']> {
410
478
  try {
411
479
  const data = getServerTrackingData(server)
412
480
  const originalResponse = (await originalListToolsHandler(request, extra)) as ListToolsResult
413
- let tools = originalResponse.tools || []
481
+ // Injection must not mutate arrays reused or frozen by the server.
482
+ let tools = [...(originalResponse.tools || [])]
414
483
 
415
484
  if (data) {
416
485
  cacheToolAnalyticsParameterOwnership(data.toolAnalyticsParameterOwnership, tools)
417
486
  }
418
-
419
487
  if (data && isContextEnabled(data.options.context)) {
420
- tools = addContextParameterToTools(tools, getContextDescription(data.options.context))
421
- }
422
-
423
- if (data?.options.enableConversationId) {
424
- tools = addConversationIdToTools(tools, resolveMissingCapabilityToolName(data.options))
488
+ tools = addContextParameterToTools(tools, getContextDescription(data.options.context), data.logger)
425
489
  }
426
490
 
427
- if (data?.options.reportMissing) {
491
+ if (data) {
428
492
  const missingToolName = resolveMissingCapabilityToolName(data.options)
429
- const alreadyPresent = tools.some((tool) => tool?.name === missingToolName)
430
- if (!alreadyPresent) {
431
- tools.push(getReportMissingToolDescriptor(missingToolName))
493
+ let injectedMissingCapabilityTool = false
494
+ if (data.options.reportMissing) {
495
+ const alreadyPresent = tools.some((tool) => tool?.name === missingToolName)
496
+ if (alreadyPresent) {
497
+ data.logger(
498
+ `Warning: Cannot inject missing-capability tool "${missingToolName}" because a real tool already uses that name. The real tool will not be intercepted.`
499
+ )
500
+ } else {
501
+ tools.push(getReportMissingToolDescriptor(missingToolName))
502
+ injectedMissingCapabilityTool = true
503
+ }
504
+ }
505
+
506
+ if (data.options.enableConversationId) {
507
+ tools = addConversationIdToTools(tools, missingToolName, injectedMissingCapabilityTool, data.logger)
432
508
  }
433
509
  }
434
510
 
@@ -439,13 +515,13 @@ async function getTracedToolsList(
439
515
 
440
516
  return tools
441
517
  } catch (error) {
442
- log(
518
+ logger(
443
519
  `Warning: Original list tools handler failed, this suggests an error PostHog MCP analytics did not cause - ${error}`
444
520
  )
445
521
  event.error = captureException(error)
446
522
  event.isError = true
447
523
  event.duration = event.timestamp ? Date.now() - event.timestamp.getTime() : 0
448
- captureEvent(server, event)
524
+ captureEvent(server, event, logger)
449
525
  throw error
450
526
  }
451
527
  }
@@ -541,7 +617,7 @@ function mintStatelessSessionOnInitialize(
541
617
  setServerTrackingData(server, data)
542
618
  return sessionId
543
619
  } catch (error) {
544
- log(`Warning: PostHog MCP analytics failed to mint a stateless session id - ${error}`)
620
+ data.logger(`Warning: PostHog MCP analytics failed to mint a stateless session id - ${error}`)
545
621
  return undefined
546
622
  }
547
623
  }
@@ -571,7 +647,7 @@ function upgradeMintedTokenToNegotiated(
571
647
  })
572
648
  writeSessionIdToTransport(transport, token)
573
649
  } catch (error) {
574
- log(`Warning: PostHog MCP analytics failed to upgrade the stateless session token - ${error}`)
650
+ data.logger(`Warning: PostHog MCP analytics failed to upgrade the stateless session token - ${error}`)
575
651
  }
576
652
  }
577
653
 
@@ -599,11 +675,12 @@ export async function handleInitializeRequest(
599
675
  server: MCPServerLike,
600
676
  originalInitializeHandler: MCPRequestHandler,
601
677
  request: MCPRequestLike,
602
- extra?: CompatibleRequestHandlerExtra
678
+ extra: CompatibleRequestHandlerExtra | undefined,
679
+ logger: LoggerFn
603
680
  ): Promise<unknown> {
604
681
  const data = getServerTrackingData(server)
605
682
  if (!data) {
606
- log(
683
+ logger(
607
684
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
608
685
  )
609
686
  return await originalInitializeHandler(request, extra)
@@ -640,7 +717,7 @@ export async function handleInitializeRequest(
640
717
  if (mintedSessionId) {
641
718
  upgradeMintedTokenToNegotiated(server, data, mintedSessionId, negotiatedProtocolVersion)
642
719
  }
643
- captureEvent(server, event)
720
+ captureEvent(server, event, data.logger)
644
721
  return result
645
722
  }
646
723
 
@@ -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
  }
@@ -57,12 +56,7 @@ export async function resolveToolCallIntent(
57
56
  extra?: CompatibleRequestHandlerExtra
58
57
  ): Promise<ResolvedIntent | null> {
59
58
  const contextArgument = getContextArgument(request)
60
- if (
61
- analyticsOwnsContext &&
62
- isContextEnabled(data.options.context) &&
63
- request.params?.name !== 'get_more_tools' &&
64
- contextArgument
65
- ) {
59
+ if (analyticsOwnsContext && isContextEnabled(data.options.context) && contextArgument) {
66
60
  return { intent: contextArgument, source: 'context_parameter' }
67
61
  }
68
62
 
@@ -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
  }
@@ -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 CallToolResult, type ListToolsResult } from '@modelcontextprotocol/sdk/types.js'
7
- import { log } from './logger'
7
+ import { log, type LoggerFn } from './logger'
8
8
 
9
9
  export const GET_MORE_TOOLS_NAME = 'get_more_tools' as const
10
10
 
@@ -67,7 +67,7 @@ export function getMoreToolsResult(): CallToolResult {
67
67
  }
68
68
  }
69
69
 
70
- export function handleReportMissing(args: { context: string }): CallToolResult {
71
- log(`Missing tool reported: ${JSON.stringify(args)}`)
70
+ export function handleReportMissing(args: { context: string }, logger: LoggerFn = log): CallToolResult {
71
+ logger(`Missing tool reported: ${JSON.stringify(args)}`)
72
72
  return getMoreToolsResult()
73
73
  }