@posthog/mcp 0.11.7 → 0.13.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 +51 -2
- package/dist/extensions/analytics-parameters.d.ts +22 -2
- package/dist/extensions/analytics-parameters.d.ts.map +1 -1
- package/dist/extensions/analytics-parameters.js +53 -10
- package/dist/extensions/analytics-parameters.mjs +37 -1
- package/dist/extensions/capture.d.ts.map +1 -1
- package/dist/extensions/capture.js +14 -8
- package/dist/extensions/capture.mjs +2 -0
- package/dist/extensions/client-identity.js +18 -14
- package/dist/extensions/compatibility.js +16 -12
- package/dist/extensions/constants.d.ts +3 -0
- package/dist/extensions/constants.d.ts.map +1 -1
- package/dist/extensions/constants.js +28 -18
- package/dist/extensions/constants.mjs +5 -2
- package/dist/extensions/context-parameters.d.ts +2 -2
- package/dist/extensions/context-parameters.d.ts.map +1 -1
- package/dist/extensions/context-parameters.js +14 -38
- package/dist/extensions/context-parameters.mjs +2 -30
- package/dist/extensions/conversation-id.js +18 -14
- package/dist/extensions/detect.js +23 -19
- package/dist/extensions/event-types.js +15 -11
- package/dist/extensions/exceptions.js +12 -8
- package/dist/extensions/headers.js +16 -12
- package/dist/extensions/ids.js +12 -8
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-highlevel.js +16 -9
- package/dist/extensions/instrument-highlevel.mjs +5 -2
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-lowlevel.js +13 -8
- package/dist/extensions/instrument-lowlevel.mjs +2 -1
- package/dist/extensions/instrumentation.d.ts +16 -0
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +31 -14
- package/dist/extensions/instrumentation.mjs +11 -1
- package/dist/extensions/intent.js +14 -10
- package/dist/extensions/internal.js +17 -13
- package/dist/extensions/lib-identity.js +12 -8
- package/dist/extensions/logger.js +17 -13
- package/dist/extensions/mcp-payloads.js +16 -12
- package/dist/extensions/mcp-sdk-compat.js +23 -19
- package/dist/extensions/model-parameters.d.ts +48 -0
- package/dist/extensions/model-parameters.d.ts.map +1 -0
- package/dist/extensions/model-parameters.js +84 -0
- package/dist/extensions/model-parameters.mjs +31 -0
- package/dist/extensions/output-instructions.js +15 -11
- package/dist/extensions/posthog-events.js +14 -8
- package/dist/extensions/posthog-events.mjs +2 -0
- package/dist/extensions/posthog-mcp.d.ts +23 -12
- package/dist/extensions/posthog-mcp.d.ts.map +1 -1
- package/dist/extensions/posthog-mcp.js +49 -11
- package/dist/extensions/posthog-mcp.mjs +37 -3
- package/dist/extensions/request-headers.js +15 -11
- package/dist/extensions/sanitization.d.ts +2 -2
- package/dist/extensions/sanitization.d.ts.map +1 -1
- package/dist/extensions/sanitization.js +13 -8
- package/dist/extensions/sanitization.mjs +1 -0
- package/dist/extensions/session-token.js +17 -13
- package/dist/extensions/session-token.mjs +2 -2
- package/dist/extensions/session.js +16 -12
- package/dist/extensions/sink.js +14 -10
- package/dist/extensions/tools.js +15 -11
- package/dist/extensions/tracing-helpers.js +15 -11
- package/dist/extensions/transport-identity.js +15 -11
- package/dist/extensions/truncation.d.ts.map +1 -1
- package/dist/extensions/truncation.js +22 -16
- package/dist/extensions/truncation.mjs +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -18
- package/dist/index.mjs +7 -7
- package/dist/types.d.ts +55 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/types.mjs +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +15 -11
- package/dist/version.mjs +1 -1
- package/package.json +28 -11
- package/src/extensions/analytics-parameters.ts +88 -2
- package/src/extensions/capture.ts +4 -0
- package/src/extensions/constants.ts +4 -0
- package/src/extensions/context-parameters.ts +10 -59
- package/src/extensions/instrument-highlevel.ts +4 -0
- package/src/extensions/instrument-lowlevel.ts +2 -0
- package/src/extensions/instrumentation.ts +51 -0
- package/src/extensions/model-parameters.ts +98 -0
- package/src/extensions/posthog-events.ts +8 -0
- package/src/extensions/posthog-mcp.ts +84 -12
- package/src/extensions/sanitization.ts +6 -2
- package/src/extensions/truncation.ts +3 -0
- package/src/index.ts +3 -0
- package/src/types.ts +55 -4
- package/src/version.ts +1 -1
|
@@ -4,11 +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 { MCPAnalyticsOptions } from '../types'
|
|
7
|
-
import {
|
|
8
|
-
canInjectAnalyticsParameter,
|
|
9
|
-
hasAnalyticsParameter,
|
|
10
|
-
type AnalyticsInjectableJsonSchema,
|
|
11
|
-
} from './analytics-parameters'
|
|
7
|
+
import { addAnalyticsParameterToTool, type AnalyticsInjectableJsonSchema } from './analytics-parameters'
|
|
12
8
|
import { DEFAULT_CONTEXT_PARAMETER_DESCRIPTION } from './constants'
|
|
13
9
|
import { log, type LoggerFn } from './logger'
|
|
14
10
|
|
|
@@ -27,8 +23,8 @@ export function getContextDescription(context: MCPAnalyticsOptions['context']):
|
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
/**
|
|
30
|
-
* Adds a context parameter to a tool's JSON Schema.
|
|
31
|
-
* This
|
|
26
|
+
* Adds a context parameter to a tool's JSON Schema, via the shared injector.
|
|
27
|
+
* This is called AFTER the MCP SDK has converted Zod schemas to JSON Schema,
|
|
32
28
|
* so we only need to handle JSON Schema format.
|
|
33
29
|
*
|
|
34
30
|
* Skips injection (with warning) for:
|
|
@@ -41,58 +37,13 @@ export function addContextParameterToTool<TTool extends ContextInjectableTool>(
|
|
|
41
37
|
contextDescriptionOverride?: string,
|
|
42
38
|
logger: LoggerFn = log
|
|
43
39
|
): TTool {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} else {
|
|
52
|
-
logger(`WARN: Tool "${toolName}" has complex schema (oneOf/allOf/anyOf/$ref). Skipping context injection.`)
|
|
53
|
-
}
|
|
54
|
-
return modifiedTool
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!modifiedTool.inputSchema) {
|
|
58
|
-
modifiedTool.inputSchema = {
|
|
59
|
-
type: 'object',
|
|
60
|
-
properties: {},
|
|
61
|
-
required: [],
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const contextDescription = contextDescriptionOverride || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION
|
|
66
|
-
|
|
67
|
-
// Deep copy: the server may reuse or freeze the schema object it handed us.
|
|
68
|
-
modifiedTool.inputSchema = JSON.parse(JSON.stringify(modifiedTool.inputSchema)) as AnalyticsInjectableJsonSchema
|
|
69
|
-
|
|
70
|
-
const inputSchema = modifiedTool.inputSchema as AnalyticsInjectableJsonSchema
|
|
71
|
-
|
|
72
|
-
if (!inputSchema.properties) {
|
|
73
|
-
inputSchema.properties = {}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// The MCP SDK emits `additionalProperties: false` when converting Zod schemas;
|
|
77
|
-
// left in place it would make the injected `context` key invalid.
|
|
78
|
-
if (inputSchema.additionalProperties === false) {
|
|
79
|
-
inputSchema.additionalProperties = undefined
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
inputSchema.properties.context = {
|
|
83
|
-
type: 'string',
|
|
84
|
-
description: contextDescription,
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (Array.isArray(inputSchema.required)) {
|
|
88
|
-
if (!inputSchema.required.includes('context')) {
|
|
89
|
-
inputSchema.required.push('context')
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
inputSchema.required = ['context']
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return modifiedTool
|
|
40
|
+
return addAnalyticsParameterToTool(
|
|
41
|
+
tool,
|
|
42
|
+
'context',
|
|
43
|
+
contextDescriptionOverride || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
|
|
44
|
+
'context',
|
|
45
|
+
logger
|
|
46
|
+
)
|
|
96
47
|
}
|
|
97
48
|
|
|
98
49
|
export function addContextParameterToTools<TTool extends ContextInjectableTool>(
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
stripOwnedAnalyticsArguments,
|
|
18
18
|
} from './analytics-parameters'
|
|
19
19
|
import { isContextEnabled } from './context-parameters'
|
|
20
|
+
import { isCaptureModelEnabled } from './model-parameters'
|
|
20
21
|
import { MCPAnalyticsEventType } from './event-types'
|
|
21
22
|
import { getServerTrackingData } from './internal'
|
|
22
23
|
import type { LoggerFn } from './logger'
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
handleListToolsRequest,
|
|
28
29
|
patchRequestHandlers,
|
|
29
30
|
captureToolCall,
|
|
31
|
+
getVirtualToolParameterOwnership,
|
|
30
32
|
isToolAdvertised,
|
|
31
33
|
readToolMetaCategory,
|
|
32
34
|
type HandlerPatch,
|
|
@@ -189,6 +191,7 @@ function addTracingToToolCallbackInternal(
|
|
|
189
191
|
const cleanedArgs = stripOwnedAnalyticsArguments(args, {
|
|
190
192
|
context: isContextEnabled(options?.context) && analyticsOwnsParameter(inputSchema, 'context'),
|
|
191
193
|
conversationId: options?.enableConversationId === true && analyticsOwnsParameter(inputSchema, 'conversation_id'),
|
|
194
|
+
llmModel: isCaptureModelEnabled(options?.captureModel) && analyticsOwnsParameter(inputSchema, 'llm_model'),
|
|
192
195
|
})
|
|
193
196
|
try {
|
|
194
197
|
if (cleanedArgs === undefined) {
|
|
@@ -247,6 +250,7 @@ async function handleToolCallRequest(
|
|
|
247
250
|
extra,
|
|
248
251
|
eventType: MCPAnalyticsEventType.mcpMissingCapability,
|
|
249
252
|
explicitContextIntent: context,
|
|
253
|
+
parameterOwnership: getVirtualToolParameterOwnership(data, toolName),
|
|
250
254
|
execute: async () => handleReportMissing({ context }, data.logger),
|
|
251
255
|
})
|
|
252
256
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
patchRequestHandlers,
|
|
15
15
|
registerFallbackRequestHandler,
|
|
16
16
|
captureToolCall,
|
|
17
|
+
getVirtualToolParameterOwnership,
|
|
17
18
|
isToolAdvertised,
|
|
18
19
|
type HandlerPatch,
|
|
19
20
|
} from './instrumentation'
|
|
@@ -90,6 +91,7 @@ async function handleToolCallRequest(
|
|
|
90
91
|
extra,
|
|
91
92
|
eventType: MCPAnalyticsEventType.mcpMissingCapability,
|
|
92
93
|
explicitContextIntent: context,
|
|
94
|
+
parameterOwnership: getVirtualToolParameterOwnership(data, toolName),
|
|
93
95
|
execute: async () => handleReportMissing({ context }, data.logger),
|
|
94
96
|
})
|
|
95
97
|
}
|
|
@@ -16,6 +16,13 @@ import type {
|
|
|
16
16
|
} from '../types'
|
|
17
17
|
import { getAnalyticsParameterOwnership, stripOwnedAnalyticsArguments } from './analytics-parameters'
|
|
18
18
|
import { addContextParameterToTools, getContextDescription, isContextEnabled } from './context-parameters'
|
|
19
|
+
import {
|
|
20
|
+
addModelParameterToTools,
|
|
21
|
+
getModelArgument,
|
|
22
|
+
getModelDescription,
|
|
23
|
+
isCaptureModelEnabled,
|
|
24
|
+
setEventModel,
|
|
25
|
+
} from './model-parameters'
|
|
19
26
|
import {
|
|
20
27
|
addConversationIdToTools,
|
|
21
28
|
type ConversationIdResolution,
|
|
@@ -179,6 +186,7 @@ function getActiveAnalyticsParameterOwnership(
|
|
|
179
186
|
contextOwnershipKnown: ownership !== undefined,
|
|
180
187
|
context: !isMissingCapabilityTool && isContextEnabled(data.options.context) && ownership?.context === true,
|
|
181
188
|
conversationId: data.options.enableConversationId === true && ownership?.conversationId === true,
|
|
189
|
+
llmModel: isCaptureModelEnabled(data.options.captureModel) && ownership?.llmModel === true,
|
|
182
190
|
// Deliberately read off `listed`, never the override: only the advertised
|
|
183
191
|
// JSON Schema can say whether `tools/list` declared `_mcp_instructions` (an
|
|
184
192
|
// override is built from the live registry, which holds Zod on the
|
|
@@ -263,6 +271,13 @@ async function prepareToolCallEvent(
|
|
|
263
271
|
|
|
264
272
|
await applyResolvedMetadata(event, data, request, extra)
|
|
265
273
|
setEventIntent(event, await resolveToolCallIntent(data, request, canCaptureContextIntent, extra))
|
|
274
|
+
// Unlike intent, the model is only read under positive ownership: with
|
|
275
|
+
// ownership unresolved, `llm_model` may be the application's own argument,
|
|
276
|
+
// and recording a customer value as the calling agent's model is worse
|
|
277
|
+
// than a gap in coverage.
|
|
278
|
+
if (ownership.llmModel) {
|
|
279
|
+
setEventModel(event, getModelArgument(request))
|
|
280
|
+
}
|
|
266
281
|
return { event, requestAttribution }
|
|
267
282
|
} catch (error) {
|
|
268
283
|
data.logger(
|
|
@@ -493,6 +508,31 @@ export function patchRequestHandlers(server: MCPServerLike, patches: Record<stri
|
|
|
493
508
|
}) as MCPServerLike['setRequestHandler']
|
|
494
509
|
}
|
|
495
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Ownership for the `get_more_tools` virtual tool, resolved without the
|
|
513
|
+
* `tools/list` cache.
|
|
514
|
+
*
|
|
515
|
+
* The missing-capability branch is only entered when the application does not
|
|
516
|
+
* advertise a tool by this name, so the descriptor is the SDK's own and what it
|
|
517
|
+
* declares is known statically. An instance that never served a listing — a
|
|
518
|
+
* per-request `McpServer`/`Server`, the topology in ADR-0011 — would otherwise
|
|
519
|
+
* read every injected parameter as not-ours and neither capture nor strip it.
|
|
520
|
+
*
|
|
521
|
+
* `conversation_id` still comes from the cache on purpose: resolving it here too
|
|
522
|
+
* would start minting a handle, and appending its prompt-back block, on
|
|
523
|
+
* instances that today mint none. That changes session anchoring (ADR-0004)
|
|
524
|
+
* rather than closing this gap.
|
|
525
|
+
*/
|
|
526
|
+
export function getVirtualToolParameterOwnership(
|
|
527
|
+
data: MCPAnalyticsData,
|
|
528
|
+
toolName: string
|
|
529
|
+
): AnalyticsParameterOwnership {
|
|
530
|
+
return {
|
|
531
|
+
...getAnalyticsParameterOwnership(getReportMissingToolDescriptor(toolName).inputSchema),
|
|
532
|
+
conversationId: data.toolAnalyticsParameterOwnership.get(toolName)?.conversationId === true,
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
496
536
|
/**
|
|
497
537
|
* Checks the server's raw listing for a real owner of a candidate virtual tool.
|
|
498
538
|
* This does not depend on a previous client request and does not call the
|
|
@@ -664,6 +704,17 @@ async function getTracedToolsList(
|
|
|
664
704
|
}
|
|
665
705
|
}
|
|
666
706
|
|
|
707
|
+
// After the virtual tool is appended, so it advertises `llm_model` too.
|
|
708
|
+
// Its ownership was cached above from the un-injected descriptor, so the
|
|
709
|
+
// SDK already strips and captures the argument on its calls — without
|
|
710
|
+
// injecting here the agent would never be asked for one, and
|
|
711
|
+
// missing-capability reports would be the only calls with no model.
|
|
712
|
+
// `context` is deliberately not injected into it: the virtual tool
|
|
713
|
+
// declares its own, and that argument is the report itself.
|
|
714
|
+
if (isCaptureModelEnabled(data.options.captureModel)) {
|
|
715
|
+
tools = addModelParameterToTools(tools, getModelDescription(data.options.captureModel), data.logger)
|
|
716
|
+
}
|
|
717
|
+
|
|
667
718
|
if (data.options.enableConversationId) {
|
|
668
719
|
tools = addConversationIdToTools(tools, data.logger)
|
|
669
720
|
tools = addInstructionsToOutputSchemas(tools, data.logger)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { MCPAnalyticsOptions, MCPRequestLike, McpEvent } from '../types'
|
|
2
|
+
import { addAnalyticsParameterToTool, type AnalyticsInjectableJsonSchema } from './analytics-parameters'
|
|
3
|
+
import { DEFAULT_MODEL_PARAMETER_DESCRIPTION } from './constants'
|
|
4
|
+
import { log, type LoggerFn } from './logger'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Self-reported model capture (`captureModel`).
|
|
8
|
+
*
|
|
9
|
+
* MCP keeps servers deliberately model-ignorant: the wire carries client
|
|
10
|
+
* name/version and protocol version, never the LLM behind the client, and no
|
|
11
|
+
* spec revision changes that. The one place the information exists on the
|
|
12
|
+
* server side of the connection is the agent itself — harnesses inject the
|
|
13
|
+
* model id into the system prompt, so the agent can state it the same way it
|
|
14
|
+
* states its intent through the `context` parameter.
|
|
15
|
+
*
|
|
16
|
+
* This module injects a required `llm_model` string parameter into every tool
|
|
17
|
+
* (mirroring `context-parameters.ts`), strips it before the tool runs, and
|
|
18
|
+
* captures it as `$mcp_llm_model` with `$mcp_llm_model_source = "self_reported"`.
|
|
19
|
+
* The source property is deliberate: like `clientInfo` in the MCP spec, the
|
|
20
|
+
* value is self-reported and unverified — right for degradation analytics
|
|
21
|
+
* ("does our MCP get worse on model X?"), never for billing or security.
|
|
22
|
+
*
|
|
23
|
+
* Reasoning effort is deliberately NOT captured: it never crosses the wire,
|
|
24
|
+
* and models cannot reliably self-report it (harnesses apply it as a sampling
|
|
25
|
+
* parameter the model never sees), so any captured value would be noise.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface ModelInjectableTool {
|
|
29
|
+
inputSchema?: AnalyticsInjectableJsonSchema
|
|
30
|
+
name?: string
|
|
31
|
+
[key: string]: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Unlike `context`, model capture is opt-in: off unless explicitly enabled. */
|
|
35
|
+
export function isCaptureModelEnabled(captureModel: MCPAnalyticsOptions['captureModel']): boolean {
|
|
36
|
+
return captureModel === true || (typeof captureModel === 'object' && captureModel !== null)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getModelDescription(captureModel: MCPAnalyticsOptions['captureModel']): string | undefined {
|
|
40
|
+
return typeof captureModel === 'object' && captureModel !== null ? captureModel.description : undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adds an `llm_model` parameter to a tool's JSON Schema, via the shared
|
|
45
|
+
* injector so schema handling stays identical to `context`: a tool that already
|
|
46
|
+
* declares `llm_model` owns it, and complex schemas can't safely gain keys.
|
|
47
|
+
*/
|
|
48
|
+
export function addModelParameterToTool<TTool extends ModelInjectableTool>(
|
|
49
|
+
tool: TTool,
|
|
50
|
+
modelDescriptionOverride?: string,
|
|
51
|
+
logger: LoggerFn = log
|
|
52
|
+
): TTool {
|
|
53
|
+
return addAnalyticsParameterToTool(
|
|
54
|
+
tool,
|
|
55
|
+
'llm_model',
|
|
56
|
+
modelDescriptionOverride || DEFAULT_MODEL_PARAMETER_DESCRIPTION,
|
|
57
|
+
'model',
|
|
58
|
+
logger
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function addModelParameterToTools<TTool extends ModelInjectableTool>(
|
|
63
|
+
tools: TTool[],
|
|
64
|
+
modelDescriptionOverride?: string,
|
|
65
|
+
logger: LoggerFn = log
|
|
66
|
+
): TTool[] {
|
|
67
|
+
return tools.map((tool) => addModelParameterToTool(tool, modelDescriptionOverride, logger))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reads the self-reported model off a tool-call request. Returns `undefined`
|
|
72
|
+
* for a missing, blank, or `"unknown"` value — the parameter description asks
|
|
73
|
+
* agents to pass `unknown` when uncertain, and an honest "I don't know" must
|
|
74
|
+
* not become a property value queries would group by.
|
|
75
|
+
*/
|
|
76
|
+
export function getModelArgument(request: MCPRequestLike): string | undefined {
|
|
77
|
+
return normalizeModel(request.params?.arguments?.llm_model)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeModel(model: unknown): string | undefined {
|
|
81
|
+
if (typeof model !== 'string') {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
const trimmed = model.trim()
|
|
85
|
+
if (!trimmed || trimmed.toLowerCase() === 'unknown') {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
return trimmed
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function setEventModel(event: McpEvent, model: string | undefined): void {
|
|
92
|
+
const normalizedModel = normalizeModel(model)
|
|
93
|
+
if (!normalizedModel) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
event.llmModel = normalizedModel
|
|
97
|
+
event.llmModelSource = 'self_reported'
|
|
98
|
+
}
|
|
@@ -170,6 +170,14 @@ function addCommonEventProperties(event: Event, properties: Record<string, unkno
|
|
|
170
170
|
if (event.userIntentSource) {
|
|
171
171
|
properties[PostHogMCPAnalyticsProperty.IntentSource] = event.userIntentSource
|
|
172
172
|
}
|
|
173
|
+
// Self-reported by the calling agent (`captureModel`), so present only on the
|
|
174
|
+
// requests where the agent supplied a definite value — never inferred.
|
|
175
|
+
if (event.llmModel) {
|
|
176
|
+
properties[PostHogMCPAnalyticsProperty.LlmModel] = event.llmModel
|
|
177
|
+
}
|
|
178
|
+
if (event.llmModelSource) {
|
|
179
|
+
properties[PostHogMCPAnalyticsProperty.LlmModelSource] = event.llmModelSource
|
|
180
|
+
}
|
|
173
181
|
if (event.isError !== undefined) {
|
|
174
182
|
properties[PostHogMCPAnalyticsProperty.IsError] = event.isError
|
|
175
183
|
}
|
|
@@ -3,14 +3,17 @@ import { PostHog, type PostHogOptions } from 'posthog-node'
|
|
|
3
3
|
import type {
|
|
4
4
|
InitializeCaptureData,
|
|
5
5
|
JsonRecord,
|
|
6
|
+
MCPAnalyticsOptions,
|
|
6
7
|
McpCaptureCommon,
|
|
7
8
|
McpEvent,
|
|
8
9
|
MissingCapabilityCaptureData,
|
|
9
10
|
PreparedToolCall,
|
|
11
|
+
PrepareToolCallOptions,
|
|
10
12
|
PrepareToolListOptions,
|
|
11
13
|
ToolCallCaptureData,
|
|
12
14
|
ToolsListCaptureData,
|
|
13
15
|
} from '../types'
|
|
16
|
+
import { analyticsOwnsParameter, stripOwnedAnalyticsArguments } from './analytics-parameters'
|
|
14
17
|
import {
|
|
15
18
|
addContextParameterToTools,
|
|
16
19
|
getContextDescription,
|
|
@@ -22,6 +25,13 @@ import { captureException } from './exceptions'
|
|
|
22
25
|
import { normalizeHeaderString } from './headers'
|
|
23
26
|
import { applyMcpLibIdentity } from './lib-identity'
|
|
24
27
|
import { log } from './logger'
|
|
28
|
+
import {
|
|
29
|
+
addModelParameterToTool,
|
|
30
|
+
getModelArgument,
|
|
31
|
+
getModelDescription,
|
|
32
|
+
isCaptureModelEnabled,
|
|
33
|
+
setEventModel,
|
|
34
|
+
} from './model-parameters'
|
|
25
35
|
import { McpEventSink } from './sink'
|
|
26
36
|
import { GET_MORE_TOOLS_NAME, getReportMissingToolDescriptor } from './tools'
|
|
27
37
|
|
|
@@ -37,6 +47,11 @@ export interface PostHogMCPOptions extends PostHogOptions {
|
|
|
37
47
|
* can't drift. Defaults to `get_more_tools`.
|
|
38
48
|
*/
|
|
39
49
|
missingCapabilityToolName?: string
|
|
50
|
+
/**
|
|
51
|
+
* Inject a required `llm_model` argument into advertised tools and capture
|
|
52
|
+
* the agent's self-reported value. Off by default.
|
|
53
|
+
*/
|
|
54
|
+
captureModel?: MCPAnalyticsOptions['captureModel']
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/**
|
|
@@ -57,7 +72,10 @@ export interface PostHogMCPOptions extends PostHogOptions {
|
|
|
57
72
|
* ```ts
|
|
58
73
|
* import { PostHogMCP } from "@posthog/mcp"
|
|
59
74
|
*
|
|
60
|
-
* const posthog = new PostHogMCP("phc_your_project_token", {
|
|
75
|
+
* const posthog = new PostHogMCP("phc_your_project_token", {
|
|
76
|
+
* host: "https://us.i.posthog.com",
|
|
77
|
+
* captureModel: true,
|
|
78
|
+
* })
|
|
61
79
|
*
|
|
62
80
|
* posthog.captureToolCall({
|
|
63
81
|
* toolName: "search_docs",
|
|
@@ -78,10 +96,13 @@ export class PostHogMCP extends PostHog {
|
|
|
78
96
|
// The get_more_tools name lives here (not on the per-call options) so that
|
|
79
97
|
// prepareToolList (inject) and prepareToolCall (detect) always agree.
|
|
80
98
|
readonly #missingCapabilityToolName: string
|
|
99
|
+
readonly #captureModel: MCPAnalyticsOptions['captureModel']
|
|
100
|
+
readonly #modelParameterOwnership = new Map<string, boolean>()
|
|
81
101
|
|
|
82
102
|
constructor(apiKey: string, options: PostHogMCPOptions = {}) {
|
|
83
103
|
super(apiKey, options)
|
|
84
104
|
this.#missingCapabilityToolName = options.missingCapabilityToolName ?? GET_MORE_TOOLS_NAME
|
|
105
|
+
this.#captureModel = options.captureModel
|
|
85
106
|
applyMcpLibIdentity(this)
|
|
86
107
|
}
|
|
87
108
|
|
|
@@ -97,6 +118,10 @@ export class PostHogMCP extends PostHog {
|
|
|
97
118
|
event.isError = data.isError
|
|
98
119
|
event.errorType = data.errorType
|
|
99
120
|
applyIntent(event, data.intent, data.intentSource)
|
|
121
|
+
setEventModel(event, data.llmModel)
|
|
122
|
+
if (event.llmModel && data.llmModelSource) {
|
|
123
|
+
event.llmModelSource = data.llmModelSource
|
|
124
|
+
}
|
|
100
125
|
if (data.isError) {
|
|
101
126
|
event.error = captureException(data.error ?? `Tool ${data.toolName} returned an error`)
|
|
102
127
|
}
|
|
@@ -137,9 +162,9 @@ export class PostHogMCP extends PostHog {
|
|
|
137
162
|
/**
|
|
138
163
|
* Decorate your `tools/list` response with PostHog's analytics affordances:
|
|
139
164
|
* injects the `context` argument into every tool (so agents state their intent,
|
|
140
|
-
* captured as `$mcp_intent`)
|
|
141
|
-
* `
|
|
142
|
-
*
|
|
165
|
+
* captured as `$mcp_intent`), injects `llm_model` when the constructor's
|
|
166
|
+
* `captureModel` option is enabled, and appends `get_more_tools` when
|
|
167
|
+
* `reportMissing` is on. Returns a new array; your tools are untouched.
|
|
143
168
|
*
|
|
144
169
|
* The appended `get_more_tools` descriptor carries only the base MCP tool fields
|
|
145
170
|
* (name, description, input schema) — not any framework-specific fields your
|
|
@@ -165,18 +190,41 @@ export class PostHogMCP extends PostHog {
|
|
|
165
190
|
if (options.reportMissing && !prepared.some((tool) => tool?.name === this.#missingCapabilityToolName)) {
|
|
166
191
|
prepared = [...prepared, getReportMissingToolDescriptor(this.#missingCapabilityToolName) as TTool]
|
|
167
192
|
}
|
|
193
|
+
|
|
194
|
+
if (isCaptureModelEnabled(this.#captureModel)) {
|
|
195
|
+
this.#modelParameterOwnership.clear()
|
|
196
|
+
const ownershipByName = new Map<string, boolean>()
|
|
197
|
+
for (const tool of prepared) {
|
|
198
|
+
if (typeof tool.name !== 'string') {
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
const ownsModel = analyticsOwnsParameter(tool.inputSchema, 'llm_model')
|
|
202
|
+
ownershipByName.set(tool.name, (ownershipByName.get(tool.name) ?? true) && ownsModel)
|
|
203
|
+
}
|
|
204
|
+
for (const [toolName, ownsModel] of ownershipByName) {
|
|
205
|
+
this.#modelParameterOwnership.set(toolName, ownsModel)
|
|
206
|
+
}
|
|
207
|
+
const modelDescription = getModelDescription(this.#captureModel)
|
|
208
|
+
prepared = prepared.map((tool) =>
|
|
209
|
+
typeof tool.name === 'string' && ownershipByName.get(tool.name) === false
|
|
210
|
+
? tool
|
|
211
|
+
: addModelParameterToTool(tool, modelDescription)
|
|
212
|
+
)
|
|
213
|
+
}
|
|
168
214
|
return prepared
|
|
169
215
|
}
|
|
170
216
|
|
|
171
217
|
/**
|
|
172
|
-
* Read an incoming `tools/call` before you dispatch it: pulls
|
|
173
|
-
*
|
|
174
|
-
* arguments (so your handler and its schema validation never see it), and flags
|
|
218
|
+
* Read an incoming `tools/call` before you dispatch it: pulls analytics values
|
|
219
|
+
* from SDK-owned arguments, strips those arguments before validation, and flags
|
|
175
220
|
* whether the call targeted the `get_more_tools` virtual tool.
|
|
176
221
|
*
|
|
177
|
-
* Pass the returned
|
|
222
|
+
* Pass the returned intent and model fields to {@link captureToolCall}, and
|
|
178
223
|
* dispatch the returned `args` to your tool.
|
|
179
224
|
*
|
|
225
|
+
* On stateless or multi-replica servers, pass the original tool descriptor
|
|
226
|
+
* so ownership does not depend on which process served `tools/list`.
|
|
227
|
+
*
|
|
180
228
|
* This only extracts the explicit `context` argument (`intentSource:
|
|
181
229
|
* 'context_parameter'`); it does not infer intent. If you run your own
|
|
182
230
|
* inference, pass that string with `intentSource: 'inferred'` straight to
|
|
@@ -185,22 +233,42 @@ export class PostHogMCP extends PostHog {
|
|
|
185
233
|
*
|
|
186
234
|
* @example
|
|
187
235
|
* ```ts
|
|
188
|
-
* const { intent, intentSource, args, isMissingCapability } =
|
|
236
|
+
* const { intent, intentSource, llmModel, llmModelSource, args, isMissingCapability } =
|
|
237
|
+
* posthog.prepareToolCall(name, rawArgs)
|
|
189
238
|
* if (isMissingCapability) {
|
|
190
|
-
* posthog.captureMissingCapability({ context: intent, ...identity })
|
|
239
|
+
* posthog.captureMissingCapability({ context: intent, llmModel, llmModelSource, ...identity })
|
|
191
240
|
* return getMoreToolsResult()
|
|
192
241
|
* }
|
|
193
242
|
* const result = await runTool(name, args)
|
|
194
243
|
* posthog.captureToolCall({ toolName: name, intent, intentSource, ...identity })
|
|
195
244
|
* ```
|
|
196
245
|
*/
|
|
197
|
-
prepareToolCall(
|
|
246
|
+
prepareToolCall(
|
|
247
|
+
name: string,
|
|
248
|
+
args?: Record<string, unknown>,
|
|
249
|
+
options: PrepareToolCallOptions = {}
|
|
250
|
+
): PreparedToolCall {
|
|
198
251
|
const rawContext = args?.context
|
|
199
252
|
const intent = typeof rawContext === 'string' && rawContext.trim() ? rawContext.trim() : undefined
|
|
253
|
+
const ownsModel =
|
|
254
|
+
isCaptureModelEnabled(this.#captureModel) &&
|
|
255
|
+
(options.originalTool
|
|
256
|
+
? analyticsOwnsParameter(options.originalTool.inputSchema, 'llm_model')
|
|
257
|
+
: this.#modelParameterOwnership.get(name) === true)
|
|
258
|
+
const llmModel = ownsModel ? getModelArgument({ params: { arguments: args } }) : undefined
|
|
259
|
+
const strippedArgs = stripContext(args)
|
|
200
260
|
return {
|
|
201
261
|
intent,
|
|
202
262
|
intentSource: intent ? 'context_parameter' : undefined,
|
|
203
|
-
|
|
263
|
+
llmModel,
|
|
264
|
+
llmModelSource: llmModel ? 'self_reported' : undefined,
|
|
265
|
+
args: ownsModel
|
|
266
|
+
? (stripOwnedAnalyticsArguments(strippedArgs, {
|
|
267
|
+
context: false,
|
|
268
|
+
conversationId: false,
|
|
269
|
+
llmModel: true,
|
|
270
|
+
}) as Record<string, unknown> | undefined)
|
|
271
|
+
: strippedArgs,
|
|
204
272
|
isMissingCapability: name === this.#missingCapabilityToolName,
|
|
205
273
|
}
|
|
206
274
|
}
|
|
@@ -215,6 +283,10 @@ export class PostHogMCP extends PostHog {
|
|
|
215
283
|
event.resourceName = this.#missingCapabilityToolName
|
|
216
284
|
event.parameters = data.parameters
|
|
217
285
|
applyIntent(event, data.context, 'context_parameter')
|
|
286
|
+
setEventModel(event, data.llmModel)
|
|
287
|
+
if (event.llmModel && data.llmModelSource) {
|
|
288
|
+
event.llmModelSource = data.llmModelSource
|
|
289
|
+
}
|
|
218
290
|
this.#emit(event)
|
|
219
291
|
}
|
|
220
292
|
|
|
@@ -15,8 +15,8 @@ function isRecord(value: unknown): value is SanitizedRecord {
|
|
|
15
15
|
/**
|
|
16
16
|
* Sanitizes an event by redacting non-text content blocks from responses
|
|
17
17
|
* and large base64-encoded strings from parameters, and applying the same
|
|
18
|
-
* string redaction (PostHog tokens, base64 blobs, sensitive keys) to
|
|
19
|
-
*
|
|
18
|
+
* string redaction (PostHog tokens, base64 blobs, sensitive keys) to values
|
|
19
|
+
* supplied by the agent.
|
|
20
20
|
*
|
|
21
21
|
* This is a synchronous operation that returns a new object without mutating the original.
|
|
22
22
|
* It should run after customer redaction in the event pipeline.
|
|
@@ -39,6 +39,10 @@ export function sanitizeEvent<T extends Event | McpEvent>(event: T): T {
|
|
|
39
39
|
result.userIntent = sanitizeCapturedValue(result.userIntent) as string
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
if (result.llmModel != null) {
|
|
43
|
+
result.llmModel = sanitizeCapturedValue(result.llmModel) as string
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
if (result.error != null) {
|
|
43
47
|
result.error = sanitizeExceptionValues(result.error)
|
|
44
48
|
}
|
|
@@ -435,6 +435,9 @@ export function truncateEvent<T extends Event | McpEvent>(event: T): T {
|
|
|
435
435
|
result.clientUserAgent = truncateString(result.clientUserAgent, MAX_METADATA_LENGTH)
|
|
436
436
|
result.vendorClient = truncateString(result.vendorClient, MAX_METADATA_LENGTH)
|
|
437
437
|
result.errorType = truncateString(result.errorType, MAX_METADATA_LENGTH)
|
|
438
|
+
// Agent-supplied free text, same trust level as a header. Real model ids are
|
|
439
|
+
// tens of characters; anything past the metadata cap is junk, not a model.
|
|
440
|
+
result.llmModel = truncateString(result.llmModel, MAX_METADATA_LENGTH)
|
|
438
441
|
|
|
439
442
|
// Error field limits — operate on the core `$exception_list` shape
|
|
440
443
|
if (result.error != null && typeof result.error === 'object') {
|
package/src/index.ts
CHANGED
|
@@ -228,9 +228,12 @@ export type {
|
|
|
228
228
|
McpCaptureCommon,
|
|
229
229
|
MCPAnalyticsContextOptions,
|
|
230
230
|
MCPAnalyticsIntentSource,
|
|
231
|
+
MCPAnalyticsModelOptions,
|
|
232
|
+
MCPAnalyticsModelSource,
|
|
231
233
|
MCPAnalyticsOptions,
|
|
232
234
|
MissingCapabilityCaptureData,
|
|
233
235
|
PreparedToolCall,
|
|
236
|
+
PrepareToolCallOptions,
|
|
234
237
|
PrepareToolListOptions,
|
|
235
238
|
RequestHeaderBag,
|
|
236
239
|
ToolCallCaptureData,
|