Rust MCP Analytics installation
Contents
posthog_rs::mcp is experimental and not officially supported. Its API, its options, and the $mcp_* events it captures may change in any posthog-rs release.
Try it, and report bugs or send patches to posthog-rs. Don't build production reporting on it yet.
posthog_rs::mcp ships inside the posthog-rs crate, behind the mcp feature. It instruments servers built on rmcp, the official Rust MCP SDK, over stdio and Streamable HTTP. It serves both the 2025-11-25 and 2026-07-28 protocol revisions.
You need Rust 1.88+, rmcp 3.7 or later, posthog-rs 0.29.0 or later, and a PostHog project token. Dispatching MCP requests without an rmcp server? See Custom dispatchers.
- 1
Install the crate
RequiredTerminalThe
mcpfeature pulls inrmcpwith itsserverfeature and turns on the async client. Your server already depends onrmcpfor its tools. The snippets on this page also needtokiowith itsmacrosandrt-multi-threadfeatures, andrmcpwithtransport-iofor stdio ortransport-streamable-http-serverfor HTTP. - 2
Wrap your server
RequiredPass your
ServerHandlerand a shared client toposthog_rs::mcp::instrument, then serve the wrapped server as usual:RustNotesServeris your ownrmcpserver, such as a#[tool_router]struct with a#[tool_handler]ServerHandlerimpl. The wrapped server answers every request as yours would. It only adds the analytics arguments to your tools, a conversation handle to tool results, and, when you turn it on, theget_more_toolstool.For Streamable HTTP, hand the wrapped server to
StreamableHttpServicein place of yours:RustYour server must implement
Clone. Each clone of the wrapped server serves its own connection and session. Clones share everything else. - 3
Send queued events on shutdown
Requiredposthog-rssends events in the background. Callposthog.shutdown().awaitbefore the process exits, as above. On a server you stop with a signal, call it from your shutdown handler. - 4
Check your first events
RequiredConnect an agent to your server and call a tool. Then run this query in the SQL editor, or through the
execute-sqltool of the PostHog MCP server:You should see a
$mcp_tool_callrow for the tool you called. Events can take a minute to arrive. No rows? See Troubleshooting. See the event reference for every property.
For a runnable server, see examples/mcp_server.rs. The example reads the token from POSTHOG_API_KEY, so pass it your POSTHOG_PROJECT_TOKEN:
Configuration
instrument uses the default options. To change them, build McpOptions with McpOptionsBuilder and wrap your server through McpAnalytics:
| Option | Default | What it does |
|---|---|---|
context(bool) | true | Adds a required context argument to capture agent intent. The SDK removes it before your tool runs. |
context_description(text) | – | Replaces the context argument's description. Keep the default's instruction to leave out personal details. |
intent_fallback(callback) | – | Fn(&RequestInfo) -> Option<String>. Supplies intent when the agent sends no context. Captured with $mcp_intent_source: "inferred". |
capture_model(bool) | true | Captures the calling model from the request _meta, or from a required llm_model argument the SDK adds. |
model_description(text) | – | Replaces the llm_model argument's description. |
conversation_id(bool) | true | Adds an optional conversation_id argument to group calls across requests. When the SDK mints a handle, it appends a last {"conversation_id": ...} text block to the result. |
identify(callback) | – | Fn(&RequestInfo) -> Option<Identity>. Maps a request to a person and groups. See Identify the caller. |
missing_capability_tool(name) | off | Adds the get_more_tools virtual tool under this name. A blank name uses get_more_tools. |
exception_autocapture(bool) | true | Sends a $exception event for each failed tool call. For a panic, the posthog-rs panic hook sends the $exception instead when it's installed, which needs a global client with capture_panics on. |
capture_parameters(bool) | true | Captures the tool arguments as $mcp_parameters. |
capture_responses(bool) | true | Captures the tool result as $mcp_response. A failed call's error text is captured either way. |
before_send(hook) | – | FnMut(Event) -> Option<Event>. Changes or drops each MCP event after redaction. Dropping a $mcp_tool_call also drops its $exception. |
A tool that declares its own context, llm_model, or conversation_id argument keeps it. A tool whose input schema uses $ref, allOf, anyOf, or oneOf at the top level gets no added arguments, so the SDK captures no intent or model from its arguments. This can happen when Parameters<T> wraps an enum. Callbacks that panic never break the request. A panicking identify makes the request anonymous, and a panicking before_send drops the event.
The SDK logs its warnings through tracing, never to stdout. On a stdio server, point your tracing subscriber at stderr, because stdout carries the protocol.
Identify the caller
The identify callback receives a RequestInfo with the JSON-RPC method, the tool name and arguments, the request _meta, and, on HTTP, the request's http::request::Parts. Read the user your HTTP middleware attached to the request extensions:
with_property values go out as $set on every MCP event, and with_group values as $groups. Without an identity, events are anonymous and create no person profile. Never put a raw credential, such as an Authorization header, in a property: person properties aren't redacted. The Rust SDK doesn't send a separate $identify event.
Sessions
Every event has a $session_id:
- Streamable HTTP with sessions: the SDK hashes the
Mcp-Session-Idheader intoses_plus 32 hex characters. Every PostHog MCP SDK computes the same value, so a session served from several languages groups together. Theinitializerequest comes before the server assigns that header, so its$mcp_initializegets a freshses_ID of its own. A stateless session token minted by another PostHog SDK is read back, with its client name and version. - stdio: the SDK generates a
ses_plus UUIDv7 ID per connection, and starts a new one when a tool call comes 30 minutes or more after the previous call ended. - Stateless HTTP: there's no transport session, so the SDK uses conversation IDs. It returns a handle in the first result, and calls that echo it share a
$session_id. A client that ignores the handle gets a new session per request. Useidentifyto group those calls by user.
A valid conversation handle takes priority over the transport session, so a conversation that reconnects stays in one session.
Intent from client metadata
When the client sends _meta["io.modelcontextprotocol/aiInvocation"].userIntent, the SDK captures it as $mcp_intent with $mcp_intent_source: "client_metadata". It takes priority over the context argument, and the intent fallback doesn't run.
Report missing capabilities
Set missing_capability_tool to give agents a get_more_tools tool. An agent calls it when no tool fits its request, and the context argument is its report. The SDK answers the call itself, so your handlers never see it. The call sends $mcp_missing_capability, not $mcp_tool_call.
The SDK adds the tool to the first page of tools/list. It doesn't advertise the tool when your server has a tool of the same name. It finds your tools through your server's get_tool, which #[tool_handler] servers implement, and through the tools your server listed. A server that has neither can't tell the SDK, so give the virtual tool a name your own tools don't use.
See Tracking missing capabilities for how to read the reports.
Custom dispatchers
For a server that isn't an rmcp ServerHandler, such as a gateway or your own JSON-RPC router, use the manual API on McpAnalytics. It sends the tool call events: $mcp_tool_call, $exception, $mcp_unknown_tool, and $mcp_input_required. It doesn't send $mcp_tools_list or $mcp_initialize.
Create one McpAnalytics with McpAnalytics::new(posthog.clone(), options) and share it. When you answer tools/list, pass your tools through list_tools, which adds the analytics arguments. For each tools/call, start the call with the arguments as the agent sent them, run the tool with the arguments the SDK hands back, and finish the call:
std::io::Error stands in for your own error type. ToolCallError::new takes any std::error::Error.
call.arguments()no longer holds thecontext,llm_model, orconversation_idarguments, when the SDK knows it added them: the tool isToolRegistration::Registered, or it went throughlist_toolsearlier.ToolRegistration::NotRegisteredsends$mcp_unknown_toolinstead of a tool call. UseToolRegistration::NoRegistrywhen you can't tell. Then the SDK removes the analytics arguments only for tools it saw inlist_tools.ToolCallRequestalso takes the request_metawith.meta(), the HTTP request with.http(), your server'sImplementationwith.server(), and the arrival time with.started_at().TransportSession::Idgroups requests that share a session ID. For a one-client connection, create oneConnection::new()per connection and passTransportSession::Connection. Without a transport session, the call is a stateless request.- On a
2026-07-28round, callcall.input_required(&round)to send$mcp_input_required. The retry that completes the call is a call of its own. ToolCallError::newnames the failure by the error's type name without its path or generics, such asUpstreamTimeout. ABox,Arc,Rc, reference, ordyntype givesError.error_typesets the$mcp_error_typelabel, such asvalidation,permission, orrate_limited. The$exceptionkeeps the error's own type.
The manual API never answers get_more_tools. Only instrument does.
Events
The Rust SDK sends $mcp_tool_call, $exception for failed calls, $mcp_tools_list, $mcp_initialize, $mcp_unknown_tool, $mcp_input_required, and, when you turn it on, $mcp_missing_capability. Only $mcp_tool_call carries $mcp_source.
$lib: MCP events reportposthog-rs-mcp. The client you pass in keeps its own$libfor everything else it sends.- Errors: an
McpErrorfrom your tool counts underMcpError, orvalidationfor invalid params. A panic counts underPanic, and the SDK re-raises it after capturing. A result withis_errorset is a failed call with the typeError, orvalidationwhenrmcpcouldn't parse the arguments. - Unknown tools: a call that names a tool your server doesn't have, and that your server rejects as not found, sends
$mcp_unknown_toolinstead of$mcp_tool_call. input_requiredrounds: a tool that ends a2026-07-28round by asking the client for input sends$mcp_input_requiredwith the request methods, never their contents. On an older revision,rmcpanswers the round with an error, so it's a failed$mcp_tool_callwith$mcp_error_typeset toinput_required.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| No events at all | Check that the process calls posthog.shutdown().await before it exits, because queued events are lost without it. Check the token is the project token (phc_...) and the host matches your region: https://us.i.posthog.com or https://eu.i.posthog.com. |
| A stdio client can't connect, or reports invalid JSON | Something writes to stdout, which carries the protocol. Send logs and your tracing subscriber to stderr. |
Events arrive, but $mcp_intent is empty | The tool's input schema uses $ref, allOf, anyOf, or oneOf at the top level, so the SDK adds no context argument. Use a plain object schema, or set intent_fallback. |
| Every request is a new session on HTTP | The server is stateless and the client ignores the conversation handle. See Sessions, and use identify to group calls by user. |
$mcp_initialize is in a different session from the calls after it | Expected on Streamable HTTP. See Sessions. |
Events are anonymous after you set identify | request.http is None on stdio, and your middleware must insert the type you read before the request reaches the MCP service. |
Limits
The Rust SDK doesn't yet have:
- Resource and prompt events
- Task handling. A tool that returns a task handle sends no event
- A session token for stateless servers. It reads tokens other SDKs mint, but doesn't mint its own
- Client name and version on stateless HTTP servers for clients older than
2026-07-28, unless the client replays a PostHog session token. A2026-07-28client sends them in each request's_meta - An event properties callback, or an analytics handle for custom events. Call
captureon yourposthog-rsclient instead $identifyand the merge of anonymous events into a user- Stack traces and model properties on the
$exception - Wizard support
Some behavior to know about:
- On a stateless server, an
input_requiredround and its retry land in different sessions. - Events also go through the
before_sendhooks of yourposthog-rsclient.





