How it works
How it works, and what never leaves your server
Nothing about your server has to change so that you can see it. Not its URL, not its auth, not its dependencies, and not its uptime.
Where your traffic goes
Straight to your server, exactly as it did before. The SDK is a package inside your process, so there is no hop to add, no URL to change and no certificate to manage. What leaves for us leaves separately, after your response has already gone back.
Not on the request path
Recording happens after your handler returns. A tool call never waits on us.
Not a dependency
Zero runtime dependencies. Nothing enters your tree and nothing of ours can be compromised through one.
Not able to break it
Every path through the SDK is wrapped. If recording fails, it fails silently rather than turning a working call into an error.
The three design decisions behind it
Each one is a place where the easier product to build was the wrong one to ship.
Inside your server, not in front of it
An npm package you install, not a proxy. Your server keeps its own URL, so anything that already links to it still works and your OAuth still works. Nothing of ours sits in the request path. If we are down, your server keeps serving.
Never your arguments, never your results
Sizes and fingerprints only. Your arguments become twelve characters of a one-way hash — enough to tell whether two calls were the same, and not enough for anything else. There is no option to turn this off, because a guarantee you can switch off is not one.
import { watch } from "@mcpulse/sdk"
watch (server, { key })
✓ same server back
Two lines, then it is measuring
One import, one wrap, after your tools are registered. watch() hands back the same server, so nothing downstream changes. No runtime dependencies at all — it will not drag anything into your tree.
What leaves your process
One object per tool call. Every field is a dimension, a duration, a size or a hash — there is no field that could hold customer data, which is a stronger statement than a promise about how the fields are used.
{
"v": 1,
"type": "call",
"session_id": "s_7f2a91",
"tool_name": "search_orders",
"client_name": "claude-desktop",
"started_at": "2026-08-09T14:22:31Z",
"duration_ms": 240,
"outcome": "ok",
"response_bytes": 1420,
"is_empty": false,
"args_hash": "9c1b4e2f0a11"
} | Field | Kind | Why it is safe |
|---|---|---|
| session_id | opaque id | Groups calls in one conversation. Generated by us, means nothing outside your account. |
| tool_name | your name | The name you registered. This is the one field you chose, and it is a name rather than data. |
| client_name | dimension | Which client connected — claude-desktop, cursor. Reported by the client itself. |
| started_at | timestamp | When the call began, so a day can be drawn by the hour. |
| duration_ms | number | How long your handler took. Bucketed on our side, because percentiles cannot be summed. |
| outcome | enum | One of four: ok, empty, tool_error, crashed. Nothing else is possible. |
| response_bytes | number | The size of your answer, which is what context actually costs. Not the answer. |
| is_empty | boolean | Whether a successful call returned nothing usable. The failure the protocol calls success. |
| args_hash | hash | Twelve hex characters of a SHA-256 over the arguments, keys sorted. One way, and truncated. |
And once at startup, the tool list with the byte size of each schema. That second payload is what makes dead tools findable: a tool with no calls has no rows anywhere, so absence has to be recorded from the other side.
Every call ends as exactly one of four things
Telling crashed
from tool_error
takes some doing. The MCP SDK catches everything a tool throws and turns it into a result
with isError: true,
so from outside the request handler a bug and a deliberate error are the same object. The
SDK wraps your tool callbacks as well as the request, so what actually happened is known
rather than guessed from an error message.
-
ok
Ran and returned a result.
-
bad_args
Arguments failed validation — your handler never ran.
-
tool_error
Ran and returned isError: true.
-
crashed
Threw.
And a fifth state riding alongside them
is_empty
marks a call that succeeded and returned nothing usable — an empty array, an empty object,
a blank string. Those are the failures nobody reports: the protocol calls them success, the
model gets nothing it can use, and you never hear about it.
What MCPulse cannot see
The design that keeps your data out also keeps some answers out. It is worth knowing which before you install, rather than after you go looking for one.
-
Why a call failed
We record that your handler threw, not what it threw. The message could contain anything, so it never leaves. Your own logs have the stack trace.
-
What the model was trying to do
There is no prompt, no reasoning and no user text anywhere in the payload. We can tell you a tool was retried with different arguments; we cannot tell you what was asked.
-
Which specific argument was wrong
bad_args is counted, and identical argument shapes are grouped by hash so you can see one pattern repeating. Which field, and what was in it, stays with you.
-
Whether the answer was correct
A call that returned 1,420 bytes of confident nonsense is recorded as ok. Correctness needs a reader, and we are not reading your results.
A product that could answer all four would be reading your traffic, and then the honest version of this page would be a much longer document about retention.
Common questions
What happens to my server if MCPulse goes down?
Nothing. The SDK runs inside your process and sends its own batches outward, so your tool calls never traverse anything of ours. A failed batch is dropped the way a network error is dropped. This is the whole reason it is a package and not a proxy: a proxy that is down is a server that is down.
Can you see my tool arguments or my results?
No. One row leaves your process per call. It holds which tool ran, how long it took, how big the answer was, and a one-way fingerprint of the arguments. There is no field that could hold customer data — which is a stronger thing to be able to say than a promise about how the fields get used. The fingerprint is twelve characters of a SHA-256 with the keys sorted: enough to tell whether two calls were the same, not enough for anything else. There is no option to turn this off, because a guarantee you can switch off is not one.
Will it slow my tools down?
Recording happens after your handler has returned and batches are sent on their own schedule, so a tool call never waits on us. The SDK has no runtime dependencies, so it drags nothing into your tree. And every path through it is wrapped: if the recording itself fails, it fails silently rather than turning a working tool call into an error. Instrumentation that can break the thing it measures is worse than no instrumentation.
Do I have to change my server's URL or its auth?
No. You add a package and wrap the server object you already built; `watch()` hands the same server back, so nothing downstream sees a difference. Your URL, your OAuth, your deployment and your dependencies are untouched. That is the point of not being a proxy: a proxy would move your traffic to a new address, which breaks your OAuth and breaks every directory that already lists you.
Every question — languages, getting at your own data, and what a security review will ask. Or read the docs.
Now point it at your own server
Two lines inside your process. Your URL, your OAuth, your deployment and your dependencies all stay exactly as they are.
Free forever. No card, no trial to expire. All ten MCP SDKs supported.