Skip to content

feat(mcp): support model capture in custom dispatchers - #4735

Open
lucasheriques wants to merge 1 commit into
mainfrom
codex/mcp-custom-dispatcher-model-capture
Open

feat(mcp): support model capture in custom dispatchers#4735
lucasheriques wants to merge 1 commit into
mainfrom
codex/mcp-custom-dispatcher-model-capture

Conversation

@lucasheriques

Copy link
Copy Markdown
Contributor

Problem

Custom MCP dispatchers cannot capture the calling model, so their tool-call analytics never include model attribution.

The wrapped-server path supports captureModel, but PostHogMCP does not expose the same behavior. PostHog's Hono MCP server uses this custom-dispatcher path.

Changes

  • PostHogMCP callers can enable self-reported model capture with captureModel: 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_model and $mcp_llm_model_source through the standard sanitization pipeline.
  • Application-owned llm_model fields remain untouched, and unknown values remain uncaptured.
  • The change has no visual effect.

Release info Sub-libraries affected

Libraries affected

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react-native-plugin
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/next
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/openfeature-node-provider
  • @posthog/openfeature-web-provider
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin
  • @posthog/types
  • @posthog/browser-common
  • @posthog/mcp

Checklist

  • Tests for new code
  • Accounted for the impact of any changes across different platforms
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Authored with Codex after testing PostHog's custom-dispatcher integration path.
  • Used the debugging-mcp-analytics, writing-tests, writing-code-comments, and writing-pr-descriptions skills.
  • Extended the nearest public API test. All 672 MCP unit tests, lint, and build pass locally.

event.isError = data.isError
event.errorType = data.errorType
applyIntent(event, data.intent, data.intentSource)
setEventModel(event, data.llmModel)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@veria-ai

veria-ai Bot commented Sep 1, 2026

Copy link
Copy Markdown

PR overview

This 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

@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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-app

graphite-app Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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.

Comment on lines +194 to +204
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)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = true

Fix: Clear the map before rebuilding:

if (isCaptureModelEnabled(this.#captureModel)) {
  this.#modelParameterOwnership.clear() // Add this line
  const ownershipByName = new Map<string, boolean>()
  // ... rest of logic
}
Suggested change
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

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@lucasheriques

Copy link
Copy Markdown
Contributor Author

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant