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

Trace corpus

Fixtures for the golden-corpus tests (crates/mcp-trace-validator/tests/golden.rs):

  • good/ — sessions that must validate with verdict pass.
  • violations/ — single-issue sessions, each falsifying at least one check; named after the requirement whose check they exist to kill.
  • draft/{good,violations}/ — the same two kinds for revision 2026-07-28, judged against that revision’s registry behind the draft-2026-07-28 feature.
  • draft/captured/ — sessions recorded off the wire from implementations this repository did not write. No verdict is asserted for them; their whole report is byte-pinned instead. See Captured versus authored below.
  • golden/ — the byte-pinned expected report for every trace, with golden/draft/ holding the 2026-07-28 corpus’s. Regenerate only via cargo xtask bless and review the diff like code.
  • golden/exclusions/ — one ledger per revision, holding the clauses that revision’s registry declines to judge from a trace, and why.

The revisions keep separate golden directories because both name traces after requirement IDs drawn from their own registry: base-045-… is one clause at 2025-11-25 and a different one at 2026-07-28, so a shared directory would let one revision’s golden answer for the other’s trace.

What a golden pins

A report states two kinds of fact, and they are pinned separately (ADR-0013).

golden/<stem>.json holds what the trace decided: the revision, the whole totals, and every pass / fail / warn / not-observed / not-applicable / unsupported row, in registry order.

golden/exclusions/<revision>.json holds what the registry decided: the excluded rows, whose outcome and prose come from the registry alone and are identical for every trace judged against it — engine::build_row never consults the session for them. One distinct set per revision, and before the split every golden restated its revision’s whole set: at the time that was 59% of the corpus’s lines.

2025-11-252026-07-28
Goldens5999
Excluded rows the ledger holds86149
Distinct not-observed sets across them3080

Every cell is verified against the corpus by cargo xtask ci (xtask::coverage::corpus). It is checked rather than trusted because it had already drifted twice: this paragraph is prose, and prose does not recount itself.

Nothing stopped being pinned. Splicing a golden back together with its ledger reproduces the whole report, and assert_reconstructs_the_full_report proves it does on every trace, on every run — so a judged row that went missing, a ledger that drifted from its registry, or rows that interleave in any order but the registry’s all fail the suite. totals.excluded stays in every golden as the per-trace tie back to the ledger: a clause entering or leaving the excluded set moves every golden for that revision, one line each.

Only excluded collapses. not-observed and not-applicable are per-trace evidence, and the table’s third row is the measure of it: near one distinct set per golden, in both revisions. Collapsing those would delete exactly what makes the captures differentiate each other.

Violation naming contract

A violation trace is named area-nnn-<slug>.jsonl and must produce a Fail/Warn row with findings for exactly requirement AREA-NNN — the golden harness enforces the attribution by name (violation_traces_fail_and_match_goldens, and draft::draft_violation_traces_fail_and_match_goldens for draft/), so a defect re-routed to a different requirement cannot re-bless silently. A stem that does not begin with a requirement ID fails the suite loudly.

Captured versus authored

Everything in good/, violations/ and draft/{good,violations}/ is authored: hand-written to exercise one clause. That is what makes them precise, and it is also their limit — an authored fixture can only ever confirm its author’s reading of the specification. A check that is wrong in the same way the author is wrong passes its unit tests and its corpus alike, and neither notices.

draft/captured/ exists to close that gap. Its traces are real recordings of traffic between implementations neither of which was written to satisfy these checks, so they can disagree with us — and when they do, the disagreement is information rather than a test failure. Their goldens are pinned without any pass/fail expectation, because a real implementation is whatever it actually is; what the golden protects is which requirements fire, on which events, with what detail. A check that starts misfiring on real traffic moves it — which is precisely the half of the report that stays in the per-trace file.

Provenance ledger

Every trace’s origin, in one reviewable place that survives history rewrites (the invariant test in golden.rs fails if a trace is added without a ledger row). All current traces share one provenance: hand-authored for this repository as synthetic sessions (no third-party traffic, no recorded production data), written against the 2025-11-25 spec text fetched live from modelcontextprotocol.io on 2026-06-09 (re-verified clause-by-clause against the live text on 2026-06-11) and validated against the embedded registry at the commit that introduced them.

Traces under draft/captured/ are the exception and record their own origin below: the implementations on both ends, the tool that recorded them, and when.

good/

