feat(mcp): support model capture in custom dispatchers - #4735
feat(mcp): support model capture in custom dispatchers#4735lucasheriques wants to merge 1 commit into
Conversation
| event.isError = data.isError | ||
| event.errorType = data.errorType | ||
| applyIntent(event, data.intent, data.intentSource) | ||
| setEventModel(event, data.llmModel) |
There was a problem hiding this comment.
Low: Model identifiers bypass event redaction
An MCP client can place a token or other sensitive value in llm_model and cause it to be sent unredacted as $mcp_llm_model. sanitizeEvent() sanitizes agent-controlled intent, parameters, responses, and errors, but not llmModel; sanitize this field with sanitizeCapturedValue() in that shared pipeline before capture.
PR overviewThis pull request adds support for capturing LLM model identifiers when MCP requests are handled through custom dispatchers. One low-risk issue remains: a client-controlled model identifier is captured without passing through the event redaction pipeline, so sensitive content placed in that field could be sent as telemetry. No issues have yet been addressed, and sanitizing this field alongside other captured MCP values would resolve the remaining concern. Open issues (1)
Fixed/addressed: 0 · PR risk: 3/10 |
Prompt To Fix All With AI### Issue 1
packages/mcp/src/extensions/posthog-mcp.ts:120
**Direct model capture skips normalization**
When a custom dispatcher passes `llmModel` directly to `captureToolCall`, the value bypasses the trimming and `unknown` filtering performed by `prepareToolCall`, causing malformed identifiers or `unknown` to be emitted as `$mcp_llm_model`.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "feat(mcp): support model capture in cust..." | Re-trigger Greptile |
| event.isError = data.isError | ||
| event.errorType = data.errorType | ||
| applyIntent(event, data.intent, data.intentSource) | ||
| setEventModel(event, data.llmModel) |
There was a problem hiding this comment.
Direct model capture skips normalization
When a custom dispatcher passes llmModel directly to captureToolCall, the value bypasses the trimming and unknown filtering performed by prepareToolCall, causing malformed identifiers or unknown to be emitted as $mcp_llm_model.
Knowledge Base Used: Convex and MCP integrations
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/mcp/src/extensions/posthog-mcp.ts
Line: 120
Comment:
**Direct model capture skips normalization**
When a custom dispatcher passes `llmModel` directly to `captureToolCall`, the value bypasses the trimming and `unknown` filtering performed by `prepareToolCall`, causing malformed identifiers or `unknown` to be emitted as `$mcp_llm_model`.
**Knowledge Base Used:** [Convex and MCP integrations](https://app.greptile.com/posthog-org-19734/-/custom-context/knowledge-base/posthog/posthog-js/-/docs/specialized-platform-integrations.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Graphite Automations"sdk release label" took an action on this PR • (09/01/26)1 label was added to this PR based on Adam Bowker's automation. "Add graphite merge queue [copy]" took an action on this PR • (09/01/26)2 labels were added to this PR based on Lucas Faria's automation. |
| const ownershipByName = new Map<string, boolean>() | ||
| for (const tool of prepared) { | ||
| if (typeof tool.name !== 'string') { | ||
| continue | ||
| } | ||
| const ownsModel = analyticsOwnsParameter(tool.inputSchema, 'llm_model') | ||
| ownershipByName.set(tool.name, (ownershipByName.get(tool.name) ?? true) && ownsModel) | ||
| } | ||
| for (const [toolName, ownsModel] of ownershipByName) { | ||
| this.#modelParameterOwnership.set(toolName, ownsModel) | ||
| } |
There was a problem hiding this comment.
Ownership tracking accumulates stale entries across multiple prepareToolList() calls. If the same PostHogMCP instance calls prepareToolList() multiple times with different tool sets, #modelParameterOwnership retains old tool names. When prepareToolCall() is invoked for a tool from a previous list that's no longer in the current set, it uses outdated ownership data.
Example failure scenario:
client.prepareToolList([{ name: 'tool-a', inputSchema: { properties: {} } }]) // SDK owns model
client.prepareToolList([{ name: 'tool-b', inputSchema: { properties: {} } }]) // Different tools
client.prepareToolCall('tool-a', { llm_model: 'x' }) // Uses stale ownership = trueFix: Clear the map before rebuilding:
if (isCaptureModelEnabled(this.#captureModel)) {
this.#modelParameterOwnership.clear() // Add this line
const ownershipByName = new Map<string, boolean>()
// ... rest of logic
}| const ownershipByName = new Map<string, boolean>() | |
| for (const tool of prepared) { | |
| if (typeof tool.name !== 'string') { | |
| continue | |
| } | |
| const ownsModel = analyticsOwnsParameter(tool.inputSchema, 'llm_model') | |
| ownershipByName.set(tool.name, (ownershipByName.get(tool.name) ?? true) && ownsModel) | |
| } | |
| for (const [toolName, ownsModel] of ownershipByName) { | |
| this.#modelParameterOwnership.set(toolName, ownsModel) | |
| } | |
| this.#modelParameterOwnership.clear() | |
| const ownershipByName = new Map<string, boolean>() | |
| for (const tool of prepared) { | |
| if (typeof tool.name !== 'string') { | |
| continue | |
| } | |
| const ownsModel = analyticsOwnsParameter(tool.inputSchema, 'llm_model') | |
| ownershipByName.set(tool.name, (ownershipByName.get(tool.name) ?? true) && ownsModel) | |
| } | |
| for (const [toolName, ownsModel] of ownershipByName) { | |
| this.#modelParameterOwnership.set(toolName, ownsModel) | |
| } | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
|
@PostHog/mcp-analytics could you review this custom-dispatcher parity change? PostHog’s Hono MCP server depends on it before it can dogfood model capture. |
Problem
Custom MCP dispatchers cannot capture the calling model, so their tool-call analytics never include model attribution.
The wrapped-server path supports
captureModel, butPostHogMCPdoes not expose the same behavior. PostHog's Hono MCP server uses this custom-dispatcher path.Changes
PostHogMCPcallers can enable self-reported model capture withcaptureModel: true.prepareToolList()injects the required model field and records ownership by tool name.prepareToolCall()removes SDK-owned model arguments before dispatch and returns the captured model metadata.captureToolCall()emits$mcp_llm_modeland$mcp_llm_model_sourcethrough the standard sanitization pipeline.llm_modelfields remain untouched, andunknownvalues remain uncaptured.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)