@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.
- package/dist/extensions/capture.d.ts +2 -1
- package/dist/extensions/capture.d.ts.map +1 -1
- package/dist/extensions/capture.js +2 -3
- package/dist/extensions/capture.mjs +2 -3
- package/dist/extensions/compatibility.d.ts +3 -2
- package/dist/extensions/compatibility.d.ts.map +1 -1
- package/dist/extensions/compatibility.js +19 -20
- package/dist/extensions/compatibility.mjs +15 -16
- package/dist/extensions/context-parameters.d.ts +3 -2
- package/dist/extensions/context-parameters.d.ts.map +1 -1
- package/dist/extensions/context-parameters.js +4 -8
- package/dist/extensions/context-parameters.mjs +4 -7
- package/dist/extensions/conversation-id.d.ts +3 -2
- package/dist/extensions/conversation-id.d.ts.map +1 -1
- package/dist/extensions/conversation-id.js +5 -6
- package/dist/extensions/conversation-id.mjs +5 -5
- package/dist/extensions/instrument-highlevel.d.ts +2 -1
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-highlevel.js +29 -30
- package/dist/extensions/instrument-highlevel.mjs +30 -31
- package/dist/extensions/instrument-lowlevel.d.ts +2 -1
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-lowlevel.js +15 -11
- package/dist/extensions/instrument-lowlevel.mjs +16 -12
- package/dist/extensions/instrumentation.d.ts +9 -8
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +84 -40
- package/dist/extensions/instrumentation.mjs +80 -39
- package/dist/extensions/intent.d.ts.map +1 -1
- package/dist/extensions/intent.js +2 -3
- package/dist/extensions/intent.mjs +2 -3
- package/dist/extensions/internal.d.ts.map +1 -1
- package/dist/extensions/internal.js +5 -6
- package/dist/extensions/internal.mjs +5 -6
- package/dist/extensions/logger.d.ts +12 -4
- package/dist/extensions/logger.d.ts.map +1 -1
- package/dist/extensions/logger.js +12 -3
- package/dist/extensions/logger.mjs +10 -4
- package/dist/extensions/sink.d.ts +4 -2
- package/dist/extensions/sink.d.ts.map +1 -1
- package/dist/extensions/sink.js +11 -10
- package/dist/extensions/sink.mjs +11 -10
- package/dist/extensions/tools.d.ts +2 -1
- package/dist/extensions/tools.d.ts.map +1 -1
- package/dist/extensions/tools.js +2 -2
- package/dist/extensions/tools.mjs +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -16
- package/dist/index.mjs +19 -17
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +3 -3
- package/src/extensions/capture.ts +3 -3
- package/src/extensions/compatibility.ts +16 -16
- package/src/extensions/context-parameters.ts +8 -12
- package/src/extensions/conversation-id.ts +14 -7
- package/src/extensions/instrument-highlevel.ts +45 -44
- package/src/extensions/instrument-lowlevel.ts +30 -15
- package/src/extensions/instrumentation.ts +119 -42
- package/src/extensions/intent.ts +2 -8
- package/src/extensions/internal.ts +7 -6
- package/src/extensions/logger.ts +22 -7
- package/src/extensions/sink.ts +17 -12
- package/src/extensions/tools.ts +3 -3
- package/src/index.ts +21 -19
- package/src/types.ts +2 -0
- 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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
491
|
+
if (data) {
|
|
428
492
|
const missingToolName = resolveMissingCapabilityToolName(data.options)
|
|
429
|
-
|
|
430
|
-
if (
|
|
431
|
-
tools.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
678
|
+
extra: CompatibleRequestHandlerExtra | undefined,
|
|
679
|
+
logger: LoggerFn
|
|
603
680
|
): Promise<unknown> {
|
|
604
681
|
const data = getServerTrackingData(server)
|
|
605
682
|
if (!data) {
|
|
606
|
-
|
|
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
|
|
package/src/extensions/intent.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
173
|
-
captureEvent(server, identifyEvent)
|
|
171
|
+
data.logger(`Identified session ${sessionId}`)
|
|
172
|
+
captureEvent(server, identifyEvent, data.logger)
|
|
174
173
|
}
|
|
175
174
|
} else {
|
|
176
|
-
|
|
175
|
+
data.logger(`Warning: Supplied identify function returned null for session ${sessionId}`)
|
|
177
176
|
}
|
|
178
177
|
} catch (error) {
|
|
179
|
-
|
|
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
|
-
|
|
200
|
+
data.logger(`eventProperties callback error: ${e}`)
|
|
200
201
|
return null
|
|
201
202
|
}
|
|
202
203
|
}
|
package/src/extensions/logger.ts
CHANGED
|
@@ -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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
25
|
-
if (
|
|
25
|
+
function writeLog(logger: LoggerFn | undefined, message: string): void {
|
|
26
|
+
if (logger) {
|
|
26
27
|
try {
|
|
27
|
-
|
|
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
|
+
}
|
package/src/extensions/sink.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
140
|
+
this.logger(
|
|
136
141
|
`Captured PostHog event ${fullEvent.id} | ${fullEvent.eventType} | ${fullEvent.duration} ms | ${
|
|
137
|
-
fullEvent.identifyActorGivenId
|
|
142
|
+
fullEvent.identifyActorGivenId ? 'identified' : 'anonymous'
|
|
138
143
|
}`
|
|
139
144
|
)
|
|
140
145
|
} catch (err) {
|
|
141
|
-
|
|
146
|
+
this.logger(`Failed to capture PostHog event ${fullEvent.id}: ${err}`)
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
149
|
}
|
package/src/extensions/tools.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|