Skip to content

MCP analytics for Rust MCP servers - #308

Merged
lucasheriques merged 7 commits into
PostHog:mainfrom
wthrajat:feat/mcp-analytics
Oct 26, 2026
Merged

lucasheriques merged 7 commits into
PostHog:mainfrom
wthrajat:feat/mcp-analytics

Conversation

@wthrajat

@wthrajat wthrajat commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

closes #258

  • This adds mcp analytics like the ruby, python, go and node sdks have.

  • For more context, I had a bit of discussion with @pauldambra on twitter here about this.

  • Also screenshots are attached here MCP analytics for Rust MCP servers #308 (comment)

    • turn it on with the new mcp feature
    • wrap your rmcp server with one call and its tool calls show up in posthog's mcp analytics
    • mcp events say posthog-rs-mcp as the lib so they're easy to tell apart
    • it's experimental for now
    • the mcp feature needs rust 1.88, the rest of the crate still works on 1.78

    💚 How did you test it?

    • unit tests + plus integration tests against a real rmcp server (stdio and http)
    • ran it with opencode against a real posthog project and checked the events in the mcp analytics page

    📝 Checklist

    • All commits are signed (see Commit signing)
    • I reviewed the submitted code.
    • I added tests to verify the changes.
    • I updated the docs if needed.
    • No breaking change or entry added to the changelog.

    If releasing new changes

    • Ran sampo add to generate a changeset file

@wthrajat
wthrajat requested a review from a team as a code owner October 26, 2026 02:01
@wthrajat

Copy link
Copy Markdown
Contributor Author

some screenshots:

1) opencode and deepseek calling 2 rmcp servers

image

2) failed call sent with $lib posthog-rs-mcp

image

3) three calls from one conversation grouped into one session

image

@signed-commit-check

signed-commit-check Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Commit signatures resolved

GitHub now verifies every commit signature in this PR. No further action is needed.

@lucasheriques lucasheriques left a comment •

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.

Reviewed against the cross-SDK MCP analytics spec and ran the example server end to end over stdio; the events match the spec. I pushed a few fixes on top: panics in tool handlers are now recorded as failed calls (then re-raised), $mcp_interface is never dropped by the size bound, payload redaction cost is bounded per event, a panic isn't double-reported when capture_panics is on, and get_more_tools is listed on the first tools/list page like TS and Python. Each commit says how it was tested. Thanks @wthrajat, this is a really solid port.

@wthrajat

Copy link
Copy Markdown
Contributor Author

