Capturing agent intent

Contents

Intent explains why an agent called a tool. Use it to understand the goals behind tool usage.

The SDK captures intent as a single property – $mcp_intent – that can come from one of two sources:

  1. A context argument the agent passes on every tool call. Captured with $mcp_intent_source = "context_parameter".
  2. A fallback callback you supply on instrument(). Captured with $mcp_intent_source = "inferred".

Explicit context always wins. If the agent passes a non-empty context, the fallback is not invoked. The Rust SDK also reads a third source first: userIntent from the client's io.modelcontextprotocol/aiInvocation request metadata, captured with $mcp_intent_source = "client_metadata".

The context argument

Intent capture is on by default. The SDK adds a context string to compatible tool schemas and removes it before your handler runs when it can confirm it injected the field. An application-owned context argument is preserved.

Every SDK adds the context argument by default. TypeScript, Go, Ruby, and Rust advertise it as required. Python makes it optional on raw low-level servers and standalone FastMCP. Calls can omit it without failing validation.

What the agent sees in the schema:

JSON
{
"type": "object",
"properties": {
"context": {
"type": "string",
"description": "Explain in 15-25 words, in third person, why this tool is called and how it supports the user's goal. For analytics only. ..."
},
"...": "your real tool arguments"
},
"required": ["context", "..."]
}

Your handler receives only your own arguments. The SDK removes context before the handler runs.

The full default prompt also tells the agent never to include personal data and to generalize people into roles such as "a user" or "the customer".

PostHog captures this value as $mcp_intent:

"Retrieving a customer's recent pageviews to triage a drop in conversion"

If the agent includes personal data anyway, the SDK replaces emails, phone numbers, IP addresses, card numbers, and US Social Security numbers with [redacted] before sending. See Privacy and redaction.

Customize the prompt

If you want to nudge the agent toward a specific style of context (use case, user goal, ticket id, etc.), pass an object:

instrument(server, posthog, {
context: {
description: "Describe the user's underlying goal in one sentence, not the tool you're calling.",
},
})

The Go SDK uses a fixed prompt.

Disabling the injected argument

Turn off the injected argument with context: false in TypeScript and Ruby, MCPAnalyticsOptions(context=False) in Python, posthogmcpsdk.WithContextParameter(false) in Go, or .context(false) on McpOptionsBuilder in Rust. Use an intent fallback if you still want to capture intent. In Go, with WithContextParameter(false), only WithIntentFallback supplies intent. The SDK doesn't read a tool's own context argument. Model capture and conversation IDs have separate options. Disable those options to stop their schema changes.

The intent fallback callback

A client can omit the SDK-injected context argument. Without a fallback, the event has no $mcp_intent.

The SDK calls the fallback (intentFallback in TypeScript, intent_fallback in Python, Ruby, and Rust, WithIntentFallback in Go) when the agent provides no context. It captures a non-empty result as $mcp_intent with $mcp_intent_source = "inferred".

The SDK does no inference of its own. It doesn't call an LLM. It doesn't inspect your tool arguments. It doesn't cache results. Whatever logic you want goes in your callback.

Deterministic, per-tool

Use a synchronous callback when you can derive intent from the tool name and arguments:

instrument(server, posthog, {
intentFallback: (request) => {
const tool = request.params?.name
const args = request.params?.arguments ?? {}
if (tool === "search_events") return `Searching events for "${args.query}"`
return tool ? `Invoking ${tool}` : null
},
})

Using transport metadata

extra carries MCP transport details – useful when the agent's user-agent or auth context hints at intent:

TypeScript
import { getRequestHeaders } from "@posthog/mcp"
intentFallback: (request, extra) => {
const ua = getRequestHeaders(extra)?.["user-agent"]
return `${ua ?? "unknown client"} invoked ${request.params?.name}`
}

In TypeScript, getRequestHeaders reads headers on both MCP SDK majors – see MCP SDK v2.

LLM-derived intent

An LLM call in intentFallback adds latency to each tool call that lacks context. Cache results where possible. Handle failures in the callback:

TypeScript
intentFallback: async (request) => {
try {
return await summariseIntent(request.params)
} catch {
return null // the SDK swallows null gracefully
}
}

Filtering on intent source

$mcp_intent_source is set to "context_parameter" or "inferred" only when an intent was captured. If neither a context argument nor a fallback result was available, both $mcp_intent and $mcp_intent_source are absent on the event.

If you want to know what fraction of your traffic is contextualized:

SQL
SELECT
properties.$mcp_intent_source AS source,
count() AS calls
FROM events
WHERE event = '$mcp_tool_call'
AND timestamp > now() - INTERVAL 7 DAY
GROUP BY source
ORDER BY calls DESC

A high share of inferred means most clients do not supply context. Review context.description or improve intentFallback.

Gotchas

`get_more_tools` reports its source as `context_parameter`

The virtual get_more_tools tool always reports $mcp_intent_source = "context_parameter", even though the SDK is what defined the schema. Defensible – the agent did type a string – but filter it out of source-attribution queries if the number matters.

The schema `required` field isn't enforced

Omitting the SDK-injected context argument doesn't fail validation. Your tool's own required arguments still apply. Use intentFallback to capture intent when the agent omits context.

Skip `intentFallback` for tight internal servers

You do not need a fallback if your internal client always supplies context. Add one if clients can omit this argument.

Still have questions?

Was this page useful?