Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The trace format

A trace is JSON Lines: one event per line. Each event carries a capture-assigned seq (the only ordering authority — never inferred later), a direction (client-to-server / server-to-client), a transport, and a kind:

  • message events hold the JSON-RPC payload (as parsed JSON: every value, not its whitespace, member order, or the spelling of its numbers);
  • http events record the conformance-relevant headers, a response’s status, and a client request’s method — Streamable HTTP binds different obligations to POST, GET, and DELETE, so a clause addressed to one of them is judged only where the recording says which it was; and
  • lifecycle events mark transport open/close.

The format is published as a JSON Schema (draft 2020-12), trace-event.schema.json, for recorders written in any language. It states every rule the validator’s reader applies to one record — a test runs both over every corpus record and one violation of each rule — and says where JSON Schema cannot follow: the document-level rules (one record per line, seq strictly increasing), and integers written as 1.0, which the reader refuses. Unknown members are ignored, so producers may add their own.

The redaction rules that keep credential-bearing headers out of a trace by construction are in 05-security-model.md.

One worked example

The example below is embedded verbatim from the README, where a test (readme_examples.rs) pins it to the validator’s real output — so what you read here cannot drift from what the tool actually produces. It is a session that reuses a request ID, and the verdict that catches it:

{"seq":0,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-host","version":"1.0.0"}}}}
{"seq":1,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"my-server","version":"1.0.0"}}}}
{"seq":2,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","method":"notifications/initialized"}}
{"seq":3,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"method":"tools/list"}}

and the validator answers with the violated clause, addressed to the offending event:

  FAIL  BASE-003 (MUST NOT)
        seq 3: request "tools/list" reuses id 1, already used by the same party at seq 0
        spec: "The request ID MUST NOT have been previously used by the requestor within the same session."
        see:  https://modelcontextprotocol.io/specification/2025-11-25/basic#requests
totals: 17 pass, 1 fail, 0 warn, 86 excluded, 0 unsupported, 6 not applicable, 32 not observed
verdict: fail

The totals line distinguishes the verdict’s components, and two of them are not passes. The not-applicable rows are capability-gated requirements this session never negotiated (the resources and prompts clauses); the not-observed rows are clauses whose subject matter never appeared at all — nothing was paginated, no binary content was sent, no error was returned. A four-line session earns 17 passes, not a hundred. See Architecture for why that distinction is load-bearing.