@posthog/mcp 0.10.5 → 0.10.7

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 (44) hide show
  1. package/dist/extensions/capture.d.ts +2 -2
  2. package/dist/extensions/capture.d.ts.map +1 -1
  3. package/dist/extensions/capture.js +2 -2
  4. package/dist/extensions/capture.mjs +2 -2
  5. package/dist/extensions/context-parameters.d.ts.map +1 -1
  6. package/dist/extensions/context-parameters.js +1 -4
  7. package/dist/extensions/context-parameters.mjs +1 -4
  8. package/dist/extensions/conversation-id.d.ts +1 -1
  9. package/dist/extensions/conversation-id.d.ts.map +1 -1
  10. package/dist/extensions/conversation-id.js +2 -2
  11. package/dist/extensions/conversation-id.mjs +2 -2
  12. package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
  13. package/dist/extensions/instrument-highlevel.js +5 -5
  14. package/dist/extensions/instrument-highlevel.mjs +6 -6
  15. package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
  16. package/dist/extensions/instrument-lowlevel.js +9 -4
  17. package/dist/extensions/instrument-lowlevel.mjs +10 -5
  18. package/dist/extensions/instrumentation.d.ts +6 -6
  19. package/dist/extensions/instrumentation.d.ts.map +1 -1
  20. package/dist/extensions/instrumentation.js +112 -42
  21. package/dist/extensions/instrumentation.mjs +110 -43
  22. package/dist/extensions/intent.d.ts.map +1 -1
  23. package/dist/extensions/intent.js +1 -1
  24. package/dist/extensions/intent.mjs +1 -1
  25. package/dist/extensions/internal.d.ts +3 -2
  26. package/dist/extensions/internal.d.ts.map +1 -1
  27. package/dist/extensions/internal.js +25 -9
  28. package/dist/extensions/internal.mjs +19 -6
  29. package/dist/extensions/session.js +3 -1
  30. package/dist/extensions/session.mjs +3 -1
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.js +1 -1
  33. package/dist/version.mjs +1 -1
  34. package/package.json +2 -2
  35. package/src/extensions/capture.ts +13 -6
  36. package/src/extensions/context-parameters.ts +1 -7
  37. package/src/extensions/conversation-id.ts +2 -1
  38. package/src/extensions/instrument-highlevel.ts +8 -13
  39. package/src/extensions/instrument-lowlevel.ts +17 -6
  40. package/src/extensions/instrumentation.ts +166 -51
  41. package/src/extensions/intent.ts +1 -6
  42. package/src/extensions/internal.ts +21 -6
  43. package/src/extensions/session.ts +1 -1
  44. package/src/version.ts +1 -1
@@ -12,6 +12,7 @@ import type {
12
12
  MCPServerLike,
13
13
  McpEvent,
14
14
  ServerClientInfoLike,
15
+ SessionInfo,
15
16
  } from '../types'
16
17
  import { getAnalyticsParameterOwnership, stripOwnedAnalyticsArguments } from './analytics-parameters'
17
18
  import { addContextParameterToTools, getContextDescription, isContextEnabled } from './context-parameters'
@@ -27,11 +28,11 @@ import { captureEvent } from './capture'
27
28
  import { MCPAnalyticsEventType } from './event-types'
28
29
  import { captureException } from './exceptions'
29
30
  import { resolveToolCallIntent, setEventIntent, setExplicitContextIntent } from './intent'
30
- import { getServerTrackingData, handleIdentify, setServerTrackingData } from './internal'
31
+ import { getServerTrackingData, handleIdentify, setServerTrackingData, withIdentity } from './internal'
31
32
  import type { LoggerFn } from './logger'
32
33
  import { buildCapturedMcpParameters } from './mcp-payloads'
33
34
  import { getLiteralValue, getObjectShape } from './mcp-sdk-compat'
34
- import { getSessionId, newSessionId } from './session'
35
+ import { getSessionId, getSessionInfo, newSessionId } from './session'
35
36
  import { encodeSessionId, readMcpSessionHeader, writeSessionIdToTransport } from './session-token'
36
37
  import { getReportMissingToolDescriptor, resolveMissingCapabilityToolName } from './tools'
37
38
  import { applyResolvedMetadata, isToolResultError } from './tracing-helpers'
@@ -98,20 +99,27 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
98
99
  explicitContextIntent,
99
100
  takeCapturedError,
100
101
  } = params
101
-
102
- const ownership = getActiveAnalyticsParameterOwnership(data, request.params?.name, parameterOwnership)
102
+ const resolvedEventType = eventType ?? MCPAnalyticsEventType.mcpToolsCall
103
+ const ownership = getActiveAnalyticsParameterOwnership(
104
+ data,
105
+ request.params?.name,
106
+ parameterOwnership,
107
+ resolvedEventType === MCPAnalyticsEventType.mcpMissingCapability
108
+ )
103
109
  const conversation = resolveConversationId(
104
110
  ownership.conversationId,
105
111
  request.params?.arguments,
106
112
  request.params?.name,
107
- resolveMissingCapabilityToolName(data.options)
113
+ resolvedEventType === MCPAnalyticsEventType.mcpMissingCapability
114
+ ? resolveMissingCapabilityToolName(data.options)
115
+ : ''
108
116
  )
109
117
  const downstreamRequest = cloneRequestWithoutOwnedAnalyticsArguments(request, ownership)
110
118
 
111
119
  // Prepare the event in isolation: if identity/metadata/intent resolution
112
120
  // throws, we drop instrumentation for this call but still run the tool.
113
121
  const startTime = new Date()
