@posthog/mcp 0.25.2 → 0.26.0
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/README.md +3 -4
- package/dist/extensions/analytics-parameters.d.ts +2 -2
- package/dist/extensions/analytics-parameters.d.ts.map +1 -1
- package/dist/extensions/analytics-parameters.js +9 -8
- package/dist/extensions/analytics-parameters.mjs +9 -8
- package/dist/extensions/capture.js +1 -1
- package/dist/extensions/capture.mjs +1 -1
- package/dist/extensions/constants.d.ts +2 -1
- package/dist/extensions/constants.d.ts.map +1 -1
- package/dist/extensions/constants.js +3 -2
- package/dist/extensions/constants.mjs +3 -2
- package/dist/extensions/context-parameters.d.ts.map +1 -1
- package/dist/extensions/context-parameters.js +1 -1
- package/dist/extensions/context-parameters.mjs +1 -1
- package/dist/extensions/event-types.d.ts +1 -0
- package/dist/extensions/event-types.d.ts.map +1 -1
- package/dist/extensions/event-types.js +2 -1
- package/dist/extensions/event-types.mjs +2 -1
- package/dist/extensions/feedback.d.ts.map +1 -1
- package/dist/extensions/feedback.js +2 -5
- package/dist/extensions/feedback.mjs +2 -5
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-highlevel.js +15 -3
- package/dist/extensions/instrument-highlevel.mjs +16 -4
- package/dist/extensions/instrument-lowlevel.js +19 -6
- package/dist/extensions/instrument-lowlevel.mjs +19 -6
- package/dist/extensions/instrumentation.d.ts +5 -0
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +29 -5
- package/dist/extensions/instrumentation.mjs +29 -5
- package/dist/extensions/model-parameters.d.ts +1 -1
- package/dist/extensions/model-parameters.d.ts.map +1 -1
- package/dist/extensions/model-parameters.js +1 -1
- package/dist/extensions/model-parameters.mjs +1 -1
- package/dist/extensions/posthog-events.d.ts.map +1 -1
- package/dist/extensions/posthog-events.js +3 -2
- package/dist/extensions/posthog-events.mjs +3 -2
- package/dist/extensions/posthog-mcp.d.ts +8 -1
- package/dist/extensions/posthog-mcp.d.ts.map +1 -1
- package/dist/extensions/posthog-mcp.js +7 -1
- package/dist/extensions/posthog-mcp.mjs +8 -2
- package/dist/extensions/sanitization.d.ts.map +1 -1
- package/dist/extensions/sanitization.js +2 -1
- package/dist/extensions/sanitization.mjs +2 -1
- package/dist/extensions/tools.d.ts +11 -0
- package/dist/extensions/tools.d.ts.map +1 -1
- package/dist/extensions/tools.js +10 -5
- package/dist/extensions/tools.mjs +8 -6
- package/dist/extensions/truncation.js +1 -1
- package/dist/extensions/truncation.mjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +10 -2
- 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/analytics-parameters.ts +7 -11
- package/src/extensions/capture.ts +1 -1
- package/src/extensions/constants.ts +2 -1
- package/src/extensions/context-parameters.ts +1 -0
- package/src/extensions/event-types.ts +1 -0
- package/src/extensions/feedback.ts +2 -10
- package/src/extensions/instrument-highlevel.ts +29 -4
- package/src/extensions/instrument-lowlevel.ts +14 -8
- package/src/extensions/instrumentation.ts +60 -8
- package/src/extensions/model-parameters.ts +2 -1
- package/src/extensions/posthog-events.ts +5 -1
- package/src/extensions/posthog-mcp.ts +18 -2
- package/src/extensions/sanitization.ts +7 -2
- package/src/extensions/tools.ts +13 -10
- package/src/extensions/truncation.ts +1 -1
- package/src/index.ts +1 -0
- package/src/types.ts +11 -2
- package/src/version.ts +1 -1
package/dist/version.js
CHANGED
package/dist/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const version = "0.
|
|
1
|
+
const version = "0.26.0";
|
|
2
2
|
export { version };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"bugs": {
|
|
5
5
|
"url": "https://github.com/PostHog/posthog-js/issues"
|
|
6
6
|
},
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"zod4": "npm:zod@^4.4.3",
|
|
55
55
|
"@vitest/coverage-v8": "^1.6.1",
|
|
56
56
|
"vitest": "^1.6.1",
|
|
57
|
-
"
|
|
58
|
-
"posthog-
|
|
57
|
+
"posthog-node": "^5.63.1",
|
|
58
|
+
"@posthog-tooling/tsconfig-base": "1.1.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@modelcontextprotocol/sdk": ">=1.26.0",
|
|
@@ -50,7 +50,7 @@ export interface AnalyticsInjectableTool {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Adds a
|
|
53
|
+
* Adds a string parameter to a tool's JSON Schema. Called AFTER the MCP
|
|
54
54
|
* SDK has converted Zod schemas to JSON Schema, so only JSON Schema needs
|
|
55
55
|
* handling.
|
|
56
56
|
*
|
|
@@ -67,6 +67,7 @@ export function addAnalyticsParameterToTool<TTool extends AnalyticsInjectableToo
|
|
|
67
67
|
parameterName: string,
|
|
68
68
|
description: string,
|
|
69
69
|
injectionLabel: string,
|
|
70
|
+
required: boolean,
|
|
70
71
|
logger: LoggerFn = log
|
|
71
72
|
): TTool {
|
|
72
73
|
const modifiedTool = { ...tool }
|
|
@@ -85,11 +86,7 @@ export function addAnalyticsParameterToTool<TTool extends AnalyticsInjectableToo
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
if (!modifiedTool.inputSchema) {
|
|
88
|
-
modifiedTool.inputSchema = {
|
|
89
|
-
type: 'object',
|
|
90
|
-
properties: {},
|
|
91
|
-
required: [],
|
|
92
|
-
}
|
|
89
|
+
modifiedTool.inputSchema = { type: 'object', properties: {} }
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
// Deep copy: the server may reuse or freeze the schema object it handed us.
|
|
@@ -112,12 +109,11 @@ export function addAnalyticsParameterToTool<TTool extends AnalyticsInjectableToo
|
|
|
112
109
|
description,
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
if (required) {
|
|
113
|
+
const existing = Array.isArray(inputSchema.required) ? inputSchema.required : []
|
|
114
|
+
if (!existing.includes(parameterName)) {
|
|
115
|
+
inputSchema.required = [...existing, parameterName]
|
|
118
116
|
}
|
|
119
|
-
} else {
|
|
120
|
-
inputSchema.required = [parameterName]
|
|
121
117
|
}
|
|
122
118
|
|
|
123
119
|
return modifiedTool
|
|
@@ -42,7 +42,7 @@ export function captureEvent(
|
|
|
42
42
|
const sessionInfo = requestAttribution ?? getSessionInfo(server, data, eventInput.sessionId)
|
|
43
43
|
|
|
44
44
|
const duration =
|
|
45
|
-
eventInput.duration
|
|
45
|
+
eventInput.duration ?? (eventInput.timestamp ? Date.now() - eventInput.timestamp.getTime() : undefined)
|
|
46
46
|
|
|
47
47
|
const fullEvent: McpEvent = {
|
|
48
48
|
id: eventInput.id || '',
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
export const INACTIVITY_TIMEOUT_IN_MINUTES = 30
|
|
7
7
|
|
|
8
|
-
export const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = `
|
|
8
|
+
export const DEFAULT_CONTEXT_PARAMETER_DESCRIPTION = `A few words on why you are calling this tool, for the server developer's analytics. Describe people and things by role, such as "a customer". Do not quote the user or include names, emails, or other personal details.`
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_MODEL_PARAMETER_DESCRIPTION = `The model identifier you are running as, for example "claude-opus-4-8" or "gpt-5.2". Pass "unknown" if you are not certain. Recorded for the server developer's analytics.`
|
|
11
11
|
|
|
@@ -36,6 +36,7 @@ export const PostHogMCPAnalyticsEvent = {
|
|
|
36
36
|
ResourcesList: '$mcp_resources_list',
|
|
37
37
|
ToolCall: '$mcp_tool_call',
|
|
38
38
|
ToolsList: '$mcp_tools_list',
|
|
39
|
+
UnknownTool: '$mcp_unknown_tool',
|
|
39
40
|
} as const
|
|
40
41
|
|
|
41
42
|
export type PostHogMCPAnalyticsEvent = (typeof PostHogMCPAnalyticsEvent)[keyof typeof PostHogMCPAnalyticsEvent]
|
|
@@ -21,6 +21,7 @@ export const MCPAnalyticsEventType = {
|
|
|
21
21
|
mcpResourcesRead: 'mcp:resources/read',
|
|
22
22
|
mcpToolsCall: 'mcp:tools/call',
|
|
23
23
|
mcpToolsList: 'mcp:tools/list',
|
|
24
|
+
mcpUnknownTool: 'mcp:unknown_tool',
|
|
24
25
|
} as const
|
|
25
26
|
|
|
26
27
|
export type MCPAnalyticsEventType = (typeof MCPAnalyticsEventType)[keyof typeof MCPAnalyticsEventType]
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import { PostHogMCPAnalyticsProperty } from './constants'
|
|
13
13
|
import { log, type LoggerFn } from './logger'
|
|
14
14
|
import { sanitizeFreeText, sanitizeFreeTextValue } from './mcp-payloads'
|
|
15
|
+
import { VIRTUAL_TOOL_ANNOTATIONS } from './tools'
|
|
15
16
|
import { TRUNCATION_SUFFIX } from './truncation'
|
|
16
17
|
|
|
17
18
|
export const SEND_FEEDBACK_TOOL_NAME = 'send_feedback' as const
|
|
@@ -150,16 +151,7 @@ export function getFeedbackToolDescriptor(options: CollectFeedbackOptions = {}):
|
|
|
150
151
|
},
|
|
151
152
|
required: ['feedback_type', 'summary', ...extraRequired],
|
|
152
153
|
},
|
|
153
|
-
annotations: {
|
|
154
|
-
title: 'Send feedback',
|
|
155
|
-
readOnlyHint: true,
|
|
156
|
-
// Interacts with an external entity: the report lands in analytics.
|
|
157
|
-
openWorldHint: true,
|
|
158
|
-
// Only records the feedback, so repeat calls are harmless — and advertising
|
|
159
|
-
// it as idempotent makes agents more willing to call it proactively.
|
|
160
|
-
idempotentHint: true,
|
|
161
|
-
destructiveHint: false,
|
|
162
|
-
},
|
|
154
|
+
annotations: { ...VIRTUAL_TOOL_ANNOTATIONS },
|
|
163
155
|
}
|
|
164
156
|
}
|
|
165
157
|
|
|
@@ -30,7 +30,7 @@ import { isCaptureModelEnabled } from './model-parameters'
|
|
|
30
30
|
import { MCPAnalyticsEventType } from './event-types'
|
|
31
31
|
import { getServerTrackingData } from './internal'
|
|
32
32
|
import type { LoggerFn } from './logger'
|
|
33
|
-
import { createWrappedTool, getToolFunction, hasToolFunction } from './mcp-sdk-compat'
|
|
33
|
+
import { createWrappedTool, getToolFunction, hasToolFunction, readRequestHandlerMethod } from './mcp-sdk-compat'
|
|
34
34
|
import { getReportMissingToolDescriptor, handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
35
35
|
import {
|
|
36
36
|
handleInitializeRequest,
|
|
@@ -236,7 +236,8 @@ async function handleToolCallRequest(
|
|
|
236
236
|
originalCallToolHandler: MCPRequestHandler,
|
|
237
237
|
request: MCPRequest,
|
|
238
238
|
extra: MCPRequestExtra,
|
|
239
|
-
logger: LoggerFn
|
|
239
|
+
logger: LoggerFn,
|
|
240
|
+
registryDispatches: () => boolean
|
|
240
241
|
): Promise<unknown> {
|
|
241
242
|
const data = getServerTrackingData(server)
|
|
242
243
|
if (!data) {
|
|
@@ -284,7 +285,10 @@ async function handleToolCallRequest(
|
|
|
284
285
|
})
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
const registeredTool =
|
|
288
|
+
const registeredTool =
|
|
289
|
+
toolName && Object.prototype.hasOwnProperty.call(highLevelServer._registeredTools, toolName)
|
|
290
|
+
? highLevelServer._registeredTools[toolName]
|
|
291
|
+
: undefined
|
|
288
292
|
|
|
289
293
|
// Strip SDK-owned arguments before the MCP SDK validates the registered Zod
|
|
290
294
|
// schema. The callback wrapper repeats the ownership-aware cleanup as a
|
|
@@ -299,6 +303,7 @@ async function handleToolCallRequest(
|
|
|
299
303
|
? getAnalyticsParameterOwnership(registeredTool.inputSchema, registeredTool.outputSchema)
|
|
300
304
|
: undefined,
|
|
301
305
|
inputSchema: registeredTool?.inputSchema,
|
|
306
|
+
isUnknownTool: !!toolName && !registeredTool && registryDispatches(),
|
|
302
307
|
takeCapturedError: () => {
|
|
303
308
|
const captured = extra?.__mcp_analytics_error
|
|
304
309
|
if (extra) {
|
|
@@ -313,6 +318,7 @@ export function instrumentHighLevelServer(server: HighLevelMCPServerLike, logger
|
|
|
313
318
|
try {
|
|
314
319
|
const lowLevelServer = server.server
|
|
315
320
|
const mcpAnalyticsData = getServerTrackingData(lowLevelServer)
|
|
321
|
+
const registryDispatches = trackRegistryDispatch(server)
|
|
316
322
|
|
|
317
323
|
const handlers: Record<string, HandlerPatch> = {
|
|
318
324
|
initialize: (trackedServer, originalHandler, request, extra) =>
|
|
@@ -320,7 +326,7 @@ export function instrumentHighLevelServer(server: HighLevelMCPServerLike, logger
|
|
|
320
326
|
'tools/list': (trackedServer, originalHandler, request, extra) =>
|
|
321
327
|
handleListToolsRequest(trackedServer, originalHandler, request, extra, logger),
|
|
322
328
|
'tools/call': (trackedServer, originalHandler, request, extra) =>
|
|
323
|
-
handleToolCallRequest(server, trackedServer, originalHandler, request, extra, logger),
|
|
329
|
+
handleToolCallRequest(server, trackedServer, originalHandler, request, extra, logger, registryDispatches),
|
|
324
330
|
'resources/list': traceResourceRequest(MCPAnalyticsEventType.mcpResourcesList, logger),
|
|
325
331
|
// Both listings publish `$mcp_resources_list`; the captured
|
|
326
332
|
// `request.method` is what tells a static listing from a templated one.
|
|
@@ -342,6 +348,25 @@ export function instrumentHighLevelServer(server: HighLevelMCPServerLike, logger
|
|
|
342
348
|
}
|
|
343
349
|
}
|
|
344
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Whether the MCP SDK's own `tools/call` dispatcher, which reads `_registeredTools`,
|
|
353
|
+
* is the one serving calls. Adapters such as @rekog/mcp-nest bind their own
|
|
354
|
+
* dispatcher instead, and keep tools the registry never holds; on SDK v2 they
|
|
355
|
+
* replace the dispatcher the McpServer constructor installed.
|
|
356
|
+
*/
|
|
357
|
+
function trackRegistryDispatch(server: HighLevelMCPServerLike): () => boolean {
|
|
358
|
+
const lowLevelServer = server.server
|
|
359
|
+
let replaced = false
|
|
360
|
+
const setRequestHandler = lowLevelServer.setRequestHandler.bind(lowLevelServer) as (...args: unknown[]) => unknown
|
|
361
|
+
lowLevelServer.setRequestHandler = ((...args: unknown[]) => {
|
|
362
|
+
if (server._toolHandlersInitialized && readRequestHandlerMethod(args[0]) === 'tools/call') {
|
|
363
|
+
replaced = true
|
|
364
|
+
}
|
|
365
|
+
return setRequestHandler(...args)
|
|
366
|
+
}) as MCPServerLike['setRequestHandler']
|
|
367
|
+
return () => server._toolHandlersInitialized === true && !replaced
|
|
368
|
+
}
|
|
369
|
+
|
|
345
370
|
function seedToolDescriptionsFromRegistry(cache: Map<string, string>, tools: Record<string, RegisteredTool>): void {
|
|
346
371
|
for (const [name, tool] of Object.entries(tools)) {
|
|
347
372
|
if (typeof tool?.description === 'string') {
|
|
@@ -132,7 +132,9 @@ async function handleToolCallRequest(
|
|
|
132
132
|
})
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
const
|
|
135
|
+
const lookup = lookUpOriginalTool(data, toolName)
|
|
136
|
+
const originalTool = lookup.registered ? lookup.tool : undefined
|
|
137
|
+
const hasDispatcher = originalCallToolHandler !== undefined && originalCallToolHandler !== unknownToolHandler
|
|
136
138
|
return await captureToolCall({
|
|
137
139
|
server,
|
|
138
140
|
data,
|
|
@@ -145,19 +147,23 @@ async function handleToolCallRequest(
|
|
|
145
147
|
? getAnalyticsParameterOwnership(originalTool.inputSchema)
|
|
146
148
|
: undefined,
|
|
147
149
|
inputSchema: originalTool?.inputSchema,
|
|
150
|
+
isUnknownTool: !!toolName && (!hasDispatcher || lookup.registered === false),
|
|
148
151
|
})
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
type OriginalToolLookup =
|
|
155
|
+
| { registered: true; tool: { inputSchema?: unknown } }
|
|
156
|
+
| { registered: false }
|
|
157
|
+
| { registered: undefined }
|
|
158
|
+
|
|
159
|
+
function lookUpOriginalTool(data: MCPAnalyticsData, toolName: string | undefined): OriginalToolLookup {
|
|
160
|
+
if (!toolName || !data.options.resolveOriginalTool) return { registered: undefined }
|
|
156
161
|
try {
|
|
157
|
-
|
|
162
|
+
const tool = data.options.resolveOriginalTool(toolName)
|
|
163
|
+
return tool ? { registered: true, tool } : { registered: false }
|
|
158
164
|
} catch (error) {
|
|
159
165
|
data.logger(`Warning: resolveOriginalTool failed for tool ${toolName} - ${error}`)
|
|
160
|
-
return undefined
|
|
166
|
+
return { registered: undefined }
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
|
|
@@ -128,6 +128,11 @@ interface TraceToolCallParams {
|
|
|
128
128
|
* read it here to capture the real stack rather than the result envelope.
|
|
129
129
|
*/
|
|
130
130
|
takeCapturedError?: () => unknown
|
|
131
|
+
/**
|
|
132
|
+
* The SDK knows the server has no tool by this name, so a rejected call is
|
|
133
|
+
* recorded as `$mcp_unknown_tool`, not as a tool call.
|
|
134
|
+
*/
|
|
135
|
+
isUnknownTool?: boolean
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
/**
|
|
@@ -152,6 +157,7 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
|
|
|
152
157
|
extraEventProperties,
|
|
153
158
|
omitCapturedParameters,
|
|
154
159
|
takeCapturedError,
|
|
160
|
+
isUnknownTool,
|
|
155
161
|
} = params
|
|
156
162
|
const resolvedEventType = eventType ?? MCPAnalyticsEventType.mcpToolsCall
|
|
157
163
|
// The SDK's own virtual tools carry their intent in their own arguments, so
|
|
@@ -190,6 +196,7 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
|
|
|
190
196
|
resolvedEventType,
|
|
191
197
|
canCaptureContextIntent
|
|
192
198
|
)
|
|
199
|
+
const hostProperties = preparedEvent?.event.properties
|
|
193
200
|
if (preparedEvent && resolvedEventType === MCPAnalyticsEventType.mcpToolsCall) {
|
|
194
201
|
const sessionSchemas =
|
|
195
202
|
(preparedEvent.event.sessionId ? data.toolInputSchemas.get(preparedEvent.event.sessionId) : undefined) ??
|
|
@@ -217,14 +224,25 @@ export async function captureToolCall(params: TraceToolCallParams): Promise<unkn
|
|
|
217
224
|
try {
|
|
218
225
|
result = await execute(downstreamRequest)
|
|
219
226
|
} catch (error) {
|
|
220
|
-
|
|
227
|
+
if (preparedEvent && conversation.minted) {
|
|
228
|
+
preparedEvent.event.conversationId = undefined
|
|
229
|
+
}
|
|
230
|
+
if (isUnknownTool) {
|
|
231
|
+
publishUnknownToolEvent(server, preparedEvent, hostProperties, data.logger)
|
|
232
|
+
} else {
|
|
233
|
+
publishFailedToolEvent(server, preparedEvent, error, startTime, data.logger)
|
|
234
|
+
}
|
|
221
235
|
throw error
|
|
222
236
|
}
|
|
223
237
|
|
|
224
238
|
const finalResult = applyConversationInstructions(preparedEvent?.event ?? null, result, conversation, ownership)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
239
|
+
if (isUnknownTool && isToolResultError(result)) {
|
|
240
|
+
publishUnknownToolEvent(server, preparedEvent, hostProperties, data.logger)
|
|
241
|
+
} else {
|
|
242
|
+
// `result`, not `finalResult`: the error is read from what the tool produced,
|
|
243
|
+
// before the conversation handle was written into it. See below.
|
|
244
|
+
publishSuccessfulToolEvent(server, preparedEvent, finalResult, startTime, data.logger, takeCapturedError, result)
|
|
245
|
+
}
|
|
228
246
|
return finalResult
|
|
229
247
|
}
|
|
230
248
|
|
|
@@ -469,7 +487,6 @@ function publishFailedToolEvent(
|
|
|
469
487
|
preparedEvent: PreparedToolEvent | null,
|
|
470
488
|
error: unknown,
|
|
471
489
|
startTime: Date,
|
|
472
|
-
conversation: ConversationIdResolution,
|
|
473
490
|
logger: LoggerFn
|
|
474
491
|
): void {
|
|
475
492
|
if (!preparedEvent) {
|
|
@@ -477,9 +494,6 @@ function publishFailedToolEvent(
|
|
|
477
494
|
}
|
|
478
495
|
const { event, requestAttribution } = preparedEvent
|
|
479
496
|
try {
|
|
480
|
-
if (conversation.minted) {
|
|
481
|
-
event.conversationId = undefined
|
|
482
|
-
}
|
|
483
497
|
event.isError = true
|
|
484
498
|
event.error = captureException(error)
|
|
485
499
|
event.duration = Date.now() - startTime.getTime()
|
|
@@ -489,6 +503,44 @@ function publishFailedToolEvent(
|
|
|
489
503
|
}
|
|
490
504
|
}
|
|
491
505
|
|
|
506
|
+
/**
|
|
507
|
+
* The agent asked for a tool the server does not have, so the call is not a
|
|
508
|
+
* tool call: only the requested name, identity, and session are recorded.
|
|
509
|
+
*/
|
|
510
|
+
function publishUnknownToolEvent(
|
|
511
|
+
server: MCPServerLike,
|
|
512
|
+
preparedEvent: PreparedToolEvent | null,
|
|
513
|
+
properties: McpEvent['properties'],
|
|
514
|
+
logger: LoggerFn
|
|
515
|
+
): void {
|
|
516
|
+
if (!preparedEvent) {
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
const { event, requestAttribution } = preparedEvent
|
|
520
|
+
try {
|
|
521
|
+
captureEvent(
|
|
522
|
+
server,
|
|
523
|
+
{
|
|
524
|
+
eventType: MCPAnalyticsEventType.mcpUnknownTool,
|
|
525
|
+
timestamp: event.timestamp,
|
|
526
|
+
resourceName: event.resourceName,
|
|
527
|
+
sessionId: event.sessionId,
|
|
528
|
+
conversationId: event.conversationId,
|
|
529
|
+
clientName: event.clientName,
|
|
530
|
+
clientVersion: event.clientVersion,
|
|
531
|
+
protocolVersion: event.protocolVersion,
|
|
532
|
+
clientUserAgent: event.clientUserAgent,
|
|
533
|
+
vendorClient: event.vendorClient,
|
|
534
|
+
properties,
|
|
535
|
+
},
|
|
536
|
+
logger,
|
|
537
|
+
requestAttribution
|
|
538
|
+
)
|
|
539
|
+
} catch (publishError) {
|
|
540
|
+
logger(`Warning: PostHog MCP analytics failed to publish unknown tool event - ${publishError}`)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
492
544
|
type ResourceEventType = typeof MCPAnalyticsEventType.mcpResourcesList | typeof MCPAnalyticsEventType.mcpResourcesRead
|
|
493
545
|
|
|
494
546
|
interface TraceRequestParams {
|
|
@@ -10,7 +10,7 @@ import { log, type LoggerFn } from './logger'
|
|
|
10
10
|
* vendor metadata, while other harnesses inject the model id into the system
|
|
11
11
|
* prompt so the agent can state it like intent through the `context` parameter.
|
|
12
12
|
*
|
|
13
|
-
* This module injects
|
|
13
|
+
* This module injects an optional `llm_model` string parameter into every tool
|
|
14
14
|
* (mirroring `context-parameters.ts`), strips it before the tool runs, and
|
|
15
15
|
* captures the best available value as `$mcp_llm_model`. Client metadata wins
|
|
16
16
|
* over self-report, and `$mcp_llm_model_source` preserves that provenance.
|
|
@@ -54,6 +54,7 @@ export function addModelParameterToTool<TTool extends ModelInjectableTool>(
|
|
|
54
54
|
'llm_model',
|
|
55
55
|
modelDescriptionOverride || DEFAULT_MODEL_PARAMETER_DESCRIPTION,
|
|
56
56
|
'model',
|
|
57
|
+
false,
|
|
57
58
|
logger
|
|
58
59
|
)
|
|
59
60
|
}
|
|
@@ -18,6 +18,7 @@ const BUILT_IN_EVENT_NAME_BY_TYPE = {
|
|
|
18
18
|
[MCPAnalyticsEventType.mcpResourcesRead]: PostHogMCPAnalyticsEvent.ResourceRead,
|
|
19
19
|
[MCPAnalyticsEventType.mcpToolsCall]: PostHogMCPAnalyticsEvent.ToolCall,
|
|
20
20
|
[MCPAnalyticsEventType.mcpToolsList]: PostHogMCPAnalyticsEvent.ToolsList,
|
|
21
|
+
[MCPAnalyticsEventType.mcpUnknownTool]: PostHogMCPAnalyticsEvent.UnknownTool,
|
|
21
22
|
} satisfies Record<MCPAnalyticsEventType, PostHogMCPAnalyticsEvent>
|
|
22
23
|
|
|
23
24
|
function getDistinctId(event: Event): string {
|
|
@@ -121,7 +122,10 @@ function addPersonProcessingProperty(event: Event, properties: Record<string, un
|
|
|
121
122
|
function addCommonEventProperties(event: Event, properties: Record<string, unknown>): void {
|
|
122
123
|
if (event.resourceName) {
|
|
123
124
|
properties[PostHogMCPAnalyticsProperty.ResourceName] = event.resourceName
|
|
124
|
-
if (
|
|
125
|
+
if (
|
|
126
|
+
event.eventType === MCPAnalyticsEventType.mcpToolsCall ||
|
|
127
|
+
event.eventType === MCPAnalyticsEventType.mcpUnknownTool
|
|
128
|
+
) {
|
|
125
129
|
properties[PostHogMCPAnalyticsProperty.ToolName] = event.resourceName
|
|
126
130
|
}
|
|
127
131
|
}
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
PrepareToolListOptions,
|
|
18
18
|
ToolCallCaptureData,
|
|
19
19
|
ToolsListCaptureData,
|
|
20
|
+
UnknownToolCaptureData,
|
|
20
21
|
} from '../types'
|
|
21
22
|
import {
|
|
22
23
|
buildFeedbackEventProperties,
|
|
@@ -54,7 +55,7 @@ import {
|
|
|
54
55
|
} from './model-parameters'
|
|
55
56
|
import { McpEventSink } from './sink'
|
|
56
57
|
import { addInstructionsToOutputSchemas, mirrorInstructionsIntoStructuredContent } from './output-instructions'
|
|
57
|
-
import { deriveSessionIdFromConversation } from './session'
|
|
58
|
+
import { deriveSessionIdFromConversation, newSessionId } from './session'
|
|
58
59
|
import { validateServerBuild } from './server-build'
|
|
59
60
|
import { GET_MORE_TOOLS_NAME, getReportMissingToolDescriptor } from './tools'
|
|
60
61
|
|
|
@@ -479,6 +480,21 @@ export class PostHogMCP extends PostHog {
|
|
|
479
480
|
this.#emit(event)
|
|
480
481
|
}
|
|
481
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Capture a call to a tool your server does not have. Emits
|
|
485
|
+
* `$mcp_unknown_tool`. Use it instead of {@link captureToolCall} when your
|
|
486
|
+
* dispatcher rejects the name, so the call is not counted as a tool call. A
|
|
487
|
+
* call that names no tool emits nothing.
|
|
488
|
+
*/
|
|
489
|
+
captureUnknownTool(data: UnknownToolCaptureData): void {
|
|
490
|
+
if (!data.toolName) {
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
const event = baseEvent(MCPAnalyticsEventType.mcpUnknownTool, data)
|
|
494
|
+
event.resourceName = data.toolName
|
|
495
|
+
this.#emit(event)
|
|
496
|
+
}
|
|
497
|
+
|
|
482
498
|
/**
|
|
483
499
|
* Capture a `send_feedback` call as an agent-feedback report. Emits
|
|
484
500
|
* `$mcp_feedback` with the report's `$mcp_feedback_*` properties and its
|
|
@@ -534,7 +550,7 @@ export class PostHogMCP extends PostHog {
|
|
|
534
550
|
function baseEvent(eventType: MCPAnalyticsEventType, common: McpCaptureCommon): McpEvent {
|
|
535
551
|
const event: McpEvent = {
|
|
536
552
|
eventType,
|
|
537
|
-
sessionId: common.sessionId,
|
|
553
|
+
sessionId: common.sessionId || newSessionId(),
|
|
538
554
|
conversationId: common.conversationId,
|
|
539
555
|
protocolVersion: common.protocolVersion,
|
|
540
556
|
// There is no `extra` on this path, so the host reads the request headers and
|
|
@@ -4,6 +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 { ErrorProperties, Event, McpEvent } from '../types'
|
|
7
|
+
import { MCPAnalyticsEventType } from './event-types'
|
|
7
8
|
import { sanitizeCapturedValue, sanitizeFreeText } from './mcp-payloads'
|
|
8
9
|
|
|
9
10
|
type SanitizedRecord = Record<string, unknown>
|
|
@@ -34,9 +35,13 @@ export function sanitizeEvent<T extends Event | McpEvent>(event: T): T {
|
|
|
34
35
|
|
|
35
36
|
// Every event type, not just `resources/read`: the `$exception` sibling
|
|
36
37
|
// carries the same name, and a tool or prompt name is free text an
|
|
37
|
-
// application can spell as a URL too.
|
|
38
|
+
// application can spell as a URL too. An unknown tool's name is whatever the
|
|
39
|
+
// agent typed, so it also gets the intent's personal-data redaction.
|
|
38
40
|
if (result.resourceName != null) {
|
|
39
|
-
result.resourceName =
|
|
41
|
+
result.resourceName =
|
|
42
|
+
result.eventType === MCPAnalyticsEventType.mcpUnknownTool
|
|
43
|
+
? sanitizeFreeText(result.resourceName)
|
|
44
|
+
: (sanitizeCapturedValue(result.resourceName) as string)
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
// The intent comes straight from an agent-narrated `context` string, so it can
|
package/src/extensions/tools.ts
CHANGED
|
@@ -17,6 +17,18 @@ export function resolveMissingCapabilityToolName(options?: { missingCapabilityTo
|
|
|
17
17
|
return options?.missingCapabilityToolName ?? GET_MORE_TOOLS_NAME
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A virtual tool call sends one event to the server owner's PostHog project: it
|
|
22
|
+
* writes outside the conversation, overwrites nothing, records again on every
|
|
23
|
+
* repeat, and reaches nothing else.
|
|
24
|
+
*/
|
|
25
|
+
export const VIRTUAL_TOOL_ANNOTATIONS = {
|
|
26
|
+
readOnlyHint: false,
|
|
27
|
+
destructiveHint: false,
|
|
28
|
+
idempotentHint: false,
|
|
29
|
+
openWorldHint: false,
|
|
30
|
+
} as const
|
|
31
|
+
|
|
20
32
|
type ReportMissingToolDescriptor = CompatibleToolsListLike['tools'][number]
|
|
21
33
|
|
|
22
34
|
export function getReportMissingToolDescriptor(name: string = GET_MORE_TOOLS_NAME): ReportMissingToolDescriptor {
|
|
@@ -34,16 +46,7 @@ export function getReportMissingToolDescriptor(name: string = GET_MORE_TOOLS_NAM
|
|
|
34
46
|
},
|
|
35
47
|
required: ['context'],
|
|
36
48
|
},
|
|
37
|
-
annotations: {
|
|
38
|
-
title: 'Get More Tools',
|
|
39
|
-
readOnlyHint: true,
|
|
40
|
-
// Interacts with an external entity: the report lands in analytics.
|
|
41
|
-
openWorldHint: true,
|
|
42
|
-
// Only records the gap, so repeat calls are harmless — and advertising it
|
|
43
|
-
// as idempotent makes agents more willing to call it proactively.
|
|
44
|
-
idempotentHint: true,
|
|
45
|
-
destructiveHint: false,
|
|
46
|
-
},
|
|
49
|
+
annotations: { ...VIRTUAL_TOOL_ANNOTATIONS },
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -190,7 +190,7 @@ function truncateString(str: string | undefined, maxLength: number): string | un
|
|
|
190
190
|
if (str.length <= maxLength) {
|
|
191
191
|
return str
|
|
192
192
|
}
|
|
193
|
-
return str.slice(0, maxLength) + TRUNCATION_SUFFIX
|
|
193
|
+
return str.slice(0, maxLength - TRUNCATION_SUFFIX.length) + TRUNCATION_SUFFIX
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
function truncateStackFrames(frames: StackFrame[] | undefined): StackFrame[] | undefined {
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -224,8 +224,9 @@ export interface MCPAnalyticsOptions {
|
|
|
224
224
|
* fresh instance per request strips them before the handler runs. Ownership
|
|
225
225
|
* follows the same rule as a served listing: a Zod schema is read the way the
|
|
226
226
|
* MCP SDK advertises it, and a host that lists its own JSON Schema returns that.
|
|
227
|
-
* Ownership learned from a listing on the instance wins.
|
|
228
|
-
*
|
|
227
|
+
* Ownership learned from a listing on the instance wins. A rejected call to a
|
|
228
|
+
* tool this returns `undefined` for is recorded as `$mcp_unknown_tool`, not as
|
|
229
|
+
* a tool call. Ignored on a high-level `McpServer`, which reads its tool registry.
|
|
229
230
|
*/
|
|
230
231
|
resolveOriginalTool?: (toolName: string) => { inputSchema?: unknown } | undefined
|
|
231
232
|
/**
|
|
@@ -494,6 +495,8 @@ export interface ServerClientInfoLike {
|
|
|
494
495
|
|
|
495
496
|
export interface HighLevelMCPServerLike {
|
|
496
497
|
_registeredTools: { [name: string]: RegisteredTool }
|
|
498
|
+
/** Set once the MCP SDK has installed its own `tools/list` and `tools/call` handlers. */
|
|
499
|
+
_toolHandlersInitialized?: boolean
|
|
497
500
|
registerTool?(name: string, config: { description?: string; inputSchema?: unknown }, handler: ToolCallback): void
|
|
498
501
|
server: MCPServerLike
|
|
499
502
|
tool?(name: string, cb: ToolCallback): void
|
|
@@ -848,6 +851,12 @@ export interface MissingCapabilityCaptureData extends McpCaptureCommon {
|
|
|
848
851
|
parameters?: unknown
|
|
849
852
|
}
|
|
850
853
|
|
|
854
|
+
/** Payload for {@link PostHogMCP.captureUnknownTool}. Emits `$mcp_unknown_tool`. */
|
|
855
|
+
export interface UnknownToolCaptureData extends McpCaptureCommon {
|
|
856
|
+
/** The tool name the agent asked for → `$mcp_tool_name`, sanitized as free text. */
|
|
857
|
+
toolName: string
|
|
858
|
+
}
|
|
859
|
+
|
|
851
860
|
/** Payload for {@link PostHogMCP.captureFeedback}. Emits `$mcp_feedback`. */
|
|
852
861
|
export interface FeedbackCaptureData extends McpCaptureCommon {
|
|
853
862
|
/**
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "0.
|
|
1
|
+
export const version = "0.26.0"
|