Skip to main content
Ten SDKs, ten languages, one set of numbers. This page is what makes that true. Read it if you are writing your own producer, porting MCPulse to an eleventh language, or debugging why two servers disagree. If you are just installing an SDK, What is sent is the page you want.

Why this is a page at all

args_hash answers one question: were these two calls made with the same arguments? That is what separates a model retrying a reworded request from a client paging through results, and it is what first-call success is computed from. The question only has an answer if every SDK turns the same arguments into the same bytes. Sorting keys and calling your language’s JSON encoder is not enough — no two languages agree on what that produces.

Canonical JSON (RFC 8785)

args_hash is the first 12 lowercase hex characters of the SHA-256 of the RFC 8785 canonical form of the arguments, hashed over its UTF-8 bytes.
Absent arguments — a tool that takes none — hash as the empty object {}, not as a failure. Arguments that cannot be represented as JSON hash as 000000000000.

The rules

RFC 8785 was written to match ECMAScript’s JSON.stringify, which is why the TypeScript SDK is nearly free and every other language needs work.

What each language gets wrong by default

Every one of these silently sends the same call to a different bucket. None of them raise an error. Java, Kotlin and .NET get key ordering free: their strings are already UTF-16.
The UTF-16 ordering rule only bites above the Basic Multiplane. U+1F680 (🚀) encodes as the surrogate pair D83D DE80, so it sorts before U+FFFD (�) — while sorting by code point or UTF-8 byte puts it after.

The conformance fixtures

Every SDK ships the same 23-case fixture file and runs it as a test. Each case carries an input, its canonical form, and the resulting hash:
A producer is conformant when it reproduces both strings for all 23. Every case in the file is there because some language gets it wrong by default. Where each SDK keeps its copy: Copy one into your own project and run it. If it passes, your hashes are interchangeable with all ten.
Never edit a fixture to make a failing test pass. These hashes are in the product’s history — rewriting one rewrites what every stored row means.

The three rules

Anything calling itself an MCPulse producer keeps these, in this order.
1

Never throw

Every entry point swallows. If telemetry fails inside a tool call, the tool fails and the author blames the telemetry.
2

Never block

Record, buffer, return. Nothing waits on the network on the path a model is waiting on.
3

Never store customer data

Sizes and hashes leave the process. Arguments and results do not, and no option turns that off.
Rule 1 has a language-specific trap in most runtimes:
  • Python — a bare except: that catches asyncio.CancelledError hangs the server on shutdown. Catch Exception, never BaseException.
  • Pythonasyncio.create_task without a strong reference lets the task be garbage collected mid-flight, and the payload vanishes silently.
  • Go — a send on an unbuffered channel from the request path blocks the request. Buffer it and use select with default.
  • Java / Kotlin / .NET — an unbounded work queue is a memory leak. Bound it and drop the oldest.

Sessions

One session id and one buffer per process, per destination — keyed by endpoint + key, never a bare singleton. Not per watch() call. A streamable-HTTP server builds a fresh server object per request, so watch() runs per request too. With a session per call there can never be a retry, and the server reports a perfect first-call-success score however badly it is doing. Session id format: s_ followed by 12 lowercase hex characters from a CSPRNG. Random, never derived from the machine.

Sizes

response_bytes and schema_bytes are, today, the UTF-16 code unit length of the compact JSON — what JavaScript’s String.length returns.
The fields say bytes and hold code units, so "café" measures 4 and an emoji measures 2. This is a known wart. Every SDK reproduces it deliberately so the numbers stay comparable across a customer’s servers; each has a single utf16_length helper so a wire-wide fix is one line per language.