How it works
Three decisions, and they are all the same decision
Nothing about your server should have to change so that you can see it — not its URL, not its auth, not its dependencies, and not its uptime.
Inside your server, not in front of it
An npm package you install, not a proxy. Directory-listed servers cannot change their URL and OAuth breaks the moment traffic is redirected — so the SDK sits beside your traffic instead of in it. If we are down, your server keeps serving.
Never your arguments, never your results
Sizes and hashes only. args_hash is twelve hex characters of a SHA-256 over the arguments with keys sorted: 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"
} 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.
A fifth state rides 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.