@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.
- package/dist/extensions/analytics-parameters.d.ts +5 -0
- package/dist/extensions/analytics-parameters.d.ts.map +1 -1
- package/dist/extensions/analytics-parameters.js +57 -3
- package/dist/extensions/analytics-parameters.mjs +47 -2
- 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 +4 -3
- package/dist/extensions/context-parameters.d.ts.map +1 -1
- package/dist/extensions/context-parameters.js +4 -5
- package/dist/extensions/context-parameters.mjs +4 -4
- package/dist/extensions/conversation-id.d.ts +3 -8
- package/dist/extensions/conversation-id.d.ts.map +1 -1
- package/dist/extensions/conversation-id.js +6 -22
- package/dist/extensions/conversation-id.mjs +5 -17
- 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 +31 -51
- package/dist/extensions/instrument-highlevel.mjs +30 -50
- 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 +8 -9
- package/dist/extensions/instrument-lowlevel.mjs +8 -9
- package/dist/extensions/instrumentation.d.ts +7 -8
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +57 -32
- package/dist/extensions/instrumentation.mjs +58 -33
- package/dist/extensions/intent.d.ts +1 -1
- package/dist/extensions/intent.d.ts.map +1 -1
- package/dist/extensions/intent.js +3 -4
- package/dist/extensions/intent.mjs +3 -4
- 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 +19 -16
- package/dist/index.mjs +20 -17
- package/dist/types.d.ts +7 -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 +4 -4
- package/src/extensions/analytics-parameters.ts +67 -1
- package/src/extensions/capture.ts +3 -3
- package/src/extensions/compatibility.ts +16 -16
- package/src/extensions/context-parameters.ts +9 -7
- package/src/extensions/conversation-id.ts +12 -25
- package/src/extensions/instrument-highlevel.ts +52 -75
- package/src/extensions/instrument-lowlevel.ts +14 -10
- package/src/extensions/instrumentation.ts +101 -36
- package/src/extensions/intent.ts +8 -3
- 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 +22 -19
- package/src/types.ts +8 -0
- 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 {
|
|
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:
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
104
|
+
ownership.conversationId,
|
|
95
105
|
request.params?.arguments,
|
|
96
106
|
request.params?.name,
|
|
97
107
|
resolveMissingCapabilityToolName(data.options)
|
|
98
108
|
)
|
|
99
|
-
const downstreamRequest =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
606
|
+
extra: CompatibleRequestHandlerExtra | undefined,
|
|
607
|
+
logger: LoggerFn
|
|
543
608
|
): Promise<unknown> {
|
|
544
609
|
const data = getServerTrackingData(server)
|
|
545
610
|
if (!data) {
|
|
546
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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 (
|
|
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
|
-
|
|
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
|
}
|