114
- const event = await prepareToolCallEvent(
122
+ const preparedEvent = await prepareToolCallEvent(
115
123
  server,
116
124
  data,
117
125
  request,
@@ -120,32 +128,37 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
120
128
  startTime,
121
129
  conversation,
122
130
  ownership,
123
- eventType ?? MCPAnalyticsEventType.mcpToolsCall
131
+ resolvedEventType
124
132
  )
125
- if (event && explicitContextIntent) {
126
- setExplicitContextIntent(event, explicitContextIntent)
133
+ if (preparedEvent && explicitContextIntent) {
134
+ setExplicitContextIntent(preparedEvent.event, explicitContextIntent)
127
135
  }
128
136
 
129
137
  let result: unknown
130
138
  try {
131
139
  result = await execute(downstreamRequest)
132
140
  } catch (error) {
133
- publishFailedToolEvent(server, event, error, startTime, conversation, data.logger)
141
+ publishFailedToolEvent(server, preparedEvent, error, startTime, conversation, data.logger)
134
142
  throw error
135
143
  }
136
144
 
137
- const finalResult = applyConversationPromptBack(event, result, conversation)
138
- publishSuccessfulToolEvent(server, event, finalResult, startTime, data.logger, takeCapturedError)
145
+ const finalResult = applyConversationPromptBack(preparedEvent?.event ?? null, result, conversation)
146
+ publishSuccessfulToolEvent(server, preparedEvent, finalResult, startTime, data.logger, takeCapturedError)
139
147
  return finalResult
140
148
  }
141
149
 
150
+ interface PreparedToolEvent {
151
+ event: McpEvent
152
+ requestAttribution: SessionInfo
153
+ }
154
+
142
155
  function getActiveAnalyticsParameterOwnership(
143
156
  data: MCPAnalyticsData,
144
157
  toolName: string | undefined,
145
- override?: AnalyticsParameterOwnership
158
+ override: AnalyticsParameterOwnership | undefined,
159
+ isMissingCapabilityTool: boolean
146
160
  ): AnalyticsParameterOwnership {
147
161
  const ownership = override ?? (toolName ? data.toolAnalyticsParameterOwnership.get(toolName) : undefined)
148
- const isMissingCapabilityTool = toolName === resolveMissingCapabilityToolName(data.options)
149
162
  return {
150
163
  context: !isMissingCapabilityTool && isContextEnabled(data.options.context) && ownership?.context === true,
151
164
  conversationId:
@@ -181,10 +194,14 @@ async function prepareToolCallEvent(
181
194
  conversation: ConversationIdResolution,
182
195
  ownership: AnalyticsParameterOwnership,
183
196
  eventType: MCPAnalyticsEventType
184
- ): Promise<McpEvent | null> {
197
+ ): Promise<PreparedToolEvent | null> {
185
198
  try {
186
199
  const sessionId = getSessionId(server, extra)
187
- await handleIdentify(server, data, sessionId, request, extra)
200
+ // Snapshot token/client/protocol metadata synchronously, before identify or
201
+ // metadata callbacks can yield and let another request replace shared state.
202
+ const sessionInfo = getSessionInfo(server, data, sessionId)
203
+ const identity = await handleIdentify(server, data, sessionId, request, sessionInfo, extra)
204
+ const requestAttribution = withIdentity(sessionInfo, identity)
188
205
 
189
206
  const toolName = request.params?.name
190
207
  const event: McpEvent = {
@@ -204,7 +221,7 @@ async function prepareToolCallEvent(
204
221
 
205
222
  await applyResolvedMetadata(event, data, request, extra)
206
223
  setEventIntent(event, await resolveToolCallIntent(data, request, ownership.context, extra))
207
- return event
224
+ return { event, requestAttribution }
208
225
  } catch (error) {
209
226
  data.logger(
210
227
  `Warning: PostHog MCP analytics instrumentation failed for tool ${request.params?.name}, the tool will still run - ${error}`
@@ -237,15 +254,16 @@ function applyConversationPromptBack(
237
254
 
238
255
  function publishSuccessfulToolEvent(
239
256
  server: MCPServerLike,
240
- event: McpEvent | null,
257
+ preparedEvent: PreparedToolEvent | null,
241
258
  result: unknown,
242
259
  startTime: Date,
243
260
  logger: LoggerFn,
244
261
  takeCapturedError?: () => unknown
245
262
  ): void {
246
- if (!event) {
263
+ if (!preparedEvent) {
247
264
  return
248
265
  }
266
+ const { event, requestAttribution } = preparedEvent
249
267
  try {
250
268
  if (isToolResultError(result)) {
251
269
  event.isError = true
@@ -256,7 +274,7 @@ function publishSuccessfulToolEvent(
256
274
  }
257
275
  event.response = result
258
276
  event.duration = Date.now() - startTime.getTime()
259
- captureEvent(server, event, logger)
277
+ captureEvent(server, event, logger, requestAttribution)
260
278
  } catch (error) {
261
279
  logger(`Warning: PostHog MCP analytics failed to publish tool event - ${error}`)
262
280
  }
@@ -264,15 +282,16 @@ function publishSuccessfulToolEvent(
264
282
 
265
283
  function publishFailedToolEvent(
266
284
  server: MCPServerLike,
267
- event: McpEvent | null,
285
+ preparedEvent: PreparedToolEvent | null,
268
286
  error: unknown,
269
287
  startTime: Date,
270
288
  conversation: ConversationIdResolution,
271
289
  logger: LoggerFn
272
290
  ): void {
273
- if (!event) {
291
+ if (!preparedEvent) {
274
292
  return
275
293
  }
294
+ const { event, requestAttribution } = preparedEvent
276
295
  try {
277
296
  if (conversation.minted) {
278
297
  event.conversationId = undefined
@@ -280,7 +299,7 @@ function publishFailedToolEvent(
280
299
  event.isError = true
281
300
  event.error = captureException(error)
282
301
  event.duration = Date.now() - startTime.getTime()
283
- captureEvent(server, event, logger)
302
+ captureEvent(server, event, logger, requestAttribution)
284
303
  } catch (publishError) {
285
304
  logger(`Warning: PostHog MCP analytics failed to publish failed tool event - ${publishError}`)
286
305
  }
@@ -307,11 +326,27 @@ export type HandlerPatch = (
307
326
  * that register handlers post-construction work — e.g. `@rekog/mcp-nest` hands a
308
327
  * bare server to instrument() and only then registers its handlers.
309
328
  */
329
+ const originalRequestHandlers = new WeakMap<MCPServerLike, Map<string, MCPRequestHandler>>()
330
+
331
+ function rememberOriginalRequestHandler(
332
+ server: MCPServerLike,
333
+ handlerName: string,
334
+ originalHandler: MCPRequestHandler
335
+ ): void {
336
+ let handlers = originalRequestHandlers.get(server)
337
+ if (!handlers) {
338
+ handlers = new Map()
339
+ originalRequestHandlers.set(server, handlers)
340
+ }
341
+ handlers.set(handlerName, originalHandler)
342
+ }
343
+
310
344
  export function patchRequestHandlers(server: MCPServerLike, patches: Record<string, HandlerPatch>): void {
311
345
  // Monkey patch existing handlers.
312
346
  for (const [handlerName, patch] of Object.entries(patches)) {
313
347
  const originalHandler = server._requestHandlers.get(handlerName)
314
348
  if (originalHandler) {
349
+ rememberOriginalRequestHandler(server, handlerName, originalHandler)
315
350
  server._requestHandlers.set(handlerName, (request, extra) => patch(server, originalHandler, request, extra))
316
351
  }
317
352
  }
@@ -322,14 +357,55 @@ export function patchRequestHandlers(server: MCPServerLike, patches: Record<stri
322
357
  const shape = getObjectShape(requestSchema)
323
358
  const handlerName = shape?.method ? getLiteralValue(shape.method) : undefined
324
359
  const patch = typeof handlerName === 'string' ? patches[handlerName] : undefined
325
- if (!patch) {
360
+ if (!patch || typeof handlerName !== 'string') {
326
361
  return originalSetRequestHandler(requestSchema, originalHandler)
327
362
  }
328
363
 
329
- return originalSetRequestHandler(requestSchema, (request, extra) => patch(server, originalHandler, request, extra))
364
+ // Register first so the MCP SDK's request/result validation stays inside
365
+ // our analytics wrapper, matching handlers that existed before instrument().
366
+ const result = originalSetRequestHandler(requestSchema, originalHandler)
367
+ const registeredHandler = server._requestHandlers.get(handlerName)
368
+ if (registeredHandler) {
369
+ rememberOriginalRequestHandler(server, handlerName, registeredHandler)
370
+ server._requestHandlers.set(handlerName, (request, extra) => patch(server, registeredHandler, request, extra))
371
+ }
372
+ return result
330
373
  }) as MCPServerLike['setRequestHandler']
331
374
  }
332
375
 
376
+ /**
377
+ * Checks the server's raw listing for a real owner of a candidate virtual tool.
378
+ * This does not depend on a previous client request and does not call the
379
+ * instrumented list wrapper, so it neither injects PostHog tools nor captures a
380
+ * synthetic tools/list event. `undefined` fails open to the real dispatcher.
381
+ */
382
+ export async function isToolAdvertised(
383
+ server: MCPServerLike,
384
+ toolName: string,
385
+ extra: CompatibleRequestHandlerExtra | undefined,
386
+ logger: LoggerFn
387
+ ): Promise<boolean | undefined> {
388
+ const listHandler = originalRequestHandlers.get(server)?.get('tools/list')
389
+ if (!listHandler || !server._requestHandlers.has('tools/list')) {
390
+ return undefined
391
+ }
392
+
393
+ try {
394
+ const response = (await listHandler({ method: 'tools/list', params: {} }, extra)) as ListToolsResult
395
+ if (!response || !Array.isArray(response.tools)) {
396
+ return undefined
397
+ }
398
+ // Match the page the current list instrumentation can expose. Pagination
399
+ // passthrough is handled separately from missing-capability ownership.
400
+ return response.tools.some((tool) => tool?.name === toolName)
401
+ } catch (error) {
402
+ logger(
403
+ `Warning: PostHog MCP analytics could not determine whether "${toolName}" is advertised; delegating to the server - ${error}`
404
+ )
405
+ return undefined
406
+ }
407
+ }
408
+
333
409
  /**
334
410
  * Captures each `tools/list` and injects the SDK-managed tools (context
335
411
  * parameter, conversation id, `get_more_tools`) into the returned list.
@@ -343,8 +419,12 @@ export async function handleListToolsRequest(
343
419
  ): Promise<{ tools: ListToolsResult['tools'] }> {
344
420
  const data = getServerTrackingData(server)
345
421
  const startTime = new Date()
422
+ const sessionId = getSessionId(server, extra)
423
+ // Snapshot before metadata resolution or the list handler can yield to a
424
+ // concurrent request using the same instrumented server.
425
+ const requestAttribution = getSessionInfo(server, data, sessionId)
346
426
  const event: McpEvent = {
347
- sessionId: getSessionId(server, extra),
427
+ sessionId,
348
428
  parameters: buildCapturedMcpParameters(request),
349
429
  eventType: MCPAnalyticsEventType.mcpToolsList,
350
430
  timestamp: startTime,
@@ -355,7 +435,15 @@ export async function handleListToolsRequest(
355
435
  await applyResolvedMetadata(event, data, request, extra)
356
436
  }
357
437
 
358
- const tools = await getTracedToolsList(server, originalListToolsHandler, request, extra, event, logger)
438
+ const tools = await getTracedToolsList(
439
+ server,
440
+ originalListToolsHandler,
441
+ request,
442
+ extra,
443
+ event,
444
+ logger,
445
+ requestAttribution
446
+ )
359
447
 
360
448
  if (!data) {
361
449
  logger(
@@ -371,7 +459,7 @@ export async function handleListToolsRequest(
371
459
  event.error = captureException('No tools were sent to MCP client.')
372
460
  event.isError = true
373
461
  event.duration = Date.now() - startTime.getTime()
374
- captureEvent(server, event, data.logger)
462
+ captureEvent(server, event, data.logger, requestAttribution)
375
463
  return { tools }
376
464
  }
377
465
 
@@ -379,7 +467,7 @@ export async function handleListToolsRequest(
379
467
  event.listedToolNames = collectListedToolNames(tools)
380
468
  event.isError = false
381
469
  event.duration = Date.now() - startTime.getTime()
382
- captureEvent(server, event, data.logger)
470
+ captureEvent(server, event, data.logger, requestAttribution)
383
471
  return { tools }
384
472
  }
385
473
 
@@ -409,30 +497,39 @@ async function getTracedToolsList(
409
497
  request: MCPRequestLike,
410
498
  extra: CompatibleRequestHandlerExtra | undefined,
411
499
  event: McpEvent,
412
- logger: LoggerFn
500
+ logger: LoggerFn,
501
+ requestAttribution: SessionInfo
413
502
  ): Promise<ListToolsResult['tools']> {
414
503
  try {
415
504
  const data = getServerTrackingData(server)
416
505
  const originalResponse = (await originalListToolsHandler(request, extra)) as ListToolsResult
417
- let tools = originalResponse.tools || []
506
+ // Injection must not mutate arrays reused or frozen by the server.
507
+ let tools = [...(originalResponse.tools || [])]
418
508
 
419
509
  if (data) {
420
510
  cacheToolAnalyticsParameterOwnership(data.toolAnalyticsParameterOwnership, tools)
421
511
  }
422
-
423
512
  if (data && isContextEnabled(data.options.context)) {
424
513
  tools = addContextParameterToTools(tools, getContextDescription(data.options.context), data.logger)
425
514
  }
426
515
 
427
- if (data?.options.enableConversationId) {
428
- tools = addConversationIdToTools(tools, resolveMissingCapabilityToolName(data.options), data.logger)
429
- }
430
-
431
- if (data?.options.reportMissing) {
516
+ if (data) {
432
517
  const missingToolName = resolveMissingCapabilityToolName(data.options)
433
- const alreadyPresent = tools.some((tool) => tool?.name === missingToolName)
434
- if (!alreadyPresent) {
435
- tools.push(getReportMissingToolDescriptor(missingToolName))
518
+ let injectedMissingCapabilityTool = false
519
+ if (data.options.reportMissing) {
520
+ const alreadyPresent = tools.some((tool) => tool?.name === missingToolName)
521
+ if (alreadyPresent) {
522
+ data.logger(
523
+ `Warning: Cannot inject missing-capability tool "${missingToolName}" because a real tool already uses that name. The real tool will not be intercepted.`
524
+ )
525
+ } else {
526
+ tools.push(getReportMissingToolDescriptor(missingToolName))
527
+ injectedMissingCapabilityTool = true
528
+ }
529
+ }
530
+
531
+ if (data.options.enableConversationId) {
532
+ tools = addConversationIdToTools(tools, missingToolName, injectedMissingCapabilityTool, data.logger)
436
533
  }
437
534
  }
438
535
 
@@ -449,7 +546,7 @@ async function getTracedToolsList(
449
546
  event.error = captureException(error)
450
547
  event.isError = true
451
548
  event.duration = event.timestamp ? Date.now() - event.timestamp.getTime() : 0
452
- captureEvent(server, event, logger)
549
+ captureEvent(server, event, logger, requestAttribution)
453
550
  throw error
454
551
  }
455
552
  }
@@ -558,9 +655,10 @@ function mintStatelessSessionOnInitialize(
558
655
  */
559
656
  function upgradeMintedTokenToNegotiated(
560
657
  server: MCPServerLike,
561
- data: MCPAnalyticsData,
562
658
  mintedSessionId: string,
563
- negotiatedProtocolVersion: string | undefined
659
+ sessionInfo: SessionInfo,
660
+ negotiatedProtocolVersion: string | undefined,
661
+ logger: LoggerFn
564
662
  ): void {
565
663
  try {
566
664
  const transport = server.transport
@@ -569,13 +667,13 @@ function upgradeMintedTokenToNegotiated(
569
667
  }
570
668
  const token = encodeSessionId({
571
669
  sessionId: mintedSessionId,
572
- clientName: data.sessionInfo.clientName,
573
- clientVersion: data.sessionInfo.clientVersion,
670
+ clientName: sessionInfo.clientName,
671
+ clientVersion: sessionInfo.clientVersion,
574
672
  protocolVersion: negotiatedProtocolVersion,
575
673
  })
576
674
  writeSessionIdToTransport(transport, token)
577
675
  } catch (error) {
578
- data.logger(`Warning: PostHog MCP analytics failed to upgrade the stateless session token - ${error}`)
676
+ logger(`Warning: PostHog MCP analytics failed to upgrade the stateless session token - ${error}`)
579
677
  }
580
678
  }
581
679
 
@@ -617,7 +715,17 @@ export async function handleInitializeRequest(
617
715
  // Mint first so the `$mcp_initialize` event below already carries the minted id.
618
716
  const mintedSessionId = mintStatelessSessionOnInitialize(server, data, request, extra)
619
717
  const sessionId = getSessionId(server, extra)
620
- await handleIdentify(server, data, sessionId, request, extra)
718
+ // Snapshot before identify, metadata, or the initialize handler can yield to
719
+ // another request using the same instrumented server.
720
+ const sessionInfo = getSessionInfo(server, data, sessionId)
721
+ const initializeClientInfo = readInitializeClientInfo(request)
722
+ const requestSessionInfo: SessionInfo = {
723
+ ...sessionInfo,
724
+ clientName: initializeClientInfo?.name ?? sessionInfo.clientName,
725
+ clientVersion: initializeClientInfo?.version ?? sessionInfo.clientVersion,
726
+ }
727
+ const identity = await handleIdentify(server, data, sessionId, request, requestSessionInfo, extra)
728
+ const requestAttribution = withIdentity(requestSessionInfo, identity)
621
729
 
622
730
  const event: McpEvent = {
623
731
  sessionId,
@@ -640,12 +748,19 @@ export async function handleInitializeRequest(
640
748
  // re-mint the token so pods replaying it report the negotiated version too.
641
749
  const negotiatedProtocolVersion = readProtocolVersion(result, request)
642
750
  event.protocolVersion = negotiatedProtocolVersion
643
- data.sessionInfo.protocolVersion = negotiatedProtocolVersion
644
- setServerTrackingData(server, data)
751
+ // Do not let a delayed initialize overwrite whichever session became current
752
+ // while its callbacks or the original handler were awaiting.
753
+ if (data.sessionId === sessionId) {
754
+ data.sessionInfo = { ...data.sessionInfo, protocolVersion: negotiatedProtocolVersion }
755
+ setServerTrackingData(server, data)
756
+ }
645
757
  if (mintedSessionId) {
646
- upgradeMintedTokenToNegotiated(server, data, mintedSessionId, negotiatedProtocolVersion)
758
+ upgradeMintedTokenToNegotiated(server, mintedSessionId, requestSessionInfo, negotiatedProtocolVersion, data.logger)
647
759
  }
648
- captureEvent(server, event, data.logger)
760
+ captureEvent(server, event, data.logger, {
761
+ ...requestAttribution,
762
+ protocolVersion: negotiatedProtocolVersion,
763
+ })
649
764
  return result
650
765
  }
651
766
 
@@ -56,12 +56,7 @@ export async function resolveToolCallIntent(
56
56
  extra?: CompatibleRequestHandlerExtra
57
57
  ): Promise<ResolvedIntent | null> {
58
58
  const contextArgument = getContextArgument(request)
59
- if (
60
- analyticsOwnsContext &&
61
- isContextEnabled(data.options.context) &&
62
- request.params?.name !== 'get_more_tools' &&
63
- contextArgument
64
- ) {
59
+ if (analyticsOwnsContext && isContextEnabled(data.options.context) && contextArgument) {
65
60
  return { intent: contextArgument, source: 'context_parameter' }
66
61
  }
67
62
 
@@ -9,6 +9,7 @@ import type {
9
9
  MCPRequestLike,
10
10
  MCPServerLike,
11
11
  McpEvent,
12
+ SessionInfo,
12
13
  UserIdentity,
13
14
  } from '../types'
14
15
  import { MCPAnalyticsEventType } from './event-types'
@@ -128,10 +129,13 @@ export async function handleIdentify(
128
129
  data: MCPAnalyticsData,
129
130
  sessionId: string,
130
131
  request: MCPRequestLike,
132
+ requestAttribution: SessionInfo,
131
133
  extra?: CompatibleRequestHandlerExtra
132
- ): Promise<void> {
134
+ ): Promise<UserIdentity | undefined> {
135
+ const identityBeforeRequest = data.identifiedSessions.get(sessionId)
136
+ const sessionSourceBeforeIdentify = data.sessionSource
133
137
  if (!data.options.identify) {
134
- return
138
+ return identityBeforeRequest
135
139
  }
136
140
 
137
141
  const identifyEvent: McpEvent = {
@@ -162,23 +166,34 @@ export async function handleIdentify(
162
166
  // announced); revisit with the stateless-by-default rework.
163
167
  const changed = previousIdentity !== undefined && !areIdentitiesEqual(previousIdentity, mergedIdentity)
164
168
  const firstSeen = previousIdentity === undefined
165
- const announcedAtInitialize = data.sessionSource === 'token' && request.method !== 'initialize'
169
+ const announcedAtInitialize = sessionSourceBeforeIdentify === 'token' && request.method !== 'initialize'
166
170
  const shouldPublish = changed || (firstSeen && !announcedAtInitialize)
167
171
 
168
172
  data.identifiedSessions.set(sessionId, mergedIdentity)
169
173
 
170
174
  if (shouldPublish) {
171
175
  data.logger(`Identified session ${sessionId}`)
172
- captureEvent(server, identifyEvent, data.logger)
176
+ captureEvent(server, identifyEvent, data.logger, withIdentity(requestAttribution, mergedIdentity))
173
177
  }
174
- } else {
175
- data.logger(`Warning: Supplied identify function returned null for session ${sessionId}`)
178
+ return mergedIdentity
176
179
  }
180
+
181
+ data.logger(`Warning: Supplied identify function returned null for session ${sessionId}`)
177
182
  } catch (error) {
178
183
  data.logger(
179
184
  `Error: User supplied identify function threw an error while identifying session ${sessionId} - ${error}`
180
185
  )
181
186
  }
187
+ return identityBeforeRequest
188
+ }
189
+
190
+ export function withIdentity(sessionInfo: SessionInfo, identity: UserIdentity | undefined): SessionInfo {
191
+ return {
192
+ ...sessionInfo,
193
+ identifyActorGivenId: identity?.distinctId,
194
+ identifyActorData: identity?.properties || {},
195
+ identifyActorGroups: identity?.groups,
196
+ }
182
197
  }
183
198
 
184
199
  /**
@@ -125,5 +125,5 @@ export function getSessionInfo(
125
125
 
126
126
  data.sessionInfo = sessionInfo
127
127
  setServerTrackingData(server, data)
128
- return data.sessionInfo
128
+ return { ...sessionInfo }
129
129
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '0.10.5'
1
+ export const version = '0.10.7'