Skip to content

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.

The model claude, cursor… tools/call your server process Your handler unchanged watch() 2 lines result back to the model, same as before batched · sent after your response MCPulse sizes, hashes
The solid line is your traffic and nothing of ours is on it. The dashed line is the recording, which leaves afterwards and separately — if MCPulse is unreachable, that batch is dropped and your tool call has already returned.

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.

01

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.

{ email: "ada@…" } 9c1b4e2f0a11
02

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

03

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 Why it is safe
session_id Groups calls in one conversation. Generated by us, means nothing outside your account.
tool_name The name you registered. This is the one field you chose, and it is a name rather than data.
client_name Which client connected — claude-desktop, cursor. Reported by the client itself.
started_at When the call began, so a day can be drawn by the hour.
duration_ms How long your handler took. Bucketed on our side, because percentiles cannot be summed.
outcome One of four: ok, empty, tool_error, crashed. Nothing else is possible.
response_bytes The size of your answer, which is what context actually costs. Not the answer.
is_empty Whether a successful call returned nothing usable. The failure the protocol calls success.
args_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.