What MCPulse receives from your server, field by field
Three payload types, every field in each, and why it is there. The list is short on purpose, and the wire format is a published schema you can check it against.
“We only collect what we need” is a sentence every analytics vendor writes, and it is unfalsifiable. This post 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 the shared wire schema, and anything that does not match it is dropped. A field that is not in the schema cannot be stored, because there is nowhere to put it.
1. The call payload
Sent once per tool call, after your handler has returned.
{
"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"
}
| 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 pairs can be found. Means nothing outside your data |
client_name |
From the client’s own handshake, e.g. cursor |
Every metric split by client |
tool_name |
Your tool’s name | A metric attached to no tool is not 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 |
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 | Empty answers. 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 — because 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 your server boots. It carries the 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 is not 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 is absent: the descriptions and the schemas themselves. The hashes and the normalisation are computed inside your process precisely so that the text never has to leave it. We can tell you a description changed; we cannot tell you what it says.
The version fields are nullable. Several SDKs cannot 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
}
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 is harder to read. At startup, the declaration itself is reported as field names and tool names only, never the paths, which 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 address stored against a call, no account identifier from your side.
There is 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 at 2am.
How to check this yourself
You do not have to take this post’s word for it.
- Turn on
debug. The SDK logs every payload it sends to stderr, 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 TypeScript package is on GitHub. Every language’s package is checked against the same conformance fixture, so ten implementations cannot drift into sending ten different things.
That is the difference between this list and a privacy policy: every line of it can be verified from your own machine.