thanks @lucasheriques!! :D
lmk if there are follow ups like session token minting (now that the tower middleware from #294 exists) or the send_feedback if they're useful

(looks like your commits are missing signatures)

rmcp does not catch handler panics, so a panicking tool sent no
$mcp_tool_call at all: the worst failures were invisible. The spec
counts a panic as a final outcome. The wrapper now catches the unwind,
sends the failed call ($mcp_error_type "Panic", the panic message) and
its $exception, then resumes the panic so the server behaves exactly as
it would uninstrumented.

Tested: new a_panicking_handler_is_a_failed_call fails before, passes
after; cargo test --features mcp --test test_mcp_connection (21 pass);
cargo clippy --features mcp -D warnings; cargo +1.88.0 check --locked
--lib --features mcp.
$mcp_interface is required on every MCP event, but it was not in the
always-kept list, so an event over the byte bound (a listing of a few
thousand tools) lost it along with the other optional properties.

Tested: an_oversized_event_keeps_its_interface fails without the fix,
passes with it.
Each payload string was redacted up to its own 32,768-character limit,
so a result of 100 large text blocks scanned ~3.3M characters on the
response path, only for the event bound to cut them to 256 afterwards.
Strings in a payload now share a budget of one event's size; once it is
spent, the rest are cut to 64 characters before redaction scans them.

Tested: strings_past_the_event_budget_are_cut_short fails without the
budget, passes with it. Release build, 99 x 40 KB blocks: ~120 ms ->
~10 ms per test run. cargo test --features mcp,tower: 484 pass.
From the pre-PR review:
- Catch a panic raised while the handler builds its future, not only
  while it runs, by awaiting it inside an async block.
- Report a panic's $exception as unhandled: rmcp never turns it into a
  response, unlike a returned error.
- Debit the payload budget by what redaction scans, not what it keeps,
  so strings redacted to a short token cannot keep every scan at full
  cost.
- Tests pin the exact cut (first string at its limit, strings past the
  budget at 64) and that the client gets no answer from a panic.

Tested: cargo test --features mcp (481 pass); cargo clippy --features
mcp -D warnings.
With the client's opt-in capture_panics hook installed, a panicking tool
was reported twice: once by the hook, once by MCP analytics. The hook
already sent the $exception, so the call now sends only its
$mcp_tool_call. The panic test also asserts the client gets no answer
within 250 ms, which pins that the panic is resumed.

Tested: cargo test --features mcp,tower (484 pass); cargo check
--no-default-features --features mcp. The hook branch has no test of its
own: the hook is process-global and installable only through the global
client.
The virtual tool was appended to the last page, so a client that reads
only the first page of a paginated server never saw it. TS (#4967) and
Python (#962) list it on the first page, the one every client reads;
Rust now matches.

Tested: the_virtual_tool_is_listed_once_on_the_first_page fails before
((0, 1)), passes after; cargo test --features mcp,tower (485 pass);
cargo clippy --features mcp -D warnings.
@lucasheriques
lucasheriques merged commit 8408705 into PostHog:main Oct 26, 2026
30 checks passed
lucasheriques added a commit to PostHog/posthog.com that referenced this pull request Oct 28, 2026
* docs(mcp-analytics): add the Rust SDK

posthog-rs 0.29.0 (on crates.io) ships experimental MCP analytics for rmcp
servers behind the `mcp` feature (PostHog/posthog-rs#308). Add a Rust
installation page (install, instrument, McpOptionsBuilder options, identify,
sessions, get_more_tools, the manual API, events with $lib posthog-rs-mcp,
limits), a Rust column in the SDK feature table, the sidebar entry, and Rust
tabs/mentions on start-here, events, and intent.

Tested: every Rust snippet compiled with `cargo check` against
posthog-rs =0.29.0 + rmcp 3.7; changed MDX parses with @mdx-js/mdx v1;
prettier --check passes on src/navs/index.js.

* docs(mcp-analytics): correct Rust SDK claims from the pre-PR review

Two-model review (Opus, Sonnet) of the Rust page against posthog-rs 0.29.0
found overclaims and gaps. Fix: manual API sends tool-call events only;
error type naming and the rmcp `validation` case; client_metadata intent
source; get_more_tools collision check and placement; required llm_model,
appended conversation handle, composed-schema skip; argument stripping
needs Registered or list_tools; rmcp 3.7+ and Clone; panic $exception via
hook; Limits (tasks, stateless client info, no properties callback, no
stack trace). Mark Rust in the Go-only notes on events, identifying-users,
privacy, intent, start-here, and sdk-v2.

Tested: all changed MDX parses with @mdx-js/mdx v1; each claim checked
against sdks/rust at tag 0.29.0.

* docs(mcp-analytics): answer failed Rust manual calls, narrow client identity limit

The custom dispatcher example recorded a failed call but never answered it, so a copied dispatcher left clients waiting for a timeout. The limits listed client name and version as missing on every stateless server, but posthog-rs 0.29.0 reads them from a 2026-07-28 request's _meta (RequestScope::client_identity), so only older clients without a replayed token lack them.

* docs(mcp-analytics): correct the Rust panic, initialize session, and identify notes

Addresses wthrajat's review: the panic hook needs capture_panics on the
global client, not just the error-tracking feature; $mcp_initialize gets
its own ses_ id because initialize precedes Mcp-Session-Id; Rust resolves
identify once per request. Also drops a duplicated Go sentence on start-here.

* docs(mcp-analytics): scope the BeforeSend note to Go

* docs(mcp-analytics): make the Rust snippets compile, check events by query, add troubleshooting

- Every Rust block on the Rust and start-here pages now compiles against
  posthog-rs 0.29.0 as written: imports added, the identify user type and
  the custom-dispatcher types spelled out, a compiled Streamable HTTP snippet.
- The example run command reuses POSTHOG_PROJECT_TOKEN, the one variable the
  page asks for.
- "Check your first events" is a SQL query, so an agent can run it through the
  PostHog MCP server's execute-sql tool.
- New troubleshooting table keyed by symptom.

Tested: a scratch crate compiles each ```rust block extracted from the .mdx
files (9/9 compile); removing one import from a copy makes that block fail.

* Correct Rust $exception properties after the master merge

0.29.0 sends no $mcp_server_build, and the Rust note contradicted the
paragraph above it. State the Rust differences once, in that paragraph.
Found by an independent verification pass against the 0.29.0 crate source.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add MCP Analytics support

2 participants