TraceExercises
http-session.jsonlStreamable HTTP session: session-ID assignment and echo, MCP-Protocol-Version headers, Accept/Content-Type discipline, ping (TRAN-011/013/017/018/025/029/039/040 pass paths). It carries all three client request forms, which is what lets the two Accept clauses be told apart: POSTs offering both media types (TRAN-025), a standalone-stream GET offering text/event-stream (TRAN-039), and a session-terminating DELETE carrying Accept: */*. That last exchange is a regression guard — the clause defining session termination places no Accept obligation on it, but until 2026-08-20 both clauses were applied to every client request whatever its method, so this exact teardown drew two MUST-level failures from a client that had violated nothing.
stdio-feature-session.jsonlEvery feature area conformant in one session: tools (incl. outputSchema + structuredContent, and a call returning an embedded resource against a declared resources capability — TOOL-009’s pass path), resources (read/blob/subscribe/updated), prompts (text/image/audio/embedded), logging, completion, pagination cursor flow, and a well-formed params._meta key (BASE-019/020’s pass path)
stdio-full-session.jsonlHandshake plus ping, tools/list, tools/call over stdio
stdio-minimal-init.jsonlSmallest conformant session: the three-message handshake

violations/

Each file injects exactly the violation its name states; golden/ shows every row the trace decided, including any intrinsic secondary findings the injected defect causes (a malformed notification also fails lifecycle accounting, for example).

TraceFalsifies
base-001-request-id-boolean.jsonlBASE-001
base-002-request-id-null.jsonlBASE-002
base-003-request-id-reuse.jsonlBASE-003
base-004-request-answered-twice-cross-flavor.jsonlBASE-004 (the second of an error+result double-answer)
base-004-result-unknown-id.jsonlBASE-004
base-005-notification-with-id.jsonlBASE-005
base-006-error-missing-message.jsonlBASE-006
base-007-error-code-float.jsonlBASE-007
base-008-jsonrpc-version.jsonlBASE-008
base-009-error-unknown-id.jsonlBASE-009
base-010-response-without-result.jsonlBASE-010
base-019-meta-key-bad-prefix.jsonlBASE-019, BASE-020 (shared base.meta-key-format check)
comp-001-capability-undeclared.jsonlCOMP-001
life-001-first-message-not-initialize.jsonlLIFE-001
life-002-initialize-missing-protocolversion.jsonlLIFE-002
life-003-missing-initialized.jsonlLIFE-003
life-004-client-request-before-init-response.jsonlLIFE-004
life-005-server-request-before-initialized.jsonlLIFE-005
life-006-result-version-invalid.jsonlLIFE-006
life-007-initialize-protocolversion-not-string.jsonlLIFE-007
life-009-undeclared-capability-use.jsonlLIFE-009
life-010-initialize-result-missing-capabilities.jsonlLIFE-010
log-001-capability-undeclared.jsonlLOG-001
page-002-cursor-never-issued.jsonlPAGE-002
page-002-fabricated-cursor-rejected.jsonlPAGE-002, and with it PAGE-003’s pass path: the client fabricates a cursor and the server answers -32602 rather than honouring it. The two halves of one event belong to different parties, so the trace falsifies the client’s clause and evidences the server’s.
prom-001-capability-undeclared.jsonlPROM-001
prom-003-image-data-invalid.jsonlPROM-003
prom-004-audio-data-invalid.jsonlPROM-004
prom-005-embedded-resource-malformed.jsonlPROM-005
prom-008-required-argument-unvalidated.jsonlPROM-008
res-001-capability-undeclared.jsonlRES-001
res-004-uri-bad-scheme.jsonlRES-004
res-006-blob-not-base64.jsonlRES-006
tool-001-capability-undeclared.jsonlTOOL-001
tool-003-input-schema-null.jsonlTOOL-003
tool-005-name-length.jsonlTOOL-005
tool-006-name-charset.jsonlTOOL-006
tool-008-name-duplicate.jsonlTOOL-008
tool-009-embedded-resource-without-capability.jsonlTOOL-009
tool-010-structured-without-text.jsonlTOOL-010
tool-011-output-schema-no-structured-result.jsonlTOOL-011
tran-004-stdout-invalid-message.jsonlTRAN-004
tran-005-stdin-invalid-message.jsonlTRAN-005
tran-011-session-id-invisible-ascii.jsonlTRAN-011
tran-013-session-id-not-echoed.jsonlTRAN-013
tran-017-protocol-version-header-missing.jsonlTRAN-017
tran-018-protocol-version-mismatched.jsonlTRAN-018
tran-025-accept-header-missing.jsonlTRAN-025 (a POST with no Accept header at all)
tran-025-accept-header-json-missing.jsonlTRAN-025 (a POST offering text/event-stream only — the half of the clause that read as a pass until the check learned the request method)
tran-039-get-accept-header-missing.jsonlTRAN-039 (a standalone-stream GET offering application/json only)
tran-049-message-not-posted.jsonlTRAN-049 (a ping sent by PUT after a clean handshake)
tran-026-http-post-batch.jsonlTRAN-026 (a batch array POSTed after a clean handshake)
tran-027-notification-answered-200.jsonlTRAN-027 (the notifications/initialized POST answered 200 instead of 202 Accepted)
tran-029-content-type-unexpected.jsonlTRAN-029 (an initialize request answered 200 with Content-Type: text/html)
tran-040-get-answered-json.jsonlTRAN-040 (a standalone-stream GET answered 200 with application/json rather than text/event-stream or 405)

2026-07-28 captured (corpus/draft/captured/)

Two recordings of the same client driving the same scenarios, differing in one variable: which revision the server was serving. That is what makes the pair worth committing — a single capture shows what one implementation does, while a matched pair shows what the revision costs, and every difference between the two reports is attributable to that one change.

Fieldofficial-suite-2026-07-28-scenarios.jsonlofficial-suite-2026-07-28-stateless.jsonl
ClientThe official MCP conformance suite, 0.2.0-alpha.12, driving its 2026-07-28 scenario set (the pin cargo xtask draft-readiness holds)The same client, the same scenarios, the same run
Servermcp-everything-server serving 2025-11-25 — held to a revision it does not implement, so genuine non-conformance is the expected contentmcp-everything-server --protocol-version 2026-07-28, its stateless mode
Recorded bymcp-everything-server’s tap, during cargo xtask draft-readiness, 2026-10-04same run, second leg
Contents99 events / 24 POST exchanges99 events / 24 POST exchanges
Our verdict59 pass, 1 fail, 0 warn, 64 not observed, 149 excluded60 pass, 0 fail, 0 warn, 64 not observed, 149 excluded
The official runner’s verdict38 passing / 4 failing42 passing / 0 failing

Both carry server/discover, tools/list, tools/call, completion/complete, resources/{list,read}, prompts/{list,get} and progress notifications; every request carries the revision’s per-request _meta envelope and its SEP-2243 metadata headers, and neither carries an initialize anywhere. Their tools/list results differ by one entry, which is the point of recording both: test_url_elicitation is listed by the legacy server and not by the stateless one, because URL-mode elicitation is a feature 2026-07-28 removed.

The one finding. CACH-001 — the legacy server’s complete results carry no ttlMs, on resources/list and three resources/reads. That is our 2025-11-25 server being held to a revision it does not implement, which is the correct answer, and the stateless capture is the control that proves the check reports the server rather than the recording.

Two findings that used to be here were ours, not the implementations’. An earlier version of this pair reported TRAN-058 (client POSTs missing Mcp-Method/Mcp-Name) and TRAN-068 (SSE responses missing X-Accel-Buffering: no). Both were artifacts of the tap’s recording allowlist, which predated SEP-2243 and did not record those headers — the client sent them (rmcp’s transport rejects a 2026-07-28 request without Mcp-Method before dispatch, and the suite scored 23/23) and the server sent them (rmcp sets X-Accel-Buffering on every SSE response it builds). The allowlist was extended; both clauses now pass on both captures. The lesson is recorded rather than quietly fixed: a check can only be as honest as the recording it reads, and a capture path that silently drops evidence manufactures findings against conforming implementations.

The same lesson, a third time, one field over (2026-08-20). The tap recorded an exchange’s headers but not its method, so TRAN-025 and TRAN-039 — the Accept obligations of a POST and of a GET — were enforced as one rule applied to every client request whatever its form. That was wrong twice over, and both halves were found by pointing this workspace’s own reference host at its own reference server over HTTP and validating the tap: the conforming session-teardown DELETE, which owes no Accept header at all, drew two MUST-level failures, while a POST omitting application/json — the verbatim violation TRAN-025 names — was reported pass, because the one shared rule could only demand what both clauses had in common. EventBody::Http now carries the method, each clause is judged against the requests it binds, and an exchange whose method the capture did not record is judged by neither. These two captures were re-recorded so the method is present; that is why their Recorded by row reads 2026-08-20.

Why the runner’s 23/23 and our 60-vs-61 were both right — and how the runner caught up. The suite’s 2026-07-28 scenarios exercise features — list a thing, call a thing, read a thing — and a 2025-11-25 server answers all of them, because rmcp serves a per-request-versioned POST whichever revision the handler advertises. At 0.2.0-alpha.9 that was the whole of the runner’s reading, so it scored both servers 23/23. The registry here judges the specification’s prose instead, so it saw the one place the two servers actually differ: CACH-001, no ttlMs on cacheable results.

0.2.0-alpha.11 found the same thing, six weeks later. Its new wire-schema-valid check validates every message against the negotiated revision’s JSON schema, and it fails the 2025-11-25 leg’s four resource scenarios for must have required property 'cacheScope' / 'ttlMs' — that clause, independently. Re-measured, the legacy leg scores 37 passing / 4 failing and the stateless leg 41 / 0, so the runner now separates the two servers it could not tell apart before. Neither instrument was wrong; they were measuring different things, and one of them saw this first. The pair is still the evidence for taking both readings — now with a worked example of the prose-level reading arriving earlier than the schema-level one.

The 64 not-observed rows are the honest denominator: of the 124 clauses this revision’s registry can judge, these sessions carried subject matter for 60. They open no subscription, present no cursor, draw no error, and send no malformed _meta, so those clauses are neither passed nor failed here — they are untested, and the report says which.

Regenerate both with cargo xtask draft-readiness and copy target/draft-readiness/<revision>/tap/001-stateless.jsonl; the ephemeral port in the host header changes per run, so re-copying is a deliberate act with a golden diff, not something to do casually.

The stdio capture

Fieldreference-host-2026-07-28-stdio.jsonl
ClientThis workspace’s mcp-reference-host, on rmcp’s stateless client lifecycle
Servermcp-everything-server --transport stdio --protocol-version 2026-07-28
Recorded byThe host’s own --trace-dir capture, during cargo xtask draft-capture, 2026-08-18
Contentsserver/discover, a full subscriptions/listen lifecycle, a 16-tool sweep with four MRTR rounds (three elicitations and one sampling), and a discovery-driven sweep of everything that is not a tool: resources/{list,templates/list,read}, prompts/{list,get} for all four prompts, completion/complete, and one read of a URI the catalog does not contain. Every call carries a W3C traceparent in its _meta (BASE-040), and the session closes with a notifications/cancelled naming a request the server had already answered, then one more call the server may answer — the only shape a recording can take for a MUST NOT (TRAN-123/TRAN-124)
Our verdict81 pass, 0 fail, 0 warn, 43 not observed, 149 excluded

It is the only capture that exercises subscriptions/listen. The official suite drives no subscription, so the four judged SUBS clauses — and BASE-039, which binds a subscription stream’s notifications — are backed by this file alone: an acknowledgment, a filter the server narrowed, four tagged notifications and an empty graceful-closure result. The same holds for the MRTR clauses. In the two HTTP captures every one of those rows now reads not observed; here they read pass.

Two defects were found by enriching it, and neither could have been found any other way. Until the session read a resource the catalog does not have, nothing in this workspace had ever exercised the server’s not-found path at this revision — and it answered -32002, which 2026-07-28 withdraws outright (basic/index#error-codes lists it under “Implementations of this protocol version MUST NOT emit these codes” and names -32602 as the replacement). The second was ours: the host’s recorder assigned seq when the send future completed rather than when the message was handed to the transport, so a reply received while that future was still pending landed in the trace ahead of its own request. That reads as the server answering an id nobody asked, and it failed BASE-046 on roughly one capture in three. Both are fixed, both have regression tests, and both are the argument for driving more traffic rather than more fixtures.

That asymmetry is the reason to keep all three, and it used to be invisible. Until the vacuous-pass fix, those rows read identically across the three files — pass everywhere, whether or not the session had opened a stream — and this paragraph recorded that as a known reporting limitation. It is a limitation no longer: every check counts the subjects it examined, so a clause with nothing to look at reports not-observed. Read in the other direction, the captures are complementary rather than redundant: the HTTP pair evidences the transport header and status clauses, which no stdio recording can carry, and this one evidences the subscription, MRTR, prompts, resources, logging, completion and error-code clauses, which theirs never reach.

What the 43 not-observed rows still are, and why. Twenty-five are Streamable HTTP clauses in the TRAN-056…TRAN-102 band that a stdio recording structurally cannot carry — the band holds 27 judged clauses, and the two a stdio session does reach (TRAN-060, TRAN-066) are judged here. Eight are server rejection rules — BASE-031, BASE-032, BASE-035, BASE-036, VERS-001, VERS-002, VERS-008, LOG-010 — reachable only by a client that deliberately sends something malformed, which this session does not: it is the conforming capture, and a probe session is a separate recording with a separate expected report. Five need surface this server does not have (CACH-015/CACH-016 need a catalog large enough to paginate; TOOL-033/TOOL-034 need an x-mcp-header designation; PROM-017 needs a prompt carrying audio). TOOL-022 is the interesting one: rmcp’s client caches tools/list under the server’s own ttlMs, so a second listing never reaches the wire — a conforming client cannot exercise the deterministic-order clause within the TTL, which is a property of the caching feature working rather than a gap to close (the official suite’s runner does not cache, and judges it on both of its captures). The remaining four are reachable and not yet driven: TRAN-128 and DISC-002’s dual-era probe, MRTR-024, and VERS-004.

The HTTP capture of the same session

Fieldreference-host-2026-07-28-http.jsonl
ClientThe same mcp-reference-host, driving the identical session over Streamable HTTP
Servermcp-everything-server --transport http --protocol-version 2026-07-28
Recorded byThe server’s tap, during cargo xtask draft-capture, 2026-08-18
Contents159 events — the stdio session’s 85 messages plus 74 http events carrying status and headers
Our verdict92 pass, 0 fail, 0 warn, 32 not observed, 149 excluded

Recorded by the server, not the host, and that is the whole point. The host’s recorder sits at rmcp’s Transport seam, which carries protocol messages and nothing else — redaction by construction, and no HTTP framing to record even if it wanted to. So a host-side HTTP recording would report the Streamable HTTP clauses as not observed exactly like the stdio one. The server’s tap sits above the transport and sees the status line and the conformance-relevant headers, which is why this leg is the only recording in the corpus that can bear on them at all. Same session, both ends, one file each: the difference between the two reports is attributable to the transport and to nothing else.

At 92 of the 124 judgeable clauses it is the best-covered capture here. Its 32 not-observed rows include the server-rejection rules a conforming client never triggers, the pagination and x-mcp-header clauses this server’s surface does not reach, and TOOL-022 (rmcp’s client caches tools/list under the server’s own ttlMs, so a second listing never reaches the wire).

The probe session

Fieldprobe-2026-07-28-http.jsonl
Clientmcp-reference-host --probe: nine hand-built HTTP requests, each wrong on purpose
Servermcp-everything-server --transport http --protocol-version 2026-07-28
Recorded byThe server’s tap, during cargo xtask draft-capture, 2026-08-18
ContentsA _meta envelope missing a required field; an unimplemented protocol version and the retry after it; a header/body version mismatch; an unknown method; a log level outside RFC 5424’s eight; a fabricated cursor; a tool needing a capability the request never declared; and the removed initialize handshake
Our verdictJudged against conformance/probe-baseline.json, not for cleanliness

A conforming client cannot exercise a rejection rule. The clauses of this revision that say what a server owes a request it must not serve all reported not observed on every recording here, because nothing had ever sent such a request. This file is that request, nine times over.

The probes are built outside rmcp, and that is the point rather than a shortcut: rmcp’s client is what makes the other captures trustworthy — it builds the _meta envelope, mirrors the SEP-2243 headers, and will not emit an ill-formed request — so it is structurally incapable of being the probe. The bytes here are the fixture.

Its verdict is a ledger, not a pass. The probe breaks client-side clauses by construction: BASE-030 because its first request omits a required _meta field, TRAN-071/TRAN-072 because two probes are about exactly those headers. Demanding a clean report would mean demanding a probe that probes nothing. So every finding is listed in conformance/probe-baseline.json with a reason, and the gate holds the set in both directions: a finding not in the ledger is a new defect or a regression, and a listed finding that stopped occurring is either a fix that should retire its entry or a check that quietly stopped firing.

It has already worked in both directions. The first probe run drew two server-side findings — the server served a request naming log level "chatty", and honoured a cursor it never issued — which went into the ledger as open defects. When they were fixed, the gate refused the change until their entries were retired, which is the half of a ratchet that is easy to leave out. Every rejection clause the probe reaches now passes; thirteen clauses pass on this capture and on no other (BASE-031, BASE-032, BASE-035, BASE-036, LOG-010, TRAN-073, TRAN-074, TRAN-075, TRAN-098, TRAN-102, VERS-001, VERS-002, VERS-008). The fabricated cursor is still sent, and judged by nothing: on 2026-10-04 PAGE-010 and PAGE-011 were excluded at this revision, because its caching rules make a cursor issued before the recording began a valid one (the registry entries record the reasoning). The server still refuses it, and a_fabricated_cursor_is_refused_on_the_stateless_revision in mcp-everything-server’s tests holds it to that.

Its provenance is weaker than the pair above, and deliberately labelled so. Both ends of this session are ours: the official suite drives servers over --url only, so there is no third-party client for stdio to be recorded against. What it still supplies that an authored fixture cannot is that neither end was written to satisfy these checks, and that the machinery producing every byte — the stateless lifecycle, the _meta envelope, the MRTR retry loop — is rmcp’s, not this repository’s.

It is also the only capture that judges a verdict rather than pinning a report. cargo xtask draft-capture fails on any finding, because a finding in a session where both ends are ours is a defect here rather than news about somebody else’s code. Regenerate with BLESS=1 cargo xtask draft-capture (which rewrites this file) followed by cargo xtask bless.

What it caught. The first recording of this session reported six failures the HTTP captures could not: TRAN-060/066/119/120 and MRTR-001, because the interactive tools still sent elicitation/create and sampling/createMessage as independent server-to-client requests — the mechanism SEP-2322 replaced — and LOG-008, because the logging tool emitted notifications/message for a request that had not asked for them. The official suite’s 2026-07-28 scenarios exercise no interactive tool and no logging tool, so an HTTP-only corpus would have shipped both defects. That is the argument for capturing both transports rather than treating one as representative.

What the captures evidence, together

The table below is generated from the committed golden reports by cargo xtask draft-coverage and verified by cargo xtask ci — per ADR-0001 these numbers are a projection of the data, not prose anyone maintains. The same gate parses every count stated elsewhere in the living Markdown — the “N of the M judgeable clauses” claims and the “N pass, M fail” verdicts alike — and fails when one disagrees, which is how the counts in this section stopped being wrong. A number quoted rather than asserted goes in backticks, and is read as a specimen instead.

CaptureJudgedpassfailwarnNot observed
official-suite-2026-07-28-scenarios60591064
official-suite-2026-07-28-stateless60600064
probe-2026-07-28-http66559258
reference-host-2026-07-28-http92920032
reference-host-2026-07-28-stdio81810043
Union11212

Across all 5 captures, 112 of the 124 judgeable clauses are evidenced by at least one recording. Each capture’s own judged count is what that recording carried subject matter for; everything else it reports not observed rather than counting it as a pass.

The 12 clauses no capture reaches: CACH-015, CACH-016, MRTR-024, PROM-017, TOOL-033, TOOL-034, TRAN-061, TRAN-070, TRAN-079, TRAN-080, TRAN-096, VERS-004.

The probe session closed the largest group — the rejection rules — and the cancellation round closed the next. What is left divides in three, and only the first is ordinary backlog.

Eight need server surface this reference does not have: pagination for CACH-015/CACH-016, an x-mcp-header designation for TOOL-033/TOOL-034 and TRAN-079/TRAN-080/TRAN-096, and a prompt carrying audio for PROM-017.

Two cannot be reached by a conforming host at all, and saying so is more useful than closing them. VERS-004 judges the shape of an extension identifier, so a capture would have to declare an extension — and this host implements none; declaring one to light up a check is precisely the vacuous evidence the corpus exists to avoid. MRTR-024 is a server re-asking after a client’s shortfall, and a retry that omits requested input is the client violation MRTR-015 names: driving it would make the reference host non-conforming, which is the one thing this capture may not be. Both are evidenced by authored violation traces instead, where the fault belongs.

One is a recording gap rather than a session gap: TRAN-070 needs a response stream to close mid-session, and the server’s tap records no lifecycle events at all, so no HTTP capture can carry one however the session is driven.

BASE-047 used to be on this list. It left without a trace being written, because it was never a corpus gap: the check behind it counted only malformed responses as subjects, so every capture full of well-formed results reported the clause not observed — the tool saying it had seen nothing to judge about a session that had carried dozens of them.

2026-07-28 authored (corpus/draft/)

Fixtures for the in-progress revision, judged by the 2026-07-28 registry behind the draft-2026-07-28 feature. Hand-authored rather than captured: no SDK yet serves the stateless surface end to end, so these are minimal traces written to exercise one clause each, which is also what keeps a violation attributable to the check it kills.

Where a trace falsifies more than one requirement, the row says so and why. There are only two reasons, and neither is sloppy authorship: several clauses state one rule across several sections and share a check by design, or one clause’s antecedent is itself a violation by the other party — a server can only fail to reject a bad header if the client sent one. Every other row falsifies exactly the requirement it is named for.

TraceExercises
stateless-session.jsonlConformant 2026-07-28 stateless session over stdio: every request carries its own _meta envelope (protocolVersion + clientCapabilities), every result carries resultType, request ids are reused only after their response. It closes with two calls in flight, a notifications/cancelled for one of them, and the server answering only the other — TRAN-124’s pass path, which a recording can only carry by showing a permitted message where the forbidden one would be. It also carries a complete conforming MRTR round — input_required with an elicitation/create request and a requestState, then a retry under a new id echoing the state and supplying inputResponses — so the MRTR checks pass on real content. The Streamable HTTP clauses report not observed here: there is no HTTP framing for them to read, which is why streamable-http-session.jsonl exists.
mrtr-parallel-rounds.jsonlTwo tools/call rounds in flight at once — the shape the official Python SDK produces for concurrent calls: both requests, both input_required results, then the retries in the opposite order, each echoing its own requestState and answering its own inputRequests. Until retries were paired by the state they echo, the second retry was judged against the most recent round — the other call’s — and this conforming exchange failed MRTR-003/015/016/017.
subscription-server-teardown.jsonlA stdio subscription the server ends: acknowledgment, one requested change notification, then notifications/cancelled with requestId naming the subscriptions/listen request — the teardown basic/patterns/cancellation makes mandatory (“A server MUST send notifications/cancelled referencing a subscriptions/listen request ID when it tears down that subscription stream”). It carries no subscriptionId tag, since requestId already names the subscription; until 2026-10-04 BASE-039 failed it for the missing tag, and SUBS-001 failed the tagged form as an unrequested notification type, so the mandated message could not pass either way.
resource-read-existing-empty.jsonlA listed resource (file:///project/empty.log) read back with an empty contents array. RES-022 forbids that shape only “for a non-existent resource”, and the page concedes the other reading — “it could mean the resource exists but has no content” — so with nothing in the session saying this resource is missing, nothing is reported. Until 2026-10-04 the shape alone failed the MUST.
base-030-request-meta-missing-required-field.jsonlRequest _meta omits io.modelcontextprotocol/clientCapabilities (BASE-030)
base-030-malformed-request-id-reused-correctly.jsonlA tools/list whose _meta omits clientCapabilities is correctly rejected with -32602; the client then reuses id 1 — legal once answered (BASE-045) — for a well-formed tools/list, which is served (BASE-030 for the first request only). The golden pins BASE-031 passing: correlated by id text alone, the second request’s result was held against the first, malformed one.
base-031-malformed-meta-answered-with-result.jsonlServer answers a _meta-incomplete request with a result instead of -32602 (BASE-031)
base-031-reused-id-malformed-answered.jsonlThe reverse order: a well-formed tools/list id 1 is served, then a _meta-incomplete request reusing id 1 is also served (BASE-031; BASE-030 for the client’s envelope). Exactly one BASE-031 finding, on the second answer — keyed by id alone, the first, legitimate answer was reported too, for a request sent after it.
base-032-invalid-params-not-http-400.jsonl-32602 returned with HTTP 200 rather than 400 (BASE-032)
base-034-input-request-for-undeclared-capability.jsonlServer returns input_required asking for elicitation/create the request never declared (BASE-034)
base-035-missing-capability-error-without-capabilities.jsonl-32021 carries no data.requiredCapabilities (BASE-035)
base-036-missing-capability-not-http-400.jsonl-32021 returned with HTTP 500 rather than 400 (BASE-036)
base-039-subscription-notification-untagged.jsonlNotification on a subscriptions/listen stream with no io.modelcontextprotocol/subscriptionId (BASE-039)
base-040-malformed-traceparent.jsonltraceparent that is not W3C Trace Context shaped (BASE-040)
base-045-request-id-reused-in-flight.jsonlRequest id reused while the first is still outstanding (BASE-045) — legal at 2025-11-25 only after a response, and this trace reuses before one
base-048-result-without-result-type.jsonlResult omits the resultType SEP-2322 requires (BASE-048)
base-083-legacy-error-code.jsonlError code -32010 from the legacy sub-range, which new implementations SHOULD NOT use (BASE-083, a warning). Named base-055-… until 2026-10-04, when BASE-055 carried this use at MUST NOT alongside the allocation rule it actually states.
base-057-undefined-reserved-error-code.jsonlError code -32055: inside the MCP-reserved sub-range but undefined (BASE-057)
base-058-withdrawn-error-code.jsonlError code -32002, withdrawn by this revision (BASE-058)
base-060-app-code-in-reserved-range.jsonlApplication-defined -32500 placed inside the JSON-RPC reserved range (BASE-060)
streamable-http-session.jsonlConformant 2026-07-28 Streamable HTTP session: server/discover under client capabilities advertising a correctly prefixed extension identifier (VERS-004), a paginated tools/list whose two pages agree on cacheScope (CACH-015/016) and which declares x-mcp-header annotations on a string and an integer property, a tools/call mirroring both into Mcp-Param-Region/Mcp-Param-Limit — the integer inside the IEEE 754 safe range (TOOL-034) — over an SSE response with X-Accel-Buffering: no, and a resources/read whose non-ASCII Mcp-Name rides the Base64 sentinel. The continuation page is fetched after a transport-close, which is ordinary here (every POST gets its own response stream) and is the only way a recording can show a server declining to send more for the request that close cancelled — TRAN-070’s pass path.
http-concurrent-posts.jsonlTwo tools/call POSTs in flight at once, recorded the way the capture proxy interleaves them — both requests’ http events, then both bodies, then both statuses and both answers — followed by one serial call. Every header matches its own body. Paired by “the next client message”, the second POST’s headers landed on the first body and a conforming exchange failed TRAN-058/TRAN-097/TRAN-100; overlapping POSTs are now left unpaired, and the serial call after them pairs and is judged as before.
http-upstream-abort.jsonlTwo requests in flight; the capture proxy records a transport-abort — what it writes when the upstream server fails and it answers the client 502 itself — and the server then answers the other request normally. An abort is a failed transport, not a client closing its stream, and a lifecycle event names no request; until 2026-10-04 TRAN-070 read any close or abort as cancelling every request in flight and failed the server for the second answer.
http-stream-closed-by-client.jsonlA tools/call whose SSE response stream closes while it is the only request in flight — the cancellation signal at this revision — after which the server sends nothing more for it, and a later tools/list is answered normally (TRAN-070’s pass path, now that a close is attributed only when exactly one request is in flight).
stdio-cancellation-race.jsonlA tools/call cancelled by notification whose progress and result were already on their way: both land after the cancellation in the recording, then a later tools/list is answered. basic/patterns/cancellation names this race (“this notification MAY arrive after the request has already finished”; “Both parties MUST handle these race conditions gracefully”), and nothing shows the server had read the cancellation when it wrote them. Until 2026-10-04 TRAN-124 convicted every message recorded after a cancellation.
logging-per-request.jsonlPer-request logging done right: a declared logging capability, a tools/call carrying io.modelcontextprotocol/logLevel that receives a log while it is the only request in flight, then a second call that sets no level and receives none (LOG-007/008/009 pass paths).
stdio-x-mcp-header-large-integer.jsonlThe tool-034 session over stdio: an x-mcp-header-annotated integer argument of 2^53. Mirroring is a Streamable HTTP mechanism (“Clients using other transports (e.g., stdio) MAY ignore x-mcp-header annotations entirely”), so no value is put through a header and TOOL-034 does not bind; until 2026-10-04 it failed the MUST here.
tran-058-request-metadata-headers-missing.jsonlPOST carries neither Mcp-Method nor Mcp-Name (TRAN-058)
tran-060-client-posts-a-response.jsonlClient POSTs a JSON-RPC response (TRAN-060). Also falsifies BASE-046: at this revision a server cannot issue the request such a response would answer, so an unsolicited id is the only shape the violation can take.
tran-061-notification-answered-200.jsonlA notifications/cancelled POST answered 200 instead of 202 Accepted (TRAN-061)
tran-066-independent-server-request.jsonlServer sends elicitation/create as its own request on the response stream instead of an MRTR input request (TRAN-066)
tran-068-sse-without-accel-buffering.jsonlSSE response omits X-Accel-Buffering: no (TRAN-068, SHOULD → warn)
tran-070-message-after-stream-close.jsonlServer answers a request whose response stream had already closed, which this revision treats as cancellation (TRAN-070) — the close is attributable because exactly one request is in flight when it is recorded
tran-071-protocol-version-header-missing.jsonlPOST request without MCP-Protocol-Version (TRAN-071)
tran-072-protocol-version-header-mismatched.jsonlHeader says 2025-11-25, body _meta says 2026-07-28; the server rejects it correctly, isolating the client’s fault (TRAN-072)
tran-072-mismatch-rejected-without-id.jsonlThe client’s header/body version disagreement (TRAN-072), refused with 400 and -32020 — but with "id": null. The golden pins TRAN-073 passing: the POST and its answer are the only exchange in flight, so the framing ties the null-id answer to the request, and the right code is the right code.
tran-073-header-mismatch-not-rejected.jsonlThe same disagreement, answered with a result (TRAN-073). Necessarily also falsifies TRAN-072 — the client fault is this clause’s antecedent.
tran-073-mismatch-refused-without-id.jsonlThe same disagreement refused with 400 {"id":null,"error":{"code":-32600,...}} — what the official Python SDK’s server returns, treating the 2025-11-25 header as a legacy request (TRAN-073; TRAN-072 for the client’s header, the antecedent). Paired by id alone the answer belonged to no request and TRAN-073 reported not observed.
tran-074-unsupported-version-without-supported-list.jsonl-32022 without the data.supported list the clause requires (TRAN-074)
tran-074-unsupported-version-accepted.jsonlA request naming a version the server’s own server/discover result omits, answered with a result (TRAN-074)
tran-075-method-not-found-not-404.jsonl-32601 returned with HTTP 200 rather than 404 (TRAN-075)
tran-077-mcp-name-unencoded.jsonlNon-ASCII Mcp-Name carried unencoded (TRAN-077, TRAN-086, TRAN-087 — one rule stated in three sections, one shared transport.header-value-encoding check)
tran-079-x-mcp-header-not-mirrored.jsonltools/call supplies a designated argument without its Mcp-Param-* header (TRAN-079)
tran-080-x-mcp-header-name-invalid.jsonlOne inputSchema with three annotation faults: a non-token name, a case-insensitive duplicate, and an annotation on a number property (TRAN-080)
tran-089-sentinel-markers-miscased.jsonl=?BASE64?…?= — the sentinel markers must be exactly lowercase (TRAN-089). The server rejects it correctly, so nothing else fires.
tran-092-sentinel-pattern-unencoded.jsonlA plain value that matches the sentinel pattern, carried verbatim rather than encoded (TRAN-092)
tran-096-invalid-param-header-accepted.jsonlA recognized Mcp-Param-Region whose value has leading whitespace, answered with a result (TRAN-096). Also falsifies TRAN-077/086/087 — the unencodable value the server had to reject is itself the client’s encoding fault.
tran-077-param-header-value-rejected.jsonlThe same unencodable Mcp-Param-Region, rejected with -32020 and HTTP 400 as the rule requires (TRAN-077, TRAN-086, TRAN-087 — the client’s encoding fault, which is all that is left to fail). TRAN-096’s pass path: the value a server must reject is itself a client fault, so the conforming half of that clause cannot live in good/ and is carried here instead.
tran-097-header-body-mismatch-accepted.jsonlMcp-Param-Region: us-east1 against arguments.region = "us-west1", answered with a result (TRAN-097, TRAN-100 — one rule stated in two sections)
tran-097-integer-header-different-number.jsonlMcp-Param-Limit: 41 against an integer arguments.limit = 42, served (TRAN-097, TRAN-100; TRAN-058 is not involved — the client’s own mirroring defect has no separate clause here). Pins that the numeric comparison TRAN-101 recommends, which since 2026-10-04 lets 42.0 match 42, still rejects a different number.
tran-097-mismatch-after-concurrent-posts.jsonlThe same session as good/http-concurrent-posts.jsonl, except the final, serial POST carries Mcp-Name: beta against params.name = "alpha" and is served (TRAN-097, TRAN-100; TRAN-058 for the client’s header — the antecedent of a failure to reject). Pins that the ambiguity rule withholds judgement only on the overlapping POSTs: once they drain, pairing — and conviction — resume.
tran-098-header-mismatch-without-400.jsonlHeaderMismatch returned with HTTP 500 rather than 400 (TRAN-098, TRAN-102 — one rule stated in two sections)
tran-074-unsupported-version-without-400.jsonl-32022 returned with HTTP 200 rather than 400 (TRAN-074). The status half of that clause had no trace of its own until transport.unsupported-version-status was split out; it had been riding the kills of the sibling rules it was bundled with.
tool-019-tools-undeclared.jsonltools/call answered though discovery declared no tools capability (TOOL-019)
tool-020-declared-tools-list-unimplemented.jsonltools declared, but tools/list refused with -32601 (TOOL-020)
tool-022-tools-list-order-changes.jsonlTwo tools/list results with the same tools in a different order (TOOL-022)
tool-034-mirrored-integer-out-of-range.jsonlAn x-mcp-header-annotated argument of 2^53, outside the IEEE 754 safe range, sent over Streamable HTTP (TOOL-034). Recorded as stdio until 2026-10-04; the stdio copy is now good/stdio-x-mcp-header-large-integer.jsonl.
tool-038-embedded-resource-undeclared.jsonlA tools/call result embedding a resource with no resources capability declared (TOOL-038)
res-012-resources-undeclared.jsonlresources/read answered though discovery declared no resources capability (RES-012)
res-013-declared-resources-list-unimplemented.jsonlresources declared, but resources/list refused with -32601 (RES-013)
res-022-read-empty-contents.jsonlA resources/read of file:///project/gone.txt answered -32602 “Resource not found”, then a second read of the same URI answered with an empty contents array (RES-022). The clause binds a non-existent resource, and the not-found answer is the session’s own evidence that this one is; without it an empty array is the existing-but-empty reading the page concedes (see good/resource-read-existing-empty.jsonl). Rewritten 2026-10-04: the original trace carried the empty array alone.
prom-012-prompts-undeclared.jsonlprompts/get answered though discovery declared no prompts capability (PROM-012). The result it wrongly serves is itself well formed — base64 audio with a valid MIME type — which is PROM-017’s pass path
prom-013-declared-prompts-list-unimplemented.jsonlprompts declared, but prompts/list refused with -32601 (PROM-013)
comp-007-completions-undeclared.jsonlcompletion/complete answered though the server/discover result declared no completions capability (COMP-007)
log-007-logging-undeclared.jsonlnotifications/message emitted though discovery declared no logging capability (LOG-007)
log-008-log-without-requested-level.jsonlA log notification in a session where no request set io.modelcontextprotocol/logLevel (LOG-008)
log-008-log-for-request-without-level.jsonlThe same session, except the second call — no logLevel, and the only request in flight — receives a log (LOG-008). An earlier request did ask for logs, so the session-wide reading passed this until 2026-10-04; with one request in flight the notification can only be that request’s.
log-009-log-on-subscription-stream.jsonlA log notification tagged with a subscription id, so travelling on a subscription’s stream (LOG-009)
log-010-unrecognized-log-level-accepted.jsonlA request declaring log level verbose, served rather than rejected with -32602 (LOG-010)
cach-001-cacheable-result-without-hints.jsonlA complete tools/list result with no ttlMs caching hint (CACH-001)
cach-001-ttl-without-cache-scope.jsonlA complete tools/list result carrying ttlMs but no cacheScope (CACH-001). The page defines “caching hints” as both fields (“Cacheable Results in MCP use two fields to provide caching hints”), and the schema’s CacheableResult requires both; until 2026-10-04 the check accepted ttlMs alone. Three single-issue fixtures that had carried ttlMs without a scope (res-012, tool-022, tool-034) gained "cacheScope":"public" so they stay single-issue.
cach-008-negative-ttl.jsonlttlMs: -1, which servers must never provide (CACH-008)
cach-015-page-scope-changes.jsonlA paginated tools/list whose second page switches from private to public (CACH-015, and CACH-016 — the same rule and its worked example)
subs-001-unrequested-notification-type.jsonlA prompts/list_changed on a subscription whose filter asked only for tools-list changes (SUBS-001)
subs-001-cancelled-for-another-request.jsonlA notifications/cancelled delivered on a subscription stream (tagged with its id) but cancelling an ordinary tools/call, not the subscription — not the teardown the cancellation page permits, so still an unrequested notification on the stream (SUBS-001). Pins that the teardown exemption reads requestId, not merely the method.
subs-002-notification-before-acknowledgment.jsonlA notification ahead of notifications/subscriptions/acknowledged on the same subscription id (SUBS-002)
subs-006-graceful-close-result-not-empty.jsonlA graceful-closure response carrying delivered alongside resultType (SUBS-006, and SUBS-005 — one rule stated in the cancellation list and again under Graceful Closure)
mrtr-004-input-required-on-unsupported-method.jsonlinput_required answering a resources/list, which is not one of the three requests that may draw one (MRTR-004)
mrtr-006-input-request-unknown-method.jsonlinputRequests asking for tools/list, which is not an ElicitRequest, CreateMessageRequest or ListRootsRequest (MRTR-006)
mrtr-011-input-required-empty.jsonlinput_required carrying neither inputRequests nor requestState, opening a round that cannot be completed (MRTR-011)
mrtr-015-retry-without-input-responses.jsonlRetry echoes the state but supplies no inputResponses for the input it was asked for (MRTR-015). The server then asks again rather than erroring — MRTR-024’s pass path, which needs a shortfall to answer and so cannot occur in a conforming session
mrtr-016-request-state-not-echoed.jsonlRetry rewrites requestState instead of echoing it (MRTR-016). Also falsifies MRTR-003 and MRTR-017 by design — the same rule stated from the other side, “MUST NOT modify”, sharing one check.
mrtr-016-parallel-retry-alters-state.jsonlThe same two interleaved rounds as good/mrtr-parallel-rounds.jsonl, with one retry presenting an edited requestState (MRTR-016; MRTR-003/017 by design). An altered state matches no round, so the retry is paired by the request it repeats — tool lookup — and judged against that round, not the more recent one for summarize.
mrtr-018-unsolicited-request-state.jsonlRetry invents a requestState for a round that issued none (MRTR-018)
mrtr-019-retry-reuses-id.jsonlRetry reuses the original request’s JSON-RPC id (MRTR-019) — legal under BASE-045, since the first was already answered, and forbidden here
mrtr-020-request-state-on-another-method.jsonlA prompts/get carrying the requestState a tools/call round issued (MRTR-020)
mrtr-024-shortfall-answered-with-error.jsonlRetry omits requested input and the server answers -32602 rather than asking again (MRTR-024). Necessarily also falsifies MRTR-015 — the client’s shortfall is this clause’s antecedent.
tran-123-cancellation-without-request-id.jsonlnotifications/cancelled carrying only a reason, naming no request to cancel (TRAN-123)
tran-124-message-after-cancel-notification.jsonlServer answers a request the client cancelled by notification (TRAN-124) — after answering a tools/list the client sent later than the cancellation, which on stdio’s ordered stream proves the server had read it. stdio’s cancellation signal is the notification, not a stream close, so transport.no-messages-after-cancellation — which anchors on a transport-close — cannot see this and would have passed it vacuously. Rewritten 2026-10-04: the original answered immediately after the cancellation, the race the cancellation page says implementations must tolerate (see good/stdio-cancellation-race.jsonl).
disc-001-server-discover-method-not-found.jsonlserver/discover answered with -32601, which this revision makes mandatory (DISC-001, and VERS-003 — the same rule stated under versioning). The client then falls back to initialize, carrying a full _meta envelope so the trace isolates the fallback: a dual-era client that probed first is DISC-002/TRAN-128’s pass path, and it can only be witnessed against a server that refused the probe
disc-002-dual-era-client-skips-probe.jsonlA client that speaks both eras — a modern _meta request, then an initialize fallback — whose first request is tools/list rather than the server/discover probe (DISC-002). The initialize carries a full _meta envelope so the trace isolates the missing probe rather than also failing BASE-030, and the handshake is left unanswered so no legacy-shaped result has to be judged for resultType.
vers-002-retry-with-unsupported-version.jsonlAfter a -32022 offering 2026-07-28, the client retries with 1899-01-01 — a version the list it was just handed does not contain (VERS-002)
vers-004-extension-identifier-without-prefix.jsonlClient capabilities advertise the extension ui: a valid _meta key, but extension identifiers require the prefix a _meta key may omit (VERS-004)
vers-008-initialize-error-without-versions.jsonlA modern-only server refuses a legacy initialize with a bare Method not found, naming none of the versions it does speak (VERS-008). Necessarily also falsifies BASE-030: a legacy handshake carries no _meta envelope — that is what makes it legacy — so the antecedent of this clause is a client that cannot satisfy BASE-030. BASE-031 is not also reported, and this trace is why the check was narrowed: basic/versioning’s compatibility matrix makes the rejection code implementation-defined for exactly this exchange.