DEV Community

Cover image for What our analytics SDK receives from your server, field by field
MCPulse
MCPulse

Posted on Originally published at getmcpulse.com

What our analytics SDK receives from your server, field by field

"We only collect what we need" is a sentence every analytics vendor writes, and it's unfalsifiable. You can't check it, so it carries no information.

This is the falsifiable version: every field the SDK can send, in every payload type, and what each one is for.

There are three payload types. Every one is validated on arrival against a shared wire schema, and anything that doesn't match is dropped. A field that isn't in the schema cannot be stored, because there's nowhere to put it.

1. The call payload

Sent once per tool call, after the handler returns.

{
  "v": 1,
  "type": "call",
  "session_id": "s_7f2a91",
  "client_name": "claude-desktop",
  "tool_name": "search_orders",
  "started_at": "2026-10-07T14:22:31Z",
  "duration_ms": 240,
  "outcome": "ok",
  "response_bytes": 1420,
  "is_empty": false,
  "args_hash": "9c1b4e2f0a11"
}
Enter fullscreen mode Exit fullscreen mode
Field What it is Why
v Wire version So an old SDK and a new API can tell each other apart
session_id Generated by the SDK Groups calls so retries and tool pairs can be found. Means nothing outside your own data
client_name From the client's own handshake, e.g. cursor Every metric split by client — a server-wide average hides which client is struggling
tool_name Your tool's name A metric attached to no tool isn't actionable
started_at, duration_ms When, and how long Calls per day, and speed
outcome One of ok, bad_args, tool_error, crashed The four-way split. bad_args means your handler never ran
response_bytes Length of the serialised result Cost. The length — the result itself is discarded in your process
is_empty True if the result was empty Succeeded and returned nothing usable. A boolean, decided at the call site
args_hash 12 hex characters of a SHA-256 over the arguments, keys sorted Telling a retry from a repeat. Not the arguments

Free-text fields are capped — 128 characters for a session or client name, 200 for a tool name. Ingest is a public endpoint, and without caps anyone holding a key could write whatever they liked into your dashboard.

2. The startup payload

Sent once, when the server boots. Same v, session_id and client_name, plus:

Field What it is Why
tools[].name Each registered tool Dead-tool detection — the only way to know a tool exists if nobody calls it
tools[].schema_bytes Length of each tool's serialised schema Schema cost per session
tools[].schema_hash 12-character hash of the schema Did the interface change between deploys?
tools[].description_hash 12-character hash of the description Did the wording change?
tools[].description_norm Hash of the description after normalising: lowercased, stopwords dropped, stemmed, sorted Do two tools say the same thing in different words?
tools[].policy Whether a tool declares it needs authorisation or a verified identity So a tool clients deliberately withhold isn't reported as dead
sdk, mcp_sdk_version, transport, protocol_version Which MCPulse SDK, which MCP SDK version, stdio or HTTP How errors reach the model depends on these, not on your code

Note what's absent: the descriptions and the schemas themselves.

The hashes and the normalisation are computed inside your process precisely so the text never has to leave it. We can tell you a description changed. We cannot tell you what it says.

The two description hashes do different jobs, which is why there are two. The exact hash answers did the bytes move. The normalised one answers did the meaning move — so a cosmetic rewrite doesn't register as churn, and two tools sharing a normalised hash means they converged on the same wording independently.

The version fields are nullable. Several SDKs can't determine the MCP SDK version reliably, and a payload that says "unknown" is more useful than one that guesses.

3. The agreement payload — only if you turn it on

Off by default. If you declare that two tools return the same underlying value, the SDK compares them in your process and sends only the verdict:

{
  "v": 1, "type": "agreement", "session_id": "s_7f2a91",
  "field": "global_liquidity",
  "tool_a": "getGlobalLiquidity", "tool_b": "getPillars",
  "agreed": false,
  "checked_at": "2026-10-07T14:22:31Z",
  "window_ms": 300000
}
Enter fullscreen mode Exit fullscreen mode

Your field's name, the two tool names, and true or false. No value, no difference between the values, and no hash of a value — a hash is just a value that's harder to read.

The comparison has to happen locally for a second reason beyond privacy: 25.22, 25.220 and "25.22" are the same number and three different hashes. Comparing hashes server-side would report a false divergence on every one of them.

At startup the declaration itself is reported as field names and tool names only, never the paths — paths would describe the shape of your results.

What is never sent

Tool arguments. Tool results. Description text. Schema text. Anything about the person using the client — no user ID, no IP stored against a call, no account identifier from your side.

There's no option to send any of them, at any plan.

A guarantee you can switch off is a default, and defaults get changed by whoever is debugging something urgent at 2am — which is exactly the moment nobody is thinking about what ends up in a vendor's database.

How to check this yourself

You don't have to take the post's word for it.

Turn on debug. The SDK logs every payload it sends to stderr — never stdout, which is the transport on a stdio server — so you can read exactly what leaves your process.

Read the schema. The wire format is a single Zod schema shared by the API and every SDK. The API rejects anything outside it.

Read the SDK. The packages are on GitHub. Every language's package is checked against the same conformance fixture, so ten implementations can't drift into sending ten different things.

That's the difference between this list and a privacy policy: every line of it can be verified from your own machine.

If you're building this yourself

The generalisable part isn't the field list. It's the pattern: do the computation where the data already is, so the data never has to move.

Whether a description changed, whether two tools describe themselves alike, whether two tools agree on a number — all of those sound like they need the content. None of them do, if you're running inside the process that already has it.

And the enforcement has to be structural rather than a policy. A schema that rejects unknown fields means the wrong thing can't be stored even by accident. A configuration flag means it can be stored the moment someone flips it.


I build MCPulse — two lines inside your server, never your arguments or your results.

Originally published at getmcpulse.com.

Top comments (0)