@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.
- package/dist/extensions/capture.d.ts +2 -2
- package/dist/extensions/capture.d.ts.map +1 -1
- package/dist/extensions/capture.js +2 -2
- package/dist/extensions/capture.mjs +2 -2
- package/dist/extensions/context-parameters.d.ts.map +1 -1
- package/dist/extensions/context-parameters.js +1 -4
- package/dist/extensions/context-parameters.mjs +1 -4
- package/dist/extensions/conversation-id.d.ts +1 -1
- package/dist/extensions/conversation-id.d.ts.map +1 -1
- package/dist/extensions/conversation-id.js +2 -2
- package/dist/extensions/conversation-id.mjs +2 -2
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-highlevel.js +5 -5
- package/dist/extensions/instrument-highlevel.mjs +6 -6
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-lowlevel.js +9 -4
- package/dist/extensions/instrument-lowlevel.mjs +10 -5
- package/dist/extensions/instrumentation.d.ts +6 -6
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +112 -42
- package/dist/extensions/instrumentation.mjs +110 -43
- package/dist/extensions/intent.d.ts.map +1 -1
- package/dist/extensions/intent.js +1 -1
- package/dist/extensions/intent.mjs +1 -1
- package/dist/extensions/internal.d.ts +3 -2
- package/dist/extensions/internal.d.ts.map +1 -1
- package/dist/extensions/internal.js +25 -9
- package/dist/extensions/internal.mjs +19 -6
- package/dist/extensions/session.js +3 -1
- package/dist/extensions/session.mjs +3 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +2 -2
- package/src/extensions/capture.ts +13 -6
- package/src/extensions/context-parameters.ts +1 -7
- package/src/extensions/conversation-id.ts +2 -1
- package/src/extensions/instrument-highlevel.ts +8 -13
- package/src/extensions/instrument-lowlevel.ts +17 -6
- package/src/extensions/instrumentation.ts +166 -51
- package/src/extensions/intent.ts +1 -6
- package/src/extensions/internal.ts +21 -6
- package/src/extensions/session.ts +1 -1
- 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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
131
|
+
resolvedEventType
|
|
124
132
|
)
|
|
125
|
-
if (
|
|
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,
|
|
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,
|
|
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
|
|
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<
|
|
197
|
+
): Promise<PreparedToolEvent | null> {
|
|
185
198
|
try {
|
|
186
199
|
const sessionId = getSessionId(server, extra)
|
|
187
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
434
|
-
if (
|
|
435
|
-
tools.
|
|
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
|
-
|
|
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:
|
|
573
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
644
|
-
|
|
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,
|
|
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
|
|
package/src/extensions/intent.ts
CHANGED
|
@@ -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<
|
|
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 =
|
|
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
|
-
|
|
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
|
/**
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.10.
|
|
1
|
+
export const version = '0.10.7'
|