@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
@@ -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
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'
7
7
  import type {
8
+ AnalyticsParameterOwnership,
8
9
  CompatibleRequestHandlerExtra,
9
10
  MCPAnalyticsData,
10
11
  MCPRequestLike,
@@ -12,12 +13,12 @@ import type {
12
13
  McpEvent,
13
14
  ServerClientInfoLike,
14
15
  } from '../types'
16
+ import { getAnalyticsParameterOwnership, stripOwnedAnalyticsArguments } from './analytics-parameters'
15
17
  import { addContextParameterToTools, getContextDescription, isContextEnabled } from './context-parameters'
16
18
  import {
17
19
  addConversationIdToTools,
18
20
  type ConversationIdResolution,
19
21
  canInjectConversationIdPromptBack,
20
- cloneRequestWithoutConversationId,
21
22
  injectConversationIdPromptBack,
22
23
  resolveConversationId,
23
24
  } from './conversation-id'
@@ -27,7 +28,7 @@ import { MCPAnalyticsEventType } from './event-types'
27
28
  import { captureException } from './exceptions'
28
29
  import { resolveToolCallIntent, setEventIntent, setExplicitContextIntent } from './intent'
29
30
  import { getServerTrackingData, handleIdentify, setServerTrackingData } from './internal'
30
- import { log } from './logger'
31
+ import type { LoggerFn } from './logger'
31
32
  import { buildCapturedMcpParameters } from './mcp-payloads'
32
33
  import { getLiteralValue, getObjectShape } from './mcp-sdk-compat'
33
34
  import { getSessionId, newSessionId } from './session'
@@ -45,11 +46,7 @@ import { applyResolvedMetadata, isToolResultError } from './tracing-helpers'
45
46
 
46
47
  type MCPRequestHandler = (request: MCPRequestLike, extra?: CompatibleRequestHandlerExtra) => Promise<unknown>
47
48
 
48
- /**
49
- * Runs the underlying tool. Receives the request with the SDK-injected
50
- * `conversation_id` stripped; an adapter is free to ignore it (the high-level
51
- * path strips arguments inside the wrapped callback instead).
52
- */
49
+ /** Runs the underlying tool with SDK-owned analytics arguments removed. */
53
50
  type ToolExecutor = (downstreamRequest: MCPRequestLike) => Promise<unknown>
54
51
 
55
52
  interface TraceToolCallParams {
@@ -58,6 +55,8 @@ interface TraceToolCallParams {
58
55
  request: MCPRequestLike
59
56
  extra?: CompatibleRequestHandlerExtra
60
57
  execute: ToolExecutor
58
+ /** Optional schema-derived ownership override for adapters with direct registry access. */
59
+ parameterOwnership?: AnalyticsParameterOwnership
61
60
  /**
62
61
  * Event type to capture. Defaults to a tool call; the `get_more_tools` virtual
63
62
  * tool passes `mcpMissingCapability` so it records a capability gap rather than
@@ -88,15 +87,26 @@ interface TraceToolCallParams {
88
87
  * throws, and the tool's own errors are always re-thrown to the caller.
89
88
  */
90
89
  export async function captureToolCall(params: TraceToolCallParams): Promise<unknown> {
91
- const { server, data, request, extra, execute, eventType, explicitContextIntent, takeCapturedError } = params
92
-
90
+ const {
91
+ server,
92
+ data,
93
+ request,
94
+ extra,
95
+ execute,
96
+ parameterOwnership,
97
+ eventType,
98
+ explicitContextIntent,
99
+ takeCapturedError,
100
+ } = params
101
+
102
+ const ownership = getActiveAnalyticsParameterOwnership(data, request.params?.name, parameterOwnership)
93
103
  const conversation = resolveConversationId(
94
- data.options.enableConversationId ?? false,
104
+ ownership.conversationId,
95
105
  request.params?.arguments,
96
106
  request.params?.name,
97
107
  resolveMissingCapabilityToolName(data.options)
98
108
  )
99
- const downstreamRequest = conversation.conversationId ? cloneRequestWithoutConversationId(request) : request
109
+ const downstreamRequest = cloneRequestWithoutOwnedAnalyticsArguments(request, ownership)
100
110
 
101
111
  // Prepare the event in isolation: if identity/metadata/intent resolution
102
112
  // throws, we drop instrumentation for this call but still run the tool.
@@ -109,6 +119,7 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
109
119
  extra,
110
120
  startTime,
111
121
  conversation,
122
+ ownership,
112
123
  eventType ?? MCPAnalyticsEventType.mcpToolsCall
113
124
  )
114
125
  if (event && explicitContextIntent) {
@@ -119,15 +130,47 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
119
130
  try {
120
131
  result = await execute(downstreamRequest)
121
132
  } catch (error) {
122
- publishFailedToolEvent(server, event, error, startTime, conversation)
133
+ publishFailedToolEvent(server, event, error, startTime, conversation, data.logger)
123
134
  throw error
124
135
  }
125
136
 
126
137
  const finalResult = applyConversationPromptBack(event, result, conversation)
127
- publishSuccessfulToolEvent(server, event, finalResult, startTime, takeCapturedError)
138
+ publishSuccessfulToolEvent(server, event, finalResult, startTime, data.logger, takeCapturedError)
128
139
  return finalResult
129
140
  }
130
141
 
142
+ function getActiveAnalyticsParameterOwnership(
143
+ data: MCPAnalyticsData,
144
+ toolName: string | undefined,
145
+ override?: AnalyticsParameterOwnership
146
+ ): AnalyticsParameterOwnership {
147
+ const ownership = override ?? (toolName ? data.toolAnalyticsParameterOwnership.get(toolName) : undefined)
148
+ const isMissingCapabilityTool = toolName === resolveMissingCapabilityToolName(data.options)
149
+ return {
150
+ context: !isMissingCapabilityTool && isContextEnabled(data.options.context) && ownership?.context === true,
151
+ conversationId:
152
+ !isMissingCapabilityTool && data.options.enableConversationId === true && ownership?.conversationId === true,
153
+ }
154
+ }
155
+
156
+ function cloneRequestWithoutOwnedAnalyticsArguments(
157
+ request: MCPRequestLike,
158
+ ownership: AnalyticsParameterOwnership
159
+ ): MCPRequestLike {
160
+ const args = request.params?.arguments
161
+ const cleanedArgs = stripOwnedAnalyticsArguments(args, ownership)
162
+ if (cleanedArgs === args || !request.params) {
163
+ return request
164
+ }
165
+ return {
166
+ ...request,
167
+ params: {
168
+ ...request.params,
169
+ arguments: cleanedArgs as typeof request.params.arguments,
170
+ },
171
+ }
172
+ }
173
+
131
174
  async function prepareToolCallEvent(
132
175
  server: MCPServerLike,
133
176
  data: MCPAnalyticsData,
@@ -136,6 +179,7 @@ async function prepareToolCallEvent(
136
179
  extra: CompatibleRequestHandlerExtra | undefined,
137
180
  startTime: Date,
138
181
  conversation: ConversationIdResolution,
182
+ ownership: AnalyticsParameterOwnership,
139
183
  eventType: MCPAnalyticsEventType
140
184
  ): Promise<McpEvent | null> {
141
185
  try {
@@ -159,10 +203,10 @@ async function prepareToolCallEvent(
159
203
  stampMetaClientInfo(event, request)
160
204
 
161
205
  await applyResolvedMetadata(event, data, request, extra)
162
- setEventIntent(event, await resolveToolCallIntent(data, request, extra))
206
+ setEventIntent(event, await resolveToolCallIntent(data, request, ownership.context, extra))
163
207
  return event
164
208
  } catch (error) {
165
- log(
209
+ data.logger(
166
210
  `Warning: PostHog MCP analytics instrumentation failed for tool ${request.params?.name}, the tool will still run - ${error}`
167
211
  )
168
212
  return null
@@ -196,6 +240,7 @@ function publishSuccessfulToolEvent(
196
240
  event: McpEvent | null,
197
241
  result: unknown,
198
242
  startTime: Date,
243
+ logger: LoggerFn,
199
244
  takeCapturedError?: () => unknown
200
245
  ): void {
201
246
  if (!event) {
@@ -211,9 +256,9 @@ function publishSuccessfulToolEvent(
211
256
  }
212
257
  event.response = result
213
258
  event.duration = Date.now() - startTime.getTime()
214
- captureEvent(server, event)
259
+ captureEvent(server, event, logger)
215
260
  } catch (error) {
216
- log(`Warning: PostHog MCP analytics failed to publish tool event - ${error}`)
261
+ logger(`Warning: PostHog MCP analytics failed to publish tool event - ${error}`)
217
262
  }
218
263
  }
219
264
 
@@ -222,7 +267,8 @@ function publishFailedToolEvent(
222
267
  event: McpEvent | null,
223
268
  error: unknown,
224
269
  startTime: Date,
225
- conversation: ConversationIdResolution
270
+ conversation: ConversationIdResolution,
271
+ logger: LoggerFn
226
272
  ): void {
227
273
  if (!event) {
228
274
  return
@@ -234,9 +280,9 @@ function publishFailedToolEvent(
234
280
  event.isError = true
235
281
  event.error = captureException(error)
236
282
  event.duration = Date.now() - startTime.getTime()
237
- captureEvent(server, event)
283
+ captureEvent(server, event, logger)
238
284
  } catch (publishError) {
239
- 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}`)
240
286
  }
241
287
  }
242
288
 
@@ -292,7 +338,8 @@ export async function handleListToolsRequest(
292
338
  server: MCPServerLike,
293
339
  originalListToolsHandler: MCPRequestHandler,
294
340
  request: MCPRequestLike,
295
- extra?: CompatibleRequestHandlerExtra
341
+ extra: CompatibleRequestHandlerExtra | undefined,
342
+ logger: LoggerFn
296
343
  ): Promise<{ tools: ListToolsResult['tools'] }> {
297
344
  const data = getServerTrackingData(server)
298
345
  const startTime = new Date()
@@ -308,23 +355,23 @@ export async function handleListToolsRequest(
308
355
  await applyResolvedMetadata(event, data, request, extra)
309
356
  }
310
357
 
311
- const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event)
358
+ const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event, logger)
312
359
 
313
360
  if (!data) {
314
- log(
361
+ logger(
315
362
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
316
363
  )
317
364
  return { tools }
318
365
  }
319
366
 
320
367
  if (tools.length === 0) {
321
- log(
368
+ data.logger(
322
369
  'Warning: No tools found in the original list. This is likely due to the tools not being registered before PostHog MCP analytics.instrument().'
323
370
  )
324
371
  event.error = captureException('No tools were sent to MCP client.')
325
372
  event.isError = true
326
373
  event.duration = Date.now() - startTime.getTime()
327
- captureEvent(server, event)
374
+ captureEvent(server, event, data.logger)
328
375
  return { tools }
329
376
  }
330
377
 
@@ -332,10 +379,22 @@ export async function handleListToolsRequest(
332
379
  event.listedToolNames = collectListedToolNames(tools)
333
380
  event.isError = false
334
381
  event.duration = Date.now() - startTime.getTime()
335
- captureEvent(server, event)
382
+ captureEvent(server, event, data.logger)
336
383
  return { tools }
337
384
  }
338
385
 
386
+ function cacheToolAnalyticsParameterOwnership(
387
+ cache: Map<string, AnalyticsParameterOwnership>,
388
+ tools: ListToolsResult['tools']
389
+ ): void {
390
+ // Merge pages and concurrent enumerations; repeated tool names overwrite stale schemas.
391
+ for (const tool of tools) {
392
+ if (tool?.name) {
393
+ cache.set(tool.name, getAnalyticsParameterOwnership(tool.inputSchema))
394
+ }
395
+ }
396
+ }
397
+
339
398
  function collectListedToolNames(tools: ListToolsResult['tools'] | undefined): string[] | undefined {
340
399
  if (!tools || tools.length === 0) {
341
400
  return
@@ -349,19 +408,24 @@ async function getTracedToolsList(
349
408
  originalListToolsHandler: MCPRequestHandler,
350
409
  request: MCPRequestLike,
351
410
  extra: CompatibleRequestHandlerExtra | undefined,
352
- event: McpEvent
411
+ event: McpEvent,
412
+ logger: LoggerFn
353
413
  ): Promise<ListToolsResult['tools']> {
354
414
  try {
355
415
  const data = getServerTrackingData(server)
356
416
  const originalResponse = (await originalListToolsHandler(request, extra)) as ListToolsResult
357
417
  let tools = originalResponse.tools || []
358
418
 
419
+ if (data) {
420
+ cacheToolAnalyticsParameterOwnership(data.toolAnalyticsParameterOwnership, tools)
421
+ }
422
+
359
423
  if (data && isContextEnabled(data.options.context)) {
360
- tools = addContextParameterToTools(tools, getContextDescription(data.options.context))
424
+ tools = addContextParameterToTools(tools, getContextDescription(data.options.context), data.logger)
361
425
  }
362
426
 
363
427
  if (data?.options.enableConversationId) {
364
- tools = addConversationIdToTools(tools, resolveMissingCapabilityToolName(data.options))
428
+ tools = addConversationIdToTools(tools, resolveMissingCapabilityToolName(data.options), data.logger)
365
429
  }
366
430
 
367
431
  if (data?.options.reportMissing) {
@@ -379,13 +443,13 @@ async function getTracedToolsList(
379
443
 
380
444
  return tools
381
445
  } catch (error) {
382
- log(
446
+ logger(
383
447
  `Warning: Original list tools handler failed, this suggests an error PostHog MCP analytics did not cause - ${error}`
384
448
  )
385
449
  event.error = captureException(error)
386
450
  event.isError = true
387
451
  event.duration = event.timestamp ? Date.now() - event.timestamp.getTime() : 0
388
- captureEvent(server, event)
452
+ captureEvent(server, event, logger)
389
453
  throw error
390
454
  }
391
455
  }
@@ -481,7 +545,7 @@ function mintStatelessSessionOnInitialize(
481
545
  setServerTrackingData(server, data)
482
546
  return sessionId
483
547
  } catch (error) {
484
- 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}`)
485
549
  return undefined
486
550
  }
487
551
  }
@@ -511,7 +575,7 @@ function upgradeMintedTokenToNegotiated(
511
575
  })
512
576
  writeSessionIdToTransport(transport, token)
513
577
  } catch (error) {
514
- 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}`)
515
579
  }
516
580
  }
517
581
 
@@ -539,11 +603,12 @@ export async function handleInitializeRequest(
539
603
  server: MCPServerLike,
540
604
  originalInitializeHandler: MCPRequestHandler,
541
605
  request: MCPRequestLike,
542
- extra?: CompatibleRequestHandlerExtra
606
+ extra: CompatibleRequestHandlerExtra | undefined,
607
+ logger: LoggerFn
543
608
  ): Promise<unknown> {
544
609
  const data = getServerTrackingData(server)
545
610
  if (!data) {
546
- log(
611
+ logger(
547
612
  'Warning: PostHog MCP analytics is unable to find server tracking data. Please ensure you have called instrument(server, options) before using tool calls.'
548
613
  )
549
614
  return await originalInitializeHandler(request, extra)
@@ -580,7 +645,7 @@ export async function handleInitializeRequest(
580
645
  if (mintedSessionId) {
581
646
  upgradeMintedTokenToNegotiated(server, data, mintedSessionId, negotiatedProtocolVersion)
582
647
  }
583
- captureEvent(server, event)
648
+ captureEvent(server, event, data.logger)
584
649
  return result
585
650
  }
586
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
  }
@@ -53,10 +52,16 @@ async function runIntentFallback(
53
52
  export async function resolveToolCallIntent(
54
53
  data: MCPAnalyticsData,
55
54
  request: MCPRequestLike,
55
+ analyticsOwnsContext: boolean,
56
56
  extra?: CompatibleRequestHandlerExtra
57
57
  ): Promise<ResolvedIntent | null> {
58
58
  const contextArgument = getContextArgument(request)
59
- if (isContextEnabled(data.options.context) && request.params?.name !== 'get_more_tools' && contextArgument) {
59
+ if (
60
+ analyticsOwnsContext &&
61
+ isContextEnabled(data.options.context) &&
62
+ request.params?.name !== 'get_more_tools' &&
63
+ contextArgument
64
+ ) {
60
65
  return { intent: contextArgument, source: 'context_parameter' }
61
66
  }
62
67
 
@@ -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
  }