Analytics for MCP servers, for the Python MCP SDK.
One import, one wrap — see which of your tools actually work for the models calling them.
getmcpulse.com · Docs · Dashboard
from mcp.server.mcpserver import MCPServer
from mcpulse import watch
mcp = MCPServer("my-server")
watch(mcp, key="mp_live_…")That is the whole integration. The server is instrumented in place and handed straight back, so the call drops in around an existing server without moving anything else.
pip install mcpulse-sdkThe distribution is mcpulse-sdk; the import stays mcpulse. They differ
because mcpulse on PyPI belongs to an unrelated project, and PyPI has no
scoping to fall back on the way npm does — so this package mirrors the npm name
(@mcpulse/sdk) instead.
No runtime dependencies. mcp is your dependency, not ours.
| Option | Default | Meaning |
|---|---|---|
key |
— | Ingest key, mp_live_…, minted per MCP in the dashboard |
endpoint |
https://api.getmcpulse.com |
Point at a local API while developing |
enabled |
True |
False makes watch() a no-op — useful in tests and CI |
debug |
False |
Log what is sent, and why a send failed, to stderr |
agree |
off | Fields that more than one tool returns. See below |
agree_window_ms |
300000 |
How long a value stays comparable |
agree_tolerance |
0.0001 |
Relative, for floats |
An empty key turns it off, so a server started without its key configured is silent rather than a source of 401s on every flush.
Opt-in, and it catches the failure every other metric here calls healthy.
Two tools return the same underlying field. A stale cache key, or a versioned key a cron did not follow, and they disagree for hours. Every number stays green the whole time — the calls succeed, the results are non-empty, there are no retries and the latency is fine. Callers get two different answers to one question and nothing reports it.
Name the value once, and say where each tool returns it:
watch(
mcp,
key=os.environ["MCPULSE_KEY"],
agree={
"global_liquidity": [
{"tool": "getGlobalLiquidity", "path": "globalLiquidity.value_t"},
{"tool": "getPillars", "path": "pillars.global_liquidity.value"},
],
},
agree_window_ms=300_000,
)A map rather than a list of field names, because the same value routinely ships under a different name and a different shape in each tool — which is most of why two copies of it drift apart without anyone noticing.
The comparison happens in your process, and only the verdict is sent.
{
"v": 1, "type": "agreement", "session_id": "s_7f2a91",
"field": "global_liquidity",
"tool_a": "getGlobalLiquidity", "tool_b": "getPillars",
"agreed": false, "checked_at": "2026-09-11T14:22:31Z", "window_ms": 300000
}No value, no difference, no hash of a value. Hashing could not work anyway:
25.22, 25.220 and "25.22" are the same number and three different hashes,
so a server-side check would report every representation change as a divergence
forever.
Four things worth knowing before turning it on:
agree_window_msmust be shorter than your data's refresh interval. A window that outlives a refresh compares a figure against its own predecessor and calls a legitimate change a divergence.- Integers and strings are compared exactly. The tolerance is relative and applies to floats only — a count that is off by one is off by one.
- Nothing is reported until both tools have been called inside one window. A
low-traffic tool can stay silently wrong for a long time, which is a limit of
the method rather than a clean result. Run once with
debug=Trueafter setting it up: a path that never resolves says so there, which is how a typo'd declaration shows up as something other than a passing check. - The path is searched in
structuredContent, in the JSON of a text content part, and in the result itself, so it does not matter which envelope your tool returns.
Sizes and hashes. Arguments and results do not, and no option turns that on.
Every tool call reports its name, how long it took, how it ended, how many bytes came back, whether the result was empty, and a 12-character one-way hash of the arguments — enough to tell a retry from a fresh call, and not enough to learn anything about either.
- Never throw. Every entry point swallows. If MCPulse fails inside your tool call, your tool fails and you blame us.
- Never block. Record, buffer, return. Nothing awaits the network on the path a model is waiting on — sending happens on a background thread, never on your event loop.
- Never store customer data. See above.
mcp >= 2.0— attaches through the supportedServer.middlewarechain.mcp 1.x— falls back to patching the dispatcher's handler map.
Both then wrap your own tool callbacks, which is what lets MCPulse tell a tool
that crashed from one whose arguments never validated — the MCP server converts
both to isError before anything outside can see the difference.
args_hash is the first 12 hex characters of the SHA-256 of the
RFC 8785 canonical form of the
arguments. tests/fixtures/canonical.json is the shared conformance suite that
every MCPulse SDK runs, so a call hashed here and a call hashed by the
TypeScript SDK land in the same bucket.
MIT