Conversation IDs
Contents
A PostHog $session_id is per MCP connection — it rotates when the protocol session does. That's the right granularity for "which TCP/WebSocket connection is this?" but it can split a single user conversation into multiple sessions when the client reconnects.
$mcp_conversation_id is an opt-in property that lets you stitch those calls together at the conversation level instead.
Conversation IDs are off by default and rely on the agent cooperating. Read the caveats below before enabling — there's a visible side effect on tool responses, and the value is agent-controlled.
Enabling
With this on, the SDK does three things:
- Injects an optional
conversation_idargument into every tool's JSON Schema, with a description telling the agent to reuse the value the server returns. - Mints a UUID when the agent calls a tool without
conversation_id, and returns it on the tool's response as a{"conversation_id":"…"}text block — data, not an instruction. - Captures the supplied or minted value on every event as
$mcp_conversation_id, distinct from$session_id.
The agent's conversation_id (when present) always wins. The SDK only mints when the agent doesn't supply one.
How it lands in events
A new connection (new $session_id) made by the same agent re-using the same conversation_id will share $mcp_conversation_id. You can group by it in HogQL to see the whole conversation:
Caveats
The conversation_id parameter can't be added to a schema built from oneOf / allOf / anyOf / $ref, or to a tool with no input schema. Those tools log a warning and get no handle, so their calls won't correlate. identify covers them.
A client working from a stale cached tool listing won't know to send the parameter either — ttlMs caching on tools/list makes that more likely over time.
It's returned as a {"conversation_id":"…"} text block, so consumers that surface raw tool-call content to end users will show that JSON. It's deliberately data rather than an instruction — an imperative sentence in tool output is indistinguishable from prompt injection, and hardened clients block it.
When the agent supplies a conversation_id, the SDK accepts any non-empty string. You can bind it to your own session scheme (chat id, JWT jti, request id) by having the agent send that value, but nothing prevents a misbehaving client from sending arbitrary strings. Don't use $mcp_conversation_id as a security boundary.
$session_id is what PostHog's session-level joins and identity resolution use. $mcp_conversation_id is purely a logical grouping label — handy for joins, useless for everything else.
When to skip this
If your MCP server runs over a long-lived connection that already aligns with what you'd call a "conversation" — for example, a stdio server attached to a single Claude Desktop chat — $session_id is already doing the right thing. Leave enableConversationId off.
Turn it on when:
- The same logical conversation crosses connections (HTTP/SSE clients that reconnect).
- You want to correlate MCP events with a conversation id you already own elsewhere (chat platform, support ticket, JWT) and you're happy to plumb that id through the agent.