Python MCP Analytics installation
Contents
Python MCP Analytics is in beta, so its API may change. Pin your posthog version during the beta.
MCP Analytics for Python ships inside the posthog package as posthog.mcp. It instruments:
FastMCPand the low-levelServerfrom the officialmcppackage, 1.x (mcp>=1.26)MCPServer, FastMCP's new name onmcp2.x, and the 2.x low-levelServer- jlowin's standalone FastMCP (the
fastmcppackage)
The SDK handles both the 2025-11-25 and 2026-07-28 protocol revisions. You need Python 3.10+ and a PostHog project token. Using a dispatcher with no server object? See Custom dispatchers.
The wizard installs the package and adds instrument() for you. To install by hand, follow the steps below.
- 1
Install the package
RequiredTerminalinstrument()needsmcporfastmcpat runtime. You already have one, because you built your server with it. - 2
Wrap your server
RequiredCall
instrument(server, posthog)once, before the server accepts requests:Pythoninstrument()returns an analytics handle for custom events. A second call on the same server reuses the first one. - 3
Send queued events on shutdown
Requiredinstrument()captures events in the background, and theposthogclient sends them in batches. When the process stops, wait for pending captures, then stop the client:PythonSee the complete example.
- 4
Check your first events
RequiredConnect an agent to your server and call a tool. Then open the activity feed and filter for
event = $mcp_tool_call. See the event reference for every property.
Read request headers
The two mcp majors pass request context in different shapes. Use get_request_headers(extra) in identify, intent_fallback, and event_properties. It returns a dictionary with lowercase keys on HTTP transports, returns None on stdio, and never raises:
Stateless and multi-pod servers
A stateless server creates a new instance for each request, often on a different pod. Without correlation, each request gets its own $session_id, and events after initialize lose the client name and version.
On 2025-11-25 traffic, the SDK fixes this without a shared store or sticky routing. At initialize, it puts a token with the session ID and client metadata in the Mcp-Session-Id response header. Clients send the header back, so any pod reads the same values. This works with JSON and SSE responses. The 2026-07-28 revision has no initialize, so use conversation IDs there.
On FastMCP and jlowin's fastmcp, instrument() wraps streamable_http_app() and sse_app(), which run() also uses. Set the server to stateless mode:
When you build the ASGI app yourself, such as for a low-level Server or a custom dispatcher, add the middleware once:
Configuration
Pass options as MCPAnalyticsOptions:
| Option | Default | What it does |
|---|---|---|
context | True | Add a context argument to capture agent intent. Pass MCPAnalyticsContextOptions(description=...) to change its prompt. |
intent_fallback | – | (request, extra) -> str \| None. Supplies intent when the agent sends no context. |
capture_model | True | Capture the calling model from client metadata or an llm_model argument. |
enable_conversation_id | True | Add a conversation_id argument to group calls across requests. |
identify | – | (request, extra) -> UserIdentity \| None, sync or async. Maps a request to one of your users. |
report_missing | False | Add the get_more_tools tool for missing capabilities. |
missing_capability_tool_name | "get_more_tools" | Renames the report_missing tool. |
enable_exception_autocapture | True | Emit a $exception event for each failed tool call. |
before_send | – | (event) -> event \| None. Change or drop each event before it's sent. See Privacy. |
event_properties | – | (request, extra) -> dict. Adds properties to every event. |
server_build | – | An immutable build ID, such as a Git SHA, sent as $mcp_server_build. 1 to 256 characters. |
logger | no-op | (message: str) -> None. A stdio-safe sink for SDK warnings. |
On raw low-level servers and standalone FastMCP, the SDK advertises the injected context and llm_model arguments as optional, not required. On standalone FastMCP with mcp 1.x, middleware that overrides tool listing or dispatch turns off llm_model injection. Model capture from client metadata still works.
Custom dispatchers
instrument() needs an MCP server object. A dispatcher that implements the protocol itself has none. Use PostHogMCP instead. It's a subclass of the posthog client and takes the same keyword arguments:
Capture the handshake and the tool listing the same way. Only send capture_initialize for a 2025-11-25 handshake, because 2026-07-28 has none:
PostHogMCP also takes capture_model, enable_conversation_id, server_build, missing_capability_tool_name, and mcp_exception_autocapture. The instrument() hooks (identify, intent_fallback, event_properties) don't run on this path. Pass identity on each capture_* call. On a stateless server, pass the tool's descriptor as original_tool= to prepare_tool_call, so a process that didn't serve tools/list still knows which arguments it added. PostHogMCP is a posthog client, so call flush() or shutdown() yourself.
The two transport headers in common matter. clientInfo.name reports claude-code for the CLI, Agent SDK, VS Code extension, and desktop app. client_user_agent tells those builds apart, and vendor_client separates Anthropic's products, such as Claude.ai and Claude Design. instrument() reads both headers for you. Leave them unset on stdio.
Recover the session on a stateless dispatcher
Add PostHogMcpStatelessSessionMiddleware to your ASGI app, then read the session it recovered:
The token is unsigned and holds only values the client sent at initialize. Use $session_id and $mcp_client_* as analytics labels, not for authentication.