> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getmcpulse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Wire format

> The contract every MCPulse SDK implements — canonical JSON, the hash, and the fixtures that keep ten languages agreeing.

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](/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](/metrics/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](https://www.rfc-editor.org/rfc/rfc8785) canonical form of the arguments, hashed over its UTF-8 bytes.

```
args_hash(args) = hex(sha256(utf8(canonicalize(args))))[0:12]
```

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

| Rule                                                         |                                              |
| ------------------------------------------------------------ | -------------------------------------------- |
| Object keys sort by **UTF-16 code unit**, at every depth     | Not code point, not UTF-8 byte               |
| Array order is preserved                                     | `[1,2]` and `[2,1]` are different calls      |
| Non-ASCII is **never** `\u`-escaped                          | `café` stays `café`                          |
| `<`, `>`, `&`, `/` are **not** escaped                       |                                              |
| Numbers follow ECMAScript `Number::toString`                 | `1`, `1e+21`, `1e-7`, `0.000001`, `-0` → `0` |
| Non-finite numbers are rejected                              | `NaN` and `Infinity` are not JSON            |
| Short escapes where one exists, lowercase `\u00xx` otherwise | `\b \t \n \f \r \" \\`                       |
| No whitespace anywhere                                       |                                              |

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.

| Language      | Trap                                                                                                                     |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Python        | `json.dumps` escapes non-ASCII (`ensure_ascii=True`); writes `1.0` where ECMAScript writes `1`; sorts keys by code point |
| Go            | `encoding/json` HTML-escapes `<`, `>`, `&`; map keys sort by UTF-8 byte; `strconv` writes `1e-07`                        |
| Java / Kotlin | `Double.toString(1)` is `"1.0"`; shortest-round-trip formatting only arrived in JDK 19                                   |
| .NET          | `System.Text.Json` escapes `<`, `>`, `&`; writes `1E+21` and `1E-07`                                                     |
| Rust          | `{}` on `f64` writes `1e21` out in full; `str` ordering is by UTF-8 byte                                                 |
| Ruby          | `Float#to_s` writes `1.0` and `1.0e-07`; `JSON.generate` keeps insertion order                                           |
| PHP           | `json_encode` escapes `/` **and** all non-ASCII; float printing depends on the `serialize_precision` ini setting         |
| Swift         | `NSNumber` cannot reliably distinguish `true` from `1` on Linux — `true` would serialise as `1`                          |

Java, Kotlin and .NET get key ordering free: their strings are already UTF-16.

<Note>
  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.
</Note>

## 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:

```json theme={null}
{
  "name": "a realistic call",
  "input": { "query": "quarterly revenue", "limit": 25 },
  "canonical": "{\"limit\":25,\"query\":\"quarterly revenue\"}",
  "args_hash": "52a4fe5fc814"
}
```

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:

| SDK           | Path                                 |
| ------------- | ------------------------------------ |
| TypeScript    | `tests/fixtures/canonical.json`      |
| Python        | `tests/fixtures/canonical.json`      |
| Go            | `testdata/canonical.json`            |
| Java / Kotlin | `src/test/resources/canonical.json`  |
| .NET          | `tests/MCPulse.Tests/canonical.json` |
| Rust          | `tests/canonical.json`               |
| Ruby          | `spec/fixtures/canonical.json`       |
| PHP           | `tests/canonical.json`               |
| Swift         | `Tests/MCPulseTests/canonical.json`  |

Copy one into your own project and run it. If it passes, your hashes are interchangeable with all ten.

<Warning>
  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.
</Warning>

## The three rules

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

<Steps>
  <Step title="Never throw">
    Every entry point swallows. If telemetry fails inside a tool call, the tool fails and the author blames the telemetry.
  </Step>

  <Step title="Never block">
    Record, buffer, return. Nothing waits on the network on the path a model is waiting on.
  </Step>

  <Step title="Never store customer data">
    Sizes and hashes leave the process. Arguments and results do not, and no option turns that off.
  </Step>
</Steps>

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`.
* **Python** — `asyncio.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.

<Warning>
  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.
</Warning>

## Related

* [What is sent](/sdk/what-is-sent) — the payload shapes and the outcome vocabulary
* [Ingest](/api/concepts/ingest) — the endpoint, batch limits and field caps
* [First-call success](/metrics/first-call-success) — what `args_hash` is for
