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

Introduction

This is the reader’s guide to mcp-conformance, an independent, trace-based conformance toolkit for the Model Context Protocol (MCP). It judges whether an MCP implementation conforms to a specific spec revision — the one the recorded session declares, 2026-07-28 or 2025-11-25, or both at once in a single pass — and, just as importantly, it is built so that an ecosystem with its own authoritative test suite has reason to trust the verdict.

Status: 0.x on crates.io. Before 1.0 the public API and the verdicts may change between minor releases; the changelog says so explicitly when they do; STABILITY.md lists what each release keeps stable.

Versions of this book. The site’s root documents the newest release. Each release’s own book stays at /vX.Y.Z/, and the unreleased main branch is at /dev/. The selector in the menu bar switches between them; a page that documents anything but the newest release says so at the top.

The one idea

The validator’s input is a trace: an ordered, serialized record of a protocol interaction. Its engine is a pure function — &[TraceEvent] -> Report — with no network, no clock, and no I/O of its own. Capturing the trace is mcp-trace-capture’s job — a stdio wrapper and HTTP proxy that forward bytes unchanged — and judging it is the validator’s only job. That split is what makes a verdict deterministic, replayable, language- and SDK-agnostic, and — because it is reproducible from a committed file — auditable rather than a “trust us.”

The credibility mechanism is the agreement check: on every CI run the reference server is driven by the official conformance suite (pinned, at 2025-11-25; the 2026-07-28 surface is measured weekly against the suite’s pre-release), the same sessions are captured as traces, and this toolkit’s verdicts are diffed against the official runner’s. Agreement is the default; an unexplained divergence fails the build. A validator whose verdicts are checked against the recognized authority on every commit is calibrated, not merely asserted to be correct.

The crates

CrateWhat it is
mcp-conformance-coreThe spec as data: the requirement registry, the trace schema, and RFC 8785 canonical JSON. Serde-only; no protocol SDK.
mcp-trace-captureThe recorder: a stdio wrapper and HTTP reverse proxy that write a validator-ready trace of any MCP session, linking no SDK.
mcp-trace-validatorThe deterministic judgment engine and its CLI (human / JSON / JUnit / SARIF reports, documented exit codes).
mcp-everything-serverA reference server on the official rmcp SDK that passes the suite’s server scenarios, with a session tap that records traces for the agreement check.
mcp-reference-hostA reference host (client) that passes the suite’s client scenarios and captures host-side traces.

How this book is organized

Each chapter is a curated entry point. The planning documents — charter, ecosystem register, architecture, conformance strategy, engineering standards, security model, roadmap, and decision records — remain the single source of truth, and every chapter links to the authoritative document behind it.

To use the tools:

  • Getting started — install, record a session, judge it, and read the report.
  • Capturing from your client — the configuration for Claude Desktop, VS Code and Cursor, and driving a server with no client at all.
  • Reading a report — what to do about each outcome, triaging a finding, raising coverage, and disputing a verdict.
  • Using it in CI — exit codes, JUnit and SARIF, the GitHub Action, and the container image.
  • Upgrading — what a release may change, and how to compare verdicts across versions before you move a gate.
  • Troubleshooting — what the common failures mean.

How it works:

  • Architecture — why judge traces instead of live sessions, and the design trade-offs that follow.
  • The trace format — the JSON Lines schema, with a worked example embedded from the README so it cannot drift.
  • Two revisions at once — how one trace is judged against 2025-11-25 and 2026-07-28 together, and what a migration looks like when it is measured instead of estimated.
  • The trace corpus — the golden good/violation fixtures and their provenance, embedded from corpus/README.md.
  • Conformance results — the published tier-gap measurements of real SDKs against the official suite.

Reference:

  • Requirements — every clause of 2026-07-28 and 2025-11-25 under its ID: its level, the party it binds, the verbatim quote with a link to the published text, and the checks that judge it or the reason none can.
  • Glossary — the terms the book and the tools’ output use.

Getting started

Three steps: install the two tools, record a session of your server or client, and judge the recording. Nothing here depends on the language or SDK your implementation uses — the tools read and write bytes on the wire.

Install

cargo install mcp-trace-capture mcp-trace-validator

cargo binstall mcp-trace-capture mcp-trace-validator fetches prebuilt binaries instead of compiling (Linux x86_64 and aarch64, macOS, Windows x86_64). To build the unreleased source, which may be ahead of the release: cargo install --locked --git https://github.com/tomtom215/mcp-conformance mcp-trace-capture mcp-trace-validator.

From npm or PyPI (not yet released). From the first release after 0.6.0, the same prebuilt binaries install without Rust:

npx mcp-conformance --version          # one-off, or: npm install -g mcp-conformance
pip install mcp-conformance            # into the active environment

Both put mcp-trace-capture, mcp-trace-validator and mcp-conformance on your PATH, as the release’s own binaries, byte for byte: npm installs the one @mcp-conformance/<os>-<cpu> package that fits your machine as an optional dependency of mcp-conformance (no install script, nothing downloaded beyond the packages), and pip installs the wheel for your platform. Packages exist for the five targets above; elsewhere, install from source. With npm, keep optional dependencies enabled (--omit=optional removes the binaries). If a Python environment’s scripts directory is not on PATH, python -m mcp_conformance runs the workbench. Until that release, neither registry has the packages.

Record a session

mcp-trace-capture sits between a client and a server, forwards every byte unchanged, and writes what crossed the wire to a trace file.

A stdio server. Have your client launch the wrapper, with the server’s own command after --:

mcp-trace-capture -o session.jsonl stdio -- python my_server.py

When a client launches it from its configuration file, give -o an absolute path with {session} in the file name — -o /tmp/traces/{session}.jsonl — so each launch writes the next numbered trace (001.jsonl, 002.jsonl, …). Clients relaunch servers, and the wrapper never overwrites a trace, so without {session} the second launch would refuse to start unless --force let it overwrite the first. The capture tool’s README shows the configuration for desktop clients and for the Python and TypeScript SDKs’ stdio clients.

A Streamable HTTP server. Run the proxy in front of it and point the client at the proxy instead:

mcp-trace-capture -o session.jsonl http --upstream http://localhost:3000
# the client connects to http://127.0.0.1:8080/mcp

Stop the proxy with Ctrl-C when the session is over.

Record one session per trace. A trace is judged as one session, so two clients recorded together look like one client breaking the rules (the validator says so when it sees more than one handshake). For a proxy that serves several clients, put {session} in -o and it writes each client session to its own file. 2026-07-28 sessions carry no session id for it to tell them apart by, so run one proxy per client there.

Judge it

mcp-trace-validator validate session.jsonl

Here is a short session that reuses a request id — its fourth line sends tools/list with the id the initialize already used:

{"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)
        [client] 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, 246 excluded, 0 unsupported, 6 not applicable, 78 not observed
wire schema: pass (4 messages valid)
verdict: fail

Each finding names the clause (BASE-003) and its strength (MUST NOT), the event in the trace (seq 3 — every event carries a seq, its position in the recording), what was wrong, the sentence of the specification it breaks, and a link to it. The exit status is the verdict: 0 pass (warnings included, unless --strict), 1 a clause failed (or warned, under --strict), 2 the run could not judge or report — a bad invocation (an unknown flag, a trace file that cannot be read), nothing judgeable (a trace with nothing in it, or one declaring only revisions this build does not know), or output not written — and 3 the trace is malformed. STABILITY.md keeps these meanings fixed. Reading a report goes further: triaging a finding, raising coverage, and disputing a verdict.

Reading a report

The report lists what needs attention; --all lists every clause. The totals always count every clause the revision has, in seven outcomes:

OutcomeMeaning
passThe session carried traffic the clause binds to, and every check of it found nothing wrong.
failA MUST or MUST NOT clause was broken. Each finding says where.
warnA SHOULD or SHOULD NOT clause was not followed. Warnings do not fail the run unless you pass --strict.
not observedThe session never did the thing the clause is about (nothing was paginated, no error was returned). Not a pass: there was no opportunity to break it.
not applicableThe clause applies only after a capability was negotiated, and this session did not negotiate it.
excludedThe clause cannot be judged from a recording at all — it is about an implementation’s internals, or its user interface. The registry gives the reason for each; --all prints it.
unsupportedThis build cannot judge a clause its registry names. It means a mismatched installation, and exits 2.

So a pass verdict says nothing this session did broke a clause. How much it covers is in the totals: a short session observes few clauses, and a richer session (more features, errors, pagination, both transports) puts more of the specification to the test.

Which revision. The validator judges the revision the session declares — in initialize, in each request’s _meta, or in the MCP-Protocol-Version header — and says how it chose: (declared by the trace). --revision 2025-11-25 overrides it; naming two (--revision 2025-11-25 --revision 2026-07-28) judges the session under both and shows where they differ. This build knows 2025-11-25 and 2026-07-28. A session of an older revision, such as 2025-06-18, is refused with an explanation rather than judged against rules it was not playing by.

Several traces. mcp-trace-validator validate traces/*.jsonl judges each, prints a section per trace and a closing tally, and exits with the worst trace’s status.

Other formats. --format json is the whole report as data, described by a JSON Schema; --format junit is a test report for CI; --format sarif is a SARIF 2.1.0 log for code scanning. Using it in CI covers all three.

Capturing from your client

A client records nothing by itself: you put mcp-trace-capture between it and the server. For a stdio server, the client launches the capture wrapper, which launches the server. For a streamable-HTTP server, the client talks to the capture proxy, which forwards to the server. Either way the bytes pass through unchanged and the trace is written as they go.

Two rules hold for every client configuration below:

  • Give -o an absolute path. A desktop client starts servers in a working directory of its own choosing; a relative path lands somewhere you will not look. Without -o, the trace is mcp-trace.jsonl in that directory.
  • Put {session} in the file name. Clients relaunch their servers — at startup, on reconnect, when you reopen a window — and the wrapper never overwrites a trace. With {session}, each launch writes the next number (my-server-001.jsonl, my-server-002.jsonl, …); without it, the second launch refuses to start. The directory must exist.

Desktop applications often start without your shell’s PATH, so use the absolute path to mcp-trace-capture as the command if the client cannot find it (which mcp-trace-capture prints it). Restart the client after editing its configuration.

Claude Desktop

The configuration file is claude_desktop_config.json, at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and %APPDATA%\Claude\claude_desktop_config.json on Windows (Settings → Developer → Edit Config opens it). Servers go under mcpServers; move the server’s own command behind --:

{
  "mcpServers": {
    "my-server": {
      "command": "mcp-trace-capture",
      "args": [
        "-o", "/Users/me/traces/my-server-{session}.jsonl",
        "stdio", "--",
        "python", "/Users/me/src/my_server.py"
      ]
    }
  }
}

On Windows, write the paths with escaped backslashes ("C:\\Users\\me\\traces\\my-server-{session}.jsonl"). The server’s stderr still reaches the client’s own log, as without the wrapper.

VS Code

A workspace’s servers are in .vscode/mcp.json, under servers. A stdio server:

{
  "servers": {
    "my-server": {
      "type": "stdio",
      "command": "mcp-trace-capture",
      "args": [
        "-o", "/home/me/traces/my-server-{session}.jsonl",
        "stdio", "--",
        "node", "/home/me/src/my-server/build/index.js"
      ]
    }
  }
}

A streamable-HTTP server is an entry with "type": "http" and a url: start the proxy in front of the server, and give VS Code the proxy’s URL instead of the server’s:

mcp-trace-capture -o '/home/me/traces/http-{session}.jsonl' http --upstream http://127.0.0.1:3000/mcp
{
  "servers": {
    "my-http-server": {
      "type": "http",
      "url": "http://127.0.0.1:8080/"
    }
  }
}

The proxy’s root path is the --upstream URL itself, so http://127.0.0.1:8080/ reaches http://127.0.0.1:3000/mcp; the proxy prints the URL to use when it starts. Stop it with Ctrl-C when the session is over: the trace is closed then.

Cursor

Cursor reads ~/.cursor/mcp.json (every project) and .cursor/mcp.json (one project), with servers under mcpServers. A stdio server has the same shape as Claude Desktop’s above. A remote server is an entry with a url, which for a recording is the capture proxy’s, as for VS Code:

{
  "mcpServers": {
    "my-http-server": {
      "url": "http://127.0.0.1:8080/"
    }
  }
}

Other clients

Most clients that launch stdio servers take a command and its arguments, in one of the two shapes above; the wrapper goes in front of both, with the server’s command after --. A client that connects to a URL is pointed at the proxy. The official SDKs’ stdio clients, for test suites, are covered in Using it in CI.

No client at all

To exercise a server without a client application, mcp-reference-host drives it: it lists the tools and calls each once with arguments derived from its schema, and with --sweep also drives prompts, resources, templates and completion. --trace-dir records the session:

cargo install mcp-reference-host
mcp-reference-host --server-cmd "python my_server.py" --sweep --trace-dir traces/
mcp-reference-host --url http://127.0.0.1:3000/mcp --sweep --trace-dir traces/
mcp-trace-validator validate traces/*.jsonl

--server-cmd is split on spaces, so a path containing one needs a wrapper script. --protocol-version 2026-07-28 speaks the stateless revision instead of 2025-11-25. The host’s own recording sits at the message seam and carries no HTTP headers; when transport behaviour matters, record through mcp-trace-capture instead, with the host pointed at the proxy. mcp-reference-host --help lists the rest — --subscribe, --cancel, --log-level and --probe each drive clauses a plain run leaves unobserved.

To exercise a client, run mcp-everything-server --transport http behind the proxy and point the client at it; with a known-good server on the other end, a finding is almost certainly the client’s.

Recording an SDK’s server

Capturing from your client records a session your own MCP host drives. This chapter is the other way round: you have a server built on an official SDK and want to know what it puts on the wire. It is worked through on the TypeScript (both of its lines) and Python SDKs’ own conformance servers — the servers each SDK runs the official suite against — but nothing here is specific to them: any Streamable HTTP server can stand in.

The commands are not copies. Each block below is included, by mdBook anchor, from interop/matrix.sh, the script the weekly scheduled job runs to keep the cross-SDK matrix true; cargo xtask sdk-matrix --check fails if this chapter stops including them, or the job stops running the script. What you read is what the job executed.

Prerequisites

Build the recorder and the validator, then install the pinned SDKs:

cargo build -p mcp-trace-capture -p mcp-trace-validator
cd interop
./setup.sh

setup.sh installs each SDK at the version interop/sdk-pins.env names, every one with integrity: npm from package-lock.json (npm ci), Python from the hash-locked requirements.txt (pip --require-hashes), Go from go.sum, Rust from Cargo.lock. A conformance server that lives in an SDK’s repository rather than its package is fetched from the exact commit the pins name and refused unless its SHA-256 matches. It needs node 20+, Python 3.11+, go, cargo and git.

Recording one session

Every session is the same four steps — start the server, put mcp-trace-capture’s proxy in front of it, drive one session through the proxy, stop the proxy so it writes the trace — followed by judging it:

# record NAME REVISION PORT -- SERVER...
#
# Starts SERVER (listening on 127.0.0.1:PORT), puts the capture proxy in
# front of it on PORT+100, drives one session through the proxy with the
# client for REVISION, stops the proxy (which writes the trace), and judges
# the trace. The ports are fixed rather than ephemeral because the proxy
# records the Host header, and a committed trace should not change with them.
record() {
  local name=$1 revision=$2 port=$3 proxy=$(($3 + 100)) status=0
  shift 4
  "$@" > "$OUT/$name.server.log" 2>&1 & local server=$!
  pids+=("$server")
  wait_for_port "$port"
  "$CAPTURE" -o "$OUT/$name.jsonl" http \
    --upstream "http://127.0.0.1:$port" --listen "127.0.0.1:$proxy" \
    2> "$OUT/$name.capture.log" & local capture=$!
  pids+=("$capture")
  wait_for_port "$proxy"
  export INTEROP_ERRORS=record
  case $revision in
    2025-11-25) node ts-client.mjs http "http://127.0.0.1:$proxy/mcp" || status=$? ;;
    2026-07-28) "$PYTHON" py-client.py http "http://127.0.0.1:$proxy/mcp" || status=$? ;;
  esac
  kill -INT "$capture"; wait "$capture" || true
  kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true
  judge "$name" "$revision" "$status"
}

The client is chosen by revision, and is the same for every server: the TypeScript SDK’s v1 client (ts-client.mjs) speaks 2025-11-25, the Python SDK’s (py-client.py) speaks 2026-07-28. Each lists what the server advertises, calls a tool, calls one that does not exist, reads a resource and gets a prompt. With INTEROP_ERRORS=record an error answer is logged and the session carries on: the server’s answer is the evidence, so the client must not stop at it.

The TypeScript SDK, v1

The v1 line is the single package @modelcontextprotocol/sdk, in maintenance. The server is test/conformance/src/everythingServer.ts from the SDK’s repository at the release tag. It imports the SDK by its path inside that repository; setup.sh rewrites those imports to the published package, so it runs on the pinned npm release, and tsx runs the TypeScript directly:

record sdk-typescript-2025-11-25 2025-11-25 38301 -- env PORT=38301 node_modules/.bin/tsx upstream/ts-everything-server.ts
record sdk-typescript-2026-07-28 2026-07-28 38301 -- env PORT=38301 node_modules/.bin/tsx upstream/ts-everything-server.ts

The second line is a session the server cannot serve: the TypeScript SDK’s 1.x line implements the specification up to 2025-11-25 (its SUPPORTED_PROTOCOL_VERSIONS stops there), so the Python client’s discovery falls back to the initialize handshake. py-client.py exits 3 when that happens, and the script files the recording as …not-served.jsonl rather than judging a session nobody asked for.

The TypeScript SDK, v2

The v2 line is a set of packages, @modelcontextprotocol/server and its companions, and it does serve 2026-07-28: its server answers server/discover with supportedVersions of ["2026-07-28"], and still answers the initialize handshake for 2025-11-25, from one process. The server is the same path in the SDK’s repository, test/conformance/src/everythingServer.ts, at the release tag. Nothing is rewritten: the file imports the published packages by name, which setup.sh installs from the lockfile (@modelcontextprotocol/server, node and express, pinned together in sdk-pins.env). Both revisions are recorded:

# The v2 line's server answers both revisions from one process: the initialize
# handshake for 2025-11-25 and server/discover for 2026-07-28.
record sdk-typescript-v2-2025-11-25 2025-11-25 38305 -- env PORT=38305 node_modules/.bin/tsx upstream/ts2-everything-server.ts
record sdk-typescript-v2-2026-07-28 2026-07-28 38305 -- env PORT=38305 node_modules/.bin/tsx upstream/ts2-everything-server.ts

The Python SDK

The server is the SDK repository’s examples/servers/everything-server, run on the hash-locked mcp release:

record sdk-python-2025-11-25 2025-11-25 38302 -- "$PYTHON" upstream/py_everything_server.py --port 38302
record sdk-python-2026-07-28 2026-07-28 38302 -- "$PYTHON" upstream/py_everything_server.py --port 38302

Judging, and what comes out

./matrix.sh ../target/debug

prints one line per session — the verdict and the totals — and leaves each trace and its JSON report in interop/out-matrix/. Read any one in full the usual way:

../target/debug/mcp-trace-validator validate out-matrix/sdk-python-2026-07-28.jsonl

A finding is a result, not a failure of the script: it exits non-zero only when a session could not be recorded at all. The same script records the Go and Rust SDKs’ servers, and the committed results of all of them are the cross-SDK matrix. To check a fresh run against them:

cargo xtask sdk-matrix --recaptured interop/out-matrix

Recording your own server

Replace the server command in a record line with yours, and give it a free port. If your server speaks stdio rather than HTTP, record it the way Getting started shows instead — mcp-trace-capture -o session.jsonl stdio -- <server> — and point a client at that command.

Testing a server or client

A recording judges only what its session did; most clauses read not observed because nothing exercised them. The official conformance suite exercises an implementation on purpose. mcp-conformance test runs the two together: the pinned suite drives your server or client, mcp-trace-capture records the traffic as it goes, and every recorded session is judged clause by clause. You get the suite’s checks, the clause report, and whether the two agree — from one command.

mcp-conformance is on main, not yet released (ADR-0021). Build it from a checkout with cargo install --path crates/mcp-conformance-cli.

Before you start

test needs Node.js 18 or newer, with npx, on PATH: the suite is a Node package. It checks, and tells you what to install when Node is missing; it installs nothing itself. npx fetches @modelcontextprotocol/conformance at exactly the version this build pins, and test refuses to go on if the suite reports any other version.

A server

Over streamable HTTP, give the endpoint URL. The suite reaches it through a recording proxy on 127.0.0.1:

mcp-conformance test --url http://localhost:3000/mcp

Over stdio, give the command. It is split at whitespace and run without a shell:

mcp-conformance test --command "python my_server.py"

The suite drives servers only over streamable HTTP, so test puts a loopback bridge in front of a stdio server and starts one server process per suite session, each under the recorder’s stdio wrapper. The trace is the stdio wire your server spoke. Three of the suite’s scenarios exercise the HTTP transport itself — dns-rebinding-protection, server-sse-polling and server-sse-multiple-streams — and would judge the bridge rather than your server, so they are not run; the report lists them as not run, with why.

To run some scenarios rather than the suite’s default set, name each:

mcp-conformance test --url http://localhost:3000/mcp --scenario tools-list --scenario ping

A client

Give the command that starts your client. The suite runs it through a shell, once per scenario, with the URL of the suite’s test server appended:

mcp-conformance test --client-command "node my_client.js"

By default that is the suite’s four protocol scenarios (initialize, tools_call, elicitation-sep1034-client-defaults, sse-retry). The auth/* scenarios need the suite’s authorization server as well as its MCP one, which the recording proxy does not stand in front of; name them with --scenario to run them anyway.

Reading the result

The human report has three parts:

  • The official suite — each scenario with its checks’ statuses, and the message of any that failed or warned. The suite’s verdict is its own: a FAILURE or a WARNING fails it.
  • The clause report — one validator report per recorded session, as mcp-trace-validator validate prints it (Reading a report). A session that holds nothing to judge (a request the server refused before any message was exchanged, say) is listed as not judged, with the reason.
  • Agreement — every failing clause is a divergence from the suite’s verdict unless a divergence baseline explains it, and every baseline entry that explained nothing is stale. Agreement holds when there is neither.

--divergences names a baseline in the format of this repository’s conformance/agreement-divergences.json: each entry a requirement ID, a triage class (our-bug, suite-bug or spec-ambiguity) and an upstream link. --expected-failures passes the suite its own baseline of scenarios expected to fail.

mcp-conformance test --url http://localhost:3000/mcp --divergences divergences.json --expected-failures expected-failures.yaml

For CI, --format json carries all three parts; junit and sarif are the validator’s own renderings of the clause report (Using it in CI), and the suite’s checks and the agreement go to stderr whatever the format. -o writes the report to a file:

mcp-conformance test --command "python my_server.py" --format sarif -o conformance.sarif

The exit code is 0 when the suite passed and the two agree, 1 for findings — a suite check that failed or warned, or a disagreement — and 2 when the run could not be made or judged. The suite’s results and every trace are kept in the work directory, printed at the end (--work-dir chooses it): judge a trace again, or attach it to a bug report.

This repository’s own subjects

CI runs test against this repository’s everything server, over both transports, and its reference host, and holds each report to cargo xtask conformance’s results for the same subject: the same scenarios with the same check statuses, every failing clause the gate’s recordings show, and nothing beyond them that conformance/wire-findings.json does not list with its triage class and why only the wire shows it. From a checkout, after cargo build -p mcp-everything-server -p mcp-conformance-cli and cargo build -p mcp-reference-host --features cli:

mcp-conformance test --command "target/debug/mcp-everything-server --transport stdio"
mcp-conformance test --client-command target/debug/mcp-reference-host --divergences conformance/client-agreement-divergences.json

Watching a session live

mcp-trace-validator gives its verdict after the session; mcp-conformance test gives one after the suite has run. While you are building a server or a client you want to hear about a violation when it happens. mcp-conformance watch records a session exactly as mcp-trace-capture does and judges it as it runs: every new finding is printed as the exchange that shows it completes, a one-line tally keeps count, and the validator’s full report follows when the session ends.

mcp-conformance is on main, not yet released (ADR-0021). Build it from a checkout with cargo install --path crates/mcp-conformance-cli.

Starting it

watch takes the recorder’s two modes, and its flags by the same names — -o for the trace file (with {session} for a file per session), --force, --max-message-bytes. For a stdio server, configure your client to launch the wrapper in place of the server, exactly as capturing from your client describes for mcp-trace-capture:

mcp-conformance watch stdio -- python my_server.py

For a streamable-HTTP server, point your client at the proxy instead of the server:

mcp-conformance watch http --upstream http://localhost:3000/mcp
# the client connects to http://127.0.0.1:8080/   (the proxy's root is --upstream itself)

The proxy’s root path is the --upstream URL itself, and any other path is appended to it. With --upstream http://localhost:3000 (no path), the client connects to http://127.0.0.1:8080/mcp instead; giving the path in both places reaches /mcp/mcp, which the server answers with a 404.

The proxy listens on 127.0.0.1:8080 unless you say otherwise:

mcp-conformance watch -o traces/{session}.jsonl http --upstream http://localhost:3000/mcp --listen 127.0.0.1:9000

The trace file is the one mcp-trace-capture would have written, so everything else in this book applies to it unchanged. Stop an HTTP watch with Ctrl-C; a stdio one ends when your client stops the server.

What it prints

Everything goes to stderr: in stdio mode stdout carries the session itself. A finding looks like this — the outcome, the clause ID and its level, the event it points at, the detail, then the clause and where it is published:

mcp-conformance watch: WARN TOOL-006 (SHOULD) seq 5: tool name "weather lookup,v2!" contains characters outside A-Z, a-z, 0-9, underscore, hyphen, and dot: " ,!"
    spec: "The following SHOULD be the only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.)"
    see:  https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names
mcp-conformance watch: WARN TOOL-007 (SHOULD NOT) seq 5: tool name "weather lookup,v2!" contains characters outside A-Z, a-z, 0-9, underscore, hyphen, and dot: " ,!"
    spec: "Tool names SHOULD NOT contain spaces, commas, or other special characters."
    see:  https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names
mcp-conformance watch: 3 exchange(s); findings so far: 0 fail, 2 warn

That is the corpus’s tool-006-name-charset session replayed through watch stdio (the test that replays it checks this page’s lines against what watch printed).

On a terminal the tally is one line rewritten in place; in a log it follows each batch of new findings. When the session ends, watch prints mcp-trace-validator’s report of the whole trace — the same report validate prints — and the tally once more.

Text quoted from the session (a method name, a tool name, an error message) is printed with control characters escaped as \u{…}, as the validator’s own report prints it: a recording is untrusted input, and an escape sequence in a tool name must not reach your terminal raw.

When a finding appears

After every completed exchange — a response paired with its request, a notification, or anything that is not a request still waiting for its answer, such as a line that is not JSON-RPC at all — the trace so far is judged at the revision it declares. A finding is printed the first time a pass shows it, and never again; the end-of-session report lists it with the rest. A violating response or notification is reported by the pass its own arrival triggers; a violating request, by the pass of the exchange its response completes (a request never answered is judged at the end).

Two things are deliberately not findings:

  • An outcome that moves. As the session reaches a clause it goes from not observed to pass. That is progress, not news, and watch says nothing.
  • A verdict about the future. Right after the initialize response, the trace so far has no initialized notification, so LIFE-003 fails — until the client sends it a moment later. Checks that conclude from what the rest of the trace does not hold wait for the end of the session. The repository holds this to the corpus: every committed trace, replayed line by line, prints nothing live that the whole trace does not show, and prints everything else by the pass at or after the line that makes it visible. Should a session the corpus does not cover still retract a finding, the end of the run names it as withdrawn.

It never slows the session

watch is read-only, like the recorder it is built on: bytes are forwarded unchanged, in order, and nothing waits for judging. The recorder hands each line it writes to a channel and goes on; judging happens on a thread of its own. If judging falls behind, the findings lag and the session does not — a test holds this, with a judge that sleeps 50 ms a pass behind a server sending 200 notifications 2 ms apart.

Each pass re-judges the whole trace so far. Measured in release builds (4 vCPU Intel Xeon @ 2.10 GHz, cargo bench -p mcp-conformance-cli --bench live), a pass takes about 0.17 ms on the largest committed capture (41 events) and about 80 ms on a synthetic 10,001-message session. So watch paces itself: after a pass that took d, the next waits 3d, and whatever arrived meanwhile is judged in one pass. A short session gets a pass per exchange; a 10,000-message one gets one about every 320 ms, its findings lagging by up to that much, and judging never takes more than a quarter of a core.

Spans for your tracing stack

--otlp sends one span per exchange to an OpenTelemetry collector, over OTLP/HTTP with JSON encoding. Give the collector’s base URL (/v1/traces is appended) or the full traces URL:

mcp-conformance watch --otlp http://localhost:4318 stdio -- python my_server.py

The spans follow the OpenTelemetry semantic conventions for MCP (read at that commit; their status is Development, so they may still change):

  • Name {mcp.method.name} {target}, the target being the tool of a tools/call or the prompt of a prompts/get (tools/call get_weather), else the method alone.
  • Kind CLIENT, for every exchange, whoever started it. The conventions define the client span as the initiator’s view, from sending the request to receiving the answer, and the server span as the receiver’s processing. A tap on the wire sees exactly the first and never the second, so it claims no timing it does not have.
  • Attributes mcp.method.name; jsonrpc.request.id (not for a notification); mcp.session.id (HTTP); mcp.protocol.version; jsonrpc.protocol.version when not 2.0; network.transport (pipe for stdio, tcp for HTTP) with network.protocol.name and network.protocol.version for HTTP; server.address and server.port (the upstream) for a client’s request over HTTP; gen_ai.tool.name with gen_ai.operation.name = execute_tool, gen_ai.prompt.name, mcp.resource.uri (the URI up to its path: userinfo, query and fragment are dropped, since a presigned URI keeps its credential there).
  • Errors set error.type and rpc.response.status_code to the JSON-RPC code and the span status to ERROR, described by the code and a fixed name for it (JSON-RPC error -32602 (invalid params)), never by the server’s own message, which can echo what it was sent; a tool result with isError: true sets error.type = tool_error; a request the session ended before answering, unanswered.
  • Findings are span events named mcp_conformance.finding, with mcp_conformance.requirement.id, .requirement.level, .outcome, .revision, .spec.url, .check, .detail (cut at 512 characters, marked) and .trace.seq — this tool’s own namespace, since the conventions have none for conformance. A finding goes on the span of the exchange that holds its seq, or, when that span has already been sent, on the span of the exchange that surfaced it.

Each session is one trace id, each exchange a span id, both random per run from the operating system’s generator. None of the conventions’ opt-in attributes — tool arguments, tool results, prompt variables — is ever set: watch exports no payload.

Spans travel in batches, off the session’s path, through a bounded queue. A collector that is down, slow or refusing costs the spans sent to it and nothing else; the summary at the end says how many were exported and how many were not, and why. CI sends the spans of a corpus replay through a pinned OpenTelemetry Collector and reads back what it wrote.

What it listens on and what it sends

watch listens only where mcp-trace-capture would: nowhere in stdio mode, and on --listen in http mode — loopback by default, with the recorder’s warning for any other address, since anyone who can reach the proxy can send requests through it. It sends nothing anywhere unless --otlp names a collector, and then only the span fields above. An http:// collector that is not this machine gets a warning, because those fields — method and tool names, resource URIs up to their path, session ids, error codes, findings — would cross the network unencrypted. The trace file holds message bodies in full, as every recording does: read it before you share it.

Reading a report

A report answers one question per clause of the revision: did this session break it? Getting started lists the seven outcomes; this chapter is about acting on them.

The outcomes, and what to do about each

OutcomeWhat to do
failA MUST or MUST NOT was broken. Triage each finding (below).
warnA SHOULD or SHOULD NOT was not followed. Decide whether the deviation is deliberate; --strict makes warnings fail the run.
passNothing. The session carried traffic the clause binds to, and no check found a fault in it.
not observedNothing is wrong, and nothing was tested: the session never did what the clause is about. Raise coverage (below) if the clause matters to you.
not applicableThe clause depends on a capability this session did not negotiate. A session that negotiates it judges the clause.
excludedNo recording can judge the clause. --all, or mcp-trace-validator requirements <ID>, prints the registry’s reason, which often says where the clause is tested.
unsupportedThe installation is mismatched: the registry names a check this build does not have. Reinstall; the run exits 2.

Over several traces, a trace that could not be judged at all — unreadable, malformed, of a revision this build does not know, or judging nothing — is listed with its error, and the run’s verdict is incomplete.

Triaging a finding

  FAIL  BASE-003 (MUST NOT)
        [client] 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
  1. Find the event. seq 3 is the event whose seq field is 3 — in a trace written by mcp-trace-capture, the line after seq 2. Many findings name a second event (at seq 0): the earlier one the offending event conflicts with. --format sarif puts each finding on its trace line in a SARIF viewer.
  2. Find the party. The tag before the event ([client]) is the party the finding is held against: the party the clause binds, or, for a clause binding both, the sender of the message at seq (JSON carries it as the finding’s party, and each row’s actor is the party its clause binds). A finding the trace cannot pin on either side carries no tag. mcp-trace-validator requirements BASE-003 --revision 2025-11-25 prints the clause in full. When the other side of the session is not yours to fix — the official suite’s client, say — --party server counts only the server’s findings toward the verdict and exit status, and the verdict line says (server findings only).
  3. Read the clause where it is published. The spec: line is the registry’s verbatim quote, and see: links the sentence in the revision the session was judged against. Read the surrounding section before changing code: the clause may carry conditions the quote alone does not.
  4. Check the revision line at the top of the report (revision 2025-11-25 (declared by the trace)). A session judged against a revision it did not mean to speak fails clauses it never agreed to; --revision names the right one.

A few findings come from the recording rather than the implementation: two sessions in one trace (the validator notes this, and {session} in the capture’s -o separates them), or a capture started mid-session. Troubleshooting covers both.

The wire schema

Below the clause rows, every report judges each message against the revision’s own JSON Schema — the specification’s schema.json, which it calls “the source of truth for all protocol messages and structures”:

  FAIL  wire schema — fail (1 of 5 messages invalid)
        [server] seq 4: the server's message is not a valid ListResourcesResult under the 2025-11-25 schema: /result/resources/0: "uri" is a required property
        spec: "This is the source of truth for all protocol messages and structures."
        see:  https://modelcontextprotocol.io/specification/2025-11-25/basic#schema
        schema: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/<commit>/schema/2025-11-25/schema.json

The finding names the definition the message was held to and the path of the member at fault; a member that matched none of several alternatives (a content block, an input request) names the closest alternative’s defect. The schema catches what no sentence of the prose states on its own — a missing required member, a wrong type, a value outside an enumeration, image or audio data or a resource blob that is not base64 (the schema’s byte format; its uri and uri-template formats stay annotations, because asserting RFC 3986 would fail URIs the prose permits) — so it is reported beside the clauses rather than as one, has no requirement ID, and is not counted in the totals; a failure still fails the verdict. Where the published schema is stricter than schema.ts, the validator corrects it and says where (ADR-0020).

Extensions

An official extension — tasks (io.modelcontextprotocol/tasks) and skills (io.modelcontextprotocol/skills) at 2026-07-28 — is optional by definition, so conforming to the protocol cannot depend on one. A session that uses one is judged against the extension’s own clauses in a block of its own, and the block never moves the report’s totals, verdict or exit status. This session conforms to the protocol and breaks the tasks extension:

MCP trace validation — revision 2026-07-28 (declared by the trace)
extension io.modelcontextprotocol/tasks — not scored: its outcomes never change the verdict
  FAIL  TASK-001 (MUST NOT)
        [server] seq 3: a `resultType: "task"` result answered a `tools/call` request whose `_meta` client capabilities do not declare `io.modelcontextprotocol/tasks`
        spec: "A server MUST NOT return `CreateTaskResult` to a client that did not include the extension capability on its request, regardless of prior declarations."
        see:  https://modelcontextprotocol.github.io/ext-tasks/specification/2026-07-28/tasks#capability-negotiation
  extension totals: 2 pass, 1 fail, 0 warn, 24 excluded, 0 unsupported, 0 not applicable, 22 not observed — extension verdict: fail
totals: 35 pass, 0 fail, 0 warn, 320 excluded, 0 unsupported, 0 not applicable, 146 not observed
wire schema: pass (4 messages valid)
verdict: pass

The command exits 0. The official suite draws the same line: its requirement sets run an extension’s scenarios under not_scored: extension, reported and never counted. An extension appears only when the session exercised it — one of its clauses passed, failed or warned — so a session that never touched tasks or skills shows no block at all. JUnit gives the extension a suite of its own, named … (2026-07-28, extension io.modelcontextprotocol/tasks, not scored), whose findings are in passing cases’ <system-out> and never a <failure>; SARIF reports them at level note; the HTML page gives them a section after the verdict. --strict does not change any of it. Each clause links to the extension’s own specification, which its repository publishes; the extensions reference lists every one, and ADR-0023 records why.

Why so many clauses are not observed

A pass verdict says that nothing this session did broke a clause. A short session does little: one initialize and a tools/list says nothing about pagination, errors, resources, cancellation, or the HTTP transport. Those clauses report not observed, and the totals count them so that a thin session cannot read as a thorough one.

To put more of the specification to the test, record sessions that do more:

  • exercise every feature the implementation offers — tools, prompts, resources, completion, subscriptions;
  • include error paths: an unknown tool, a missing resource, invalid parameters;
  • page through a long list, cancel a request, ask for progress and logs;
  • record over both transports, since Streamable HTTP has its own clauses;
  • for a server, let mcp-reference-host drive it with --sweep (and --cancel, --subscribe, --log-level at 2026-07-28) — see Capturing from your client.

validate traces/*.jsonl judges each recording separately: coverage is per trace, not pooled across them.

Disputing a verdict

If a finding disagrees with your reading of the specification, open an issue with the requirement ID, the specification text, and — if you can share it — the trace (traces record message content in full; read one before attaching it). Disputes are triaged into one of three outcomes: a bug here (fixed), a divergence from the official conformance suite (filed upstream), or an ambiguity in the specification (filed upstream). The contributing guide has the process.

The report as a page

--format html writes the report as one HTML file — the one to attach to a bug report against an SDK, or to open while debugging:

mcp-trace-validator validate session.jsonl --format html > report.html

From the top: the verdict, with every outcome’s count (one card per revision when the trace is judged at several) and the wire-schema outcome; then Clauses to act on, each failing, warning or unsupported clause with its level, the clause verbatim, a link to its published page, and every finding with the seq it points at; then the Message timeline, every event in capture order with its direction, kind, method and id, the request a response answers (both linked), HTTP requests and responses with their recorded headers, and each finding shown at its event. A finding links to its event and the event back to the finding, and the switch above the timeline hides the events no finding names. Accounting closes the page: the passed, not-observed, not-applicable and excluded clauses, collapsed, each excluded one with the registry’s reason and each not-applicable one with the capability the session never declared — the same arithmetic as the totals, clause by clause.

Anchors are names you can type: report.html#BASE-003 opens a clause, #e12 an event (#2026-07-28-BASE-045 when two revisions share a page, #t1-e12 in a page over several traces).

The page is safe to open from anyone. It loads nothing — no script, font or image — and the only style it applies is its own, which its Content Security Policy admits by digest; everything from the trace (method names, ids, headers, payloads) is shown as text, so a recording holding <script> shows the word. It is still a trace: payloads are shown in full up to 16 KiB per message, so read it before you share it, as you would the trace.

The page is for people. Its layout changes between releases; for anything a script reads, use --format json.

The report as data

--format json is the whole report, described by its JSON Schema. A single trace gives one object: revision, revision_source (how the revision was chosen), totals per outcome, verdict, and requirements, one row per clause with its id, level and outcome. A failing or warning row also carries source (the quote, its section and URL) and findings, each with the check that found it, the seq of the event, and a detail. An extension the session exercised is an entry of extensions, with its own extension, verdict, totals and requirements — read by nothing that decides the report’s verdict. Several traces, or several --revisions, give the multi-trace and multi-revision shapes the schema also describes.

To hold an extension you implement to its clauses yourself:

mcp-trace-validator validate --format json session.jsonl \
  | jq -e '[.extensions[]? | select(.extension == "io.modelcontextprotocol/tasks")
            | .verdict] | all(. == "pass" or . == "pass-with-warnings")'

The findings alone, for a script:

mcp-trace-validator validate --format json session.jsonl \
  | jq '.requirements[] | select(.outcome == "fail" or .outcome == "warn")
        | {id, level, findings}'

The workbench

mcp-conformance serve is a local web page over a library of recordings: search them, read one as a timeline with each finding at its event, compare two, follow one that is still being recorded, see what the library as a whole has exercised, and administer it — import, tag, expire, delete, export, and make redacted copies to share. It is ADR-0021’s decision 4; it runs on your machine, reads and writes one directory, and sends nothing anywhere.

mcp-conformance serve --library ./recordings

It prints the link to open:

mcp-conformance serve: library ./recordings (12 recordings)
mcp-conformance serve: open http://127.0.0.1:41245/?token=ae0c…4f71
mcp-conformance serve: the link works once; Ctrl-C stops the workbench
mcp-conformance serve: browsers scope the cookie to 127.0.0.1 as a whole, not to port 41245: any other server there that this browser visits is sent it

Open the link (--open does it for you). It works once: the first visit trades the token for a cookie that only that browser holds, and the address bar is redirected to a URL without it. To open the workbench in another browser, restart it.

The library

A library is a directory of traces, <id>.jsonl, each with an optional sidecar, <id>.meta.json:

{
  "format": 1,
  "tags": ["nightly", "sdk-go"],
  "source": { "kind": "imported", "importer": "mcpsnoop", "imported_from": "session.jsonl",
              "not_recorded": ["… what the source format does not record …"] },
  "added_at": "2026-10-13T09:12:00Z",
  "recorded_at": "2026-10-12T17:40:03Z",
  "verdict_cache": { "sha256": "…", "tool_version": "0.6.0", "reports": [ … ] }
}

The verdict is mcp-trace-validator’s, by the same rules validate applies; the cache is used only when it was computed from the same bytes (SHA-256) by the same version, so editing a trace or upgrading recomputes it. Point a recorder at the directory — mcp-trace-capture -o 'recordings/{session}.jsonl' … — and the recording appears on the next page load. An id is the file stem, limited to ASCII letters, digits, ., _ and -; any other .jsonl file is listed as not opened, and is never read.

Searching

The search box takes terms separated by spaces; every term must match.

TermFinds the recordings that…
method:tools/callsent or received that method
tool:echo, prompt:greeting, resource:test://static-textcalled, got, read or listed it
session:3f2a…carried that Mcp-Session-Id (or are that library entry)
code:-32602hold a JSON-RPC error with that code
req:BASE-003have a finding against that requirement
server:everything, tag:nightlywere recorded against that server, or carry that tag
verdict:failhave that verdict: pass, warn, fail, unsupported, unjudged
timeout, "two words"hold the text in a finding, a name, a tag or any value above

A value ending in * matches by prefix (method:resources/*, req:TRAN-*); a leading - excludes (-tag:ci); quotes hold spaces. Results are ranked — a field term counts for more than free text, and more occurrences for more — and each links to the events it matched.

A session

The session view is the HTML report’s timeline, drawn with the report’s own stylesheet: every event in order, requests paired with their answers (with the latency when the trace records timestamps), notifications, HTTP exchanges and their headers, transport lifecycle. Findings are listed beside it; Go to event opens the event’s inspector (its full payload, up to 16 KiB shown), and each finding marked on an event links back. Only events with findings filters the timeline; HTML report downloads the same page --format html writes, to attach to a bug report.

Follow live polls the trace file once a second and adds what was appended, re-judging as it grows: point it at a trace a recorder is still writing. A last line not yet finished is left for the next poll.

Where the recording came from is stated above the verdict. For an import, that includes what the source does not record — the clauses that would need it read not observed, and nothing is filled in. For a redacted copy, it names the original and each rule applied.

Comparing two sessions

Select two recordings and Compare (or Compare with… on a session). The diff lists every clause whose outcome or number of findings differs at a revision both were judged at, then the messages aligned by direction, method (a response takes its request’s) and id, each pair the same or changed, each unpaired message only in A or only in B; a changed pair opens to both payloads. Recordings of the same client against two servers — two SDK versions, say — align message for message.

Dashboards

Verdicts by the day recorded, clause coverage per revision (how many of the registry’s clauses some recording judged, beside those the registry excludes from judging), and findings by area and by server. Every number is reproducible from the command line, and the page names the member of the output each chart draws:

mcp-conformance library --library ./recordings stats --format json

prints exactly — byte for byte, which a test holds — what the dashboards read.

Administration

Each action is a button in the page and a library subcommand:

ActionIn the pageFrom the command line
Import a trace, an mcpsnoop capture or an MCP Inspector exportImport…library import FILE [--from mcpsnoop|inspector|trace] [--transport stdio|streamable-http]
TagEdit tagslibrary tag ID --tag T…
Expire old recordingsRetention…library prune --retain-days N [--apply]
DeleteDeletelibrary delete ID…
Export (tar of traces and sidecars; each recording once, at most 100 recordings and 192 MiB)Exportlibrary export ID… -o FILE
RedactRedact…library redact ID [--header H…] [--pointer P…] [--meta] [--string S…]

Redaction makes a new recording and leaves the original alone. It replaces with "[redacted]" the values of headers named (by default Authorization, Cookie, Set-Cookie, Proxy-Authorization and Mcp-Session-Id), of JSON pointers into every message (/params/arguments/apiKey; * matches every member, as in /result/content/*/text), of every _meta member, and every occurrence of a literal string anywhere. A line that is not JSON stops the redaction rather than being copied. Member order within a line is not kept. Read the copy before sharing it: redaction removes what you name, and a recording holds whatever the session sent.

Retention deletes recordings made more than N days ago; one with no date (no recorded_at, no timestamps, no added_at, no file time) is never selected.

Importers

  • mcpsnoop (its JSON Lines capture, read from captures 0.26.1 wrote): every frame becomes a message, a batch one array again; over HTTP, each exchange’s first frame is preceded by the request (a POST, with the headers mcpsnoop kept) or the response status. It does not record Host or Mcp-Session-Id, response headers but Content-Type, transport lifecycle, or the method and time of a request without a body (a GET stream, a DELETE), which is placed just before the server’s first frame on it with its method unstated.
  • MCP Inspector (the web client’s protocol export, inspector-protocol-….json, read from @modelcontextprotocol/inspector 2.10.1): every message becomes an event, an answer folded into its request’s entry placed at the request’s time plus the recorded duration. It does not record the transport (say it: --transport), anything of HTTP, lifecycle, or messages the Inspector’s log had dropped (it keeps the latest 1000 by default).

Security

The library can hold credentials, and a local web server is reachable by every page your browser opens. So the workbench binds 127.0.0.1 (another address needs --dangerously-listen-non-loopback, and it warns); answers only requests whose Host is its own address — a DNS-rebound name is refused — and whose Origin, when present, is its own; needs the session cookie on every request; answers /api/… only to a request whose Sec-Fetch-Site is absent, same-origin or none (a page on another port of the same host is same-site and is refused); accepts a change only as a JSON body carrying its Origin; sends a Content Security Policy that allows no inline script or style and no connection elsewhere; and puts every string from a recording on the page as text, with control and bidirectional characters shown as \u{…}. The security model lists the threats and the tests.

The page and its API are for people, not a stable interface (STABILITY.md); library stats --format json is the way to read the numbers from a script.

Using it in CI

The shape is the same everywhere: the tests you already run drive your server or client through mcp-trace-capture, and a later step judges the traces they leave. The validator’s exit status is the verdict, so a failing clause fails the build.

Recording from a test suite

Wherever your tests start the server, put the capture in front of it. With the official SDKs’ stdio clients, the server’s command moves behind --:

StdioServerParameters(
    command="mcp-trace-capture",
    args=["-o", "traces/{session}.jsonl", "stdio", "--", "python", "my_server.py"],
)

For an HTTP server, start the proxy before the tests and stop it after:

mcp-trace-capture -o 'traces/http-{session}.jsonl' http --upstream http://127.0.0.1:3000 \
  2> proxy.log &
proxy=$!
# Wait until the proxy is listening; stop if it exited instead (proxy.log says why).
until grep -q 'listening on' proxy.log; do
  kill -0 "$proxy" 2> /dev/null || { cat proxy.log; exit 1; }
  sleep 0.2
done
npm test          # with the client pointed at http://127.0.0.1:8080/mcp
kill -INT "$proxy"
wait "$proxy"

The proxy prints its listening on line only once its port is bound, so waiting for it — rather than probing the port with a request, which the proxy would forward and record — keeps the first test from racing the proxy’s start.

{session} gives each test session its own numbered trace: each launch of the stdio wrapper, and each client session through the proxy (the traces/ directory must exist). The repository’s interop/ directory does exactly this with the official TypeScript and Python SDKs, and its run.sh is a working example of both transports; its matrix.sh records each official SDK’s own conformance server through the proxy (Recording an SDK’s server).

GitHub Actions

The repository is itself an action. After the step that records the traces:

- uses: tomtom215/mcp-conformance@<commit or tag>
  with:
    traces: traces/*.jsonl
- uses: github/codeql-action/upload-sarif@<sha>
  if: always()
  with:
    sarif_file: mcp-conformance.sarif
    category: mcp-conformance

The action builds mcp-trace-validator from the ref you name (or, with version: 0.6.0, downloads that release’s prebuilt binary and checks its checksum), prints the findings to the log and the job summary, writes mcp-conformance.sarif and mcp-conformance-junit.xml, and fails the step when a clause fails. Uploading the SARIF log needs permissions: security-events: write on the job; findings then appear in the repository’s code scanning view, each on the trace line that holds the event. The inputs — revision, strict, fail-on-findings, the output paths — are listed in action.yml.

Recording with the action

The action can also take the recording: set capture, give it the server and the client command that drives it, and it runs the session through mcp-trace-capture, then judges every session recorded (with any traces you name too). This is not yet released: it is on main, so pin a commit for it, and it needs the action’s own source build (version empty) until a release carries it.

# stdio: the client launches $MCP_SERVER_COMMAND in place of the server.
- uses: tomtom215/mcp-conformance@<commit>
  with:
    capture: stdio
    server: python my_server.py
    client: pytest tests/mcp -k e2e   # its fixture launches $MCP_SERVER_COMMAND

# Streamable HTTP: the client talks to $MCP_PROXY_URL, which reaches `upstream`.
- uses: tomtom215/mcp-conformance@<commit>
  with:
    capture: http
    server: node dist/server.js --port 3000   # optional: started, waited for, stopped
    upstream: http://127.0.0.1:3000/mcp
    client: npm run e2e -- --url "$MCP_PROXY_URL"
InputMeaning
capturestdio or http; empty (the default) records nothing
serverA bash command line, run from the workspace. stdio: the server to record (required). http: optional — started in the background, waited for until upstream accepts connections (60 s), and stopped after the client
upstreamhttp: the server’s URL; the recording proxy forwards to it, and MCP_PROXY_URL reaches it (a path after the proxy’s address is appended to its path)
clientA bash command line, run from the workspace, that drives the sessions. It gets MCP_SERVER_COMMAND (stdio: a single executable path that runs server under the recorder) or MCP_PROXY_URL (http)
capture-dirWhere each session is written as NNN.jsonl; default mcp-conformance-capture. It must not exist yet, or be empty

Every launch of MCP_SERVER_COMMAND, and every session through the proxy, is its own trace. The stdio wrapper carries the server’s command line itself rather than reading the environment, because clients commonly launch stdio servers with a reduced one; the server runs with whatever environment the client gives it. A client that exits non-zero fails the action — after the sessions it did record are judged and reported — and its status is the client-exit-code output. CI exercises capture on Linux runners (both transports, driving the repository’s everything server with its reference host); the stdio wrapper is a bash script, which a Windows client cannot launch as a command.

Container image

From the release that follows 0.6.0, each release also publishes both CLIs as ghcr.io/tomtom215/mcp-conformance:<version> (linux/amd64), built from the repository’s Dockerfile and attested like the binaries:

docker run --rm -v "$PWD:/work" ghcr.io/tomtom215/mcp-conformance:<version> \
  mcp-trace-validator validate --format junit traces/*.jsonl > mcp-conformance.xml
gh attestation verify oci://ghcr.io/tomtom215/mcp-conformance:<version> \
  --repo tomtom215/mcp-conformance

The image is distroless: no shell and no package manager, running as UID 65532, with /work as its working directory — mount the directory holding your traces there (and make it writable by that user to record into it). With no shell, mcp-trace-capture stdio can only launch a server that is in the image, so to record a stdio server in a container, build FROM this image and add yours; mcp-trace-capture http needs nothing more.

Any other CI

mcp-trace-validator validate traces/*.jsonl                       # findings in the log
mcp-trace-validator validate --format junit traces/*.jsonl > mcp-conformance.xml
mcp-trace-validator validate --format sarif traces/*.jsonl > mcp-conformance.sarif

Each invocation exits with the same status, so run the human report as the gate and the other two for the reports your CI ingests (GitLab, Jenkins and most others read JUnit). Several traces give one document: a JUnit suite per trace and revision, one SARIF run with each result in its own trace file.

Choosing what fails the build

You wantUse
Fail on any broken MUSTthe default
Fail on SHOULDs too--strict (JUnit and SARIF then report warnings as failures and errors)
Judge against a fixed revision, whatever the traces declare--revision 2026-07-28
Never fail, only reportfail-on-findings: false in the action, or ignore the exit status

A trace that cannot be judged — empty, malformed, or of a revision this build does not know — exits 2 or 3, never 0: a broken recording step fails the build rather than passing it. fail-on-findings: false gives that up as well: the action’s step then succeeds whatever the exit status, 2 and 3 included, so a step that relies on it should read the action’s exit-code output and fail on 2 and 3 itself. When the validator cannot start at all — a traces pattern matching nothing, or a lone trace it cannot read — the action still writes both reports, as a SARIF run marked executionSuccessful: false and a JUnit <error>, so an if: always() upload step finds a valid file.

Upgrading

Before 1.0, a minor release (0.6 → 0.7) may change what the tools decide, not only their Rust API: a check that learns to catch a defect fails a session that passed, and a check that stops failing conforming sessions passes one that failed. STABILITY.md says what counts as breaking. Every such change is in the release’s changelog entry, which since 0.5.0 opens with a table of each one and its migration (earlier entries state them in their opening paragraphs).

Before you upgrade

  1. Read the release’s table. Each row names the surface (a requirement ID, a report format, a flag), the change, and what to do: Unreleased (on main), 0.6.0, 0.5.0.

  2. Judge your recordings with both versions and compare by requirement ID, not by totals: rows are added and moved between checked and excluded between releases, so the totals move even when nothing about your session’s conformance did.

    mcp-trace-validator validate --format json session.jsonl \
      | jq -r '.requirements[] | select(.outcome == "fail" or .outcome == "warn") | .id' \
      | sort > findings-new.txt
    

    The same command with the old binary gives findings-old.txt; diff the two. A row in one and not the other is a verdict change the table explains. The report’s wire_schema member (on main since 0.6.0) is judged beside the rows; compare its outcome too.

  3. Expect SARIF alerts to move once where the table says a fingerprint changed; an alert whose finding is unchanged keeps its fingerprint.

Pin the version you gate on

A CI gate should change its verdicts when you upgrade it, not when a release happens. Pin it:

  • cargo install mcp-trace-validator --version 0.6.0 --locked, or cargo binstall [email protected] for the attested prebuilt binary;
  • the GitHub Action at a tag or commit (uses: tomtom215/[email protected]), or with version: 0.6.0 to download that release’s prebuilt binaries rather than build from the action’s source;
  • the container image by version or, stricter, by digest (ghcr.io/tomtom215/mcp-conformance:X.Y.Z@sha256:…), for releases after 0.6.0;
  • mcp-trace-validator = "=0.6.0" for the library.

mcp-trace-validator --version names the build that judged a report; on main since 0.6.0 it also names the protocol revisions that build judges.

Library users

The Rust API follows the same release rule: a minor release may break it, the changelog’s table names every break, and cargo-semver-checks compares each release with its predecessor on crates.io before it is published. A deprecated item stays for at least one minor release, deprecated with its replacement named, before it is removed. Types that will grow — reports, findings, outcomes, options — are #[non_exhaustive], so a new field or variant is not a break: match them with a wildcard arm, and set options through their builder methods (HumanOptions::default().all(false)) rather than struct literals.

Troubleshooting

Each entry starts from what you see.

The validator

error: … judged no requirement at all — an empty or contentless trace is a capture that failed, not a session that conformed (exit 2). The trace has no messages: the client never connected, connected to the server directly instead of through the capture, or the capture was stopped before the session. Check the capture’s exit summary, which counts what it recorded.

error: … records 1 client message(s) and none from the server (exit 2). The client spoke and the server never answered: the capture recorded that it could not reach the server (transport-abort), or the client’s initialize went unanswered. The client’s side alone would pass, so the run is refused rather than reported green. Check that the server is running and reachable — for the HTTP proxy, that --upstream names it — and record again.

error: the trace declares protocol revision(s) 2025-06-18, which no available registry describes (exit 2). The session ran an older revision than this build has requirements for (2025-11-25 and 2026-07-28). Judging it against either would report that revision’s rules as violations, so it is refused. --revision 2025-11-25 judges it anyway, if the differences do not matter to you.

note: this trace records 2 sessions and is judged as one. Two clients, or one client twice, were recorded into one file — a proxy left running across test runs, or a client that reconnected. Findings that span them (a request id the second session reuses, a session id that changed) come from recording them together. Put {session} in the capture’s -o (-o traces/{session}.jsonl) to get a trace per session.

note: the trace declares no protocol revision; judging it against 2026-07-28. The recording holds no initialize, no _meta protocol version and no MCP-Protocol-Version header — often a trace that starts mid-session. Pass --revision to say which rules apply.

error: malformed trace: … (exit 3). The file is not a trace as written by a recorder. Given several traces, the error starts with the trace’s name (error: traces/b.jsonl: malformed trace: …), the report lists it as not judged, and the overall verdict reads incomplete:

The error saysCause and fix
the document begins with a UTF-8 byte-order markAn editor or Windows tool added a BOM. Strip the first three bytes.
the file is UTF-16 (a hint after not valid UTF-8)Windows PowerShell’s > writes UTF-16. Re-encode: Get-Content in.jsonl | Set-Content -Encoding utf8 out.jsonl.
the document is a single JSON value, not JSON LinesA pretty-printed file or a JSON array. The message names the jq command that converts it.
blank lineJSON Lines has no blank records; remove the empty line. A hint says when it is a trailing blank line at the end of the file — one final newline is fine, an empty line after it is not.
missing field `seq` (a hint: this is a bare JSON-RPC message)The file holds JSON-RPC messages, not trace events: a trace wraps each message in an event with seq, direction, transport and kind. Record the session with mcp-trace-capture.
record is N bytes, exceeding the …-byte limitA message larger than the default limit. Re-run with the --max-line-bytes value the hint gives.
seq N is not greater than the previous event's seqTwo recordings concatenated, or a hand-edited file. Each trace’s seq must increase.

The capture tool

… exists; appending would break the trace's sequence numbers (exit 2). The output file is there from an earlier run. Put {session} in the file name for a new numbered trace each run — the usual choice in a client’s configuration, which relaunches servers — or use --force to overwrite.

The server works directly but not through the HTTP proxy, with 403. The server checks the Host header against the names it accepts. For a server on this machine (localhost, 127.0.0.0/8, ::1) the proxy passes the client’s Host through, so the server judges what the client sent, localhost:8080 included: a server that accepts only its own host:port refuses it. --no-preserve-host sends the server its own name instead, which makes its Host check pass for every request the proxy forwards, a browser’s under a rebound name included, so use it for the recording and not as a default. For a server on another machine the proxy sends the upstream’s own name, as a virtually hosted server needs, and --preserve-host passes the client’s instead. mcp-trace-capture’s README explains the trade-off, including what the default rewrite does to a server’s DNS-rebinding check.

TRAN-004 (or TRAN-117) fails on a line like "Server starting...". The server writes something other than protocol messages to stdout — log lines, a banner. The capture records each such line as it was written, and the validator judges it: it is a stdio transport violation, and the same line fails BASE-008 at 2025-11-25 (BASE-042 at 2026-07-28): every message must be JSON-RPC. Send logs to stderr.

A client configuration cannot find mcp-trace-capture. Desktop applications often start without your shell’s PATH. Use the absolute path to the binary as the command, for example the output of which mcp-trace-capture.

Still stuck

Open an issue with the command, the full output, and — if you can share it — the trace. Traces record message content in full; read one before attaching it.

Architecture

This chapter orients you in the design. The authoritative treatment — ten decisions, each with its cost — is the standalone design note, written for an upstream audience:

📄 Design note: trace-validation architecture and trade-offs

The internal workspace layout and dependency rules live in 02-architecture.md.

The foundational split: capture, then judge

The validator judges a recorded trace, not a live session. Its engine is a pure function with no network, clock, or I/O. Whoever owns the socket — the server’s session tap, the host’s capture wrapper, or mcp-trace-capture for any other implementation — produces the trace, assigning the total-order seq at capture. The judge only judges.

This buys determinism, replayability, SDK-agnostic input, and auditable verdicts. It costs one hard thing: capture fidelity becomes a first-class engineering problem — interleaving, partial writes, and SSE framing all have to be recorded faithfully by the component that holds the bytes.

The decisions that follow, in brief

  • The judge never links the SDK it judges. mcp-trace-validator does not depend on rmcp. A validator built on the SDK under test would inherit that SDK’s interpretation of the spec as ground truth — exactly the thing being examined. The cost is re-expressing the protocol as our own data.
  • The spec as data. Every normative clause becomes a registry record with its RFC 2119 level, actor, a verbatim source quote, an optional capability gate, and either one or more mechanical checks or a written reason it cannot be judged from a trace. There is no “not yet looked at” state. A scheduled job re-verifies every quote against the live spec text so the registry cannot silently drift.
  • Determinism by canonicalization. All JSON is canonicalized (RFC 8785) before any comparison, so a report is byte-identical across platforms and runs. Correctness here is a fixpoint property, tested as one — a real bug hid until a fuzzer first generated -0.0.
  • A pass has to be earned. A requirement gated on a capability that was never negotiated is reported as not-applicable, and one whose subject matter the session never carried is reported not-observed — never as a vacuous pass. Every check counts the subjects it considered, so the report can tell “complied with” from “never came up”. Inflating a pass rate with checks that had nothing to look at is how a conformance tool loses credibility.
  • Calibration against the authority. The agreement check (see the Introduction) diffs our verdicts against the official runner’s on every CI run; an unexplained divergence fails the build, and a stale recorded divergence fails it too.
  • Falsifiability. Every check is killed by at least one committed injected-violation trace; a check with no killer trace is unfalsifiable. The corpus is both the regression suite and the evidence each check does what it claims.
  • Verdicts are a contract. A change that makes a previously-passing trace fail is a breaking change even when no function signature moved; it is named in the changelog and the version is bumped accordingly. API-signature compatibility is additionally checked mechanically against the published crates.io baseline.

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 the transport’s moments: transport-open (it became usable), transport-close (it closed in an orderly way) and transport-abort (it failed or was torn down abnormally — an unreachable upstream, a response cut off, a forced stop).

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)
        [client] 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, 246 excluded, 0 unsupported, 6 not applicable, 78 not observed
wire schema: pass (4 messages valid)
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.

Two revisions at once

MCP is a dated specification. 2026-07-28 is the current revision, and 2025-11-25 stays in wide use while implementations migrate. 2026-07-28 removes the initialize handshake outright (SEP-2575), makes every request carry its own _meta envelope, and adds caching hints, multi-round-trip requests, subscriptions, and a discovery probe. That is not a version bump anyone should take on faith.

So this toolkit judges both, and can judge one trace against both in a single pass. A migration stops being an inventory of changes somebody read in a changelog and becomes a measurement.

What “both” means here

2025-11-252026-07-28
Registry entries348501
Judged by a named check102181
Carrying a documented exclusion246320
Shipped in every buildyesyes

The two registries are extracted per revision rather than sharing entries: each clause is quoted from its own revision’s published text, and a clause restated with different words gets its own ID. 2025-11-25’s BASE-003 forbids reusing a request ID within a session; 2026-07-28’s BASE-045 forbids reusing one that is still in flight, so reuse after a response is now legal. Those are two clauses, not one clause with a footnote — and pointing the new one at the old one’s check would have reported conforming traces as violations.

The cost of that method is that no clause is in force at both revisions, so a side-by-side report never shows one clause passing at one revision and failing at the other. What it shows instead is presence, which is what a migration actually consists of.

A trace judged against both

This is a conforming 2025-11-25 handshake — the client offers a version, the server answers, the client confirms:

{"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":"corpus-client","version":"0.1.0"}}}}
{"seq":1,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{},"serverInfo":{"name":"corpus-server","version":"0.1.0"}}}}
{"seq":2,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","method":"notifications/initialized"}}

Judged against both revisions at once:

$ mcp-trace-validator validate --revision 2025-11-25 --revision 2026-07-28 --all handshake.jsonl

(--all lists every clause; without it the report lists only the failing, warning and unsupported ones.) Four rows out of the report say the whole thing:

  LIFE-001   (MUST)  2025-11-25=pass  2026-07-28=absent  *differs
  DISC-001   (MUST)  2025-11-25=absent  2026-07-28=not-observed  *differs
  BASE-030   (MUST)  2025-11-25=absent  2026-07-28=fail  *differs
  BASE-045   (MUST NOT)  2025-11-25=absent  2026-07-28=pass  *differs
  • LIFE-001 — “the client MUST initiate the lifecycle” — is a clause the newer revision removes. It reads pass then absent.
  • DISC-001 — the server/discover probe that replaces the handshake — is a clause the newer revision adds, and this session carried nothing it binds to, so it reads not-observed rather than counting as a pass.
  • BASE-030 — every request must carry an io.modelcontextprotocol/clientCapabilities _meta member — is a clause the newer revision adds that this session breaks. It reads fail, and it is the migration work this trace has actually discovered.
  • BASE-045 — the narrowed request-ID rule — reads pass, because reusing an ID after its response is exactly what the new text permits.

And the verdict splits:

per revision:
  2025-11-25: 17 pass, 0 fail, 0 warn, 246 excluded, 0 unsupported, 14 not applicable, 71 not observed — verdict pass
  2025-11-25: wire schema pass (3 messages valid)
  2026-07-28: 15 pass, 8 fail, 0 warn, 320 excluded, 0 unsupported, 0 not applicable, 158 not observed — verdict fail
  2026-07-28: wire schema fail (1 of 3 messages invalid)
overall verdict: fail

A conforming session today, and eight concrete failures tomorrow — nine, with the 2026-07-28 schema’s own finding: the initialize result carries no resultType, which every result at that revision must. Both summary lines account for every clause in their revision — the counts add up to 348 and 501 — because a line that quietly omits an outcome is a line that overstates what was measured.

The example above is executed by a test (book_examples.rs) against the real validator on every cargo test, so this page cannot drift from what the tool actually prints.

Which revision a trace is judged against

A trace is not silent about which revision it belongs to — the handshake states it, a stateless session states it in every request’s _meta, and the HTTP transport states it in a header. So by default the validator judges a trace against the revision it declares, and says so in the report’s first line: revision 2026-07-28 (declared by the trace). Where there was a handshake, the server’s initialize result is the authority: the session ran under the revision it names — that is what negotiation means — so it is judged under that one alone, and a note on stderr says when the client had proposed another (--revision judges the proposal instead). A negotiated revision this build cannot judge is refused (exit 2) rather than approximated by the proposal, and so is a trace declaring only revisions this build cannot judge. A trace declaring nothing is judged against the newest supported revision, with a note on stderr.

Judging against the wrong rules is easy to do on purpose, with --revision. Force 2025-11-25 onto a 2026-07-28 recording and every clause the two revisions disagree about becomes a finding: the session opens with tools/list rather than initialize, so LIFE-001 fails; it reuses request ids after their responses, so BASE-003 fails. Both are correct answers to the question that was asked, and neither is a defect in the implementation. This session says 2026-07-28 twice over:

{"seq":0,"direction":"client-to-server","transport":"streamable-http","kind":"http","method":"POST","headers":{"accept":"application/json, text/event-stream","mcp-protocol-version":"2026-07-28"}}
{"seq":1,"direction":"client-to-server","transport":"streamable-http","kind":"message","payload":{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}}
{"seq":2,"direction":"server-to-client","transport":"streamable-http","kind":"message","payload":{"jsonrpc":"2.0","id":1,"result":{"tools":[],"resultType":"complete"}}}

so validating it with --revision 2025-11-25 says so, above the rows and again under the verdict:

  NOTE  this session declares protocol revision 2026-07-28, not 2025-11-25.
        Every outcome here judges it against rules it was not playing by;
        re-run with `--revision 2026-07-28` to judge it against its own.

It is deliberately quiet. A session that proposed one revision and negotiated another is judged under the negotiated one by default, but the client did state the proposal, so asking for that revision with --revision draws no note; a version a server refused states nothing, because the session never ran under it; and a recording that declares no revision at all gets no note, since there is nothing to disagree with.

The four ways a clause is not a pass

A side-by-side report is where these stop being pedantry, because all four appear at once and they mean different things:

ReadsMeans
absentThe clause does not exist at that revision.
excludedIt exists, but no recorded trace can judge it — the reason is stated per entry.
not-applicableIt exists and is judgeable, but it is gated on a capability this session never negotiated.
not-observedIt exists, is judgeable, is not gated — and the session simply carried nothing it binds to.

None of them is a pass, and the report never quietly promotes one. That rule is the whole reason the numbers here are smaller than a conformance tool’s numbers usually are.

What is measured at 2026-07-28 today

  • The reference server serves the stateless surface with --protocol-version 2026-07-28, and the official suite’s 2026-07-28 scenarios score 42 passing / 0 failing against it.
  • Eight committed captures — a conforming session over each transport, a probe session of deliberately malformed requests, the official runner’s two, and three of the official SDKs’ own conformance servers (the cross-SDK matrix) — evidence 148 of the 181 judgeable clauses between them. What no capture reaches is named, one clause at a time, in the corpus chapter.
  • The project’s CI re-runs that measurement weekly and ratchets it against a committed baseline: any change in either direction fails the build, so a change in the score is recorded in the commit that caused it rather than estimated.

That ratchet has already paid for itself in the direction nobody plans for. Through suite 0.2.0-alpha.9 the runner scored the reference server identically whichever revision it served — its scenarios exercise features, not revisions, so it could not tell the two apart. This project’s registry could, and flagged the 2025-11-25 server for returning cacheable results without the cache hints 2026-07-28 requires (CACH-001). Six weeks later alpha.11 added a schema check and found the same clause, independently, from the other side. The server’s SDK then fixed it: since rmcp 3.5.1 both legs score 42 passing / 0 failing.

The next revision

The specification’s unreleased next revision (draft) is watched, never judged. No build can validate against it — --revision draft is refused like any revision the build does not ship — but the project reads it weekly and records what it has changed against 2026-07-28: pages added or dropped, keyword counts that moved, and each 2026-07-28 clause whose sentence the draft rewords or removes. So far that is one wording edit: the draft bounds a header-mirrored integer “for integers represented using IEEE754 double-precision floating point numbers” where the released transport page says “for JavaScript”. The interval is the same, so the 2026-07-28 verdict is the same. When the draft ships as a dated revision, its registry starts from that record rather than from scratch.

Full detail is in the corpus chapter, the ecosystem register and the conformance strategy.

Trace corpus

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

  • good/ — sessions that must validate with verdict pass, at the revision each declares: validate with no flags passes all of them but one, the session whose server answers initialize with 2024-11-05 (stdio-server-negotiates-2024-11-05), which validate refuses by default (exit 2) and passes with --revision 2025-11-25. The CI jobs that run the shipped Action and image over this directory name that exception.
  • 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.
  • 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.
  • captured/ — the 2025-11-25 counterpart: the official SDKs’ conformance servers recorded by interop/matrix.sh, judged against that revision’s registry, likewise pinned without an asserted verdict. Their 2026-07-28 sessions sit in draft/captured/ beside the others. See The official SDKs’ conformance servers 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 so that where a golden lives says which revision’s registry produced it. Trace names would not collide without the split: a requirement ID names one clause in one revision and is never reused (STABILITY.md), so base-045-… can only be a 2026-07-28 trace — BASE-045 is not a 2025-11-25 requirement. A sentence both revisions carry has an ID in each.

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
Goldens125238
Excluded rows the ledger holds246320
Distinct not-observed sets across them86151

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. The one other prefix is wire-schema-<slug>.jsonl: such a trace must fail the report’s wire schema — the messages against the revision’s own JSON Schema, which no requirement row carries (ADR-0020).

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-client-features.jsonlEvery client-feature page conformant in one session (added 2026-10-13, review item 2.5): a client declaring elicitation in both modes, sampling.tools and roots; a form elicitation accepted with content its requestedSchema admits; a URL mode elicitation carrying an elicitationId and an RFC 3986 URL, accepted, then completed by notifications/elicitation/complete naming that id; a two-round sampling tool loop — toolChoice required answered with a tool use, then the tool result sent back under toolChoice none and answered with text; roots/list answered with a file:// root; and a -32042 listing one URL mode elicitation, completed the same way
stdio-full-session.jsonlHandshake plus ping, tools/list, tools/call over stdio
stdio-minimal-init.jsonlSmallest conformant session: the three-message handshake
stdio-request-unanswered-at-close.jsonlA handshake, then a tools/list the server never answers before the transport closes (added 2026-10-24). No clause judges it — JSON-RPC expects every request answered, but BASE-012 is excluded — so the verdict is pass; what the golden pins is the report’s unanswered_requests note: one request, and that the trace ended with a transport-close. Until 2026-10-24 the report said nothing at all.
stdio-requests-partly-unanswered.jsonlThe same handshake, then three requests of which the server answers one (tools/list) and leaves two (ping, tools/call) pending when the recording simply stops, with no close or abort (added 2026-10-24). The note counts two and says the trace stopped; the verdict is pass.
stdio-server-negotiates-2024-11-05.jsonlA 2025-11-25 client whose server answers initialize with protocolVersion 2024-11-05, a revision this build has no registry for; the client goes on under it (notifications/initialized, tools/list) and closes (added 2026-10-24). The session ran under what the server named — that is what negotiation means — so validate refuses to judge it by default (exit 2, naming both revisions) rather than judging it under the proposal, which until 2026-10-24 it did silently, as a pass. This golden is that judgment asked for explicitly: --revision 2025-11-25 judges the session under the proposal, and it passes, because nothing in it breaks a clause a 2025-11-25 registry can see.
stdio-tool-image-base64-padding.jsonlTwo tools/call results carrying image and audio content whose data is well-formed base64 at every padding length — a PNG ending ==, a WAV and a GIF ending =, a JPEG fragment with none — and one image whose data is the empty string, which encodes nothing and is well-formed under RFC 4648 §4 (the format: byte rule the schema marks data and blob with accepts it, as ajv-formats does; added 2026-10-24). The wire schema passes all five and no clause judges a tool result’s encoding, so the verdict is pass; the trace exists so that asserting byte cannot drift into rejecting padding or emptiness.
stdio-progress.jsonlProgress done right over stdio, in both directions (added 2026-10-24, when basic/utilities/progress entered the registry): rising values (0.25, 0.5, 1) for one token, ending before the result; the token reused by a later request once the first was answered, its sequence starting again below where the last one ended; an integer token running concurrently with it; and a server roots/list opting into progress under a token of its own, which the client reports on before answering. The pass path of PROG-009/010/011/013/019
stdio-task-progress.jsonlA task-augmented tools/call under a server declaring tasks.requests.tools.call (added 2026-10-24): answered with a CreateTaskResult, then progress under the request’s token tagged io.modelcontextprotocol/related-task, both before and after a tasks/get that still says working; a second, ordinary call cancelled by notification while the task exists — CANC-020’s pass path, a cancellation that names no task’s request — and left unanswered by a server that answers the later tasks/get, so it has read the cancellation (CANC-021’s pass path); then tasks/get reporting completed, tasks/result, and no progress after. The pass path of PROG-014/015/016
stdio-cancellation.jsonlCancellation done right over stdio, in both directions (added 2026-10-24, when basic/utilities/cancellation entered the registry): a tools/call cancelled by the client and left unanswered by a server that answers a later tools/list, so demonstrably read it; a second call whose answer crosses its cancellation on the wire — the race the page’s Timing Considerations name, which nothing convicts; and a server roots/list the server withdraws by notifications/cancelled, then a server ping the client answers {} while leaving the withdrawn request unanswered. The pass path of CANC-018/019/021 and PING-001 from the client’s side
http-origin-refused-without-id.jsonlA Streamable HTTP initialize carrying Host/Origin evil.example.com, refused 403 with a JSON-RPC error that has no id, then the same handshake from localhost completing. The transport’s security warning permits exactly that body (“If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden. The HTTP response body MAY comprise a JSON-RPC error response that has no id”), so BASE-009 passes. Until 2026-10-13 the framing tied the error to the refused request, whose id was readable, and BASE-009 failed it — the shape of the official suite’s dns-rebinding-protection scenario against the everything server (conformance/wire-findings.json). Added 2026-10-13
http-authorization.jsonlA protected Streamable HTTP server, conformant (added 2026-10-13, review item 2.8): initialize refused 401 with a Bearer challenge carrying resource_metadata and scope (AUTH-014’s pass path), then sent again with the same id, as the official SDKs’ clients retry, and answered; a ping refused 403 insufficient_scope naming scope and resource_metadata (AUTH-049/050/051), retried and answered; another refused 401 invalid_token (AUTH-045/049) whose application/json body is the resource server’s own {"error":"invalid_token",…} object, retried and answered. That body is the HTTP layer’s, not a JSON-RPC message; read as one it failed BASE-006/008/009 and the wire schema. The retries are why a request refused at the HTTP layer is not delivered (context/refusal.rs): judged as two deliveries, each retry failed BASE-003 and the trace counted two sessions. No metadata fetch appears, because none would: see checks/auth.rs for what a recording carries for authorization

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 (and, since 2026-10-24, the wire schema’s byte format on the same message)
prom-004-audio-data-invalid.jsonlPROM-004 (and, since 2026-10-24, the wire schema’s byte format on the same message)
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 (and, since 2026-10-24, the wire schema’s byte format on the same message)
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-018-declined-version-header-refused-400.jsonlTRAN-018, a warning: the client asks initialize for 2025-06-18, the server answers 2025-11-25, and the client’s next request carries MCP-Protocol-Version: 2025-06-18 anyway. The server answers it 400 Bad Request — TRAN-020’s pass path: the server declined that version at initialize, so by its own account the header names an unsupported one. Added 2026-10-13
tran-020-declined-version-header-accepted.jsonlTRAN-020 (the same session, the request served 200 rather than refused 400; TRAN-018 warns for the client’s header, as above). Excluded until 2026-10-13 as undecidable — the server’s supported set is not in the trace — though the server’s own initialize answer states one version it does not support. Added 2026-10-13
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-029-error-without-content-type.jsonlTRAN-029 (a tools/list request answered 500 with a JSON-RPC error and no Content-Type). The clause names no status; until 2026-10-13 only success statuses were judged, and the calibration against mcpsnoop (book/src/mcpsnoop.md) found the gap. Added 2026-10-13
tran-040-get-answered-json.jsonlTRAN-040 (a standalone-stream GET answered 200 with application/json rather than text/event-stream or 405)
base-017-input-schema-invalid.jsonlBASE-017 (a tool inputSchema whose property declares "type": "text", not a JSON Schema type — invalid in 2020-12, the default dialect)
base-086-input-schema-draft-07.jsonlBASE-086, a warning (a tool inputSchema declaring $schema draft-07: valid in its dialect, so BASE-017 passes it, and permitted, but not the 2020-12 that “Implementors are RECOMMENDED to use”)
tool-011-structured-content-not-conforming.jsonlTOOL-011 (structuredContent of the right shape, an object, whose temperature is the string "warm" where the tool’s outputSchema says number — passed until the check could apply the schema)
wire-schema-resource-missing-uri.jsonlWire schema: a resources/list result whose resource has no uri, which Resource requires
wire-schema-log-level-not-in-enum.jsonlWire schema: a notifications/message at level chatty, outside LoggingLevel
wire-schema-content-block-unknown-type.jsonlWire schema: a tools/call result whose content block has type: "txt", matching no ContentBlock member
wire-schema-tool-image-data-not-base64.jsonlWire schema: a tools/call result whose image data is not base64!. ImageContent.data carries format: byte in the specification’s schema, which the validator asserts since 2026-10-24; no clause of this revision judges a tool result’s encoding (the prompt-content and resource-blob checks read their own members), so until then this message passed the wire schema and drew nothing at all. The official suite does not catch it either: it loads ajv-formats and then registers byte as always valid (addFormat("byte", true)).
elic-004-answered-without-capability.jsonlELIC-004: a client declaring only sampling answers a form elicitation with a result. Also LIFE-009 and ELIC-006 for the server’s request — a client can only show support it never declared by answering something it should not have been sent
elic-006-url-mode-undeclared.jsonlELIC-006: a URL mode elicitation sent to a client declaring elicitation: {}, which is form mode only (LIFE-009 reads the same sub-capability). The client refuses with -32602 — ELIC-021’s pass path
elic-007-request-without-message.jsonlELIC-007: a form elicitation with no message. The wire schema rejects the same request
elic-009-form-without-schema.jsonlELIC-009: a form elicitation with no requestedSchema; the wire schema too
elic-011-url-without-elicitation-id.jsonlELIC-011: a URL mode elicitation with no elicitationId, which this revision’s table requires; the wire schema too
elic-012-url-without-scheme.jsonlELIC-012: a URL mode url of mcp.example.com/connect?…, with no scheme. The schema marks url format: uri, which ADR-0020 leaves unasserted, so the clause is the only thing that catches it
elic-014-complete-names-unknown-id.jsonlELIC-014: a completion notification naming e-99 after the session’s only URL elicitation established e-42
elic-018-url-required-error-without-list.jsonlELIC-018: a -32042 whose data lists no elicitations; the wire schema too
elic-019-url-required-error-form-mode.jsonlELIC-019: a -32042 listing a form mode elicitation; the wire schema too
elic-021-undeclared-mode-accepted.jsonlELIC-021: a client declaring form mode only accepts a URL mode request instead of refusing it with -32602. ELIC-006 and LIFE-009 for the server’s request, the clause’s antecedent
elic-049-accepted-content-violates-schema.jsonlELIC-049, a warning: accept with {"city": 75001} against a schema typing city as a string
samp-004-tools-without-sampling-tools.jsonlSAMP-004: tools sent to a client declaring plain sampling (LIFE-009 reads the same sub-capability)
samp-005-answered-without-capability.jsonlSAMP-005: a client declaring only elicitation answers a sampling request with a result. LIFE-009 for the server’s request, the antecedent
samp-006-include-context-deprecated.jsonlSAMP-006, a warning: includeContext thisServer to a client that declares sampling.context — SAMP-007’s pass path, since the declaration permits what SAMP-006 still asks the server to avoid
samp-007-include-context-undeclared.jsonlSAMP-007, a warning: includeContext allServers without sampling.context. SAMP-006 warns too: any use of the value is one it should have avoided
samp-008-tool-results-mixed.jsonlSAMP-008: a user message mixing text with a tool result. Also SAMP-009 — the message after the tool use is not one of tool results only — and SAMP-019, which restates both. The client refuses with -32602, SAMP-012’s pass path
samp-009-tool-use-unanswered.jsonlSAMP-009: a tool use followed by a plain text message (SAMP-019 restates it); the client refuses with -32602
samp-010-required-tool-choice-ignored.jsonlSAMP-010: toolChoice required answered with text and no tool use
samp-011-none-tool-choice-ignored.jsonlSAMP-011: toolChoice none answered with a tool use
samp-012-malformed-tool-messages-answered.jsonlSAMP-012, a warning: the client answers, with a result, a request whose last message is an unanswered tool use. SAMP-009 and SAMP-019 for the request, the antecedent
root-001-answered-without-capability.jsonlROOT-001: a client declaring only sampling answers roots/list with a root. LIFE-009 and ROOT-009 for the server’s request (the antecedent), and ROOT-004 warns: having declared no roots, the client should have refused with -32601
root-003-root-not-file-uri.jsonlROOT-003: a root listed as https://example.com/repos/trips
root-004-unsupported-answered-internal-error.jsonlROOT-004, a warning: a client without roots refuses roots/list with -32603 rather than -32601. LIFE-009 and ROOT-009 for the request, the antecedent
root-009-asked-without-capability.jsonlROOT-009, a warning: roots/list sent to a client that did not declare roots (LIFE-009 states the MUST behind it). The client refuses with -32601 — ROOT-004’s pass path
auth-014-challenge-without-scope.jsonlAUTH-014, a warning: the 401 challenge carries resource_metadata but no scope
auth-045-invalid-token-answered-403.jsonlAUTH-045: a challenge declaring error="invalid_token" sent with 403. AUTH-049 fails too: the error-handling table gives the same error the same status
auth-049-insufficient-scope-answered-401.jsonlAUTH-049: error="insufficient_scope" sent with 401 rather than the table’s 403. AUTH-050 warns too: its list begins with the 403
auth-050-insufficient-scope-without-resource-metadata.jsonlAUTH-050, a warning: a 403 insufficient_scope challenge naming its scopes but no resource_metadata
auth-051-insufficient-scope-names-no-scope.jsonlAUTH-051, a warning: a 403 insufficient_scope challenge with no scope. AUTH-050 warns too: scope is one of the parameters its list requires
ping-001-ping-answered-with-content.jsonlPING-001: a ping answered with {"pong":true,"uptime":4212} rather than an empty result. Added 2026-10-24, when basic/utilities/{ping,progress,cancellation} entered the registry; the rows below to canc-021 came with it
prog-009-progress-token-float.jsonlPROG-009: a request opting into progress with progressToken: 1.5, a number but not an integer. The wire schema fails the request and the notification that echoes the token too
prog-010-progress-token-reused-in-flight.jsonlPROG-010: two tools/call requests in flight at once under the same progress token, so a notification could not say which it reports on. Reuse after an answer is legal, and good/stdio-progress.jsonl does it
prog-011-progress-value-decreases.jsonlPROG-011: progress 50 then 10 for one token
prog-013-progress-token-never-issued.jsonlPROG-013: a progress notification for the token "ghost", which no request in the session provided — the session begins with initialize in the recording, so every request it issued is there, and none carried it. The shape the dogfood of 2026-10-24 found judged clean at this revision
prog-019-progress-after-completion.jsonlPROG-019: a progress notification for a stdio request after its result (PROG-013 — the token no longer names an in-progress operation — and PROG-016 share the check)
prog-015-task-progress-token-differs.jsonlPROG-015: a task created for a request carrying progressToken: "deploy-1" reports progress tagged with the task’s id under "task-1-progress" (PROG-014 states the same rule and shares the check). PROG-013 fails too: that token was never provided by any request
prog-016-task-progress-after-terminal-status.jsonlPROG-016: progress for a task after a tasks/get reported it completed (PROG-013 and PROG-019 share the check: the task is the request’s operation, and it is over)
canc-018-cancellation-of-unknown-request.jsonlCANC-018: the client cancels request 99, which it never issued in a session whose initialize the recording holds. The shape the dogfood of 2026-10-24 found judged clean at this revision
canc-019-initialize-cancelled.jsonlCANC-019: the client cancels its own initialize request before the server answers it
canc-020-task-cancelled-by-notification.jsonlCANC-020: notifications/cancelled naming the tools/call a CreateTaskResult answered; a task is cancelled with tasks/cancel
canc-021-response-after-cancellation-read.jsonlCANC-021, a warning: the server answers a request the client cancelled by notification — after answering a tools/list the client sent later than the cancellation, which on stdio’s ordered stream proves it had read the cancellation. The answer that merely crosses a cancellation is the race good/stdio-cancellation.jsonl carries, and is not reported

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 on rmcp 3.5.1, 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, same build
Recorded bymcp-everything-server’s tap, during cargo xtask draft-readiness, 2026-10-13same run, second leg
Contents99 events / 24 POST exchanges99 events / 24 POST exchanges
Our verdict70 pass, 0 fail, 1 warn, 110 not observed, 320 excluded71 pass, 0 fail, 0 warn, 110 not observed, 320 excluded
The official runner’s verdict, that run42 passing / 0 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 two entries, which is the point of recording both: test_url_elicitation and test_list_changed are listed by the legacy server (18 tools) and not by the stateless one (16), because what each tool does has no place at 2026-07-28: that revision removed notifications/elicitation/complete and URL mode’s elicitationId (URL-mode elicitation itself remains), and moved list_changed notifications into subscriptions. Each listing is the first page; neither session follows nextCursor.

The one finding left: BASE-037, a warning, on the legacy leg. Of that server’s 24 results only the server/discover one carries io.modelcontextprotocol/serverInfo in its _meta; rmcp sets it there and on subscriptions/listen and nowhere else. The stateless server names itself on all 24, as it has since 2026-10-13. The legacy server is left without the field on purpose: it implements 2025-11-25, which names the server once, in initialize, and the measurement is that server held to a revision it does not implement (ServedRevision::names_itself_on_results). It is a SHOULD that exempts a server “specifically configured not to”, so it warns and never fails a run. The same warning stands on every other rmcp server that does not add the field itself (sdk-rust-2026-07-28); the Go and Python SDK servers set it on every result (sdk-go-2026-07-28, sdk-python-2026-07-28).

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 on 2026-08-20 so the method is present, on 2026-10-04 when the draft ratchet moved to alpha.12, and on 2026-10-13 (below).

Why the runner’s 23/23 and our 59-vs-60 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. Measured at alpha.11, the legacy leg scored 37 passing / 4 failing and the stateless leg 41 / 0, so the runner separated 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.

And then the implementation closed the gap. rmcp 3.5.1 fills in missing cache hints for any peer speaking 2026-07-28 (rust-sdk #1308), so since the workspace moved to it (2026-10-12) the legacy server’s results carry ttlMs and cacheScope too (ttlMs: 0 on resources/list, rmcp’s default rather than this server’s choice), and cargo xtask draft-readiness scores both legs 42 passing / 0 failing. The pair was re-recorded on 2026-10-13, so the legacy leg no longer fails CACH-001 or the wire schema; until then it was kept as recorded on 2026-10-04 (69 pass, CACH-001 failing; the runner 38 passing / 4 failing), and this section’s history above is the evidence for what each instrument caught and when. With the hints filled in, the runner again scores both servers alike, and only the registry’s BASE-037 warning tells them apart.

The 110 not-observed rows are the honest denominator: of the 181 clauses this revision’s registry can judge, these sessions carried subject matter for 71. They open no subscription, present no cursor, draw no JSON-RPC 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-10-13
Contentsserver/discover, a full subscriptions/listen lifecycle, an 18-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 five prompts, completion/complete, and one read of a URI the catalog does not contain. Every listing is followed to its last page: the server serves each in two, the suite’s catalogue first and the rest — icons, annotations, a resource_link result, an audio prompt, x-mcp-header designations — behind a nextCursor. 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 verdict109 pass, 0 fail, 0 warn, 72 not observed, 320 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 48 not-observed rows still are, and why. Thirty-three are Streamable HTTP mechanisms a stdio recording structurally cannot carry: twenty-five clauses of the TRAN-056…TRAN-102 band (it holds 27 judged clauses, and the two a stdio session does reach, TRAN-060 and TRAN-066, are judged here), the seven rows of the x-mcp-header behaviour table (TRAN-130…TRAN-136), and TOOL-034 — header mirroring is an HTTP mechanism, and “Clients using other transports (e.g., stdio) MAY ignore x-mcp-header annotations entirely”. 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. Two, CANC-003/CANC-011, bind a server that sends notifications/cancelled, and this one ends every subscription gracefully instead. 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 not driven by this host: TRAN-128 and DISC-002’s dual-era probe, MRTR-024, and VERS-004.

The client-feature pages added twelve more on 2026-10-13, on both legs (review item 2.5). The session’s one sampling round carries no tools, so nothing it sends exercises the tool-use clauses (SAMP-026, SAMP-030…SAMP-033, SAMP-042) or the deprecated includeContext permission (SAMP-029); it asks for no roots (ROOT-013, ROOT-014, ROOT-018), and none of its elicitations is in URL mode (ELIC-064, ELIC-065). The three form elicitations and the sampling round are judged. When they were first judged one of them warned: ELIC-095 (“Clients SHOULD validate all responses against the provided schema”) on the host, whose AcceptWithDefaults policy answered the probe form — username and email required, neither with a default — with accept and empty content. That was a finding against this project’s own client, on both legs, not a reading the text leaves open, and it was fixed the same day: the policy now fills each required field without a default with a deterministic value of its type ("scripted" for both of these plain strings), and declines a form whose required fields no value can satisfy rather than accepting it with content the schema rejects (mcp_reference_host::script::accepted_content states the rule). Both legs were re-recorded with the fix and pass it.

Five used to be on that list for want of server surface; four have left it. CACH-015/CACH-016 needed a catalogue served in pages and PROM-017 a prompt carrying audio, and both are on the listings’ second pages now; TOOL-033 needed an x-mcp-header designation, and judges it as listed, which any transport carries (TOOL-045 passes beside it). The fifth, TOOL-034, is in the HTTP group above: the designation exists now, and only the transport keeps it unobserved here.

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-10-13
Contents195 events — the session’s 103 messages plus 92 http events carrying status and headers, the Mcp-Param-Region/Mcp-Param-Limit pair rmcp mirrors for the designated tool among them
Our verdict125 pass, 0 fail, 0 warn, 56 not observed, 320 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 125 of the 181 judgeable clauses it is the best-covered capture here, and the fourteen it gained on 2026-10-13 are the server’s own doing: the second pages carry the pagination clauses (CACH-015/CACH-016), the audio prompt (PROM-017), and a tool designating three arguments for Mcp-Param-* mirroring, which rmcp’s client mirrors when the tool loop calls it — two supplied and mirrored, one left out and not, which is the header table’s “value provided” and “not in arguments” rows from both sides (TOOL-033, TOOL-034, TOOL-045, TRAN-079…TRAN-082, TRAN-130, TRAN-134, TRAN-135, TRAN-137). Its 33 not-observed rows are the fourteen rejection rules only the probe exercises, seven clauses judged on the stdio leg and not on this one (CANC-005, PROG-005, PROG-008, TRAN-117, TRAN-118, TRAN-123, TRAN-124), TOOL-022 (rmcp’s client caches tools/list under the server’s own ttlMs, so a second listing never reaches the wire), VERS-004, which only the Rust SDK’s capture evidences, and the ten clauses no capture reaches, each with its reason under What the captures evidence, together.

The probe session

Fieldprobe-2026-07-28-http.jsonl
Clientmcp-reference-host --probe: ten hand-built HTTP requests, each wrong on purpose, and the two conforming tools/list pages the last of them is built from
Servermcp-everything-server --transport http --protocol-version 2026-07-28
Recorded byThe server’s tap, during cargo xtask draft-capture, 2026-10-13
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; the removed initialize handshake; and — after listing the tools, following nextCursor — a call of a tool with a designated argument whose Mcp-Param-* header is left out
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, ten 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, TRAN-079/TRAN-130 because the last one leaves out the header whose absence TRAN-136 obliges the server to reject. 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. A third went the same way on 2026-10-13: BASE-037 entered the ledger as an open server-side warning the day the clause became judged, and its entry was retired in the change that put serverInfo on every result. Every rejection clause the probe reaches now passes; fourteen 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, TRAN-136, 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.

That gate was red for part of 2026-10-13, on two warnings, and is clean again. BASE-037 became judged that day, and this server — rmcp’s result types — named itself in _meta.io.modelcontextprotocol/serverInfo only on its server/discover and subscriptions/listen results; the client-feature pages added ELIC-095 against the host (above). The gate was left red rather than loosened until both were fixed here: the everything server now puts serverInfo on every result it builds at 2026-07-28 (mcp_everything_server’s private server::identity module, applied at each handler’s return beside the caching hints), and the host’s form answers satisfy their schema. Both reference-host captures and the probe were re-recorded with the fixes, and the probe baseline’s server-side BASE-037 entry was retired in the same change.

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-scenarios717001110
official-suite-2026-07-28-stateless717100110
probe-2026-07-28-http857311196
reference-host-2026-07-28-http1251250056
reference-host-2026-07-28-stdio1091090072
sdk-go-2026-07-2883784198
sdk-python-2026-07-28656500116
sdk-rust-2026-07-28747301107
sdk-typescript-v2-2026-07-28727200109
Union14833

Across all 9 captures, 148 of the 181 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 33 clauses no capture reaches: AUTH-097, AUTH-124, AUTH-129, AUTH-130, AUTH-131, AUTH-132, AUTH-133, CANC-003, CANC-011, DISC-002, ELIC-064, ELIC-065, MRTR-024, ROOT-013, ROOT-014, ROOT-018, SAMP-026, SAMP-029, SAMP-030, SAMP-031, SAMP-032, SAMP-033, SAMP-042, TOOL-035, TOOL-046, TRAN-061, TRAN-070, TRAN-083, TRAN-084, TRAN-096, TRAN-128, TRAN-132, TRAN-133.

The probe session closed the largest group — the rejection rules — and the cancellation round closed the next. The official SDKs’ conformance servers (below) closed six more without a line of this repository changing: each of the three that serve this revision lists a tool carrying an x-mcp-header designation, which TOOL-033, TOOL-045, TRAN-080, TRAN-082 and TRAN-137 judge, and the Rust SDK’s declares an extension, which VERS-004 judges.

The reference server’s own surface closed ten more on 2026-10-13. Every listing is now served in two pages — the suite’s catalogue first, then what the suite does not name — and both captures of the host follow nextCursor: the pages are CACH-015/CACH-016’s subject, and the second prompt page carries audio for PROM-017. A second-page tool designates three arguments for Mcp-Param-* mirroring, and the host’s tool loop calls it over HTTP with two supplied and one left out, so rmcp mirrors two headers and omits the third (TOOL-034, TRAN-079, TRAN-081, TRAN-130, TRAN-134, TRAN-135). The probe then sends the same call with one header missing, which the server must refuse and does (TRAN-136).

What is left — 33 clauses — is not backlog in the ordinary sense; each stays for a reason a richer session would not remove. Ten are the transport and lifecycle clauses below; the other 23 came with the client-feature and authorization pages and are grouped after them.

Two are out of an rmcp client’s reach: TRAN-132/TRAN-133, the behaviour table’s “Parameter value is null” row. A client can only send null for a designated argument whose schema admits it, and rmcp’s client drops any tool whose designated property’s type is not a single primitive — ["string", "null"] included — so a nullable designation never reaches an rmcp client’s call, and this host synthesizes only required arguments and never a null. The row’s pass path is authored (good/http-x-mcp-header-omitted.jsonl), and the rmcp restriction is a candidate to raise upstream.

One cannot be recorded by the server’s tap: TRAN-096, a recognized Mcp-Param-… header holding invalid characters. HTTP parsing strips a value’s edge whitespace before any handler sees it, and the tap records only values that decode as visible ASCII — rmcp likewise reads anything else as no header at all, and refuses the call as a missing one — so the antecedent never reaches the recording however the request is built. Authored traces carry both halves (draft/violations/tran-096-invalid-param-header-accepted.jsonl, draft/violations/tran-077-param-header-value-rejected.jsonl).

Three belong to a client no capture has: TRAN-061 needs a client to POST a notification over Streamable HTTP, and no 2026-07-28 capture contains one — rmcp’s stateless client cancels by closing the response stream, and neither the official runner nor the SDK client sends any. DISC-002 and TRAN-128 need a dual-era client that probes before falling back to initialize, and this host speaks only the stateless revision.

Two need server behaviour this one does not have: CANC-003/CANC-011 bind a server that sends notifications/cancelled, which at this revision may only tear down a subscriptions/listen stream. This server ends every subscription gracefully instead — the closure SUBS-005/SUBS-006 judge, and pass — so tearing one down by cancellation would be a second closure path, added to light up a check; the pass path is authored (good/subscription-server-teardown.jsonl).

One cannot be reached by a conforming host at all, and saying so is more useful than closing it. 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. It is evidenced by an authored violation trace instead, where the fault belongs. VERS-004 used to be its pair — a capture would have to declare an extension, and this host implements none, and declaring one only to light up a check is precisely the vacuous evidence the corpus exists to avoid. The Rust SDK’s conformance server declares one of its own accord, and that is evidence.

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.

Seven need a protected server: AUTH-097, AUTH-124 and AUTH-129–AUTH-133 read a WWW-Authenticate challenge, and no committed capture is of a server that requires authorization. Authored traces carry each (draft/violations/auth-*).

Four need a server that advertises a malformed x-mcp-header: TOOL-035, TOOL-046, TRAN-083 and TRAN-084 bind a client to reject such a tool and never call it. This server advertises only what it implements, and every designation it serves is valid, so no client of it has anything to reject. Authored traces carry both halves (tran-082-invalid-tool-excluded-valid-called, tran-083-invalid-tool-called).

Twelve need a client feature no captured session uses. No capture carries a URL-mode elicitation (ELIC-064, ELIC-065): the everything server’s only one is test_url_elicitation, which it does not offer at 2026-07-28, and the SDK servers send none. The one sampling request captured is plain (no tools, no includeContext), so tool-enabled sampling and its toolChoice and result-pairing rules (SAMP-026, SAMP-029–SAMP-033, SAMP-042) are unreached. No server in any capture sends roots/list, so ROOT-013, ROOT-014 and ROOT-018 are too. Each has an authored violation trace under draft/violations/.

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.

The official SDKs’ conformance servers (captured/, draft/captured/)

Nine recordings of the Tier 1 SDKs’ own conformance servers (the TypeScript SDK’s two lines are two servers) — the server its maintainers run the official suite against — driven over Streamable HTTP by an official SDK client. Neither end is ours: this repository supplies only the recorder and the judge. They are the evidence behind the book’s cross-SDK matrix, which cargo xtask sdk-matrix generates from their goldens; the verdicts and counts live there, once, and are not restated here.

FieldValue
ServersTypeScript v1: test/conformance/src/everythingServer.ts at SDK tag 1.32.1, its ../../../src/ imports rewritten to the published @modelcontextprotocol/sdk 1.32.1 and nothing else. TypeScript v2: the same path at tag v2.3.1, unmodified, on the published @modelcontextprotocol/server 2.3.1 with node 2.1.1 and express 2.0.2 (one process serves both revisions: the initialize handshake for 2025-11-25, server/discover for 2026-07-28). Python: examples/servers/everything-server at tag v2.3.0, on mcp 2.3.0. Go: conformance/everything-server from the go-sdk v1.8.0 module, stateful at 2025-11-25 and stateless at 2026-07-28, as the Go SDK’s own conformance workflow runs it. Rust: conformance/src/bin/server.rs at tag rmcp-v3.7.0, compiled against rmcp 3.7.0 from crates.io with upstream’s feature set. Every pin, commit and file hash is in interop/sdk-pins.env
Clients2025-11-25: the TypeScript SDK (v1) 1.32.1 client (ts-client.mjs). 2026-07-28: the Python SDK 2.3.0 client (py-client.py). One client per revision for every server, so two rows of a revision differ by the server alone
Recorded bymcp-trace-capture http, in front of each server on fixed loopback ports, by interop/matrix.sh, 2026-10-25 (every cell re-recorded that day; the three 2026-07-28 traces that existed before are byte-identical to the 2026-10-13 ones, because a stateless session carries no value that changes between runs)
Re-verifiedWeekly: the scheduled sdk-matrix job re-records every session with the same pins and runs cargo xtask sdk-matrix --recaptured, which fails if any re-judged report differs from its golden
TraceSession
sdk-typescript-2025-11-25.jsonlThe TypeScript v1 server at 2025-11-25: handshake, the standalone GET stream, tools, resources, prompts, logging/setLevel, an unknown tool, and the DELETE teardown. The TypeScript SDK’s 1.x line does not serve 2026-07-28; the Python client’s discovery falls back to initialize against it, so there is no 2026-07-28 trace for it
sdk-typescript-v2-2025-11-25.jsonlThe TypeScript v2 server, the same session, answered through its legacy (handshake) path
sdk-python-2025-11-25.jsonlThe Python server, the same session
sdk-go-2025-11-25.jsonlThe Go server, the same session. Its first prompt is an MRTR fixture, which it answers to this 2025-11-25 client with a JSON-RPC error
sdk-rust-2025-11-25.jsonlThe Rust server, the same session
sdk-typescript-v2-2026-07-28.jsonlThe TypeScript v2 server at 2026-07-28, the same session as the Python server’s: server/discover (answered with supportedVersions ["2026-07-28"]), then tools, resources and prompts with the per-request _meta envelope
sdk-python-2026-07-28.jsonlThe Python server at 2026-07-28: server/discover, then tools, resources and prompts with the per-request _meta envelope
sdk-go-2026-07-28.jsonlThe Go server, the same session. Its first prompt answers with an input_required result asking for elicitation/create from a client that declared no elicitation capability — the findings the matrix reports against it
sdk-rust-2026-07-28.jsonlThe Rust server, the same session

The trace bytes are not reproducible, and are not normalized. Each server mints a fresh session id on every run, and nothing is rewritten after recording — a recording edited after the fact is no longer a recording. What is held fixed instead is everything that changes a result: the ports (the proxy records the Host header), the client script, the pins, and the one race the session had. The TypeScript client opens its standalone GET stream in the background, and while that GET and the next POST both awaited a status the validator — correctly — attributed neither, so TRAN-040 read not observed on some runs and pass on others; the client now waits for the GET to be answered before its next request. Five consecutive local runs then produced identical reports, seq for seq. The weekly comparison ignores a finding’s seq all the same, because a session whose replies land a line apart is the same session.

Regenerate with interop/setup.sh and interop/matrix.sh, then BLESS=1 cargo xtask sdk-matrix --recaptured interop/out-matrix, cargo xtask bless, cargo xtask sdk-matrix and cargo xtask draft-coverage, and review the golden diff.

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

Fixtures for revision 2026-07-28, judged by its registry. Hand-authored rather than captured, so each is a minimal trace written to exercise one clause, which is what keeps a violation attributable to the check it kills. Recorded 2026-07-28 sessions are in draft/captured/ (above), and the interop job records the official Python SDK’s client and server at that revision and requires them to judge clean (interop/README.md).

Where a trace falsifies more than one requirement, the row says so and why. There are only three reasons, and none is sloppy authorship: several clauses state one rule across several sections and share a check by design, 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 — or the shape a trace exists to carry is a violation in its own right: an HTTP answer with "id": null that the framing ties to its request is BASE-052’s whenever that request’s id could be read. Every other row falsifies exactly the requirement it is named for.

Every result names its server, and every request its client (2026-10-13). When BASE-033, BASE-037 and DISC-003 stopped being excluded, the authored traces were brought up to the two SHOULDs they had been written without: each result answering a request under the per-request envelope gained _meta.io.modelcontextprotocol/serverInfo (corpus-server 0.1.0), and the eight violation traces whose requests lacked clientInfo gained it. Nothing else in any trace changed, and no other row’s outcome moved; without the edit every authored trace would carry a warning unrelated to the clause it exists for. The three traces named for those clauses are the only ones left without.

Every form elicitation carries its schema (2026-10-13). When the client-feature pages entered the registry (review item 2.5), seven authored traces were found asking for form mode elicitation without the requestedSchema the page requires, or with no parameters at all — fixtures written for MRTR clauses that never needed one: mrtr-004, mrtr-015, mrtr-016-request-state-not-echoed, mrtr-018, mrtr-024, tran-066 and base-034-input-request-for-undeclared-capability. Each gained the same message and requestedSchema; nothing else changed. The wire schema had been failing all but the last of them for the omission, unrelated to the clause each exists for; it now passes them, and no existing requirement’s outcome moved.

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.
mrtr-retry-round-before-recording.jsonlA fresh stdio process receives a tools/call retry carrying inputResponses and a requestState minted by an earlier server process, and completes it; the input_required round predates the recording. requestState exists so a retry can reach an instance that never saw the round, so MRTR-015/016/018 have no round to judge it against. Until 2026-10-12 MRTR-018 read the missing round as one that issued no state and failed it.
client-features-mrtr.jsonlThe client-feature traffic of good/stdio-client-features.jsonl under MRTR, over stdio (added 2026-10-13, review item 2.5): one tools/call whose input_required asks four inputs at once — form and URL mode elicitation (no elicitationId at this revision), a tool-enabled sampling request under toolChoice required, and roots/list — retried with all four answers; a second round continuing the tool loop under toolChoice none; then the result. Every request declares elicitation (both modes), sampling.tools and roots in its own clientCapabilities
http-authorization.jsonlgood/http-authorization.jsonl at this revision (added 2026-10-13, review item 2.8): server/discover refused 401 with resource_metadata and scope (AUTH-097), resent with the same id and answered; a tools/list refused 403 insufficient_scope (AUTH-130/131/132), retried with the same id and answered — AUTH-133’s pass path, a retry the server did not challenge again — and another refused 401 invalid_token (AUTH-124/130). No challenge names offline_access (AUTH-129). The same-id retries pass BASE-045: the refused request was never read, so it was never in flight
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.
subscription-id-reused-after-close.jsonlA stdio subscription closed by its graceful-closure response, then a second subscriptions/listen reusing id 1 with a different filter (promptsListChanged only): each opening acknowledged first, each notification one its own filter requested (SUBS-001/002 pass paths). Until 2026-10-12 both checks keyed subscriptions by id alone, so the second filter judged the first stream’s tools/list_changed and SUBS-001 failed.
caching-ttl-written-as-float.jsonlA tools/list result carrying "ttlMs": 60000.0. The schema types ttlMs as JSON Schema integer, which admits any number with a zero fractional part, so this is 60000 milliseconds (CACH-001/008 pass paths) — the spelling a serializer that holds every JSON number as a double may produce. Until 2026-10-12 CACH-008 accepted only values that parsed as i64 and failed it.
traceparent-higher-version.jsonlA request traceparent of version 01 with a fifth field after the flags, answered with a result whose traceparent is downgraded to 00 — the first mutation W3C Trace Context §3.4 allows. §3.2.4 assumes future versions “will be additive” and has a parser check only that the flags are followed by “the end of the string or a dash”, so the extra field is valid (BASE-040’s pass path). Until 2026-10-12 the check demanded exactly four fields at every version and failed it.
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-030-unsupported-version-refused-as-invalid-params.jsonlOver stdio, a tools/list whose _meta omits clientCapabilities and names 2099-01-01, which the server’s server/discover result omits; the server answers -32602 (BASE-030, with MRTR-002, COMP-006, PAGE-006, TOOL-016, RES-011 and PROM-011 — the same envelope rule restated per feature, sharing its check). Both -32602 and -32022 are defined for this request and one answer carries one code, so the golden pins VERS-001 passing; until 2026-10-12 it and TRAN-074 failed it. TRAN-074, the Streamable HTTP statement of the rule, is not observed on this stdio trace.
input-required-id-reused.jsonlA tools/call declaring elicitation draws an input_required asking for elicitation/create, is retried and served under id 2, and then id 1 — answered, so free again (BASE-045) — is reused for an unrelated call declaring no capabilities. Until 2026-10-12 capabilities were looked up by request id across the whole trace, so the input_required was held against the last request with id 1, and this conforming session failed BASE-034, MRTR-012 and MRTR-013.
base-030-malformed-request-refused-without-id.jsonlThe same malformed tools/list over Streamable HTTP, rejected 400 with a -32602 whose id is null, then retried well-formed under id 1 and served (BASE-030 for the client’s envelope; BASE-052 for the null id, since the framing names a request whose id could be read). The golden pins BASE-031, BASE-032 and BASE-045 passing: until 2026-10-12 the null-id rejection answered nothing, the malformed request stayed open, and the retry’s result was held against it.
base-030-legacy-initialize-negotiated-2026-07-28.jsonlA 2025-11-25 client’s initialize, answered by a server whose result names protocolVersion 2026-07-28 (and carries resultType); the client then continues under it with a well-formed modern tools/list (added 2026-10-24). The session ran under the revision the server negotiated, so validate judges it under 2026-07-28 alone — one report, with a note on stderr naming the proposal — where the legacy initialize is a client request without its required _meta fields (BASE-030; MRTR-002, COMP-006, PAGE-006, TOOL-016, RES-011 and PROM-011 restate that one envelope rule per feature page and share the check) sent by a dual-era client that skipped the server/discover probe (DISC-002 and TRAN-128, warnings, one check). Until 2026-10-24 the same trace was judged side by side under both revisions, with a 2025-11-25 LIFE-003 failure for an initialized notification a 2026-07-28 session never owed.
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-033-request-without-client-info.jsonlA tools/list whose _meta carries the envelope but no io.modelcontextprotocol/clientInfo (BASE-033, a warning). Excluded until 2026-10-13. Added 2026-10-13
base-034-input-request-for-undeclared-capability.jsonlServer returns input_required asking for elicitation/create the request never declared (BASE-034)
base-034-input-request-for-undeclared-mode.jsonlThe request declares elicitation: {"form": {}} and a plain sampling: {}; the server asks for a URL-mode elicitation/create and a sampling/createMessage carrying tools (BASE-034; MRTR-012 and MRTR-013 share the check). client/elicitation: “Servers MUST NOT send elicitation requests with modes that are not supported by the client”; client/sampling: “Servers MUST NOT send tool-enabled sampling requests to Clients that have not declared support for tool use via the sampling.tools capability.” Only top-level capability names were compared until 2026-10-12, so this passed.
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). The 500 also carries no Content-Type, which since 2026-10-13 fails TRAN-063 too
base-037-result-without-server-info.jsonlA tools/list result with no io.modelcontextprotocol/serverInfo in its _meta (BASE-037, a warning) — what rmcp’s servers send on every result but server/discover and subscriptions/listen, where the Go and Python SDK servers name themselves on every result (draft/captured/). Excluded until 2026-10-13. Added 2026-10-13
base-039-subscription-notification-untagged.jsonlNotification on a subscriptions/listen stream with no io.modelcontextprotocol/subscriptionId (BASE-039)
base-039-subscription-notification-mistagged.jsonlA subscriptions/listen with the numeric id 1, acknowledged under tag 1, then a change notification tagged with the string "1" (BASE-039). The schema: “The value is the JSON-RPC ID of the subscriptions/listen request that opened the stream” — and JSON-RPC ids are compared as values, so "1" names no subscription. Only the tag’s presence was checked until 2026-10-12.
base-040-malformed-traceparent.jsonltraceparent that is not W3C Trace Context shaped (BASE-040)
base-040-traceparent-version-ff.jsonltraceparent with version ff, which W3C Trace Context forbids (“Version ff is forbidden”), and otherwise well formed (BASE-040). Passed until 2026-10-12, when the check accepted any two hex digits as the version.
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-052-null-id-error-then-id-reused.jsonlA well-formed tools/call for an unknown tool answered with a -32602 whose id is null, then id 1 reused for a call that is served (BASE-052). The status ties the error to its POST, whose request has the readable id 1, so the escape hatch for an id that “could not be read” does not apply. The golden pins BASE-045 passing: until 2026-10-12 the null-id answer closed nothing, and the reuse was reported as in flight.
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).
http-close-after-another-response.jsonlA subscriptions/listen stream left open while a tools/call beside it is answered; a transport-close is recorded straight after that answer, and the subscription is later closed gracefully. The final response “SHOULD terminate the stream”, so the close is the call’s stream ending, not the subscription being cancelled. Until 2026-10-12 TRAN-070 pinned the close on the one request still open and failed the graceful closure.
stdio-discover-preferred-version-unsupported.jsonlA stdio server/discover probe naming 2027-03-01, answered with a DiscoverResult listing only 2026-07-28, then a tools/list at that version. basic/versioning’s compatibility matrix names the outcome: “The stdio probe returns a DiscoverResult (or UnsupportedProtocolVersionError)”. Until 2026-10-12 TRAN-074 and VERS-001 failed the result for not being -32022; VERS-001 now passes it, and TRAN-074, an HTTP clause, is not observed on stdio.
http-dual-era-modern-request-first.jsonlA dual-era client over Streamable HTTP opening with a modern tools/list, refused 400 with -32600, then falling back to initialize (carrying a full _meta envelope and left unanswered, as in disc-002-…, so nothing else fires). HTTP’s own rule lets it “detect which era the server implements by attempting a modern request first”; the probe-first clause is stdio’s. Until 2026-10-12 DISC-002 and TRAN-128 warned here.
http-origin-refused-without-id.jsonlThe 2026-07-28 counterpart: a tools/list from evil.example.com refused 403 with an id-less error, then served from localhost. The same sentence permits the body at this revision, and BASE-052 — BASE-009’s restatement, sharing its check — passes it. Added 2026-10-13
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.
stdio-cancelled-id-reused.jsonlA tools/call id 5 cancelled by notification and answered in the race the cancellation page names; a later tools/call id 6 shows the server has read the cancellation; the client then reuses id 5 — legal once answered (BASE-045) — for a new call, which is answered (TRAN-124’s pass path). Until 2026-10-12 the stdio check never cleared a cancellation when its id was reused, as the HTTP twin does, and failed the answer to the new request.
stdio-progress.jsonlProgress done right over stdio: rising values (0.25, 0.5, 1) for one token, ending before the result; the token reused by a later request once the first was answered, its sequence starting again below where the last one ended; and an integer token running concurrently with it. The pass path of PROG-001/002/003/005/008.
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.
http-x-mcp-header-omitted.jsonlThe x-mcp-header behaviour table’s conforming rows over Streamable HTTP: a designated region passed as null and left out of the arguments, each POSTed without Mcp-Param-Region and served, then passed a value and mirrored. The pass path of TRAN-130 through TRAN-135.
tran-058-request-metadata-headers-missing.jsonlPOST carries neither Mcp-Method nor Mcp-Name (TRAN-058)
tran-058-mcp-name-mismatch-refused-without-id.jsonlA POST whose Mcp-Name disagrees with params.name (TRAN-058), refused 400 with a null-id -32020; the client corrects the header and retries reusing id 1, which is served. Also falsifies BASE-052 — the null id answers a request whose id could be read. The golden pins BASE-045, TRAN-097 and TRAN-100 passing: until 2026-10-12 the refusal answered nothing, so the retry was an in-flight reuse and its result was held against the mismatched POST.
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; BASE-084, basic/patterns’ “Servers MUST NOT initiate JSON-RPC requests”, is the same rule stated for every transport and shares the check)
tran-068-sse-without-accel-buffering.jsonlSSE response omits X-Accel-Buffering: no (TRAN-068, SHOULD → warn)
tran-068-sse-media-type-mixed-case.jsonlThe same omission on a response typed Text/Event-Stream; charset=utf-8 (TRAN-068). Media types are case-insensitive (RFC 9110 §8.3.1); until 2026-10-12 only the lowercase spelling was recognized as SSE, and this response was not judged.
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. Since 2026-10-12 it also falsifies BASE-052, inseparably from the shape under test: the framing names the request, its id 1 could be read, and only an id that could not be read may be answered with null.
tran-072-unsupported-body-version-header-mismatch.jsonlHeader 2026-07-28 against a body _meta naming 2099-01-01, a version the server’s server/discover result omits, refused 400 with -32020 (TRAN-072, the client’s fault). The request falls under both TRAN-073 (HeaderMismatch) and TRAN-074/VERS-001 (UnsupportedProtocolVersionError), which name no order, so the golden pins all three passing; until 2026-10-12 TRAN-074 and VERS-001 failed this answer.
tran-072-unsupported-body-version-refused-as-unsupported.jsonlThe same request refused with -32022 listing 2026-07-28 (TRAN-072). The other half of the pair: the golden pins TRAN-073 passing, which until 2026-10-12 failed this answer — so neither code could pass before.
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. Also falsifies BASE-052 since 2026-10-12, for the same null id: the request it answers had the readable id 1.
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-063-error-without-content-type.jsonlThe same unknown method refused correctly — 404 and -32601 — but with no Content-Type on the answer (TRAN-063). Added 2026-10-13 with the check’s extension to error statuses that carry a JSON-RPC response, which the calibration against mcpsnoop found (book/src/mcpsnoop.md)
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-130, the behaviour table’s “Client MUST include the header”, shares the check) — and the server serves it, which since 2026-10-12 also fails TRAN-136 (“Server MUST reject the request”): the client’s omission is that clause’s antecedent
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). Since 2026-10-13 it also fails the client’s TRAN-079: a header naming another value does not mirror it
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 was recorded here until 2026-10-13 as having “no separate clause”; it has one, TRAN-079 (“MUST mirror the designated parameter values”), which the calibration against mcpsnoop surfaced and this trace now also fails. 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-132-null-parameter-header-sent.jsonlA designated region passed as null (the schema allows it) yet mirrored as Mcp-Param-Region: null, which the behaviour table’s null row forbids (“Client MUST omit the header”, TRAN-132). The server serves it, which is not a fault: with no value in the body there is nothing for the header to mismatch.
tran-133-omitted-null-header-rejected.jsonlThe conforming form of the same call — region: null, no Mcp-Param-Region — refused with 400 and -32020, every other header matching its body: the server expected a header the table says it must not (TRAN-133).
tran-134-absent-parameter-header-sent.jsonlThe table’s other valueless row: region left out of the arguments, yet Mcp-Param-Region: us-west1 sent — a header for a value the body does not carry (TRAN-134).
tran-135-omitted-absent-header-rejected.jsonlregion left out of the arguments and its header correctly omitted, refused with 400 and -32020 (TRAN-135). The null row and this one are judged by separate checks, so neither finding is reported against the other’s scenario.
tran-136-missing-param-header-accepted.jsonlA designated region: "us-west1" sent without Mcp-Param-Region and served with a result instead of rejected (TRAN-136; TRAN-079 and TRAN-130 for the client’s omission, the antecedent). The shape of the session that showed the gap on 2026-10-12: header_body_match_validated compares only headers that are present, so a missing one was judged by nothing on the server’s side.
tran-079-param-header-value-not-mirrored.jsonlMcp-Param-Region: us-east1 against arguments.region = "us-west1", refused with 400 and -32020 as the server must — so the one defect is the client’s: the header does not mirror the value (TRAN-079, transport.x-mcp-header-value-mirrored). Until 2026-10-13 such a client was judged conformant, the mismatch reported only as a server’s failure to reject it; the calibration against mcpsnoop (book/src/mcpsnoop.md) found it. Added 2026-10-13
tran-079-missing-param-header-rejected.jsonlThe same omission refused with 400 and -32020 (TRAN-079 and TRAN-130 for the client). The golden pins TRAN-136 passing — its pass path can only exist beside a client fault, the way tran-072-mismatch-rejected-without-id.jsonl carries TRAN-073’s.
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-009-log-with-no-request-in-flight.jsonlA tools/call that asked for logs is answered; with only a subscriptions/listen open, the server then sends an untagged log (LOG-009). It travels on no request’s response stream, since every request it could belong to has been answered. Passed until 2026-10-12: LOG-009 judged only the subscription tag, and LOG-008 only a log some unopted request could own.
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. Since 2026-10-12 the same message also fails CANC-003/CANC-011: a server may cancel only a subscriptions/listen request, and this one cancels a call.
subs-002-notification-before-acknowledgment.jsonlA notification ahead of notifications/subscriptions/acknowledged on the same subscription id (SUBS-002)
subs-002-reused-id-first-opening-unacknowledged.jsonlSubscription 1’s first opening sends tools/list_changed ahead of its acknowledgment and is closed; a second subscriptions/listen reusing id 1 is acknowledged correctly (SUBS-002, on the first opening). Passed until 2026-10-12, when the second request replaced the first in an id-keyed map and the first opening’s messages were never examined.
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; CANC-005 is the cancellation page’s statement of the same stdio rule and shares the check)
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).
canc-003-server-cancels-tool-call.jsonlA stdio server sends notifications/cancelled for the client’s tools/call (“server busy”). At this revision a server cancels nothing but a subscriptions/listen request (CANC-003, and CANC-011 — the Behavior Requirements restatement — sharing the check). The pass path is good/subscription-server-teardown.jsonl.
prog-001-progress-token-not-integer.jsonlA request opting into progress with progressToken: 1.5, a number but not an integer (PROG-001).
prog-002-progress-token-reused-in-flight.jsonlTwo tools/call requests in flight at once under the same progress token, so a notification could not say which it reports on (PROG-002). Reuse after an answer is legal, and good/stdio-progress.jsonl does it.
prog-003-progress-value-decreases.jsonlProgress 50 then 10 for one token (PROG-003).
prog-008-progress-after-completion.jsonlA progress notification for a stdio request after its result (PROG-008, and PROG-005 — the token no longer names an active request — sharing the check).
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.
disc-003-discover-result-without-server-info.jsonlA server/discover result with no serverInfo in its _meta (DISC-003, a warning; BASE-037 warns too — the general rule, of which this is the restatement for one result type). Every captured server includes it here. Excluded until 2026-10-13. Added 2026-10-13
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)
base-073-input-schema-invalid.jsonlBASE-073 (the same invalid inputSchema, at this revision)
base-088-input-schema-draft-07.jsonlBASE-088, a warning (the same valid draft-07 inputSchema as base-086-…, at this revision)
tran-141-mcp-name-header-missing.jsonlA tools/call POST carrying Mcp-Method but not Mcp-Name, which the Standard Request Headers table requires for tools/call (“These headers are REQUIRED for compliance”, TRAN-141; TRAN-058 shares the check)
tool-045-x-mcp-header-in-defs.jsonlTOOL-045 (an x-mcp-header annotation on a $defs entry the property reaches by $ref; TRAN-137 and TRAN-082, the transport page’s statements of the same rule, share the check)
tran-082-x-mcp-header-under-any-of.jsonlTRAN-082 (the annotation inside an anyOf branch, a composition keyword the chain must not pass through; TRAN-137 and TOOL-045 share the check)
tran-082-invalid-tool-excluded-valid-called.jsonlOver Streamable HTTP, a tools/list serving deploy, annotated inside an anyOf (TRAN-082; TRAN-137 and TOOL-045 share the check), beside a valid echo; the client calls echo only. That is the behaviour TRAN-083/TRAN-084 ask for — “a single malformed tool definition does not prevent other valid tools from being used” — and the golden pins it passing. Added 2026-10-13
tran-083-invalid-tool-called.jsonlThe same invalid deploy, served alone, and the client calls it (TRAN-083 with TOOL-046, its restatement on the tools page, and TRAN-084 with TOOL-035: a client that rejected the definition and excluded the tool could not have used it). TRAN-082, TRAN-137 and TOOL-045 fail too — the server’s half of the same definition. Excluded until 2026-10-13. Added 2026-10-13
tran-137-x-mcp-header-under-items.jsonlTRAN-137 (the annotation on an array’s items schema, not a property reachable by properties keys alone; TRAN-082 and TOOL-045 share the check)
tool-040-structured-content-not-conforming.jsonlTOOL-040 (structuredContent missing conditions, which the tool’s outputSchema requires)
wire-schema-resource-missing-uri.jsonlWire schema: a resources/list result whose resource has no uri, which Resource requires; the result carries both caching hints so the trace isolates the schema defect
wire-schema-cache-scope-not-in-enum.jsonlWire schema: cacheScope: "shared", which is neither public nor private
wire-schema-content-block-unknown-type.jsonlWire schema: a tools/call result whose content block has type: "txt", matching no ContentBlock member
wire-schema-tool-image-data-not-base64.jsonlWire schema: the same image whose data is not base64!, in a 2026-07-28 tools/call result (added 2026-10-24); this revision’s schema marks ImageContent.data with format: byte as 2025-11-25’s does, and both are compiled with the same assertion
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.
elic-057-answered-without-capability.jsonlELIC-057: the call declares elicitation, but its retry — the request carrying the accepted form — declares only sampling; the page asks for the declaration “on each request”
elic-059-url-mode-undeclared.jsonlELIC-059: URL mode asked of a request declaring form mode only. BASE-034, MRTR-012 and MRTR-013 make the same observation through meta.no-undeclared-capability-reliance
elic-060-request-without-message.jsonlELIC-060: an elicitation with no message; the wire schema too
elic-062-form-without-schema.jsonlELIC-062: form mode with no requestedSchema; the wire schema too
elic-064-url-mode-without-url.jsonlELIC-064: URL mode with no url; the wire schema too
elic-065-url-not-a-valid-url.jsonlELIC-065: a url whose query carries an unencoded space (account=Jane Doe), which RFC 3986 admits only as %20. The wire schema passes it: format is not asserted (ADR-0020)
elic-095-accepted-content-violates-schema.jsonlELIC-095, a warning: accept with empty content against a schema requiring city — the shape the reference host’s captures showed until it was fixed on 2026-10-13 (below)
samp-026-tools-without-sampling-tools.jsonlSAMP-026: tools asked of a request declaring plain sampling. BASE-034, MRTR-012 and MRTR-013 make the same observation
samp-027-answered-without-capability.jsonlSAMP-027: a retry declaring only elicitation answers the sampling request its call drew
samp-028-include-context-deprecated.jsonlSAMP-028, a warning: includeContext thisServer with sampling.context declared — SAMP-029’s pass path
samp-029-include-context-undeclared.jsonlSAMP-029, a warning: includeContext allServers without sampling.context; SAMP-028 warns too
samp-030-tool-results-mixed.jsonlSAMP-030: a user message mixing text with a tool result; SAMP-031 and SAMP-042 as for samp-008 at 2025-11-25
samp-031-tool-use-unanswered.jsonlSAMP-031: a tool use followed by a plain text message; SAMP-042 restates it
samp-032-required-tool-choice-ignored.jsonlSAMP-032: toolChoice required answered with text
samp-033-none-tool-choice-ignored.jsonlSAMP-033: toolChoice none answered with a tool use
samp-034-message-without-role.jsonlSAMP-034: a sampling message with no role; the wire schema too
root-013-answered-without-capability.jsonlROOT-013: a retry declaring only sampling answers roots/list
root-014-root-not-file-uri.jsonlROOT-014: a root listed as https://example.com/repos/trips
root-018-asked-without-capability.jsonlROOT-018, a warning: roots/list asked of a request that did not declare roots. BASE-034, MRTR-012 and MRTR-013 state the MUST behind it and fail
auth-097-challenge-without-scope.jsonlAUTH-097, a warning: the 401 challenge carries resource_metadata but no scope
auth-124-invalid-token-answered-403.jsonlAUTH-124: error="invalid_token" sent with 403; AUTH-130 fails too, as for auth-045 at 2025-11-25
auth-129-offline-access-challenged.jsonlAUTH-129, a warning: the 401 challenge asks for files:read offline_access. The clause’s other half, Protected Resource Metadata’s scopes_supported, is not in a recording
auth-130-insufficient-scope-answered-401.jsonlAUTH-130: error="insufficient_scope" sent with 401; AUTH-131 warns too
auth-131-insufficient-scope-without-resource-metadata.jsonlAUTH-131, a warning: an insufficient_scope challenge without resource_metadata
auth-132-insufficient-scope-names-no-scope.jsonlAUTH-132, a warning: an insufficient_scope challenge with no scope; AUTH-131 warns too
auth-133-scope-challenged-incrementally.jsonlAUTH-133, a warning: a tools/list challenged for tools:list, retried unchanged, and challenged again for tools:read, which the first challenge did not name — the incremental challenging the page says forces “multiple authorization round-trips for a single operation”

Extensions at 2026-07-28 (corpus/draft/, ADR-0023)

Fixtures for the tasks (io.modelcontextprotocol/tasks) and skills (io.modelcontextprotocol/skills) extensions, hand-authored like the rest of corpus/draft/ and judged by the 2026-07-28 registry, whose extension clauses report in each report’s extensions block and never move its verdict. Every violation trace here leaves the core verdict pass unless its row says otherwise, which is the separation the corpus exists to show: the extension’s own verdict fails, the report’s does not. A task-… or skill-… trace is named for the extension clause it falsifies, and the golden tests look for that row in the extension block.

TraceExercises
tasks-session.jsonlConformant tasks session over stdio: server/discover declaring the extension; a tools/list whose deploy tool declares an outputSchema; a tools/call of it from a declaring client answered with a task, polled working, then input_required twice with the same inputRequests key and request (the documented snapshot), answered once with tasks/update (acknowledged empty), then completed with its result, whose structuredContent TOOL-040 judges against the outputSchema (the task handle itself is not the tool’s result: until 2026-10-13 TOOL-040 held it to the schema and failed this session); a second task failed with a JSON-RPC error and a statusMessage; a third followed on a subscriptions/listen stream asking for its taskIds (acknowledged, then notifications/tasks for it), cancelled with tasks/cancel, and the stream closed by a notifications/cancelled naming the listen request, not the task; and a non-declaring client refused -32021 for tasks/get and for a task listen. The pass path of every tasks check but TASK-035
tasks-http.jsonlConformant tasks session over Streamable HTTP: a task created by tools/call, then tasks/get with Mcp-Name: t-1, the task’s id (TASK-035’s pass path)
task-001-task-for-undeclaring-client.jsonlTASK-001: a tools/call whose request does not declare the extension is answered with a task
task-006-task-without-result-type.jsonlTASK-006: a task returned for tools/call with resultType: "complete". The core verdict fails too: a complete tools/call result without content is invalid against the revision’s schema
task-007-task-result-type-without-task.jsonlTASK-007: resultType: "task" on a result with no taskId
task-015-get-result-not-a-task.jsonlTASK-015: a tasks/get result with status running and no timestamps or ttlMs
task-015-input-required-without-requests.jsonlTASK-015: an input_required task without inputRequests
task-017-get-result-type-not-complete.jsonlTASK-017: a tasks/get result marked resultType: "task". TASK-028 and TASK-032 fail too: the three methods’ results state one rule and share its check
task-022-input-request-key-reused.jsonlTASK-022: one inputRequests key naming two different elicitations across polls. TASK-023 states the same rule and shares the check
task-024-update-ack-not-empty.jsonlTASK-024: a tasks/update acknowledged with the task’s state rather than an empty result. TASK-030, the same sentence for tasks/cancel, shares the check
task-026-input-answered-twice.jsonlTASK-026, a warning: a second tasks/update answering a key an acknowledged one already answered
task-029-task-cancelled-by-notification.jsonlTASK-029: notifications/cancelled naming the tools/call a task answered
task-033-task-listen-without-declaration.jsonlTASK-033: a non-declaring client’s listen for task notifications acknowledged instead of refused. TASK-038, the error-handling list’s statement of it, shares the check
task-034-progress-on-task-stream.jsonlTASK-034: a notifications/message on a task’s listen stream. The core verdict fails too: the filter did not request it (SUBS-001), and a log message’s own clauses (LOG-007 to LOG-009) bind it
task-035-mcp-name-missing.jsonlTASK-035: a tasks/get POSTed without Mcp-Name
task-039-get-without-declaration.jsonlTASK-039: a non-declaring client’s tasks/get answered with the task
task-041-failed-without-status-message.jsonlTASK-041, a warning: a failed task without statusMessage
task-042-failed-without-error.jsonlTASK-042: a failed task without error. TASK-015 (its fifth item) and TASK-045 state the same rule
task-043-tool-error-reported-failed.jsonlTASK-043: a tool result with isError: true carried by a failed task. TASK-008 and TASK-044 state the same rule
task-046-completed-without-result.jsonlTASK-046: a completed task without result. TASK-015 (its third item) and TASK-044 state the same rule
skills-session.jsonlConformant skills session over stdio: server/discover declaring the extension with directoryRead: true and the resources capability; a two-page skills/list (pdf-processing with a manifest of its SKILL.md and templates/invoice.md, then the nested-prefix acme/billing/refunds as "dynamic"); skills/get of the second, and of an unknown skill refused -32602; resources/list describing the SKILL.md as text/markdown with its frontmatter’s name and description; resources/read of the SKILL.md, whose SHA-256 is the manifest’s digest and whose frontmatter is the entry’s; resources/directory/read of the skill root and of templates/, each listing carrying its ttlMs and cacheScope, and of the SKILL.md itself refused -32602. The pass path of every skills check
skill-003-directory-read-not-implemented.jsonlSKILL-003: directoryRead: true declared, resources/directory/read answered -32601
skill-004-directory-read-undeclared.jsonlSKILL-004: the client calls resources/directory/read on a server that declared no directoryRead
skill-005-skills-without-resources.jsonlSKILL-005: the extension declared without the resources capability
skill-007-entry-uri-not-skill-md.jsonlSKILL-007: an entry whose uri is a README.md
skill-008-skill-md-without-frontmatter.jsonlSKILL-008: a SKILL.md read back without YAML frontmatter
skill-011-skill-under-other-scheme.jsonlSKILL-011, a warning: a skill served under github://
skill-012-path-not-ending-in-name.jsonlSKILL-012: skill://pdf-tools/SKILL.md named pdf-processing. SKILL-027 states the same rule and shares the check
skill-013-name-breaks-naming-rules.jsonlSKILL-013: a skill named PDF--Processing
skill-014-authority-not-reg-name.jsonlSKILL-014, a warning: a first path segment with a space
skill-015-prefix-segment-invalid.jsonlSKILL-015, a warning: a prefix segment containing |
skill-018-skill-md-not-markdown.jsonlSKILL-018, a warning: a SKILL.md listed as text/plain
skill-019-resource-name-not-skill-name.jsonlSKILL-019, a warning: a SKILL.md listed under the name SKILL.md
skill-020-resource-description-differs.jsonlSKILL-020, a warning: a SKILL.md listed with a description its frontmatter does not give
skill-026-entry-frontmatter-differs.jsonlSKILL-026: the file read back has the manifest’s digest but a different description from the entry’s frontmatter
skill-029-listing-names-collide.jsonlSKILL-029, a warning: two skills in one listing named pdf-processing
skill-034-entry-without-resources.jsonlSKILL-034: an entry without resources
skill-035-manifest-without-skill-md.jsonlSKILL-035: a manifest that does not list the skill’s own SKILL.md
skill-036-manifest-file-outside-skill.jsonlSKILL-036: a manifest listing skill://shared/LICENSE
skill-037-manifest-entry-without-size.jsonlSKILL-037: a manifest entry with a digest and no size
skill-042-skill-beyond-limits.jsonlSKILL-042, a warning: a manifest totalling 16,777,217 bytes, one over the limit
skill-044-list-not-implemented.jsonlSKILL-044: skills/list answered -32601 by a declaring server. SKILL-002 states the same rule
skill-045-list-result-type-not-complete.jsonlSKILL-045: a skills/list result marked resultType: "partial"
skill-047-list-without-cache-fields.jsonlSKILL-047: a skills/list result without ttlMs and cacheScope
skill-048-get-not-implemented.jsonlSKILL-048: skills/get answered -32601 by a declaring server. SKILL-002 states the same rule
skill-049-get-result-type-not-complete.jsonlSKILL-049: a skills/get result marked resultType: "partial"
skill-050-get-names-a-directory.jsonlSKILL-050: skills/get naming the skill’s directory rather than its SKILL.md (the server’s -32602 is the conforming answer)
skill-051-unknown-skill-wrong-error.jsonlSKILL-051: an unknown skill refused with -32600 rather than -32602. SKILL-071, the error-handling list, shares the check
skill-053-get-without-cache-fields.jsonlSKILL-053: a skills/get result without ttlMs and cacheScope
skill-065-directory-result-type-not-complete.jsonlSKILL-065: a resources/directory/read result marked resultType: "partial"
skill-066-directory-read-of-a-file-answered.jsonlSKILL-066: resources/directory/read of a file the manifest lists, answered with an empty listing. SKILL-071 shares the check
skill-067-skill-directory-refused.jsonlSKILL-067: resources/directory/read of templates/, a directory the manifest puts a file under, refused
skill-100-directory-result-without-cache-fields.jsonlSKILL-100: a resources/directory/read result without ttlMs and cacheScope

Conformance results

A conformance toolkit should be willing to measure real implementations — its own and others’ — against the official suite, and publish what it finds. These measurements are stewardship artifacts: every number is the official suite’s number, captured by running it, with this project’s registry used only to read each failure back to a specific spec clause. Every claim has a command that reproduces it.

This project’s reference implementations

The reference server and host are a hard CI gate, not a one-off measurement: on every run, the conformance job drives the pinned official suite against mcp-everything-server (server scenarios) and mcp-reference-host (client scenarios), and replays the captured sessions through the validator for the agreement check. A regression fails the build. The live status is whatever the latest main run reports.

The agreement check covers the wire schema too: where the suite reports its wire-schema-valid check (from 0.2.0-alpha.11, so the alpha-tracking job and the 2026-07-28 readiness run, not yet the stable pin), every message it flags is matched against the messages the validator flags in the recorded sessions, and a message flagged by only one side fails the run unless the committed conformance/wire-schema-divergences.json explains it.

rmcp tier-gap report

The published measurement of the official Rust SDK (rmcp) against the suite is the worked example of the method:

📄 rmcp tier-gap report — 2025-11-25

It records the server-scenario pass count, the requirement-level reading of each failure, a concrete close-the-gap checklist, and the exact commands to reproduce it — including a note on why the suite’s tier-check aggregate is not the figure to trust (it is GitHub-token-gated and its conformance counter carries a known upstream counting bug). The headline finding at the report’s measurement date: two failing server scenarios, neither of which is a 2025-11-25 normative-clause violation — one sits just below the spec’s RFC 2119 floor (template-argument substitution is schema prose), the other is a SEP-1330 serialization bug already filed upstream. That is the kind of distinction a requirement-level tool exists to draw; see the report for the current count and the close-the-gap steps.

pmcp tier-gap report

The same method applied to a community SDK, to show it generalizes beyond the official one:

📄 pmcp tier-gap report — 2025-11-25

pmcp ships no server wired to the suite, so the report builds a small one from the SDK’s public API (its harness is committed beside the report) and states that caveat plainly. Each failing scenario is traced to a specific limitation of the SDK’s Streamable HTTP surface, both on the wire and in the SDK’s source.

A second reader of recordings

The official suite runs live scenarios. The one other tool that judges a recorded session, mcpsnoop, is run over the whole trace corpus too, and every place its verdict differs from this project’s is triaged — which tool is right, by which clause, and what was fixed or drafted for upstream:

📄 A second opinion: mcpsnoop

The 2026-07-28 surface

The official suite has no stable release for 2026-07-28 yet, so this project measures its reference server against the suite’s pinned pre-release every week and records the score in a committed baseline; Two revisions at once has the current figures and what they have already shown.

The cross-SDK matrix

Each Tier 1 SDK the matrix covers — TypeScript, Python, Go and Rust — ships a conformance server: the server its maintainers run the official suite against. TypeScript has two lines and so two rows: v1 (@modelcontextprotocol/sdk, the maintenance line, which implements the specification up to 2025-11-25) and v2 (@modelcontextprotocol/server and its companions, which also serves 2026-07-28). This page records each of those servers, at each revision it serves, while an official SDK client drives it, and judges every recording with this project’s validator. Nothing of this project’s speaks MCP in these sessions; the recorder and the judge are the only parts of it involved.

The table below is generated by cargo xtask sdk-matrix from the golden reports committed under corpus/, and cargo xtask gates fails when it drifts from them. A scheduled job re-records every session with the pinned SDKs each week and fails if the results differ from the committed ones, so a row here is what the pinned release does today rather than what it did once. How each session is recorded, with the exact commands, is Recording an SDK’s server.

SDKPinned releaseRevisionVerdictpassfailwarnnot observedWire schemaEvidence
TypeScript v1@modelcontextprotocol/sdk 1.32.12025-11-25pass-with-warnings390162pass (18 messages)report · trace
TypeScript v1@modelcontextprotocol/sdk 1.32.12026-07-28not served——————
TypeScript v2@modelcontextprotocol/server 2.3.12025-11-25pass430059pass (18 messages)report · trace
TypeScript v2@modelcontextprotocol/server 2.3.12026-07-28pass7200109pass (14 messages)report · trace
Pythonmcp 2.3.02025-11-25pass390063pass (17 messages)report · trace
Pythonmcp 2.3.02026-07-28pass6500116pass (14 messages)report · trace
Gogo-sdk v1.8.02025-11-25pass430059pass (17 messages)report · trace
Gogo-sdk v1.8.02026-07-28fail784198pass (14 messages)report · trace
Rustrmcp 3.7.02025-11-25pass420060pass (17 messages)report · trace
Rustrmcp 3.7.02026-07-28pass-with-warnings7301107pass (14 messages)report · trace

At 2025-11-25 every server is driven by the same TypeScript SDK (v1) client (ts-client.mjs). At 2026-07-28 every server is driven by the same Python SDK client (py-client.py).

Findings. Every failing or warning row of a committed report, with the first of its findings as the validator words it; the report link holds the rest.

  • TypeScript v1, 2025-11-25 — BASE-086 (SHOULD, warn, 8 findings): the server sent tool “test_tool_with_logging”’s inputSchema declaring $schema “http://json-schema.org/draft-07/schema#”; JSON Schema 2020-12 (https://json-schema.org/draft/2020-12/schema) is the recommended dialect
  • Go, 2026-07-28 — BASE-034 (MUST NOT, fail, 1 finding): server asked for elicitation/create, which needs the elicitation capability, but the request’s clientCapabilities did not declare it
  • Go, 2026-07-28 — TRAN-068 (SHOULD, warn, 6 findings): SSE response does not carry X-Accel-Buffering: no
  • Go, 2026-07-28 — MRTR-012 (MUST NOT, fail, 1 finding): server asked for elicitation/create, which needs the elicitation capability, but the request’s clientCapabilities did not declare it
  • Go, 2026-07-28 — MRTR-013 (MUST NOT, fail, 1 finding): server asked for elicitation/create, which needs the elicitation capability, but the request’s clientCapabilities did not declare it
  • Go, 2026-07-28 — ELIC-059 (MUST NOT, fail, 1 finding): inputRequests[user_context] (elicitation/create) asks for form mode, but the request’s clientCapabilities does not declare elicitation at all
  • Rust, 2026-07-28 — BASE-037 (SHOULD, warn, 5 findings): result answering tools/list carries no io.modelcontextprotocol/serverInfo in its _meta; servers should include it in every result unless specifically configured not to

Reading a row

Each row is one recorded session. Verdict and the four counts are the report’s, with the same meaning as in Reading a report: not observed counts the clauses the session carried nothing to judge, so a short session is not mistaken for a conforming one. Wire schema is every message checked against the revision’s published JSON Schema. Not served means the server does not implement that revision — the client asked, and the server answered with an older one — so there is no session to judge.

Every server at a revision is driven by the same client running the same script, so the difference between two rows of one revision is the server’s. The script is small — list, call, read, get — so these sessions evidence the common surface. Compare like with like: a row with more passes did not necessarily conform better; it may only have been asked more.

Findings are reported, not hidden

A failing row is a finding against that SDK’s pinned release, stated with the clause it breaks and a link to the report and the recording it came from. Each was read against the recording before being published — a check can only be as honest as the capture it reads, and the corpus’s own history has findings that turned out to be the tool’s, not the implementation’s (the trace corpus). A finding you believe is wrong is a bug to file with its requirement ID; the dispute process says how each is triaged.

A second opinion: mcpsnoop

mcpsnoop is the one other tool that judges a recorded MCP session offline: mcpsnoop check reads a capture and fails on a fixed set of rules. The official suite calibrates this project against live scenarios; mcpsnoop calibrates it against a reader of recordings, which is what this project is. This page is that comparison over all 358 corpus traces, with every disagreement triaged: which tool is right, by which clause, and what was done about it.

How the comparison is made

The binary. mcpsnoop 0.26.1, the current npm release when measured, taken from the npm registry and never built from source. The mcpsnoop package is a launcher with no install script; the Go binary ships in a per-platform package. Both tarballs were fetched with npm pack and checked against the registry’s integrity:

  • [email protected] — sha512-midlHuFR9PGrrW8BUm/F16k76ccPmVfE8OVBWR7DkSQzZjaGookgTH4ACpytuWnD5VnKIxvNN2Y8smwjyq4xMw==
  • @mcpsnoop/[email protected] — sha512-mNsXyDgs4vznFP6BQz9oxAJYLEo+I2YC2F9vDICd5Z4FSUPTjHWT/A6HW+i6/FvyUu0Ymf3HnMc2zM9X2xZQ2Q==

The binary inside, package/bin/mcpsnoop, has SHA-256 81c3636fa5a371833ab4558d6ac417d9d94a86f99949e0cff32bb09ccf69c2a9. To measure, point MCPSNOOP at it and run cargo xtask mcpsnoop; without it the task skips loudly. mcpsnoop is never a gate on a pull request — a scheduled, informational job re-measures it.

The same session in front of both. Our traces are converted into the capture format mcpsnoop writes itself (xtask/src/mcpsnoop/convert.rs). On stdio the conversion is lossless. On HTTP a frame gets the headers, status and exchange the trace’s own order settles — the rules our validator’s framing applies — and no more; a frame whose context the trace cannot settle is marked unframed, and a finding that reads such context there is set aside, not counted. The conversion is checked against mcpsnoop’s own recordings on every measurement: sessions recorded through both tools at once — the reference host against the everything server, committed under conformance/mcpsnoop/native/ — must draw the same mcpsnoop findings from the converted trace as from mcpsnoop’s capture once that capture’s frames share one connection, the single field the conversion fixes rather than carries. In the committed run 3 of 3 such sessions did: http-2025-11-25 (28 findings), http-2026-07-28 (22 findings), stdio-2025-11-25 (28 findings).

What is compared. mcpsnoop’s conformance signals — invalid, warn and mismatch, the three its own rule descriptions call protocol violations — against our fail and warn rows, an extension’s among them although they never decide our verdict, and the wire schema. Its other signals (a call that ended in an error, one still pending when the capture ended, a deprecated feature) are counted per trace but say nothing about conformance. Each mcpsnoop rule is mapped to the registry clauses its condition violates, read from both texts (conformance/mcpsnoop/rules.json, tabulated below); a mapping is partial where the rule checks only part of the clause. Then, per trace: a fired rule one of whose clauses we flag is agreement; a fired rule none of whose clauses we flag is a mcpsnoop-only divergence; a clause we flag that a rule maps fully, with that rule silent, is an ours-only divergence; a clause we flag that no rule maps fully is beyond mcpsnoop, a difference of scope rather than of reading.

Summary

Corpus directoryTraces
corpus/good/14
corpus/violations/106
corpus/draft/good/28
corpus/draft/violations/201
corpus/draft/captured/9

Verdicts, ours against mcpsnoop’s conformance signals:

mcpsnoop flags the tracemcpsnoop does not
our verdict fail83139
our verdict pass or pass-with-warnings4132

Traces by how the two readings relate:

RelationTraces
both clean44
agree79
beyond mcpsnoop222
divergent13

mcpsnoop flagging less than we do is mostly scope, not disagreement: its rules cover a part of the registry, and the traces it passes while we fail are the beyond mcpsnoop row. Across the corpus we flag 268 distinct clauses (counted per revision) that no mcpsnoop rule maps fully. Findings were set aside for reading HTTP context the conversion could not supply on 7 traces.

Divergences

Every divergence the run shows, and its triage (conformance/mcpsnoop/triage.json). --check fails while any divergence is untriaged or any triage entry matches nothing.

#KindSubjectTracesClassRightResolution
1mcpsnoop-onlyinvalid-frameviolations/base-007-error-code-floatgranularitybothnone: the same defect on the same frame, reported at different granularity
2mcpsnoop-onlyrequest-id-in-flightdraft/violations/base-052-null-id-error-then-id-reusedmcpsnoop-defectoursupstream: mcpsnoop-null-id-answer
3mcpsnoop-onlyserver-rejected-header-mismatchdraft/violations/tran-098-header-mismatch-without-400, draft/violations/tran-133-omitted-null-header-rejected, draft/violations/tran-135-omitted-absent-header-rejectedmcpsnoop-defectoursupstream: mcpsnoop-trusts-header-mismatch-rejection
4ours-onlyBASE-030draft/violations/base-031-malformed-meta-answered-with-result, draft/violations/base-032-invalid-params-not-http-400revision-attributionbothnone: the revision is stated to our validator out of band and is absent from the trace
5ours-onlyMRTR-002draft/violations/base-031-malformed-meta-answered-with-result, draft/violations/base-032-invalid-params-not-http-400revision-attributionbothnone: the revision is stated to our validator out of band and is absent from the trace
6ours-onlyTOOL-016draft/violations/base-031-malformed-meta-answered-with-result, draft/violations/base-032-invalid-params-not-http-400revision-attributionbothnone: the revision is stated to our validator out of band and is absent from the trace
7ours-onlyBASE-030draft/violations/vers-008-initialize-error-without-versions, draft/violations/base-030-legacy-initialize-negotiated-2026-07-28spec-ambiguityundecidedopen: decide whether a legacy-era request inside a 2026-07-28 trace is held to that revision’s request-envelope clauses (BASE-030 and its restatements, TRAN-071); the current reading is recorded in checks/stateless/meta.rs
8ours-onlyMRTR-002draft/violations/vers-008-initialize-error-without-versions, draft/violations/base-030-legacy-initialize-negotiated-2026-07-28spec-ambiguityundecidedopen: the same decision as BASE-030 on these traces
9ours-onlyTOOL-016draft/violations/vers-008-initialize-error-without-versions, draft/violations/base-030-legacy-initialize-negotiated-2026-07-28spec-ambiguityundecidedopen: the same decision as BASE-030 on these traces
10ours-onlyLIFE-018violations/tran-017-protocol-version-header-missingmcpsnoop-defectoursupstream: mcpsnoop-version-header-2025-11-25
11ours-onlyTRAN-017violations/tran-017-protocol-version-header-missingmcpsnoop-defectoursupstream: mcpsnoop-version-header-2025-11-25
12mcpsnoop-onlyresponse-unmatchedgood/http-origin-refused-without-id, draft/good/http-origin-refused-without-idmcpsnoop-defectoursupstream: mcpsnoop-origin-refusal-unmatched
13mcpsnoop-onlyprogress-after-completiongood/stdio-task-progressmcpsnoop-defectoursopen: whether to report it upstream is the maintainer’s call; the reproduction is corpus/good/stdio-task-progress.jsonl, whose task handling the 2025-11-25 tasks page defines (PROG-014)

1. invalid-frame (mcpsnoop-only)

The server answers initialize with "code": -32602.5. We report BASE-007, the clause that names the defect; mcpsnoop’s decoder refuses the frame and reports it as not JSON-RPC at all. Both are true — a non-integer code breaks JSON-RPC 2.0, which BASE-008 incorporates — and the two readings convict the same frame. The difference is only which clause carries the finding: ours attributes it to the most specific clause and leaves BASE-008 to framing faults, mcpsnoop has no rule finer than the parse.

BASE-007 (MUST): Error codes MUST be integers. — source

BASE-008 (MUST): All messages between MCP clients and servers MUST follow the JSON-RPC 2.0 specification. — source

Resolution: none: the same defect on the same frame, reported at different granularity

2. request-id-in-flight (mcpsnoop-only)

On Streamable HTTP the client POSTs tools/call id 1 and the server answers that POST, in its own application/json body, with an error whose id is null although the request’s id was readable — a BASE-052 violation both tools report. The client then sends a new tools/call reusing id 1. mcpsnoop reports the reuse as an id already in flight: having failed to match the null-id error by id, it keeps request 1 open forever. But the answer arrived as the response to that very POST, so the client has received a response for it, and BASE-045 forbids reuse only of an id the sender has “not yet received a response for”. Our framing ties a null-id answer to the request whose exchange framed it; mcpsnoop records the exchange (exchange is in its own capture) but does not use it here. The conversion is not the cause: the request and the error carry the same exchange, exactly as mcpsnoop’s proxy records them.

BASE-045 (MUST NOT): The request ID MUST NOT match the ID of any other request the sender has issued and not yet received a response for. — source

BASE-052 (MUST): Error responses MUST include the same ID as the request they correspond to (except in error cases where the ID could not be read due a malformed request). — source

Resolution: upstream: mcpsnoop-null-id-answer

3. server-rejected-header-mismatch (mcpsnoop-only)

In each trace the server answers -32020 (HeaderMismatch) to a POST whose headers match its body: a tools/list with every header correct (tran-098), and a tools/call that omits Mcp-Param-Region because region is null or absent — exactly what the client must do (tran-133, tran-135). mcpsnoop reports each as “a routing header disagrees with the body”. By its own documentation a server’s -32020 “counts as the same condition” as a disagreement, so it takes the server’s word over its own comparison — which on these frames found nothing. The defect is the server’s (TRAN-133/TRAN-135: the server MUST NOT expect the header; TRAN-098: the rejection must be 400), and mcpsnoop’s finding attributes it to the client’s headers. Where the client really was at fault (tran-089-sentinel-markers-miscased, tran-077-param-header-value-rejected, tran-079-param-header-value-not-mirrored) the same rule agrees with us.

TRAN-133 (MUST NOT): Parameter value is null | Client MUST omit the header | Server MUST NOT expect the header — source

TRAN-135 (MUST NOT): Parameter not in arguments | Client MUST omit the header | Server MUST NOT expect the header — source

TRAN-098 (MUST): Servers MUST reject requests with a 400 Bad Request HTTP status and JSON-RPC error code -32020 (HeaderMismatch) if any validation fails. — source

Resolution: upstream: mcpsnoop-trusts-header-mismatch-rejection

4. BASE-030 (ours-only)

Each trace’s only request carries "_meta": {} — no protocol version anywhere, in the body or (base-032, HTTP) in a header. We judge the trace at 2026-07-28 because the corpus files it under draft/, which is how this repository assigns a revision; mcpsnoop infers the revision from the session and finds none, so it applies no 2026-07-28 rule. Given what each tool is told, each is right: an empty _meta is a violation at 2026-07-28 and none at 2025-11-25. mcpsnoop does flag the same field set as missing wherever the session names 2026-07-28 (base-030-request-meta-missing-required-field and the probe capture agree with us).

BASE-030 (MUST): Client requests carry the following io.modelcontextprotocol/* fields in meta; fields marked as required MUST be included on every request. — source

Resolution: none: the revision is stated to our validator out of band and is absent from the trace

5. MRTR-002 (ours-only)

The same request and the same reasoning as BASE-030 above: MRTR-002 restates the rule on the multi round-trip page.

MRTR-002 (MUST): Every request MUST include the required meta fields; see meta. — source

Resolution: none: the revision is stated to our validator out of band and is absent from the trace

6. TOOL-016 (ours-only)

The same request and the same reasoning as BASE-030 above: TOOL-016 restates the required _meta fields on the tools page.

TOOL-016 (MUST): Every request MUST include the required meta fields; see meta. — source

Resolution: none: the revision is stated to our validator out of band and is absent from the trace

7. BASE-030 (ours-only)

A legacy client sends initialize naming 2025-11-25. In vers-008 the server speaks only 2026-07-28 and refuses; in base-030-legacy-initialize-negotiated-2026-07-28 it answers naming 2026-07-28 and the session goes on under it. Judging the whole trace at 2026-07-28 (the corpus’s draft/ assignment for the first; the server’s initialize result, which is the session’s revision, for the second), we report the initialize as a request missing the required _meta fields — deliberately: checks/stateless/meta.rs exempts this request from BASE-031, citing basic/versioning‘s compatibility matrix (which says of exactly this request that it “also lacks the required _meta fields”), and states that the client’s own defect is still reported, by BASE-030. mcpsnoop 0.26.1 draws nothing on either trace. Measured 2026-10-25 on edited copies of the second trace’s converted capture: it applies request-meta-missing to a request only when that request’s own _meta carries io.modelcontextprotocol/protocolVersion 2026-07-28 (an initialize whose _meta names it and lacks the other fields is flagged, so initialize is not exempt); a request with no _meta, an empty one, or one naming 2025-11-25 draws nothing, and setting the initialize params’ protocolVersion to 2026-07-28 changes nothing. So its per-request revision is the one the request states in _meta, not params.protocolVersion and not the server’s answer; an earlier version of this note said it reads the request’s own protocolVersion, which that measurement does not support. Whether a 2026-07-28 clause binds a request that is itself speaking 2025-11-25 is not settled by the text: the matrix describes the request as lacking the fields without saying the legacy client is non-conformant for it, and nothing makes a 2025-11-25 client subject to 2026-07-28. The same question sits under our TRAN-071 finding on the probe capture’s legacy initialize.

BASE-030 (MUST): Client requests carry the following io.modelcontextprotocol/* fields in meta; fields marked as required MUST be included on every request. — source

VERS-008 (SHOULD): A server that supports only modern versions SHOULD name the protocol versions it supports in any error it returns to an initialize request, on any transport: legacy clients have no fall-forward mechanism, and this message may be the only diagnostic they can surface to users. — source

Resolution: open: decide whether a legacy-era request inside a 2026-07-28 trace is held to that revision’s request-envelope clauses (BASE-030 and its restatements, TRAN-071); the current reading is recorded in checks/stateless/meta.rs

8. MRTR-002 (ours-only)

The legacy initialize of either trace above, under MRTR-002’s restatement of the same rule.

MRTR-002 (MUST): Every request MUST include the required meta fields; see meta. — source

Resolution: open: the same decision as BASE-030 on these traces

9. TOOL-016 (ours-only)

The legacy initialize of either trace above, under TOOL-016’s restatement of the same rule.

TOOL-016 (MUST): Every request MUST include the required meta fields; see meta. — source

Resolution: open: the same decision as BASE-030 on these traces

10. LIFE-018 (ours-only)

A 2025-11-25 HTTP session negotiates, then sends a request without MCP-Protocol-Version. 2025-11-25 requires the header on every request after initialization; mcpsnoop’s rule for a missing routing header (“missing where the negotiated revision requires it”) fires only at 2026-07-28. Reproduced on a session recorded by mcpsnoop’s own proxy, so the conversion is not the cause: native/version-header-omitted-2025-11-25.mcpsnoop.jsonl (initialize, notifications/initialized with the header, tools/list without it) passes mcpsnoop check with every signal enabled.

LIFE-018 (MUST): If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server. — source

TRAN-017 (MUST): If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server, allowing the MCP server to respond based on the MCP protocol version. — source

Resolution: upstream: mcpsnoop-version-header-2025-11-25

11. TRAN-017 (ours-only)

The transport page’s statement of LIFE-018’s rule; the same missing header and the same mcpsnoop gap.

TRAN-017 (MUST): If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server, allowing the MCP server to respond based on the MCP protocol version. — source

Resolution: upstream: mcpsnoop-version-header-2025-11-25

12. response-unmatched (mcpsnoop-only)

A POST carrying Host and Origin evil.example.com is refused with HTTP 403 and a JSON-RPC error that has no id; the session then proceeds from localhost. The transport’s security warning, at both revisions, describes exactly that answer: “If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden. The HTTP response body MAY comprise a JSON-RPC error response that has no id.” The refusal happens before the request is read, so there is no id to echo, and BASE-009/BASE-052’s id rule does not reach a body the transport clause permits to have none. mcpsnoop reports the id-less error as matching no request. We did the same, as BASE-009, until 2026-10-13 — on the official suite’s dns-rebinding-protection scenario against the everything server (conformance/wire-findings.json) — and these two traces are the regression pair for that fix.

BASE-009 (MUST): Error responses MUST include the same ID as the request they correspond to (except in error cases where the ID could not be read due a malformed request). — source

BASE-052 (MUST): Error responses MUST include the same ID as the request they correspond to (except in error cases where the ID could not be read due a malformed request). — source

Resolution: upstream: mcpsnoop-origin-refusal-unmatched

13. progress-after-completion (mcpsnoop-only)

A conforming 2025-11-25 task session: the server answers a task-augmented tools/call with a CreateTaskResult (seq 4) and sends progress under the request’s token while the task is working (seq 5, and seq 10 after a tasks/get that still says working); the task is completed only at seq 12. The tasks page has the token “continue to be used for progress notifications throughout the task’s lifetime, even after the CreateTaskResult has been returned” (PROG-014) and stops progress at the task’s terminal status (PROG-016), which this session respects; our validator passes it. mcpsnoop 0.26.1 warns on both notifications as arriving “after its request completed”, with the text “2026-07-28 requires them to stop”: it takes the CreateTaskResult for the request’s completion and applies that revision’s rule to a 2025-11-25 session. The same rule fires, correctly, on prog-019 (progress after an ordinary response) and, for the earlier of its two notifications, wrongly on prog-016 for the same reason as here; both traces agree with us at the trace level.

PROG-014 (MUST): For task-augmented requests, the progressToken provided in the original request MUST continue to be used for progress notifications throughout the task’s lifetime, even after the CreateTaskResult has been returned. — source

PROG-016 (MUST): Progress notifications for tasks MUST stop after the task reaches a terminal status (completed, failed, or cancelled) — source

Resolution: open: whether to report it upstream is the maintainer’s call; the reproduction is corpus/good/stdio-task-progress.jsonl, whose task handling the 2025-11-25 tasks page defines (PROG-014)

Found on live recordings

Divergences that only a session recorded by mcpsnoop itself shows, because they turn on something our trace format does not record.

response-unmatched — mcpsnoop-defect, right: ours

Recorded live: the reference host drives the everything server over Streamable HTTP at 2025-11-25, through mcp-trace-capture and mcpsnoop’s proxy at once. The server sends elicitation/create (and sampling/createMessage) on a tool call’s SSE stream; the client answers each with a new POST, as the transport requires — and its HTTP pool sends that POST on a different TCP connection. mcpsnoop pairs a server request with its answer only within one connection, so it reports each answer as “response id has no matching request” and each server request as never answered: five false warn findings and five false pending ones. Rewriting the capture’s conn_id to one value removes exactly those ten and nothing else, and our converted trace — which carries no connection — draws the remaining findings exactly. A JSON-RPC id is scoped to the session, never to a connection, and nothing obliges a client to reuse one.

Evidence: conformance/mcpsnoop/native/http-2025-11-25.mcpsnoop.jsonl, conformance/mcpsnoop/native/http-2025-11-25.trace.jsonl.

BASE-003 (MUST NOT): The request ID MUST NOT have been previously used by the requestor within the same session. — source

TRAN-024 (MUST): Every JSON-RPC message sent from the client MUST be a new HTTP POST request to the MCP endpoint. — source

Resolution: upstream: mcpsnoop-connection-scoped-ids

protocol-version-header-missing — mcpsnoop-defect, right: ours

The live reproduction of divergence LIFE-018 above: a 2025-11-25 session through mcpsnoop’s proxy whose tools/list POST omits MCP-Protocol-Version after a completed handshake. mcpsnoop check with every signal enabled exits 0.

Evidence: conformance/mcpsnoop/native/version-header-omitted-2025-11-25.mcpsnoop.jsonl.

LIFE-018 (MUST): If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server. — source

TRAN-017 (MUST): If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server, allowing the MCP server to respond based on the MCP protocol version. — source

Resolution: upstream: mcpsnoop-version-header-2025-11-25

Upstream issue drafts

Written for mcpsnoop’s tracker and not filed: filing is the owner’s call.

mcpsnoop-connection-scoped-ids: check: server-to-client requests answered on another connection are reported as unanswered (HTTP)

Version: mcpsnoop 0.26.1 (npm, linux-x64).

What happens. On a streamable-HTTP session at 2025-11-25, the server sends elicitation/create on a tools/call SSE stream. The client answers it with a new POST, as the transport requires (“Every JSON-RPC message sent from the client MUST be a new HTTP POST request to the MCP endpoint”). The client’s HTTP pool sent that POST on a different TCP connection, so it was recorded with a different conn_id. mcpsnoop check then reports response id has no matching request (warn) on the answer and elicitation/create (id 0) never got a response (pending) on the request — once per server request in the session.

Expected. A JSON-RPC id is scoped to the session, not to a connection (2025-11-25 basic: “The request ID MUST NOT have been previously used by the requestor within the same session”), and a client may send each POST on any connection. A server request should be paired with the client’s response by id within the session.

Evidence. Recorded by mcpsnoop http in front of a streamable-HTTP server, with the reference client answering four elicitation/create and one sampling/createMessage: ten findings (five warn, five pending). Rewriting every conn_id in the capture to one value and re-running check removes exactly those ten.

Suggested fix. Key outstanding server-to-client requests by session (and direction), not by conn_id; keep the connection only for routing-header attribution.

mcpsnoop-null-id-answer: check: an id-less error returned in a POST’s own body leaves the request in flight, so a later reuse is reported

Version: mcpsnoop 0.26.1.

What happens. Streamable HTTP, 2026-07-28. The client POSTs tools/call with id: 1; the server answers that POST, in its own application/json body, with {"jsonrpc":"2.0","id":null,"error":{"code":-32602,...}}. check correctly reports response id has no matching request — the id was readable, so the server should have echoed it. The client then POSTs a new request reusing id: 1, and check reports request reuses an id already in flight.

Expected. The request is not in flight: its answer arrived as the response to the very POST that carried it, and both envelopes carry the same exchange. The specification forbids reusing an id only while “the sender has issued and not yet received a response for” it. The null id is the server’s defect, already reported; the reuse is not the client’s.

Suggested fix. On HTTP, when a response with a null (or unmatched) id arrives on an exchange that carried exactly one request, close that request — or at least do not count it as in flight for the reuse rule.

mcpsnoop-trusts-header-mismatch-rejection: check: a server’s -32020 is reported as “a routing header disagrees with the body” even when every header matches

Version: mcpsnoop 0.26.1.

What happens. Streamable HTTP, 2026-07-28. A tools/call whose tool marks region with x-mcp-header, sent with "region": null and therefore no Mcp-Param-Region header — the behaviour table says “Parameter value is null | Client MUST omit the header | Server MUST NOT expect the header”. The server wrongly rejects it with -32020 (HeaderMismatch). check reports mismatch: a routing header disagrees with the body. The same happens when the parameter is absent from the arguments, and when a server answers -32020 to a tools/list whose Mcp-Method and MCP-Protocol-Version match the body.

Expected. No header disagrees with the body in these requests, and mcpsnoop’s own comparison found none. The faulty party is the server. The rule description says a server’s -32020 “counts as the same condition”, which convicts the client’s headers on the server’s word.

Suggested fix. Report a -32020 as a mismatch only when mcpsnoop’s own comparison agrees, or could not be made (for instance, the tool definition was not captured). When the comparison ran and found the headers consistent, report the rejection as the server’s — a separate message, or none.

mcpsnoop-version-header-2025-11-25: check: a missing MCP-Protocol-Version header is not reported on 2025-11-25 sessions

Version: mcpsnoop 0.26.1.

What happens. A streamable-HTTP session negotiates 2025-11-25 (initialize, then notifications/initialized carrying MCP-Protocol-Version: 2025-11-25), then POSTs tools/list without the header. check --fail-on error,invalid,warn,mismatch,pending,late-result,duplicate,deprecated,incomplete exits 0.

Expected. 2025-11-25 requires the header: “If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server.” The mismatch signal’s description already covers a routing header that “is missing where the negotiated revision requires it”; at present only 2026-07-28 is treated as requiring it.

Reproduce. mcpsnoop http --target <server>/mcp, then with curl: an initialize POST, an initialized POST with the header and the returned Mcp-Session-Id, and a tools/list POST with the session id but no MCP-Protocol-Version. Run mcpsnoop check on the session.

Suggested fix. At 2025-11-25 (and 2025-06-18), require the header on every POST after the initialize exchange.

mcpsnoop-origin-refusal-unmatched: check: the id-less error body of a 403 Origin refusal is reported as a response with no matching request

Version: mcpsnoop 0.26.1.

What happens. Streamable HTTP, 2025-11-25 or 2026-07-28. A POST arrives with Origin: http://evil.example.com; the server refuses it with 403 Forbidden and the body {"jsonrpc":"2.0","error":{"code":-32600,"message":"Forbidden: Origin not allowed"}}. check reports response id has no matching request on that error.

Expected. No finding. The transport’s security warning reads: “If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden. The HTTP response body MAY comprise a JSON-RPC error response that has no id.” The server refuses before it reads the request, so it has no id to echo, and the specification permits the body that results.

Suggested fix. When an id-less error is the body of a 403 answering a request that carried an Origin header, treat it as that exchange’s refusal rather than as an unmatched response.

The rule map

Each mcpsnoop rule, the message template that identifies it (* is any run), and the clauses it corresponds to; italic ids are partial. Fired counts traces where the rule fired and was not set aside. A rule no trace fires was read from the binary’s strings and is untested here.

RuleTemplate2025-11-252026-07-28ContextFiredReading
jsonrpc-versionjsonrpc must be 2.0BASE-008BASE-042body1A frame whose jsonrpc member is not "2.0": one of the JSON-RPC 2.0 rules every message must follow.
invalid-frameframe on the protocol channel is not valid JSON-RPCBASE-008, TRAN-004, TRAN-005BASE-042, TRAN-117, TRAN-118body3A frame that does not parse as a JSON-RPC message at all (a non-JSON line, an array, an object of no JSON-RPC shape). A frame that parses but breaks a narrower JSON-RPC rule is left to the rules below.
request-id-typerequest id has type * MCP requires a string or integer idBASE-001BASE-043body1A request id that is neither a string nor an integer.
request-id-nullrequest id is null; MCP requires a string or integer idBASE-001, BASE-002BASE-043, BASE-044body1A request id that is null. The message itself names both clauses: null is not a string or integer id, and MCP forbids it outright.
request-id-in-flightrequest reuses an id already in flightBASE-003BASE-045pairing2A request reusing the id of one of its sender’s requests that has not been answered. At 2025-11-25 the clause is wider — no reuse within the session at all — and in-flight reuse is the part mcpsnoop checks.
response-unmatchedresponse id has no matching requestBASE-004, BASE-009BASE-046, BASE-052pairing10A response whose id names no outstanding request: the clauses requiring a response to carry the id of the request it answers.
response-duplicateduplicate response for the same idBASE-004, BASE-009BASE-046, BASE-052pairing1A second response for an id already answered, which corresponds to no outstanding request.
response-without-result-or-errorresponse has neither result nor errorBASE-010BASE-047body1A response carrying neither result nor error.
response-with-result-and-errorresponse has both result and errorBASE-008BASE-042body0A response carrying both result and error, which JSON-RPC 2.0 forbids. Template read from the binary; no corpus trace exercises it.
result-type-missingresult is missing required resultType—BASE-048body1A result without resultType. The field does not exist at 2025-11-25.
client-sent-responseclient sent a JSON-RPC response, which * no longer defines—TRAN-060, TRAN-119, BASE-084body1A client sending a JSON-RPC response at 2026-07-28, where a server sends no requests to answer.
server-sent-requestserver sent a JSON-RPC request *, which * replaced with the MRTR pattern—BASE-084, MRTR-001, TRAN-066, TRAN-120body1A server-initiated JSON-RPC request at 2026-07-28, which replaced them with multi round-trip requests.
request-meta-missingrequest _meta is missing required io.modelcontextprotocol/—BASE-030, MRTR-002, TOOL-016body6A request without one of the _meta fields 2026-07-28 requires on every request.
protocol-version-header-missingrequired routing header MCP-Protocol-Version is missingLIFE-018, TRAN-017TRAN-071http1A POST without the MCP-Protocol-Version header where the negotiated revision requires it.
method-name-header-missingrequired routing header Mcp-Method is missing
required routing header Mcp-Name is missing
—TRAN-058http2A POST without its Mcp-Method or Mcp-Name request metadata header.
param-header-missingrequired routing header Mcp-Param-* is missing for body parameter—TRAN-079, TRAN-130http4A tools/call carrying a parameter its tool marks with x-mcp-header, sent without the mirrored Mcp-Param-* header.
protocol-version-header-disagreesMCP-Protocol-Version header * disagrees with _meta protocolVersion—TRAN-072http7An MCP-Protocol-Version header that differs from the body’s _meta protocol version.
method-header-disagreesrouting header Mcp-Method * disagrees with body method—TRAN-058http0An Mcp-Method header that is not the body’s method. Template read from the binary; no corpus trace exercises it.
name-header-disagreesrouting header Mcp-Name * disagrees with body name—TRAN-058, TRAN-077, TRAN-092http3An Mcp-Name header that is not the body’s name after Base64 decoding. It catches an encoding fault only when the fault changes the value: a plain value shaped like the sentinel decodes to something else (TRAN-092), while an unencoded non-ASCII name that equals the body passes (TRAN-077).
param-header-disagreesrouting header Mcp-Param-* disagrees with body parameter—TRAN-079, TRAN-086, TRAN-087, TRAN-089, TRAN-092http4An Mcp-Param-* header that is not the mirrored body value after Base64 decoding. The encoding clauses are caught only where a fault changes the value (a miscased sentinel does not decode); an unencoded value equal to the body passes.
param-header-without-valuerouting header Mcp-Param-* is present but body parameter * is absent or null—TRAN-132, TRAN-134http2An Mcp-Param-* header sent although the body parameter is null or absent.
batch-routing-headersrouting headers are invalid on a JSON-RPC batchTRAN-026TRAN-059http0Routing headers on a POST whose body is a batch. Template read from the binary; mcpsnoop reports an HTTP batch only when routing headers ride it.
server-rejected-header-mismatcha routing header disagrees with the body—TRAN-058, TRAN-071, TRAN-072, TRAN-077, TRAN-079, TRAN-086, TRAN-087, TRAN-089, TRAN-092, TRAN-130, TRAN-132, TRAN-134http10The server answered -32020 (HeaderMismatch). mcpsnoop counts the rejection itself as the routing fault, which convicts the client’s headers on the server’s word.
response-content-typethe response to a request * the transport * application/json or text/event-streamTRAN-029TRAN-063http4A POST carrying a request answered with a Content-Type other than application/json or text/event-stream, or with none.
cacheable-without-ttlcacheable * result declares no ttlMs, which * requires—CACH-001body9A complete result of a cacheable operation without ttlMs. CACH-001 asks for caching hints; mcpsnoop checks ttlMs and not cacheScope.
cache-scope-across-pagescacheScope * on a later page of * disagrees with—CACH-015, CACH-016body1Pages of one list request carrying different cacheScope values.
cache-scope-valuecacheScope is *, and * defines only public and private—wire-schemabody1A cacheScope outside public and private. No RFC 2119 clause states the enumeration; the revision’s JSON Schema does, which the report judges as its wire schema.
negative-ttlttlMs is *, and * requires a value of zero or more—CACH-008body1A negative ttlMs.
input-required-emptyInputRequiredResult carries neither inputRequests nor requestState—MRTR-011body1An InputRequiredResult with neither inputRequests nor requestState.
input-required-on-methodInputRequiredResult on *, which * allows only on—MRTR-004body1An InputRequiredResult answering a method that may not return one.
input-request-methodinputRequests names *, and * allows only—MRTR-006body1An inputRequests entry that is not an elicitation, sampling or roots request.
input-request-undeclaredinputRequests asks for * while the request declared no—BASE-034, MRTR-012, MRTR-013body3An inputRequests entry for a capability the request did not declare at all. A declared capability whose mode or sub-capability is missing (URL-mode elicitation, sampling.tools) is not compared.
request-state-changedMRTR retry changed requestState—MRTR-016, MRTR-003, MRTR-017body2A retry echoing a requestState other than the one the server sent.
request-state-missingMRTR retry is missing requestState—MRTR-016body0A retry leaving out the requestState the server sent. Template read from the binary; no corpus trace exercises it.
request-state-inventedMRTR retry invented requestState—MRTR-018body1A retry carrying a requestState the server never sent.
subscription-before-ackarrived on subscription * before its acknowledgment—SUBS-002body2A subscription notification sent before the subscription’s acknowledgment.
subscription-untaggedcarries no io.modelcontextprotocol/subscriptionId—BASE-039body1A notification on a subscription stream without io.modelcontextprotocol/subscriptionId. BASE-039 also requires the id to be the stream’s own; mcpsnoop checks presence.
subscription-unrequestedwas not requested by subscription—SUBS-001body1A notification type the subscription did not ask for. SUBS-001 binds every notification the server sends; mcpsnoop checks those that carry a subscription id.
progress-not-increasingprogress for token * went from * requires it to increasePROG-011PROG-003body2A progress value that does not increase.
progress-after-completionprogress notification for token * arrived after its request completedPROG-019PROG-008body4A progress notification after its request was answered.
server-cancellationserver cancelled *, and * lets a server cancel only a subscriptions/listen request—CANC-003, CANC-011body2A server-sent notifications/cancelled naming anything but a subscriptions/listen request.
input-schema-not-objectadvertises an inputSchema that is not a JSON objectTOOL-003TOOL-025body1A tool whose inputSchema is not a JSON Schema object.
x-mcp-header-chaindeclares an x-mcp-header under *, which * does not allow in the chain—TRAN-082, TRAN-137, TOOL-045body5An x-mcp-header on a property not reachable through properties alone.
x-mcp-header-repeateddeclares x-mcp-header * on more than one property—TRAN-080, TOOL-033body1One x-mcp-header name on two properties of a tool: one of the constraints TRAN-080 lists.
task-for-undeclaring-clienta task handle uses the io.modelcontextprotocol/tasks extension, which the client never advertised—TASK-001, VERS-005body1A CreateTaskResult answering a request whose client capabilities do not declare the tasks extension. The tasks extension’s own clause, TASK-001 — reported in the extension’s block and never scored — names exactly this; mcpsnoop cites the core negotiation clause VERS-005, of which it is one case and which our registry excludes because each extension, not the core, says what falling back means.

Every trace

TraceRevisionOursWe flagmcpsnoopRules firedSet asideRelation
good/http-authorization2025-11-25pass—silent——both clean
good/http-origin-refused-without-id2025-11-25pass—flagsresponse-unmatched—divergent
good/http-session2025-11-25pass—silent——both clean
good/stdio-cancellation2025-11-25pass—silent——both clean
good/stdio-client-features2025-11-25pass—silent——both clean
good/stdio-feature-session2025-11-25pass—silent——both clean
good/stdio-full-session2025-11-25pass—silent——both clean
good/stdio-minimal-init2025-11-25pass—silent——both clean
good/stdio-progress2025-11-25pass—silent——both clean
good/stdio-request-unanswered-at-close2025-11-25pass—silent——both clean
good/stdio-requests-partly-unanswered2025-11-25pass—silent——both clean
good/stdio-server-negotiates-2024-11-052025-11-25pass—silent——both clean
good/stdio-task-progress2025-11-25pass—flagsprogress-after-completion—divergent
good/stdio-tool-image-base64-padding2025-11-25pass—silent——both clean
violations/auth-014-challenge-without-scope2025-11-25pass-with-warningsAUTH-014silent——beyond mcpsnoop
violations/auth-045-invalid-token-answered-4032025-11-25failAUTH-045, AUTH-049silent——beyond mcpsnoop
violations/auth-049-insufficient-scope-answered-4012025-11-25failAUTH-049, AUTH-050silent——beyond mcpsnoop
violations/auth-050-insufficient-scope-without-resource-metadata2025-11-25pass-with-warningsAUTH-050silent——beyond mcpsnoop
violations/auth-051-insufficient-scope-names-no-scope2025-11-25pass-with-warningsAUTH-050, AUTH-051silent——beyond mcpsnoop
violations/base-001-request-id-boolean2025-11-25failBASE-001, wire-schemaflagsrequest-id-type—agree
violations/base-002-request-id-null2025-11-25failBASE-001, BASE-002, wire-schemaflagsrequest-id-null—agree
violations/base-003-request-id-reuse2025-11-25failBASE-003silent——beyond mcpsnoop
violations/base-004-request-answered-twice-cross-flavor2025-11-25failBASE-004, PING-001flagsresponse-duplicate—agree
violations/base-004-result-unknown-id2025-11-25failBASE-004flagsresponse-unmatched—agree
violations/base-005-notification-with-id2025-11-25failBASE-005, LIFE-003silent——beyond mcpsnoop
violations/base-006-error-missing-message2025-11-25failBASE-006, wire-schemasilent——beyond mcpsnoop
violations/base-007-error-code-float2025-11-25failBASE-007, wire-schemaflagsinvalid-frame—divergent
violations/base-008-jsonrpc-version2025-11-25failBASE-008, wire-schemaflagsjsonrpc-version—agree
violations/base-009-error-unknown-id2025-11-25failBASE-009flagsresponse-unmatched—agree
violations/base-010-response-without-result2025-11-25failBASE-008, BASE-010, TRAN-004, wire-schemaflagsresponse-without-result-or-error—agree
violations/base-017-input-schema-invalid2025-11-25failBASE-017silent——beyond mcpsnoop
violations/base-019-meta-key-bad-prefix2025-11-25failBASE-019, BASE-020silent——beyond mcpsnoop
violations/base-086-input-schema-draft-072025-11-25pass-with-warningsBASE-086silent——beyond mcpsnoop
violations/canc-018-cancellation-of-unknown-request2025-11-25failCANC-018silent——beyond mcpsnoop
violations/canc-019-initialize-cancelled2025-11-25failCANC-019silent——beyond mcpsnoop
violations/canc-020-task-cancelled-by-notification2025-11-25failCANC-020silent——beyond mcpsnoop
violations/canc-021-response-after-cancellation-read2025-11-25pass-with-warningsCANC-021silent——beyond mcpsnoop
violations/comp-001-capability-undeclared2025-11-25failCOMP-001, LIFE-009silent——beyond mcpsnoop
violations/elic-004-answered-without-capability2025-11-25failELIC-004, ELIC-006, LIFE-009silent——beyond mcpsnoop
violations/elic-006-url-mode-undeclared2025-11-25failELIC-006, LIFE-009silent——beyond mcpsnoop
violations/elic-007-request-without-message2025-11-25failELIC-007, wire-schemasilent——beyond mcpsnoop
violations/elic-009-form-without-schema2025-11-25failELIC-009, wire-schemasilent——beyond mcpsnoop
violations/elic-011-url-without-elicitation-id2025-11-25failELIC-011, wire-schemasilent——beyond mcpsnoop
violations/elic-012-url-without-scheme2025-11-25failELIC-012silent——beyond mcpsnoop
violations/elic-014-complete-names-unknown-id2025-11-25failELIC-014silent——beyond mcpsnoop
violations/elic-018-url-required-error-without-list2025-11-25failELIC-018, wire-schemasilent——beyond mcpsnoop
violations/elic-019-url-required-error-form-mode2025-11-25failELIC-019, wire-schemasilent——beyond mcpsnoop
violations/elic-021-undeclared-mode-accepted2025-11-25failELIC-006, ELIC-021, LIFE-009silent——beyond mcpsnoop
violations/elic-049-accepted-content-violates-schema2025-11-25pass-with-warningsELIC-049silent——beyond mcpsnoop
violations/life-001-first-message-not-initialize2025-11-25failLIFE-001, LIFE-004silent——beyond mcpsnoop
violations/life-002-initialize-missing-protocolversion2025-11-25failLIFE-002, LIFE-007, wire-schemasilent——beyond mcpsnoop
violations/life-003-missing-initialized2025-11-25failLIFE-003silent——beyond mcpsnoop
violations/life-004-client-request-before-init-response2025-11-25pass-with-warningsLIFE-004silent——beyond mcpsnoop
violations/life-005-server-request-before-initialized2025-11-25pass-with-warningsLIFE-005silent——beyond mcpsnoop
violations/life-006-result-version-invalid2025-11-25failLIFE-006silent——beyond mcpsnoop
violations/life-007-initialize-protocolversion-not-string2025-11-25failLIFE-002, LIFE-007, wire-schemasilent——beyond mcpsnoop
violations/life-009-undeclared-capability-use2025-11-25failLIFE-009silent——beyond mcpsnoop
violations/life-010-initialize-result-missing-capabilities2025-11-25failLIFE-010, wire-schemasilent——beyond mcpsnoop
violations/log-001-capability-undeclared2025-11-25failLIFE-009, LOG-001silent——beyond mcpsnoop
violations/page-002-cursor-never-issued2025-11-25failPAGE-002, PAGE-003silent——beyond mcpsnoop
violations/page-002-fabricated-cursor-rejected2025-11-25failPAGE-002silent——beyond mcpsnoop
violations/ping-001-ping-answered-with-content2025-11-25failPING-001silent——beyond mcpsnoop
violations/prog-009-progress-token-float2025-11-25failPROG-009, wire-schemasilent——beyond mcpsnoop
violations/prog-010-progress-token-reused-in-flight2025-11-25failPROG-010silent——beyond mcpsnoop
violations/prog-011-progress-value-decreases2025-11-25failPROG-011flagsprogress-not-increasing—agree
violations/prog-013-progress-token-never-issued2025-11-25failPROG-013silent——beyond mcpsnoop
violations/prog-015-task-progress-token-differs2025-11-25failPROG-013, PROG-014, PROG-015silent——beyond mcpsnoop
violations/prog-016-task-progress-after-terminal-status2025-11-25failPROG-013, PROG-016, PROG-019flagsprogress-after-completion—agree
violations/prog-019-progress-after-completion2025-11-25failPROG-013, PROG-016, PROG-019flagsprogress-after-completion—agree
violations/prom-001-capability-undeclared2025-11-25failLIFE-009, PROM-001silent——beyond mcpsnoop
violations/prom-003-image-data-invalid2025-11-25failPROM-003, wire-schemasilent——beyond mcpsnoop
violations/prom-004-audio-data-invalid2025-11-25failPROM-004, wire-schemasilent——beyond mcpsnoop
violations/prom-005-embedded-resource-malformed2025-11-25failPROM-005, wire-schemasilent——beyond mcpsnoop
violations/prom-008-required-argument-unvalidated2025-11-25pass-with-warningsPROM-008silent——beyond mcpsnoop
violations/res-001-capability-undeclared2025-11-25failLIFE-009, RES-001silent——beyond mcpsnoop
violations/res-004-uri-bad-scheme2025-11-25failRES-004silent——beyond mcpsnoop
violations/res-006-blob-not-base642025-11-25failRES-006, wire-schemasilent——beyond mcpsnoop
violations/root-001-answered-without-capability2025-11-25failLIFE-009, ROOT-001, ROOT-004, ROOT-009silent——beyond mcpsnoop
violations/root-003-root-not-file-uri2025-11-25failROOT-003silent——beyond mcpsnoop
violations/root-004-unsupported-answered-internal-error2025-11-25failLIFE-009, ROOT-004, ROOT-009silent——beyond mcpsnoop
violations/root-009-asked-without-capability2025-11-25failLIFE-009, ROOT-009silent——beyond mcpsnoop
violations/samp-004-tools-without-sampling-tools2025-11-25failLIFE-009, SAMP-004silent——beyond mcpsnoop
violations/samp-005-answered-without-capability2025-11-25failLIFE-009, SAMP-005silent——beyond mcpsnoop
violations/samp-006-include-context-deprecated2025-11-25pass-with-warningsSAMP-006silent——beyond mcpsnoop
violations/samp-007-include-context-undeclared2025-11-25pass-with-warningsSAMP-006, SAMP-007silent——beyond mcpsnoop
violations/samp-008-tool-results-mixed2025-11-25failSAMP-008, SAMP-009, SAMP-019silent——beyond mcpsnoop
violations/samp-009-tool-use-unanswered2025-11-25failSAMP-009, SAMP-019silent——beyond mcpsnoop
violations/samp-010-required-tool-choice-ignored2025-11-25failSAMP-010silent——beyond mcpsnoop
violations/samp-011-none-tool-choice-ignored2025-11-25failSAMP-011silent——beyond mcpsnoop
violations/samp-012-malformed-tool-messages-answered2025-11-25failSAMP-009, SAMP-012, SAMP-019silent——beyond mcpsnoop
violations/tool-001-capability-undeclared2025-11-25failLIFE-009, TOOL-001silent——beyond mcpsnoop
violations/tool-003-input-schema-null2025-11-25failBASE-017, TOOL-003, wire-schemaflagsinput-schema-not-object—agree
violations/tool-005-name-length2025-11-25pass-with-warningsTOOL-005silent——beyond mcpsnoop
violations/tool-006-name-charset2025-11-25pass-with-warningsTOOL-006, TOOL-007silent——beyond mcpsnoop
violations/tool-008-name-duplicate2025-11-25pass-with-warningsTOOL-008silent——beyond mcpsnoop
violations/tool-009-embedded-resource-without-capability2025-11-25pass-with-warningsTOOL-009silent——beyond mcpsnoop
violations/tool-010-structured-without-text2025-11-25pass-with-warningsTOOL-010silent——beyond mcpsnoop
violations/tool-011-output-schema-no-structured-result2025-11-25failTOOL-011silent——beyond mcpsnoop
violations/tool-011-structured-content-not-conforming2025-11-25failTOOL-011silent——beyond mcpsnoop
violations/tran-004-stdout-invalid-message2025-11-25failBASE-008, TRAN-004, wire-schemaflagsinvalid-frame—agree
violations/tran-005-stdin-invalid-message2025-11-25failBASE-008, TRAN-005, wire-schemaflagsinvalid-frame—agree
violations/tran-011-session-id-invisible-ascii2025-11-25failTRAN-011silent——beyond mcpsnoop
violations/tran-013-session-id-not-echoed2025-11-25failTRAN-013silent——beyond mcpsnoop
violations/tran-017-protocol-version-header-missing2025-11-25failLIFE-018, TRAN-017silent——divergent
violations/tran-018-declined-version-header-refused-4002025-11-25pass-with-warningsTRAN-018silent——beyond mcpsnoop
violations/tran-018-protocol-version-mismatched2025-11-25pass-with-warningsTRAN-018silent——beyond mcpsnoop
violations/tran-020-declined-version-header-accepted2025-11-25failTRAN-018, TRAN-020silent——beyond mcpsnoop
violations/tran-025-accept-header-json-missing2025-11-25failTRAN-025silent——beyond mcpsnoop
violations/tran-025-accept-header-missing2025-11-25failTRAN-025silent——beyond mcpsnoop
violations/tran-026-http-post-batch2025-11-25failBASE-008, TRAN-026, wire-schemasilent——beyond mcpsnoop
violations/tran-027-notification-answered-2002025-11-25failTRAN-027silent——beyond mcpsnoop
violations/tran-029-content-type-unexpected2025-11-25failTRAN-029flagsresponse-content-type—agree
violations/tran-029-error-without-content-type2025-11-25failTRAN-029flagsresponse-content-type—agree
violations/tran-039-get-accept-header-missing2025-11-25failTRAN-039silent——beyond mcpsnoop
violations/tran-040-get-answered-json2025-11-25failTRAN-040silent——beyond mcpsnoop
violations/tran-049-message-not-posted2025-11-25failTRAN-049silent——beyond mcpsnoop
violations/wire-schema-content-block-unknown-type2025-11-25failwire-schemasilent——beyond mcpsnoop
violations/wire-schema-log-level-not-in-enum2025-11-25failwire-schemasilent——beyond mcpsnoop
violations/wire-schema-resource-missing-uri2025-11-25failwire-schemasilent——beyond mcpsnoop
violations/wire-schema-tool-image-data-not-base642025-11-25failwire-schemasilent——beyond mcpsnoop
draft/good/caching-ttl-written-as-float2026-07-28pass—silent——both clean
draft/good/client-features-mrtr2026-07-28pass—silent——both clean
draft/good/http-authorization2026-07-28pass—silent——both clean
draft/good/http-close-after-another-response2026-07-28pass—silent——both clean
draft/good/http-concurrent-posts2026-07-28pass—silent—method-name-header-missing, protocol-version-header-missingboth clean
draft/good/http-dual-era-modern-request-first2026-07-28pass—silent——both clean
draft/good/http-origin-refused-without-id2026-07-28pass—flagsresponse-unmatched—divergent
draft/good/http-stream-closed-by-client2026-07-28pass—silent——both clean
draft/good/http-upstream-abort2026-07-28pass—silent——both clean
draft/good/http-x-mcp-header-omitted2026-07-28pass—silent——both clean
draft/good/input-required-id-reused2026-07-28pass—silent——both clean
draft/good/logging-per-request2026-07-28pass—silent——both clean
draft/good/mrtr-parallel-rounds2026-07-28pass—silent——both clean
draft/good/mrtr-retry-round-before-recording2026-07-28pass—silent——both clean
draft/good/resource-read-existing-empty2026-07-28pass—silent——both clean
draft/good/skills-session2026-07-28pass—silent——both clean
draft/good/stateless-session2026-07-28pass—silent——both clean
draft/good/stdio-cancellation-race2026-07-28pass—silent——both clean
draft/good/stdio-cancelled-id-reused2026-07-28pass—silent——both clean
draft/good/stdio-discover-preferred-version-unsupported2026-07-28pass—silent——both clean
draft/good/stdio-progress2026-07-28pass—silent——both clean
draft/good/stdio-x-mcp-header-large-integer2026-07-28pass—silent——both clean
draft/good/streamable-http-session2026-07-28pass—silent——both clean
draft/good/subscription-id-reused-after-close2026-07-28pass—silent——both clean
draft/good/subscription-server-teardown2026-07-28pass—silent——both clean
draft/good/tasks-http2026-07-28pass—silent——both clean
draft/good/tasks-session2026-07-28pass—silent——both clean
draft/good/traceparent-higher-version2026-07-28pass—silent——both clean
draft/violations/auth-097-challenge-without-scope2026-07-28pass-with-warningsAUTH-097silent——beyond mcpsnoop
draft/violations/auth-124-invalid-token-answered-4032026-07-28failAUTH-124, AUTH-130silent——beyond mcpsnoop
draft/violations/auth-129-offline-access-challenged2026-07-28pass-with-warningsAUTH-129silent——beyond mcpsnoop
draft/violations/auth-130-insufficient-scope-answered-4012026-07-28failAUTH-130, AUTH-131silent——beyond mcpsnoop
draft/violations/auth-131-insufficient-scope-without-resource-metadata2026-07-28pass-with-warningsAUTH-131silent——beyond mcpsnoop
draft/violations/auth-132-insufficient-scope-names-no-scope2026-07-28pass-with-warningsAUTH-131, AUTH-132silent——beyond mcpsnoop
draft/violations/auth-133-scope-challenged-incrementally2026-07-28pass-with-warningsAUTH-133silent——beyond mcpsnoop
draft/violations/base-030-legacy-initialize-negotiated-2026-07-282026-07-28failBASE-030, COMP-006, DISC-002, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, TRAN-128silent——divergent
draft/violations/base-030-malformed-request-id-reused-correctly2026-07-28failBASE-030, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schemaflagsrequest-meta-missing—agree
draft/violations/base-030-malformed-request-refused-without-id2026-07-28failBASE-030, BASE-052, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schemaflagsrequest-meta-missing, response-unmatched—agree
draft/violations/base-030-request-meta-missing-required-field2026-07-28failBASE-030, BASE-031, CACH-001, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schemaflagscacheable-without-ttl, request-meta-missing—agree
draft/violations/base-030-unsupported-version-refused-as-invalid-params2026-07-28failBASE-030, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schemaflagsrequest-meta-missing—agree
draft/violations/base-031-malformed-meta-answered-with-result2026-07-28failBASE-030, BASE-031, CACH-001, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schemasilent——divergent
draft/violations/base-031-reused-id-malformed-answered2026-07-28failBASE-030, BASE-031, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schemaflagsrequest-meta-missing—agree
draft/violations/base-032-invalid-params-not-http-4002026-07-28failBASE-030, BASE-032, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schemasilent——divergent
draft/violations/base-033-request-without-client-info2026-07-28pass-with-warningsBASE-033silent——beyond mcpsnoop
draft/violations/base-034-input-request-for-undeclared-capability2026-07-28failBASE-034, ELIC-059, MRTR-012, MRTR-013, wire-schemaflagsinput-request-undeclared—agree
draft/violations/base-034-input-request-for-undeclared-mode2026-07-28failBASE-034, ELIC-059, MRTR-012, MRTR-013, SAMP-026silent——beyond mcpsnoop
draft/violations/base-035-missing-capability-error-without-capabilities2026-07-28failBASE-035, wire-schemasilent——beyond mcpsnoop
draft/violations/base-036-missing-capability-not-http-4002026-07-28failBASE-035, BASE-036, TRAN-063, wire-schemaflagsresponse-content-typemethod-name-header-missing, protocol-version-header-missingagree
draft/violations/base-037-result-without-server-info2026-07-28pass-with-warningsBASE-037silent——beyond mcpsnoop
draft/violations/base-039-subscription-notification-mistagged2026-07-28failBASE-039silent——beyond mcpsnoop
draft/violations/base-039-subscription-notification-untagged2026-07-28failBASE-039, wire-schemaflagssubscription-untagged—agree
draft/violations/base-040-malformed-traceparent2026-07-28failBASE-040, CACH-001, wire-schemaflagscacheable-without-ttl—agree
draft/violations/base-040-traceparent-version-ff2026-07-28failBASE-040silent——beyond mcpsnoop
draft/violations/base-045-request-id-reused-in-flight2026-07-28failBASE-045, CACH-001, wire-schemaflagscacheable-without-ttl, request-id-in-flight—agree
draft/violations/base-048-result-without-result-type2026-07-28failBASE-048, wire-schemaflagsresult-type-missing—agree
draft/violations/base-052-null-id-error-then-id-reused2026-07-28failBASE-052, wire-schemaflagsrequest-id-in-flight, response-unmatched—divergent
draft/violations/base-057-undefined-reserved-error-code2026-07-28failBASE-057silent——beyond mcpsnoop
draft/violations/base-058-withdrawn-error-code2026-07-28failBASE-058, BASE-083, wire-schemasilent——beyond mcpsnoop
draft/violations/base-060-app-code-in-reserved-range2026-07-28pass-with-warningsBASE-060silent——beyond mcpsnoop
draft/violations/base-073-input-schema-invalid2026-07-28failBASE-073silent——beyond mcpsnoop
draft/violations/base-083-legacy-error-code2026-07-28pass-with-warningsBASE-083silent——beyond mcpsnoop
draft/violations/base-088-input-schema-draft-072026-07-28pass-with-warningsBASE-088silent——beyond mcpsnoop
draft/violations/cach-001-cacheable-result-without-hints2026-07-28failCACH-001, wire-schemaflagscacheable-without-ttl—agree
draft/violations/cach-001-ttl-without-cache-scope2026-07-28failCACH-001, wire-schemasilent——beyond mcpsnoop
draft/violations/cach-008-negative-ttl2026-07-28failCACH-008, wire-schemaflagsnegative-ttl—agree
draft/violations/cach-015-page-scope-changes2026-07-28failCACH-015, CACH-016flagscache-scope-across-pages—agree
draft/violations/canc-003-server-cancels-tool-call2026-07-28failCANC-003, CANC-011flagsserver-cancellation—agree
draft/violations/comp-007-completions-undeclared2026-07-28failCOMP-007silent——beyond mcpsnoop
draft/violations/disc-001-server-discover-method-not-found2026-07-28failDISC-001, VERS-003silent——beyond mcpsnoop
draft/violations/disc-002-dual-era-client-skips-probe2026-07-28pass-with-warningsDISC-002, TRAN-128silent——beyond mcpsnoop
draft/violations/disc-003-discover-result-without-server-info2026-07-28pass-with-warningsBASE-037, DISC-003silent——beyond mcpsnoop
draft/violations/elic-057-answered-without-capability2026-07-28failELIC-057silent——beyond mcpsnoop
draft/violations/elic-059-url-mode-undeclared2026-07-28failBASE-034, ELIC-059, MRTR-012, MRTR-013silent——beyond mcpsnoop
draft/violations/elic-060-request-without-message2026-07-28failELIC-060, wire-schemasilent——beyond mcpsnoop
draft/violations/elic-062-form-without-schema2026-07-28failELIC-062, wire-schemasilent——beyond mcpsnoop
draft/violations/elic-064-url-mode-without-url2026-07-28failELIC-064, wire-schemasilent——beyond mcpsnoop
draft/violations/elic-065-url-not-a-valid-url2026-07-28failELIC-065silent——beyond mcpsnoop
draft/violations/elic-095-accepted-content-violates-schema2026-07-28pass-with-warningsELIC-095silent——beyond mcpsnoop
draft/violations/log-007-logging-undeclared2026-07-28failLOG-007, TOOL-019, wire-schemasilent——beyond mcpsnoop
draft/violations/log-008-log-for-request-without-level2026-07-28failLOG-008silent——beyond mcpsnoop
draft/violations/log-008-log-without-requested-level2026-07-28failLOG-008, wire-schemasilent——beyond mcpsnoop
draft/violations/log-009-log-on-subscription-stream2026-07-28failLOG-009, wire-schemasilent——beyond mcpsnoop
draft/violations/log-009-log-with-no-request-in-flight2026-07-28failLOG-009silent——beyond mcpsnoop
draft/violations/log-010-unrecognized-log-level-accepted2026-07-28failLOG-010, wire-schemasilent——beyond mcpsnoop
draft/violations/mrtr-004-input-required-on-unsupported-method2026-07-28failMRTR-004flagsinput-required-on-method—agree
draft/violations/mrtr-006-input-request-unknown-method2026-07-28failMRTR-006, wire-schemaflagsinput-request-method—agree
draft/violations/mrtr-011-input-required-empty2026-07-28failMRTR-011flagsinput-required-empty—agree
draft/violations/mrtr-015-retry-without-input-responses2026-07-28failMRTR-015silent——beyond mcpsnoop
draft/violations/mrtr-016-parallel-retry-alters-state2026-07-28failMRTR-003, MRTR-016, MRTR-017flagsrequest-state-changed—agree
draft/violations/mrtr-016-request-state-not-echoed2026-07-28failMRTR-003, MRTR-016, MRTR-017flagsrequest-state-changed—agree
draft/violations/mrtr-018-unsolicited-request-state2026-07-28failMRTR-018flagsrequest-state-invented—agree
draft/violations/mrtr-019-retry-reuses-id2026-07-28failMRTR-019, TOOL-023silent——beyond mcpsnoop
draft/violations/mrtr-020-request-state-on-another-method2026-07-28failMRTR-020silent——beyond mcpsnoop
draft/violations/mrtr-024-shortfall-answered-with-error2026-07-28failMRTR-015, MRTR-024silent——beyond mcpsnoop
draft/violations/prog-001-progress-token-not-integer2026-07-28failPROG-001, wire-schemasilent——beyond mcpsnoop
draft/violations/prog-002-progress-token-reused-in-flight2026-07-28failPROG-002silent——beyond mcpsnoop
draft/violations/prog-003-progress-value-decreases2026-07-28failPROG-003flagsprogress-not-increasing—agree
draft/violations/prog-008-progress-after-completion2026-07-28failPROG-005, PROG-008flagsprogress-after-completion—agree
draft/violations/prom-012-prompts-undeclared2026-07-28failPROM-012silent——beyond mcpsnoop
draft/violations/prom-013-declared-prompts-list-unimplemented2026-07-28failPROM-013silent——beyond mcpsnoop
draft/violations/res-012-resources-undeclared2026-07-28failRES-012silent——beyond mcpsnoop
draft/violations/res-013-declared-resources-list-unimplemented2026-07-28failRES-013silent——beyond mcpsnoop
draft/violations/res-022-read-empty-contents2026-07-28failRES-022silent——beyond mcpsnoop
draft/violations/root-013-answered-without-capability2026-07-28failROOT-013silent——beyond mcpsnoop
draft/violations/root-014-root-not-file-uri2026-07-28failROOT-014silent——beyond mcpsnoop
draft/violations/root-018-asked-without-capability2026-07-28failBASE-034, MRTR-012, MRTR-013, ROOT-018flagsinput-request-undeclared—agree
draft/violations/samp-026-tools-without-sampling-tools2026-07-28failBASE-034, MRTR-012, MRTR-013, SAMP-026silent——beyond mcpsnoop
draft/violations/samp-027-answered-without-capability2026-07-28failSAMP-027silent——beyond mcpsnoop
draft/violations/samp-028-include-context-deprecated2026-07-28pass-with-warningsSAMP-028silent——beyond mcpsnoop
draft/violations/samp-029-include-context-undeclared2026-07-28pass-with-warningsSAMP-028, SAMP-029silent——beyond mcpsnoop
draft/violations/samp-030-tool-results-mixed2026-07-28failSAMP-030, SAMP-031, SAMP-042silent——beyond mcpsnoop
draft/violations/samp-031-tool-use-unanswered2026-07-28failSAMP-031, SAMP-042silent——beyond mcpsnoop
draft/violations/samp-032-required-tool-choice-ignored2026-07-28failSAMP-032silent——beyond mcpsnoop
draft/violations/samp-033-none-tool-choice-ignored2026-07-28failSAMP-033silent——beyond mcpsnoop
draft/violations/samp-034-message-without-role2026-07-28failSAMP-034, wire-schemasilent——beyond mcpsnoop
draft/violations/skill-003-directory-read-not-implemented2026-07-28passSKILL-003silent——beyond mcpsnoop
draft/violations/skill-004-directory-read-undeclared2026-07-28passSKILL-004silent——beyond mcpsnoop
draft/violations/skill-005-skills-without-resources2026-07-28passSKILL-005silent——beyond mcpsnoop
draft/violations/skill-007-entry-uri-not-skill-md2026-07-28passSKILL-007silent——beyond mcpsnoop
draft/violations/skill-008-skill-md-without-frontmatter2026-07-28passSKILL-008silent——beyond mcpsnoop
draft/violations/skill-011-skill-under-other-scheme2026-07-28passSKILL-011silent——beyond mcpsnoop
draft/violations/skill-012-path-not-ending-in-name2026-07-28passSKILL-012, SKILL-027silent——beyond mcpsnoop
draft/violations/skill-013-name-breaks-naming-rules2026-07-28passSKILL-013silent——beyond mcpsnoop
draft/violations/skill-014-authority-not-reg-name2026-07-28passSKILL-014silent——beyond mcpsnoop
draft/violations/skill-015-prefix-segment-invalid2026-07-28passSKILL-015silent——beyond mcpsnoop
draft/violations/skill-018-skill-md-not-markdown2026-07-28passSKILL-018silent——beyond mcpsnoop
draft/violations/skill-019-resource-name-not-skill-name2026-07-28passSKILL-019silent——beyond mcpsnoop
draft/violations/skill-020-resource-description-differs2026-07-28passSKILL-020silent——beyond mcpsnoop
draft/violations/skill-026-entry-frontmatter-differs2026-07-28passSKILL-026silent——beyond mcpsnoop
draft/violations/skill-029-listing-names-collide2026-07-28passSKILL-029silent——beyond mcpsnoop
draft/violations/skill-034-entry-without-resources2026-07-28passSKILL-034silent——beyond mcpsnoop
draft/violations/skill-035-manifest-without-skill-md2026-07-28passSKILL-035silent——beyond mcpsnoop
draft/violations/skill-036-manifest-file-outside-skill2026-07-28passSKILL-036silent——beyond mcpsnoop
draft/violations/skill-037-manifest-entry-without-size2026-07-28passSKILL-037silent——beyond mcpsnoop
draft/violations/skill-042-skill-beyond-limits2026-07-28passSKILL-042silent——beyond mcpsnoop
draft/violations/skill-044-list-not-implemented2026-07-28passSKILL-002, SKILL-044silent——beyond mcpsnoop
draft/violations/skill-045-list-result-type-not-complete2026-07-28passSKILL-045silent——beyond mcpsnoop
draft/violations/skill-047-list-without-cache-fields2026-07-28passSKILL-047silent——beyond mcpsnoop
draft/violations/skill-048-get-not-implemented2026-07-28passSKILL-002, SKILL-048silent——beyond mcpsnoop
draft/violations/skill-049-get-result-type-not-complete2026-07-28passSKILL-049silent——beyond mcpsnoop
draft/violations/skill-050-get-names-a-directory2026-07-28passSKILL-050silent——beyond mcpsnoop
draft/violations/skill-051-unknown-skill-wrong-error2026-07-28passSKILL-051, SKILL-071silent——beyond mcpsnoop
draft/violations/skill-053-get-without-cache-fields2026-07-28passSKILL-053silent——beyond mcpsnoop
draft/violations/skill-065-directory-result-type-not-complete2026-07-28passSKILL-065silent——beyond mcpsnoop
draft/violations/skill-066-directory-read-of-a-file-answered2026-07-28passSKILL-066, SKILL-071silent——beyond mcpsnoop
draft/violations/skill-067-skill-directory-refused2026-07-28passSKILL-067silent——beyond mcpsnoop
draft/violations/skill-100-directory-result-without-cache-fields2026-07-28passSKILL-100silent——beyond mcpsnoop
draft/violations/subs-001-cancelled-for-another-request2026-07-28failCANC-003, CANC-011, SUBS-001flagsserver-cancellation—agree
draft/violations/subs-001-unrequested-notification-type2026-07-28failSUBS-001, wire-schemaflagssubscription-unrequested—agree
draft/violations/subs-002-notification-before-acknowledgment2026-07-28failSUBS-002, wire-schemaflagssubscription-before-ack—agree
draft/violations/subs-002-reused-id-first-opening-unacknowledged2026-07-28failSUBS-002flagssubscription-before-ack—agree
draft/violations/subs-006-graceful-close-result-not-empty2026-07-28pass-with-warningsSUBS-005, SUBS-006silent——beyond mcpsnoop
draft/violations/task-001-task-for-undeclaring-client2026-07-28passTASK-001flagstask-for-undeclaring-client—agree
draft/violations/task-006-task-without-result-type2026-07-28failTASK-006, wire-schemasilent——beyond mcpsnoop
draft/violations/task-007-task-result-type-without-task2026-07-28passTASK-007silent——beyond mcpsnoop
draft/violations/task-015-get-result-not-a-task2026-07-28passTASK-015silent——beyond mcpsnoop
draft/violations/task-015-input-required-without-requests2026-07-28passTASK-015silent——beyond mcpsnoop
draft/violations/task-017-get-result-type-not-complete2026-07-28passTASK-017, TASK-028, TASK-032silent——beyond mcpsnoop
draft/violations/task-022-input-request-key-reused2026-07-28passTASK-022, TASK-023silent——beyond mcpsnoop
draft/violations/task-024-update-ack-not-empty2026-07-28passTASK-024, TASK-030silent——beyond mcpsnoop
draft/violations/task-026-input-answered-twice2026-07-28passTASK-026silent——beyond mcpsnoop
draft/violations/task-029-task-cancelled-by-notification2026-07-28passTASK-029silent——beyond mcpsnoop
draft/violations/task-033-task-listen-without-declaration2026-07-28passTASK-033, TASK-038silent——beyond mcpsnoop
draft/violations/task-034-progress-on-task-stream2026-07-28failLOG-007, LOG-008, LOG-009, SUBS-001, TASK-034silent——beyond mcpsnoop
draft/violations/task-035-mcp-name-missing2026-07-28passTASK-035silent——beyond mcpsnoop
draft/violations/task-039-get-without-declaration2026-07-28passTASK-039silent——beyond mcpsnoop
draft/violations/task-041-failed-without-status-message2026-07-28passTASK-041silent——beyond mcpsnoop
draft/violations/task-042-failed-without-error2026-07-28passTASK-015, TASK-042, TASK-045silent——beyond mcpsnoop
draft/violations/task-043-tool-error-reported-failed2026-07-28passTASK-008, TASK-043, TASK-044silent——beyond mcpsnoop
draft/violations/task-046-completed-without-result2026-07-28passTASK-015, TASK-044, TASK-046silent——beyond mcpsnoop
draft/violations/tool-019-tools-undeclared2026-07-28failTOOL-019silent——beyond mcpsnoop
draft/violations/tool-020-declared-tools-list-unimplemented2026-07-28failTOOL-020silent——beyond mcpsnoop
draft/violations/tool-022-tools-list-order-changes2026-07-28pass-with-warningsTOOL-022silent——beyond mcpsnoop
draft/violations/tool-034-mirrored-integer-out-of-range2026-07-28failTOOL-034, TRAN-081silent—method-name-header-missing, param-header-missing, protocol-version-header-missingbeyond mcpsnoop
draft/violations/tool-038-embedded-resource-undeclared2026-07-28pass-with-warningsTOOL-038silent——beyond mcpsnoop
draft/violations/tool-040-structured-content-not-conforming2026-07-28failTOOL-040silent——beyond mcpsnoop
draft/violations/tool-045-x-mcp-header-in-defs2026-07-28failTOOL-045, TRAN-082, TRAN-137flagsx-mcp-header-chainmethod-name-header-missing, protocol-version-header-missingagree
draft/violations/tran-058-mcp-name-mismatch-refused-without-id2026-07-28failBASE-052, TRAN-058, TRAN-141, wire-schemaflagsname-header-disagrees, response-unmatched—agree
draft/violations/tran-058-request-metadata-headers-missing2026-07-28failTRAN-058, TRAN-141flagsmethod-name-header-missing—agree
draft/violations/tran-060-client-posts-a-response2026-07-28failBASE-046, CACH-001, TRAN-060, TRAN-119, wire-schemaflagscacheable-without-ttl, client-sent-response, response-unmatched—agree
draft/violations/tran-061-notification-answered-2002026-07-28failTRAN-061silent——beyond mcpsnoop
draft/violations/tran-063-error-without-content-type2026-07-28failTRAN-063flagsresponse-content-type—agree
draft/violations/tran-066-independent-server-request2026-07-28failBASE-084, MRTR-001, TRAN-066, TRAN-120flagsserver-sent-request—agree
draft/violations/tran-068-sse-media-type-mixed-case2026-07-28pass-with-warningsTRAN-068silent——beyond mcpsnoop
draft/violations/tran-068-sse-without-accel-buffering2026-07-28pass-with-warningsTRAN-068silent——beyond mcpsnoop
draft/violations/tran-070-message-after-stream-close2026-07-28failTRAN-070silent——beyond mcpsnoop
draft/violations/tran-071-protocol-version-header-missing2026-07-28failCACH-001, TRAN-071, wire-schemaflagscacheable-without-ttl, protocol-version-header-missing—agree
draft/violations/tran-072-mismatch-rejected-without-id2026-07-28failBASE-052, TRAN-072, wire-schemaflagsprotocol-version-header-disagrees, response-unmatched—agree
draft/violations/tran-072-protocol-version-header-mismatched2026-07-28failTRAN-072flagsprotocol-version-header-disagrees, server-rejected-header-mismatch—agree
draft/violations/tran-072-unsupported-body-version-header-mismatch2026-07-28failTRAN-072flagsprotocol-version-header-disagrees, server-rejected-header-mismatch—agree
draft/violations/tran-072-unsupported-body-version-refused-as-unsupported2026-07-28failTRAN-072flagsprotocol-version-header-disagrees—agree
draft/violations/tran-073-header-mismatch-not-rejected2026-07-28failCACH-001, TRAN-072, TRAN-073, wire-schemaflagscacheable-without-ttl, protocol-version-header-disagrees—agree
draft/violations/tran-073-mismatch-refused-without-id2026-07-28failBASE-052, TRAN-072, TRAN-073, wire-schemaflagsprotocol-version-header-disagrees, response-unmatched—agree
draft/violations/tran-074-unsupported-version-accepted2026-07-28failCACH-001, TOOL-019, TRAN-074, VERS-001, wire-schemasilent——beyond mcpsnoop
draft/violations/tran-074-unsupported-version-without-4002026-07-28failTRAN-074silent——beyond mcpsnoop
draft/violations/tran-074-unsupported-version-without-supported-list2026-07-28failTRAN-074, VERS-001, wire-schemasilent——beyond mcpsnoop
draft/violations/tran-075-method-not-found-not-4042026-07-28failTRAN-075silent——beyond mcpsnoop
draft/violations/tran-077-mcp-name-unencoded2026-07-28failCACH-001, TRAN-077, TRAN-086, TRAN-087, wire-schemaflagscacheable-without-ttl—agree
draft/violations/tran-077-param-header-value-rejected2026-07-28failTRAN-077, TRAN-086, TRAN-087flagsserver-rejected-header-mismatch—agree
draft/violations/tran-079-missing-param-header-rejected2026-07-28failTRAN-079, TRAN-130flagsparam-header-missing, server-rejected-header-mismatch—agree
draft/violations/tran-079-param-header-value-not-mirrored2026-07-28failTRAN-079flagsparam-header-disagrees, server-rejected-header-mismatch—agree
draft/violations/tran-079-x-mcp-header-not-mirrored2026-07-28failTRAN-079, TRAN-130, TRAN-136flagsparam-header-missing—agree
draft/violations/tran-080-x-mcp-header-name-invalid2026-07-28failTOOL-033, TRAN-080flagsx-mcp-header-repeated—agree
draft/violations/tran-082-invalid-tool-excluded-valid-called2026-07-28failTOOL-045, TRAN-082, TRAN-137flagsx-mcp-header-chain—agree
draft/violations/tran-082-x-mcp-header-under-any-of2026-07-28failTOOL-045, TRAN-082, TRAN-137flagsx-mcp-header-chainmethod-name-header-missing, protocol-version-header-missingagree
draft/violations/tran-083-invalid-tool-called2026-07-28failTOOL-035, TOOL-045, TOOL-046, TRAN-082, TRAN-083, TRAN-084, TRAN-137flagsx-mcp-header-chain—agree
draft/violations/tran-089-sentinel-markers-miscased2026-07-28failTRAN-089flagsparam-header-disagrees, server-rejected-header-mismatch—agree
draft/violations/tran-092-sentinel-pattern-unencoded2026-07-28failCACH-001, TRAN-092, wire-schemaflagscacheable-without-ttl, name-header-disagrees—agree
draft/violations/tran-096-invalid-param-header-accepted2026-07-28failTRAN-077, TRAN-086, TRAN-087, TRAN-096silent——beyond mcpsnoop
draft/violations/tran-097-header-body-mismatch-accepted2026-07-28failTRAN-079, TRAN-097, TRAN-100, TRAN-131flagsparam-header-disagrees—agree
draft/violations/tran-097-integer-header-different-number2026-07-28failTRAN-079, TRAN-097, TRAN-100, TRAN-131flagsparam-header-disagrees—agree
draft/violations/tran-097-mismatch-after-concurrent-posts2026-07-28failTRAN-058, TRAN-097, TRAN-100, TRAN-131, TRAN-141flagsname-header-disagreesmethod-name-header-missing, protocol-version-header-missingagree
draft/violations/tran-098-header-mismatch-without-4002026-07-28failTRAN-098, TRAN-102flagsserver-rejected-header-mismatch—divergent
draft/violations/tran-123-cancellation-without-request-id2026-07-28failCANC-005, TRAN-123, wire-schemasilent——beyond mcpsnoop
draft/violations/tran-124-message-after-cancel-notification2026-07-28failTRAN-124silent——beyond mcpsnoop
draft/violations/tran-132-null-parameter-header-sent2026-07-28failTRAN-132flagsparam-header-without-value—agree
draft/violations/tran-133-omitted-null-header-rejected2026-07-28failTRAN-133flagsserver-rejected-header-mismatch—divergent
draft/violations/tran-134-absent-parameter-header-sent2026-07-28failTRAN-134flagsparam-header-without-value—agree
draft/violations/tran-135-omitted-absent-header-rejected2026-07-28failTRAN-135flagsserver-rejected-header-mismatch—divergent
draft/violations/tran-136-missing-param-header-accepted2026-07-28failTRAN-079, TRAN-130, TRAN-136flagsparam-header-missing—agree
draft/violations/tran-137-x-mcp-header-under-items2026-07-28failTOOL-045, TRAN-082, TRAN-137flagsx-mcp-header-chainmethod-name-header-missing, protocol-version-header-missingagree
draft/violations/tran-141-mcp-name-header-missing2026-07-28failTRAN-058, TRAN-141flagsmethod-name-header-missing—agree
draft/violations/vers-002-retry-with-unsupported-version2026-07-28pass-with-warningsVERS-002silent——beyond mcpsnoop
draft/violations/vers-004-extension-identifier-without-prefix2026-07-28failVERS-004silent——beyond mcpsnoop
draft/violations/vers-008-initialize-error-without-versions2026-07-28failBASE-030, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, VERS-008silent——divergent
draft/violations/wire-schema-cache-scope-not-in-enum2026-07-28failwire-schemaflagscache-scope-value—agree
draft/violations/wire-schema-content-block-unknown-type2026-07-28failwire-schemasilent——beyond mcpsnoop
draft/violations/wire-schema-resource-missing-uri2026-07-28failwire-schemasilent——beyond mcpsnoop
draft/violations/wire-schema-tool-image-data-not-base642026-07-28failwire-schemasilent——beyond mcpsnoop
draft/captured/official-suite-2026-07-28-scenarios2026-07-28pass-with-warningsBASE-037silent——beyond mcpsnoop
draft/captured/official-suite-2026-07-28-stateless2026-07-28pass—silent——both clean
draft/captured/probe-2026-07-28-http2026-07-28failBASE-030, BASE-033, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, TRAN-071, TRAN-072, TRAN-079, TRAN-130, wire-schemaflagsparam-header-missing, protocol-version-header-disagrees, request-meta-missing, server-rejected-header-mismatch—agree
draft/captured/reference-host-2026-07-28-http2026-07-28pass—silent——both clean
draft/captured/reference-host-2026-07-28-stdio2026-07-28pass—silent——both clean
draft/captured/sdk-go-2026-07-282026-07-28failBASE-034, ELIC-059, MRTR-012, MRTR-013, TRAN-068flagsinput-request-undeclared—agree
draft/captured/sdk-python-2026-07-282026-07-28pass—silent——both clean
draft/captured/sdk-rust-2026-07-282026-07-28pass-with-warningsBASE-037silent——beyond mcpsnoop
draft/captured/sdk-typescript-v2-2026-07-282026-07-28pass—silent——both clean

Requirements — 2026-07-28

Every normative clause of the 2026-07-28 specification that the validator’s registry carries — 501 in all, 181 judged from a recorded trace and 320 excluded with the reason below. A report names a clause by the ID used as each heading here; each entry quotes the specification verbatim and links to the published text. mcp-trace-validator requirements --revision 2026-07-28 --format json prints the same data as JSON.

BASE

BASE-041

MUST · binds the client and server

Cross-cutting concerns like logging and argument completion All implementations MUST support the base protocol, versioning, and the message patterns.

basic/index

Excluded. The umbrella clause the BASE area mechanizes requirement-by-requirement; it has no observable beyond its constituents. Same role as BASE-012 at 2025-11-25, widened by this revision from ‘base protocol and lifecycle management’ to ‘base protocol, versioning, and the message patterns’.

BASE-042

MUST · binds the client and server

All messages between MCP clients and servers MUST follow the JSON-RPC 2.0 specification.

basic/index#messages

Judged by base.jsonrpc-version.

BASE-043

MUST · binds the client and server

Requests MUST include a string or integer ID.

basic/index#requests

Judged by base.request-id-type.

BASE-044

MUST NOT · binds the client and server

Unlike base JSON-RPC, the ID MUST NOT be null.

basic/index#requests

Judged by base.request-id-not-null.

BASE-045

MUST NOT · binds the client and server

The request ID MUST NOT match the ID of any other request the sender has issued and not yet received a response for.

basic/index#requests

Judged by base.request-id-unique-in-flight.

BASE-046

MUST · binds the client and server

Result responses MUST include the same ID as the request they correspond to.

basic/index#result-responses

Judged by base.result-id-matches.

BASE-047

MUST · binds the client and server

Result responses MUST include a result field.

basic/index#result-responses

Judged by base.result-field.

BASE-048

MUST · binds the client and server

The result MUST include a resultType field to indicate the type of the result.

basic/index#result-responses

Judged by base.result-type-present.

BASE-049

MUST · binds the client

The set of supported ResultType values MUST be created from the set defined in the core protocol and include any additional values of supported extensions that are advertised via capabilities.

basic/index#result-responses

Excluded. The client’s set of supported ResultType values is internal state assembled from the core protocol plus its advertised extensions; a trace shows the values a server sent, never the set the client was willing to accept.

BASE-050

MUST · binds the client

A resultType of any value unrecognized by the client MUST be considered invalid.

basic/index#result-responses

Excluded. What a client ‘considers invalid’ is internal judgment. The observable consequence — a client refusing to proceed — is client conduct after receipt, outside a server-side trace, and indistinguishable from any other reason it stopped.

BASE-051

MUST · binds the client

For backward compatibility with servers implementing earlier protocol versions, which do not include resultType, clients MUST treat an absent resultType as "complete".

basic/index#result-responses

Excluded. The backward-compatibility rule binds the client’s interpretation of an absent field, which leaves no wire artifact: the trace records that resultType was absent, not that the client read it as complete.

BASE-052

MUST · binds the client and server

Error responses MUST include the same ID as the request they correspond to (except in error cases where the ID could not be read due a malformed request).

basic/index#error-responses

Judged by base.error-id-matches.

BASE-053

MUST · binds the client and server

Error responses MUST include an error field with a code and message.

basic/index#error-responses

Judged by base.error-shape.

BASE-054

MUST · binds the client and server

Error codes MUST be integers.

basic/index#error-responses

Judged by base.error-code-integer.

BASE-055

MUST NOT · binds the client and server

New codes MUST NOT be allocated in this sub-range

basic/index#error-codes

Excluded. Binds allocation — assigning a new meaning to a code in -32000..-32019 — which is an act of whoever defines codes, not a message. A code observed on the wire carries no record of when or by whom it was allocated, so a legacy code in use is indistinguishable from a newly allocated one. Its use is the sentence’s other half, BASE-083, which is a SHOULD NOT and is checked at that level: until 2026-10-04 this entry carried both halves at MUST NOT and reported every use of -32000 as a MUST failure.

BASE-083

SHOULD NOT · binds the client and server

new implementations SHOULD NOT use codes from this sub-range at all.

basic/index#error-codes

Judged by base.error-code-legacy-subrange.

BASE-056

MUST NOT · binds the client and server

Apart from -32002 (see below), receivers MUST NOT assume any specific meaning for these codes.

basic/index#error-codes

Excluded. What a receiver assumes about a legacy code’s meaning is internal; only conduct following from the assumption could surface, and attributing it requires the assumption. Same boundary as BASE-025 for reserved _meta keys.

BASE-057

MUST NOT · binds the client and server

Implementations MUST NOT emit any code from this sub-range that is not defined by this specification and MUST use defined codes only with their specified meanings.

basic/index#error-codes

Judged by base.error-code-reserved-subrange.

BASE-058

MUST NOT · binds the client and server

Implementations of this protocol version MUST NOT emit these codes:

basic/index#error-codes

Judged by base.error-code-withdrawn.

BASE-059

SHOULD · binds the client

Clients SHOULD still accept -32002 from servers implementing earlier versions.

basic/index#error-codes

Excluded. Client-side acceptance of a legacy code is internal handling; the trace records the -32002 a server sent, not whether the client honoured it. The emitting side is judged by BASE-058.

BASE-060

SHOULD · binds the client and server

New error codes for purposes not defined by this specification SHOULD be allocated outside the JSON-RPC reserved range (-32768 to -32000); the remainder of the integer space is available for application-defined errors.

basic/index#error-codes

Judged by base.error-code-application-range.

BASE-061

MUST NOT · binds the client and server

The receiver MUST NOT send a response.

basic/index#notifications

Excluded. A response to a notification carries no correlating ID, so it is indistinguishable on the wire from any other unsolicited response; unsolicited responses are flagged by base.result-id-matches and base.error-id-matches. Same reasoning as BASE-011 at 2025-11-25.

BASE-062

MUST NOT · binds the client and server

Notifications MUST NOT include an ID.

basic/index#notifications

Judged by base.notification-no-id.

BASE-063

MUST NOT · binds the server

Servers MUST NOT rely on prior requests over the same connection to establish context (e.g., capabilities, protocol version, client identity).

basic/index#statelessness

Excluded. Reliance is internal reasoning. A server that answers correctly may or may not have consulted prior requests, and one that errors may have done so for any reason; the trace shows the answer, not the inference behind it. The adjacent observable is that every request carries its own context, judged by BASE-030.

BASE-064

SHOULD · binds the server

Servers SHOULD be prepared to handle requests associated with multiple tasks, threads, or conversations.

basic/index#statelessness

Excluded. Preparedness is a property of the server’s implementation, not of any session it took part in; a trace that happens to interleave conversations evidences capability, while one that does not evidences nothing.

BASE-065

SHOULD NOT · binds the server

Servers SHOULD NOT require that a client reuse the same connection or process to perform related operations.

basic/index#statelessness

Excluded. Falsifying this needs a server refusing an operation because it arrived on a different connection — two correlated connections in one recording, which the trace vocabulary does not span: each recorded session is one transport.

BASE-066

SHOULD NOT · binds the client

Clients SHOULD NOT use an individual task, thread, or conversation as the lifetime boundary for the stdio process.

basic/index#statelessness

Excluded. Process lifetime is host-OS conduct outside the trace event vocabulary — transport-open/close/abort record that a transport ended, not what the client tied its lifetime to. Same boundary as LIFE-014 at 2025-11-25.

BASE-067

MUST · binds the client

State that needs to span multiple requests (e.g., long-running tasks, application-level handles) MUST be referenced by an explicit identifier the client passes on each request.

basic/index#statelessness

Excluded. Which state ‘needs to span multiple requests’ is server-internal; the trace shows the arguments a client passed, not whether they carried everything the server required. A violation surfaces only as the server failing for some other stated reason.

BASE-068

SHOULD NOT · binds the client and server

Implementations using an HTTP-based transport SHOULD conform to this specification, whereas implementations using STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.

basic/index#auth

Excluded. Credential retrieval from the environment is process-local conduct with no wire footprint, and the HTTP half defers to the authorization specification, whose clauses are entered on its own pages: AUTH-086 and AUTH-087 restate this sentence there, and the challenge clauses a recording can judge carry checks. Same decision as BASE-085 at 2025-11-25.

BASE-069

MUST · binds the client and server

Implementations MUST support at least 2020-12 and SHOULD document which additional dialects they support Recommendation:

basic/index#schema-dialect

Excluded. Dialect support is validation behavior inside each implementation; a trace shows schemas exchanged, not which dialects the receiver could handle. Same reasoning as BASE-013 at 2025-11-25.

BASE-070

MUST · binds the client and server

Clients and servers MUST support JSON Schema 2020-12 for schemas without an explicit $schema field Clients and servers MUST validate schemas according to their declared or default dialect.

basic/index#implementation-requirements

Excluded. Dialect support and validation are behavior inside each implementation; a trace shows schemas exchanged, not how the receiver processed them. Same reasoning as BASE-013/BASE-014 at 2025-11-25.

BASE-071

MUST · binds the client and server

They MUST handle unsupported dialects gracefully by returning an appropriate error indicating the dialect is not supported.

basic/index#implementation-requirements

Excluded. Graceful handling is receiver conduct; the trace would show an error, but not that it was raised for an unsupported dialect rather than any other malformed-schema reason. Same reasoning as BASE-015 at 2025-11-25.

BASE-072

SHOULD · binds the client and server

Clients and servers SHOULD document which schema dialects they support

basic/index#implementation-requirements

Excluded. Documentation is a property of the project, not of any session. Same reasoning as BASE-016 at 2025-11-25.

BASE-073

MUST · binds the client and server

Schemas MUST be valid according to their declared or default dialect

basic/index#schema-validation

Judged by base.schemas-valid-for-dialect.

BASE-074

MUST NOT · binds the client and server

Implementations MUST NOT automatically dereference $ref values that resolve to a network URI.

basic/index#$ref-resolution

Excluded. Whether an implementation dereferenced a $ref is an outbound fetch to a third party, not an MCP message; nothing in the recorded session distinguishes a resolved reference from an unresolved one.

BASE-075

MUST · binds the client and server

Implementations MAY offer an opt-in mode that fetches non-local $refs but it MUST be disabled by default and SHOULD enforce an allowlist of hosts or at minimum reject loopback, link-local, and private network addresses, apply timeouts and size limits, and log dereferenced URIs.

basic/index#$ref-resolution

Excluded. The default state of an opt-in mode, its allowlist, timeouts and logging are all implementation configuration; none appears in an MCP session.

BASE-076

SHOULD · binds the client and server

Schemas that fail to validate due to an unresolved external $ref SHOULD be rejected rather than silently treated as permissive.

basic/index#$ref-resolution

Excluded. Rejection happens inside the receiver’s schema validation; the trace shows the schema that was sent, not what the receiver did with an unresolvable reference.

BASE-077

SHOULD · binds the client and server

Implementations SHOULD apply reasonable bounds, such as a maximum schema depth, a cap on the total number of subschemas, or a per-validation time budget, to prevent a malicious schema from acting as a Denial-of-Service vector against the validator.

basic/index#composition-keyword-resource-use

Excluded. Depth caps, subschema counts and time budgets are validator-internal resource policy, invisible in the messages exchanged.

BASE-088

SHOULD · binds the client and server

Implementors are RECOMMENDED to use JSON Schema 2020-12.

basic/index#schema-dialect

Judged by base.schemas-use-2020-12.

BASE-025

MUST NOT · binds the client and server

Certain key names are reserved by MCP for protocol-level metadata, as specified below; implementations MUST NOT make assumptions about values at these keys.

basic/index#_meta

Excluded. What an implementation assumes is internal; only conduct that follows from an assumption could surface, and attributing it requires the assumption. Same clause and same reasoning as BASE-018 at 2025-11-25.

BASE-026

MUST · binds the client and server

If specified, MUST be a series of labels separated by dots (.), followed by a slash (/).

basic/index#_meta

Judged by base.meta-key-format.

BASE-027

MUST · binds the client and server

Labels MUST start with a letter and end with a letter or digit; interior characters can be letters, digits, or hyphens (-).

basic/index#_meta

Judged by base.meta-key-format.

BASE-028

SHOULD · binds the client and server

Implementations SHOULD use reverse DNS notation (e.g., com.example/ rather than example.com/).

basic/index#_meta

Excluded. Whether a syntactically valid prefix is reverse-DNS for its author cannot be decided without knowing the author’s domain. Same clause and same reasoning as BASE-021 at 2025-11-25.

BASE-029

MUST · binds the client and server

Unless empty, MUST begin and end with an alphanumeric character ([a-z0-9A-Z]).

basic/index#_meta

Judged by base.meta-key-format.

BASE-030

MUST · binds the client

Client requests carry the following io.modelcontextprotocol/* fields in meta; fields marked as required MUST be included on every request.

basic/index#_meta

Judged by meta.required-request-fields.

BASE-031

MUST · binds the server

A request missing any required field is malformed; the server MUST reject it with JSON-RPC error code -32602 (Invalid params).

basic/index#_meta

Judged by meta.missing-required-field-rejected.

BASE-032

MUST · binds the server

On HTTP, the response status MUST be 400 Bad Request.

basic/index#_meta

Judged by meta.missing-required-field-http-status.

BASE-033

SHOULD · binds the client

Clients SHOULD include io.modelcontextprotocol/clientInfo on every request unless specifically configured not to do so.

basic/index#_meta

Judged by meta.client-info-on-requests.

BASE-034

MUST NOT · binds the server

A server MUST NOT rely on capabilities the client has not declared.

basic/index#_meta

Judged by meta.no-undeclared-capability-reliance.

BASE-035

MUST · binds the server

If processing a request requires a capability the client did not include in io.modelcontextprotocol/clientCapabilities, the server MUST return a MissingRequiredClientCapabilityError (-32021) whose data.requiredCapabilities lists the missing capabilities.

basic/index#_meta

Judged by meta.missing-capability-error.

BASE-036

MUST · binds the server

On HTTP, the response status MUST be 400 Bad Request.

basic/index#_meta

Judged by meta.missing-capability-http-status.

BASE-037

SHOULD · binds the server

Servers SHOULD include the following io.modelcontextprotocol/* field in every result’s meta, unless specifically configured not to do so, to identify themselves without relying on any prior connection state:

basic/index#_meta

Judged by meta.server-info-on-results.

BASE-038

SHOULD NOT · binds the client and server

Implementations SHOULD NOT use them to change the behavior of the client or server, and SHOULD NOT rely on them for security decisions.

basic/index#_meta

Excluded. Self-reported identity is advisory; whether a peer let it change behavior or drive a security decision is internal reasoning with no wire footprint, the same boundary BASE-025 draws for reserved-key assumptions.

BASE-039

MUST · binds the server

On notifications delivered via a [subscriptions/listen][subscriptions-listen] stream, the server MUST include io.modelcontextprotocol/subscriptionId in meta so the client can correlate the notification with the originating subscription request.

basic/index#_meta

Judged by meta.subscription-id-present.

BASE-040

MUST · binds the client and server

When present, their values MUST follow W3C Trace Context and W3C Baggage formats respectively.

basic/index#_meta

Judged by meta.trace-context-format.

BASE-078

MUST · binds the client

Clients that support rendering icons MUST support at least the following MIME types:

basic/index#icons

Excluded. Icon rendering support is client-internal capability with no wire footprint; the conditional premise (that the client renders icons) is equally invisible. Same reasoning as BASE-022 at 2025-11-25.

BASE-079

SHOULD · binds the client

image/png - PNG images (safe, universal compatibility) image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility) Clients that support rendering icons SHOULD also support:

basic/index#icons

Excluded. Extends the same client-internal rendering-support premise as the MUST above it; neither the support nor its absence reaches the wire.

BASE-080

MUST · binds the client

Consumers of icon metadata MUST take appropriate security precautions when handling icons to prevent compromise:

basic/index#icons

Excluded. The umbrella for consumer-side icon hygiene (fetch policy, size limits, origin checks) — all consumer-internal conduct after the recorded session ends. Same reasoning as BASE-023 at 2025-11-25.

BASE-081

MUST · binds the client

Clients MUST reject icon URIs that use unsafe schemes and redirects, such as javascript:, file:, ftp:, ws:, or local app URI schemes.

basic/index#icons

Excluded. Rejection is client-side handling after receipt; the trace records the icon metadata served, not what the client did with it. Same reasoning as BASE-024 at 2025-11-25.

BASE-084

MUST NOT · binds the server

Servers MUST NOT initiate JSON-RPC requests, and clients do not send JSON-RPC responses.

basic/patterns/index

Judged by transport.no-independent-server-requests.

TRAN

TRAN-050

MUST · binds the server

The server MUST provide a single HTTP endpoint path (hereafter referred to as the MCP endpoint) that supports POST.

basic/transports/streamable-http

Excluded. Endpoint topology — how many paths a server exposes and which methods each accepts — is deployment configuration; a recorded session shows one endpoint being used, never the set that exists. Same reasoning as TRAN-006 at 2025-11-25.

TRAN-051

MUST · binds the server

Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks.

basic/transports/streamable-http#security-&-endpoint

Excluded. Origin validation is server-internal policy applied before any MCP message; the trace records admitted sessions, not the requests a policy refused. Same reasoning as TRAN-002 at 2025-11-25; the everything server’s policy middleware is tested directly in http_policy.rs.

TRAN-052

MUST · binds the server

If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden.

basic/transports/streamable-http#security-&-endpoint

Excluded. A refused request never becomes an MCP session, so the 403 it drew is outside what the tap records. Same reasoning as TRAN-007 at 2025-11-25; enforced in code and covered by the server’s own policy tests.

TRAN-053

SHOULD · binds the server

When running locally, servers SHOULD bind only to localhost (127.0.0.1) rather than all network interfaces (0.0.0.0).

basic/transports/streamable-http#security-&-endpoint

Excluded. Bind address is process configuration with no wire footprint. Same reasoning as TRAN-008 at 2025-11-25.

TRAN-054

SHOULD · binds the server

Servers SHOULD implement proper authentication for all connections.

basic/transports/streamable-http#security-&-endpoint

Excluded. Whether authentication is properly implemented is not decidable from one session’s headers: the client’s credential is never recorded, and the server’s validation of it is internal. What a recording does show — the challenge a protected server answers with — is judged under the authorization pages’ own clauses (AUTH-097, AUTH-124, AUTH-129–AUTH-133). Same reasoning as TRAN-009 at 2025-11-25.

TRAN-055

MUST · binds the client

Every JSON-RPC message sent from the client MUST be a new HTTP POST request to the MCP endpoint.

basic/transports/streamable-http#sending-messages

Excluded. Adds to TRAN-056’s verb obligation that each message ride a new POST to the MCP endpoint. A POST body is one JSON value, which a recorder writes as one payload, so more than one message in a POST is a non-object body — TRAN-059 judges that. What remains — that the request was new rather than reused, and was addressed to the MCP endpoint — leaves no trace: the endpoint is not recorded. Same split as TRAN-024/TRAN-049 at 2025-11-25.

TRAN-056

MUST · binds the client

The client MUST use HTTP POST to send JSON-RPC messages.

basic/transports/streamable-http#sending-messages

Judged by transport.client-messages-use-post.

TRAN-057

MUST · binds the client

The client MUST include an Accept header listing both application/json and text/event-stream as supported content types.

basic/transports/streamable-http#sending-messages

Judged by transport.client-post-accept-header.

TRAN-058

MUST · binds the client

The client MUST include the request metadata headers on each POST request.

basic/transports/streamable-http#sending-messages

Judged by transport.request-metadata-headers.

TRAN-059

MUST · binds the client

The body of the HTTP POST MUST be a single JSON-RPC request or notification.

basic/transports/streamable-http#sending-messages

Judged by transport.http-post-single-message.

TRAN-060

MUST NOT · binds the client

The client MUST NOT send JSON-RPC responses.

basic/transports/streamable-http#sending-messages

Judged by transport.client-no-responses.

TRAN-061

MUST · binds the server

If the server accepts it, the server MUST return HTTP status code 202 Accepted with no body.

basic/transports/streamable-http#sending-messages

Judged by transport.accepted-input-status.

TRAN-062

MUST · binds the server

If the server cannot accept it, it MUST return an HTTP error status code (e.g., 400 Bad Request).

basic/transports/streamable-http#sending-messages

Excluded. Which inputs a server declines to accept, and why, is server-internal admission policy; a declined request produces no MCP message to judge. Same reasoning as TRAN-028 at 2025-11-25.

TRAN-063

MUST · binds the server

If the body is a JSON-RPC request, the server MUST return either Content-Type: application/json (a single JSON object) or Content-Type: text/event-stream (an SSE response stream).

basic/transports/streamable-http#sending-messages

Judged by transport.success-content-type.

TRAN-064

MUST · binds the client

The client MUST support both.

basic/transports/streamable-http#sending-messages

Excluded. Client support for both response shapes is internal capability: a session that received one shape evidences nothing about the other. Same reasoning as TRAN-030 at 2025-11-25.

TRAN-065

MUST · binds the server

These notifications MUST relate to the originating client request.

basic/transports/streamable-http#receiving-messages

Excluded. Relatedness is semantic. A notification carries no id tying it to a request, so the trace cannot decide whether it ‘relates to’ the originating one. Same reasoning as TRAN-035 at 2025-11-25.

TRAN-066

MUST NOT · binds the server

The server MUST NOT send independent JSON-RPC requests on this stream.

basic/transports/streamable-http#receiving-messages

Judged by transport.no-independent-server-requests.

TRAN-067

SHOULD · binds the server

The final JSON-RPC response SHOULD terminate the stream.

basic/transports/streamable-http#receiving-messages

Excluded. Stream termination after the final response is SSE framing; the trace records messages and transport lifecycle, not whether the stream closed promptly afterwards. Same reasoning as TRAN-036 at 2025-11-25.

TRAN-068

SHOULD · binds the server

When initiating an SSE stream, servers SHOULD include the X-Accel-Buffering: no header in the HTTP response.

basic/transports/streamable-http#receiving-messages

Judged by transport.accel-buffering-header.

TRAN-069

MUST · binds the server

Closing the SSE response stream MUST be treated by the server as cancellation of that request.

basic/transports/streamable-http#cancellation

Excluded. How a server treats a closed stream is internal: cancellation is a decision, and the trace shows only that the transport ended. The observable consequence — messages sent afterwards — is TRAN-070’s business.

TRAN-070

MUST NOT · binds the server

The server SHOULD stop work on the cancelled request as soon as practical and MUST NOT send any further messages for it.

basic/transports/streamable-http#cancellation

Judged by transport.no-messages-after-cancellation.

TRAN-071

MUST · binds the client

Every POST request to the MCP endpoint MUST include an MCP-Protocol-Version header.

basic/transports/streamable-http#protocol-version-header

Judged by transport.protocol-version-header-present.

TRAN-072

MUST · binds the client

MCP-Protocol-Version: 2026-07-28 The header value MUST match the io.modelcontextprotocol/protocolVersion field carried in the request body’s meta.

basic/transports/streamable-http#protocol-version-header

Judged by transport.protocol-version-header-matches-body.

TRAN-073

MUST · binds the server

If the values do not match, the server MUST reject the request with 400 Bad Request and a HeaderMismatch JSON-RPC error (see Server Validation).

basic/transports/streamable-http#protocol-version-header

Judged by transport.version-mismatch-rejected.

TRAN-074

MUST · binds the server

If the server does not implement the requested protocol version (whether the version is unknown to the server, or is a known version the server has chosen not to support), it MUST respond with 400 Bad Request and an [UnsupportedProtocolVersionError][unsupported-version] listing its supported versions.

basic/transports/streamable-http#protocol-version-header

Judged by transport.unsupported-version-error-http, transport.unsupported-version-status.

TRAN-075

MUST · binds the server

If the server does not implement the requested RPC method, it MUST respond with 404 Not Found and a JSON-RPC error with code -32601 (Method not found).

basic/transports/streamable-http#protocol-version-header

Judged by transport.unknown-method-404.

TRAN-076

MUST · binds the server

A server that does not support such clients MUST reject a request without the header per Server Validation.

basic/transports/streamable-http#protocol-version-header

Excluded. Whether a server supports header-less clients is its own policy; a trace shows how it answered one request, not the policy behind it. The rejection shape, when it happens, is judged by TRAN-073.

TRAN-077

MUST · binds the client

If the Mcp-Name source value cannot be safely represented as a plain ASCII header value, clients MUST encode it using the Base64 sentinel format described in Value Encoding.

basic/transports/streamable-http#standard-request-headers

Judged by transport.header-value-encoding.

TRAN-078

MUST · binds the client

While the use of x-mcp-header is optional for servers, clients MUST support this feature.

basic/transports/streamable-http#custom-headers-from-tool-parameters

Excluded. Client support for x-mcp-header is internal capability. Its observable consequence — mirroring designated values into headers — is the next clause and is checked there.

TRAN-079

MUST · binds the client

When a server’s tool definition includes x-mcp-header annotations, conforming clients MUST mirror the designated parameter values into HTTP headers.

basic/transports/streamable-http#custom-headers-from-tool-parameters

Judged by transport.x-mcp-header-mirrored, transport.x-mcp-header-value-mirrored.

TRAN-080

MUST NOT · binds the server

Constraints on x-mcp-header values: MUST NOT be empty; MUST match HTTP field-name token syntax (1*tchar, RFC 9110 Section 5.1); MUST NOT contain control characters, including carriage return (CR, \r) or line feed (LF, \n); MUST be case-insensitively unique among all x-mcp-header values in the inputSchema; MUST only be applied to parameters with primitive types (integer, string, boolean).

basic/transports/streamable-http#schema-extension

Judged by transport.x-mcp-header-name-valid.

TRAN-081

MUST · binds the client

Constraints on x-mcp-header values: Integer values MUST be within the safe range for JavaScript (−253+1 to 253−1)

basic/transports/streamable-http#schema-extension

Judged by tools.x-mcp-header-integer-range.

TRAN-082

MUST NOT · binds the server

The chain MUST NOT pass through items (or any other array keyword), composition keywords (oneOf, anyOf, allOf, not), conditional keywords (if/then/else), or $ref.

basic/transports/streamable-http#schema-extension

Judged by transport.x-mcp-header-statically-reachable.

TRAN-083

MUST · binds the client

Clients using the Streamable HTTP transport MUST reject tool definitions where any x-mcp-header value violates these constraints.

basic/transports/streamable-http#schema-extension

Judged by transport.x-mcp-header-invalid-tool-not-called.

TRAN-084

MUST · binds the client

Rejection means the client MUST exclude the invalid tool from the result of tools/list.

basic/transports/streamable-http#schema-extension

Judged by transport.x-mcp-header-invalid-tool-not-called.

TRAN-085

SHOULD · binds the client

Clients SHOULD log a warning when rejecting a tool definition, including the tool name and the reason for rejection.

basic/transports/streamable-http#schema-extension

Excluded. Logging is client-internal diagnostics with no wire footprint.

TRAN-086

MUST · binds the client

Clients MUST encode parameter values before including them in HTTP headers to ensure safe transmission and prevent injection attacks.

basic/transports/streamable-http#value-encoding

Judged by transport.header-value-encoding.

TRAN-087

MUST · binds the client

When a value cannot be safely represented as a plain ASCII header value (e.g., it contains non-ASCII characters, control characters, or has leading/trailing whitespace), clients MUST use Base64 encoding of the UTF-8 representation with the following format:

basic/transports/streamable-http#value-encoding

Judged by transport.header-value-encoding.

TRAN-088

SHOULD · binds the client

Tool and prompt names are only SHOULD-constrained to header-safe characters, so a name (or resource URI) outside the safe set is carried as:

basic/transports/streamable-http#value-encoding

Excluded. A descriptive clause explaining why names are carried encoded, not an independent obligation; the encoding rule itself is checked on the clauses above.

TRAN-089

MUST · binds the client

These markers are case-sensitive and MUST appear exactly as shown (lowercase).

basic/transports/streamable-http#value-encoding

Judged by transport.sentinel-marker-case.

TRAN-090

MUST · binds the server

Servers and intermediaries that need to inspect these values MUST decode them accordingly.

basic/transports/streamable-http#value-encoding

Excluded. Decoding is what a receiver does internally before comparison; the trace shows the encoded value on the wire and the outcome, never the decode step.

TRAN-091

MUST · binds the server

In particular, servers MUST decode an encoded Mcp-Name or Mcp-Param-{Name} value before comparing it to the corresponding request body value during Server Validation.

basic/transports/streamable-http#value-encoding

Excluded. Same internal-decode boundary as the clause above: only the comparison’s outcome reaches the wire, and that is judged by TRAN-097.

TRAN-092

MUST · binds the client

To avoid ambiguity, clients MUST also Base64-encode any plain-ASCII value that matches the sentinel pattern (i.e., starts with =?base64? and ends with ?=).

basic/transports/streamable-http#value-encoding

Judged by transport.sentinel-pattern-encoded.

TRAN-093

MUST · binds the client

When constructing a tools/call request via HTTP transport, the client MUST:

basic/transports/streamable-http#client-behavior

Excluded. The umbrella introducing the client’s tools/call construction steps; each step is a clause of its own above, and the umbrella has no observable beyond them.

TRAN-094

SHOULD · binds the client

If the server rejects a request with a HeaderMismatch error because required Mcp-Param-* headers are missing or do not match the body, the client SHOULD call tools/list to check for changes to the tool’s inputSchema, then retry the original request with the appropriate headers.

basic/transports/streamable-http#client-behavior

Excluded. Whether a client re-fetched tools/list after a HeaderMismatch is recovery conduct; a later tools/list in the same recording is equally consistent with any other reason for calling it.

TRAN-137

MUST · binds the server

Constraints on x-mcp-header values: MUST only be applied to properties that are statically reachable from the schema root: reachable via a chain consisting solely of properties keys.

basic/transports/streamable-http#schema-extension

Judged by transport.x-mcp-header-statically-reachable.

TRAN-141

MUST · binds the client

These headers are REQUIRED for compliance.

basic/transports/streamable-http#standard-request-headers

Judged by transport.request-metadata-headers.

TRAN-095

MUST · binds the server

Intermediate servers that do not recognize an Mcp-Param-{Name} header MUST forward it and otherwise ignore it, as required by the [HTTP Semantics RFC][http-semantics].

basic/transports/streamable-http#server-behavior-for-custom-headers

Excluded. Intermediary behavior: a forwarding proxy is not a party to the MCP session, and the trace records the endpoints, not the hops between them.

TRAN-096

MUST · binds the server

Servers MUST reject requests with a recognized Mcp-Param-{Name} header that contains invalid characters (see Value Encoding).

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.invalid-param-header-rejected.

TRAN-097

MUST · binds the server

Any server that processes the message body MUST validate that encoded header values, after decoding if Base64-encoded, match the corresponding values in the request body.

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.header-body-match-validated.

TRAN-098

MUST · binds the server

Servers MUST reject requests with a 400 Bad Request HTTP status and JSON-RPC error code -32020 (HeaderMismatch) if any validation fails.

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.header-mismatch-status.

TRAN-130

MUST · binds the client

Parameter value provided | Client MUST include the header

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.x-mcp-header-mirrored.

TRAN-131

MUST · binds the server

Parameter value provided | Client MUST include the header | Server MUST validate header matches body

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.header-body-match-validated.

TRAN-132

MUST · binds the client

Parameter value is null | Client MUST omit the header

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.x-mcp-header-null-omitted.

TRAN-133

MUST NOT · binds the server

Parameter value is null | Client MUST omit the header | Server MUST NOT expect the header

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.null-param-header-not-expected.

TRAN-134

MUST · binds the client

Parameter not in arguments | Client MUST omit the header

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.x-mcp-header-absent-omitted.

TRAN-135

MUST NOT · binds the server

Parameter not in arguments | Client MUST omit the header | Server MUST NOT expect the header

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.absent-param-header-not-expected.

TRAN-136

MUST · binds the server

Client omits header but value is in body | Non-conforming client | Server MUST reject the request

basic/transports/streamable-http#server-behavior-for-custom-headers

Judged by transport.missing-param-header-rejected.

TRAN-099

MUST · binds the client and server

Clients and servers MUST use case-insensitive comparisons for header names.

basic/transports/streamable-http#case-sensitivity

Excluded. Case-insensitive comparison is internal string handling. The trace lowercases header names on read (EventBody::Http), so a capture cannot even preserve the casing a party compared against.

TRAN-100

MUST · binds the server

Servers that process the request body MUST reject requests where the values specified in the headers do not match the corresponding values in the request body.

basic/transports/streamable-http#server-validation

Judged by transport.header-body-match-validated.

TRAN-101

SHOULD · binds the server

When validating integer parameter values, servers SHOULD compare the header value and the body value numerically rather than as strings (e.g., 42.0 and 42 are considered equal).

basic/transports/streamable-http#server-validation

Excluded. Numeric-versus-string comparison is internal; only the comparison’s verdict reaches the wire, and that is judged by TRAN-097.

TRAN-102

MUST · binds the server

When rejecting a request due to header validation failure, servers MUST return HTTP status 400 Bad Request and MUST include a JSON-RPC error response using the following error code:

basic/transports/streamable-http#server-validation

Judged by transport.header-mismatch-status.

TRAN-103

MUST · binds the server

For headers that permit the Base64 sentinel encoding (Mcp-Name and Mcp-Param-{Name}), servers MUST decode encoded values (see Value Encoding) before comparing them to the body value.

basic/transports/streamable-http#server-validation

Excluded. Same internal-decode boundary as the earlier decode clauses: the wire shows the encoded value and the outcome, not the decoding in between.

TRAN-104

MUST · binds the server

Intermediaries MUST return an appropriate HTTP error status (e.g., 400 Bad Request) for validation failures but are not required to return a JSON-RPC error response.

basic/transports/streamable-http#server-validation

Excluded. Intermediary behavior; see the forwarding clause above. An intermediary’s HTTP error never becomes an MCP message in this session.

TRAN-105

SHOULD · binds the server

Intermediaries that enforce policy based on mirrored headers (e.g., routing or rate-limiting by tenant) SHOULD verify that the MCP-Protocol-Version header indicates a version that requires header–body validation.

basic/transports/streamable-http#server-validation

Excluded. Intermediary policy enforcement, outside the recorded session’s endpoints.

TRAN-106

SHOULD · binds the server

If the version is older or the header is absent, the intermediary SHOULD reject the request rather than trusting unvalidated header values.

basic/transports/streamable-http#server-validation

Excluded. Intermediary rejection policy, outside the recorded session’s endpoints.

TRAN-107

SHOULD · binds the client

On 400 Bad Request, the client SHOULD inspect the response body before falling back: modern servers also use 400 for [UnsupportedProtocolVersionError][unsupported-version], MissingRequiredClientCapabilityError, and header-validation failures.

basic/transports/streamable-http#backward-compatibility

Excluded. Whether the client inspected a 400 body before falling back is internal reasoning; a subsequent fallback request is equally consistent with either.

TRAN-108

SHOULD · binds the server

A server that supports only this revision and receives such traffic from an older client SHOULD respond as follows:

basic/transports/streamable-http#earlier-streamable-http-revisions

Excluded. The umbrella introducing the backward-compatibility response table; each response it prescribes is a clause of its own, and the umbrella adds no separate observable.

TRAN-109

SHOULD NOT · binds the client and server

New implementations SHOULD NOT adopt it; existing implementations SHOULD migrate to Streamable HTTP.

basic/transports/streamable-http#http+sse-transport-2024-11-05

Excluded. Adopting or migrating away from a transport is a project decision, not a property of any session.

DISC

DISC-001

MUST · binds the server

server/discover lets a client query a server’s supported protocol versions, capabilities, and identity before sending any other requests. Servers MUST implement it.

server/discover

Judged by discover.implemented.

DISC-002

SHOULD · binds the client

A client that supports both modern (per-request meta) and legacy (initialize handshake) servers SHOULD send server/discover first; see stdio: Backward Compatibility for the fallback rules.

server/discover#when-to-call

Judged by discover.dual-era-probe-first.

DISC-003

SHOULD · binds the server

meta['io.modelcontextprotocol/serverInfo']: Name and version of the server software. Servers SHOULD include this field.

server/discover#discoverresult

Judged by discover.server-info-included.

DISC-004

SHOULD NOT · binds the client

serverInfo is self-reported by the server and is not verified by the protocol. It is intended for display, logging, and debugging. Clients SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for security decisions.

server/discover#discoverresult

Excluded. Binds what the client does with serverInfo after reading it — behaviour changes and security decisions are internal reasoning, and a trace records the messages, not the inferences drawn from them. Same boundary as BASE-022’s icon-consumer clauses.

VERS

VERS-001

MUST · binds the server

If the server does not implement the requested version (whether the version is unknown to the server, or is a known version the server has chosen not to support), it MUST respond with an UnsupportedProtocolVersionError listing the versions it does support:

basic/versioning#protocol-version-negotiation

Judged by transport.unsupported-version-error.

VERS-002

SHOULD · binds the client

The client SHOULD select a mutually supported version from the supported list and retry the request, or surface an error to the user if no compatible version exists.

basic/versioning#protocol-version-negotiation

Judged by versioning.retry-uses-supported-version.

VERS-003

MUST · binds the server

Servers MUST implement server/discover.

basic/versioning#protocol-version-negotiation

Judged by discover.implemented.

VERS-004

MUST · binds the client and server

Extension identifiers MUST follow the meta key naming rules, with a mandatory prefix.

basic/versioning#extension-negotiation

Judged by versioning.extension-identifier-format.

VERS-005

MUST · binds the client and server

If one party supports an extension but the other does not, the supporting party MUST either revert to core protocol behavior or reject the request with an appropriate error.

basic/versioning#extension-negotiation

Excluded. Both permitted responses are defined by the extension, not by this specification: what “core protocol behavior” looks like for a given extension, and which error is “appropriate”, are each stated in that extension’s own document. Where an official extension’s clauses are entered (tasks and skills, beside this revision), they judge exactly that, in the extension’s own block and never scored (ADR-0023): TASK-001, for one, is this clause for the tasks extension. Judging it here, among the core’s clauses, would let an optional extension’s behaviour move the core verdict.

VERS-006

SHOULD · binds the client and server

Extensions SHOULD document their expected fallback behavior.

basic/versioning#extension-negotiation

Excluded. Binds an extension’s documentation, not either party’s messages. The subject is not even a session participant: no recorded trace can be evidence about whether a document exists.

VERS-007

SHOULD · binds the client

Clients SHOULD cache the result for the lifetime of the server process (stdio) or origin (HTTP), and MAY persist it across restarts of the same server configuration, re-probing if the cached assumption later fails.

basic/versioning#backward-compatibility-with-initialization-based-versions

Excluded. The cached value is the era determination — client-internal state, and the clause’s horizon is the server process or origin, which outlives the recorded session. The one wire-visible candidate, a repeated probe, is not evidence: the clause’s own “re-probing if the cached assumption later fails” permits it.

VERS-008

SHOULD · binds the server

A server that supports only modern versions SHOULD name the protocol versions it supports in any error it returns to an initialize request, on any transport: legacy clients have no fall-forward mechanism, and this message may be the only diagnostic they can surface to users.

basic/versioning#backward-compatibility-with-initialization-based-versions

Judged by versioning.initialize-error-names-versions.

VERS-009

SHOULD · binds the client

Modern | Legacy | Fails. The server may reject the request with an implementation-defined error, stay silent, or even process an era-ambiguous method under legacy semantics. On stdio, clients SHOULD send server/discover first to fail deterministically; the client then surfaces an actionable error to the user.

basic/versioning#compatibility-matrix

Excluded. The compatibility matrix’s modern-client, legacy-server row. Its antecedent is the server being legacy, which is exactly what an unprobed exchange fails to reveal — the row itself says such a server may reject the request, stay silent, or process it under legacy semantics — and, as for TRAN-142, a recording cannot show that its first request was the client’s first. The dual-era form of the same advice is TRAN-128, judged where a session witnesses both eras.

TRAN

TRAN-110

MUST · binds the client and server

JSON-RPC messages MUST be UTF-8 encoded.

basic/transports/index#messages

Excluded. Restates TRAN-001 at 2025-11-25, whose exclusion this project already argued and which holds unchanged: trace documents are UTF-8 by construction, so recorded messages prove nothing was mis-encoded, and absence is not evidence because capture tooling drops undecodable bytes. Violations surface as capture-time parse failures, not trace findings.

TRAN-111

MUST · binds the client and server

A binding MUST deliver client-sent requests and notifications to the server, and server-sent responses and notifications to the client.

basic/transports/index#messages

Excluded. A delivery obligation, and a trace is a record of what arrived: a message the binding failed to deliver is absent from it, and an absent message is indistinguishable from one never sent. The clause’s converse — that no other direction exists — is a separate sentence and is enforced where it is stated: TRAN-060 and TRAN-118 for client responses, TRAN-066 and TRAN-120 for server-initiated requests.

TRAN-112

MUST · binds the client and server

Implementers who choose to support custom transports MUST preserve the JSON-RPC message format, the message patterns, and the per-request metadata model.

basic/transports/index#custom-transports

Excluded. No custom transport can appear in a trace: TransportKind admits stdio and streamable-http only, so the clause’s subject has no representation in the format this engine reads. What it requires preserved is exactly what the rest of this registry checks for the two standard bindings, against which a custom transport’s capture would have to be relabelled first.

TRAN-113

SHOULD · binds the client and server

Custom transports SHOULD document their connection establishment, message framing, and cancellation patterns to aid interoperability.

basic/transports/index#custom-transports

Excluded. Binds a transport’s documentation, not its messages. Same class as VERS-006: the artefact the clause requires is a document, and no session can be evidence about whether one exists.

TRAN-114

SHOULD · binds the client and server

Custom transports that run over a reliable bidirectional byte stream (e.g., Unix domain sockets or TCP) SHOULD reuse the stdio framing rather than defining a new one: the stdio binding is just newline-delimited JSON-RPC over a byte stream, and only its process-lifecycle rules are specific to standard streams.

basic/transports/index#custom-transports

Excluded. Binds the design of a transport that cannot be recorded as itself (see TRAN-112), and the property it asks for — which framing was reused — is settled before any message exists. Restated on the stdio page as TRAN-121, with the same reason.

TRAN-115

MUST NOT · binds the client and server

Messages are delimited by newlines, and MUST NOT contain embedded newlines.

basic/transports/stdio

Excluded. Restates TRAN-002 at 2025-11-25 verbatim, and its exclusion holds unchanged: framing is enforced at capture time, the JSON Lines trace format cannot represent an embedded-newline violation, and a parseable trace is evidence the rule held.

TRAN-116

SHOULD NOT · binds the client

The client MAY capture, forward, or ignore the server’s stderr output and SHOULD NOT assume stderr output indicates error conditions.

basic/transports/stdio

Excluded. Binds an inference the client draws from stderr — and stderr is not part of the recorded message stream at all, so neither the input to that inference nor the inference itself reaches a trace. Same boundary as TRAN-003 at 2025-11-25.

TRAN-117

MUST NOT · binds the server

The server MUST NOT write anything to its stdout that is not a valid MCP message.

basic/transports/stdio

Judged by transport.stdio-server-output-valid.

TRAN-118

MUST NOT · binds the client

The client MUST NOT write anything to the server’s stdin that is not a valid MCP message.

basic/transports/stdio

Judged by transport.stdio-client-input-valid.

TRAN-119

MUST NOT · binds the client

The client MUST NOT write JSON-RPC responses.

basic/transports/stdio#sending-messages

Judged by transport.client-no-responses.

TRAN-120

MUST NOT · binds the server

The server MUST NOT write JSON-RPC requests to stdout.

basic/transports/stdio#receiving-messages

Judged by transport.no-independent-server-requests.

TRAN-121

SHOULD · binds the client and server

Custom transports built on such streams SHOULD reuse this framing and the message rules on this page; only the subprocess-specific aspects (launch, stderr, shutdown by closing the stream, process restart) need channel-specific equivalents.

basic/transports/stdio

Excluded. The same clause as TRAN-114, stated from the stdio page’s side: a custom transport has no representation in the trace format, and which framing it reused is settled before any message exists.

TRAN-122

MUST · binds the client

Clients MUST correlate these using the io.modelcontextprotocol/subscriptionId field in meta; see [SubscriptionsListenRequest][subscriptions-listen-request].

basic/transports/stdio#receiving-messages

Excluded. Binds the client’s own correlation of notifications it received — bookkeeping inside the client, which produces no message. The wire-visible half of the same mechanism, that the server tags each notification with io.modelcontextprotocol/subscriptionId, is BASE-039 and is checked.

TRAN-123

MUST · binds the client

To cancel an in-flight request, the client MUST send a notifications/cancelled notification referencing the request’s ID.

basic/transports/stdio#cancellation

Judged by transport.cancel-notification-references-request.

TRAN-124

MUST NOT · binds the server

Servers SHOULD stop work on a cancelled request as soon as practical and MUST NOT send any further messages for it.

basic/transports/stdio#cancellation

Judged by transport.no-messages-after-cancel-notification.

TRAN-125

SHOULD · binds the client

The client SHOULD initiate shutdown by: Closing the input stream to the child process (the server); Waiting for the server to exit; If the server does not exit within a reasonable time, forcibly terminating the process using the mechanism appropriate for the operating system.

basic/transports/stdio#shutdown

Excluded. Stream closure, process waits and forced termination are host-OS actions outside the trace event vocabulary — the same reason LIFE-014 excludes the 2025-11-25 shutdown sequence. A transport-close lifecycle event records that the transport ended, not which party closed which stream or what it did next.

TRAN-126

SHOULD · binds the server

Servers SHOULD exit promptly when their standard input is closed or reads return end-of-file.

basic/transports/stdio#shutdown

Excluded. Both the trigger (stdin closing, or a read returning EOF) and the response (process exit) are host-OS events the trace vocabulary does not carry, and “promptly” is a duration — checks may not consult time, the same limit LIFE-015 records.

TRAN-127

SHOULD · binds the client

If the server process exits unexpectedly, the client SHOULD restart it.

basic/transports/stdio#unexpected-termination

Excluded. Process supervision: the antecedent is an unexpected exit and the obligation is to launch a new process, neither of which is a message. A restart also begins a new session, so the evidence would have to span two traces, and nothing in one identifies the other as its successor.

TRAN-128

SHOULD · binds the client

A client that supports both modern (per-request-metadata) MCP versions and a legacy version that requires an initialize handshake SHOULD probe with [server/discover][server-discover] before sending any other request, setting its preferred modern version in meta.

basic/transports/stdio#backward-compatibility

Judged by discover.dual-era-probe-first.

TRAN-129

MUST NOT · binds the client

The fallback MUST NOT be keyed to one specific error code: legacy servers respond to unknown pre-initialize requests with implementation-defined errors (commonly -32601 or -32602) or not at all.

basic/transports/stdio#backward-compatibility

Excluded. Binds the client’s decision function, not any message it sends. A session exhibits one probe answered by one error code and one decision taken; whether the client would have fallen back on a different code is a property of code paths that session never exercised, and no number of traces settles it.

TRAN-142

SHOULD · binds the client

A client that only supports modern versions does not need to probe, but probing is still RECOMMENDED: some legacy servers do not validate that a request arrives after initialize and would process an era-ambiguous method (such as tools/call) under legacy semantics.

basic/transports/stdio#backward-compatibility

Excluded. Asks a modern-only client to open with a server/discover probe, and a recording can show neither half. Not the antecedent: a modern-only client and a dual-era client whose server never forced a fallback send the same messages. Not the order: a modern stdio session has no handshake marking where it began, so a recording that opens on tools/call is equally one that started after the probe. The dual-era statement, TRAN-128, is judged by discover.dual-era-probe-first, which reports only where a session witnesses both eras; this clause offers no such witness.

MRTR

MRTR-001

MUST · binds the server

Servers MUST send server-to-client requests (such as roots/list, sampling/createMessage, or elicitation/create) using the MRTR pattern.

basic/patterns/mrtr

Judged by transport.no-independent-server-requests.

MRTR-002

MUST · binds the client

Every request MUST include the required meta fields; see meta.

basic/patterns/mrtr

Judged by meta.required-request-fields.

MRTR-003

MUST NOT · binds the client

Clients MUST NOT inspect, parse, modify, or make any assumptions about its contents.

basic/patterns/mrtr#inputrequiredresult

Judged by mrtr.request-state-echoed.

MRTR-004

MUST NOT · binds the server

Servers MUST NOT send InputRequiredResult responses on any other client requests.

basic/patterns/mrtr#supported-requests

Judged by mrtr.input-required-supported-methods.

MRTR-005

MUST · binds the server

inputRequests keys are server assigned identifiers and MUST be unique within the scope of the request.

basic/patterns/mrtr#server-requirements-basic-workflow

Excluded. JSON object members are unique by the time any check runs: a duplicate key is collapsed by the parser that reads the trace, so a violation cannot survive into the events a check sees. The same reason TRAN-110 and TRAN-115 exclude — the defect is destroyed at parse time, not judged and passed.

MRTR-006

MUST · binds the server

inputRequests values are request objects that MUST be one of ElicitRequest, CreateMessageRequest, or ListRootsRequest

basic/patterns/mrtr#server-requirements-basic-workflow

Judged by mrtr.input-request-methods.

MRTR-007

MUST · binds the server

If a client request contains a requestState field, servers MUST treat requestState as an attacker-controlled input.

basic/patterns/mrtr#server-requirements-basic-workflow

Excluded. A disposition toward an input, not an act: a server that treats state as trusted and one that treats it as hostile send the same messages until the state is actually tampered with, and even then what differs is the server’s internal decision. The consequences the clause draws from it — integrity protection and rejection — are MRTR-008, excluded for their own reasons.

MRTR-008

MUST · binds the server

If requestState influences authorization, resource access, or business logic, servers MUST protect its integrity (e.g. HMAC or AEAD) and MUST reject state that fails verification.

basic/patterns/mrtr#server-requirements-basic-workflow

Excluded. Both halves are invisible on the wire. requestState is opaque by construction — the specification’s own examples range over base64 JSON, encrypted JWTs and serialized binary — so no check can tell an HMAC-protected blob from an unprotected one. The antecedent is worse still: whether the state influences authorization is a fact about the server’s code. Verified instead by the implementation’s own tests, where the key is available.

MRTR-009

SHOULD · binds the server

To prevent replay, servers SHOULD include the following inside the integrity-protected requestState payload and verify each on receipt: the authenticated principal, rejecting state presented by a different principal. a short expiry (TTL), rejecting state presented after it lapses; an identifier for the originating request, e.g. the method name and a digest of its salient parameters, rejecting state presented on a request that does not match.

basic/patterns/mrtr#server-requirements-basic-workflow

Excluded. Names three fields that live inside the integrity-protected payload, which is opaque and generally encrypted — the trace carries the blob, never its contents. Two of the three also bind time and identity: expiry is a duration, which checks may not consult (LIFE-015), and the authenticated principal is not in the message.

MRTR-010

MUST · binds the server

Servers for which a given requestState must be consumed at most once (e.g., one-time redemptions) MUST enforce that invariant server-side.

basic/patterns/mrtr#server-requirements-basic-workflow

Excluded. The clause’s own words put the obligation server-side, and its antecedent — that this server’s states are single-use — is a property of the server’s design that no message declares. A trace showing one state presented twice would still not distinguish a violated invariant from a server for which none was ever claimed.

MRTR-011

MUST · binds the server

Servers MUST include at least one of inputRequests or requestState in every InputRequiredResult response.

basic/patterns/mrtr#server-requirements-basic-workflow

Judged by mrtr.input-required-has-content.

MRTR-012

MUST NOT · binds the server

Servers MUST NOT send an inputRequests that the client has not declared support for in its capabilities.

basic/patterns/mrtr#server-requirements-basic-workflow

Judged by meta.no-undeclared-capability-reliance.

MRTR-013

MUST NOT · binds the server

For example, if a client does not declare support for elicitation, the server MUST NOT include any elicitation/create requests in the inputRequests field.

basic/patterns/mrtr#server-requirements-basic-workflow

Judged by meta.no-undeclared-capability-reliance.

MRTR-014

MUST NOT · binds the server

Servers MUST NOT assume that clients will fulfill the inputRequests or retry the original request.

basic/patterns/mrtr#server-requirements-basic-workflow

Excluded. Forbids an assumption. Its wire-visible consequence would be a server that breaks when a client declines to retry, and a conforming session never exhibits that: the client either retries, which tells us nothing about the assumption, or does not, and the trace simply ends.

MRTR-015

MUST · binds the client

If a client receives an InputRequiredResult that contains the inputRequests field, the client MUST construct the requested inputs before retrying the original request.

basic/patterns/mrtr#client-requirements-basic-workflow

Judged by mrtr.retry-carries-input-responses.

MRTR-016

MUST · binds the client

If an InputRequiredResult contains the requestState field, the client MUST echo back the exact value of that field when retrying the original request.

basic/patterns/mrtr#client-requirements-basic-workflow

Judged by mrtr.request-state-echoed.

MRTR-017

MUST NOT · binds the client

Clients MUST NOT inspect, parse, modify, or make any assumptions about the requestState contents.

basic/patterns/mrtr#client-requirements-basic-workflow

Judged by mrtr.request-state-echoed.

MRTR-018

MUST NOT · binds the client

If the InputRequiredResult does not contain a requestState field, the client MUST NOT include one in the retry.

basic/patterns/mrtr#client-requirements-basic-workflow

Judged by mrtr.no-unsolicited-request-state.

MRTR-019

MUST · binds the client

The JSON-RPC id MUST be different between the initial request and the retry, as they are independent requests.

basic/patterns/mrtr#client-requirements-basic-workflow

Judged by mrtr.retry-id-differs.

MRTR-020

MUST NOT · binds the client

They MUST NOT be used for any other request that the client may be sending in parallel.

basic/patterns/mrtr#client-requirements-basic-workflow

Judged by mrtr.request-state-scoped-to-retry.

MRTR-021

SHOULD · binds the server

Servers SHOULD validate that the data provided by the client is a valid InputResponses object and that the information inside can be correctly parsed.

basic/patterns/mrtr#error-handling

Excluded. Binds an act of validation, whose outcome is the only thing that reaches the wire — and a server that validated and accepted is indistinguishable from one that never looked. The outcomes the specification does pin are separate clauses: MRTR-023 for what a rejection may ignore, MRTR-024 for what a shortfall must draw.

MRTR-022

SHOULD · binds the server

Protocol errors (malformed JSON, invalid schema, internal server errors) SHOULD return a JSON-RPC error response with an appropriate error code and message.

basic/patterns/mrtr#error-handling

Excluded. The antecedent is not identifiable from a trace: nothing in an exchange says whether the server met a protocol error, so an error response and a completed result are equally consistent with the clause. “Appropriate” is also unbounded — the clause names no code — and the shape an error must have regardless is BASE-052’s, which is checked.

MRTR-023

SHOULD · binds the server

If additional, unexpected parameters are provided in the InputResponses object, the server SHOULD ignore any information it does not recognize or need.

basic/patterns/mrtr#error-handling

Excluded. Requires an absence of consequence: a server that ignored an unexpected field and one that never read it produce identical sessions. The only failing form would be a server that rejected the request because of the extra field, and no error response says that was the reason.

MRTR-024

SHOULD · binds the server

If the client fails to send all the information requested in a previous InputRequests, and the missing information is necessary for the server to process the request, the server SHOULD respond with a new InputRequiredResult requesting the missing information again, rather than returning an error.

basic/patterns/mrtr#error-handling

Judged by mrtr.missing-input-reasked.

MRTR-025

MUST · binds the server

Servers MUST validate request state as described in the server requirements above.

basic/patterns/mrtr#security-considerations

Excluded. A pointer back to MRTR-007 and MRTR-008 rather than a new rule, and it inherits their reason: the state is an opaque blob, so no check can tell a validated one from an unvalidated one.

SUBS

SUBS-001

MUST NOT · binds the server

The server MUST NOT send notification types the client has not explicitly requested.

basic/patterns/subscriptions#opening-a-stream

Judged by subscriptions.only-requested-notifications.

SUBS-002

MUST NOT · binds the server

The server MUST send notifications/subscriptions/acknowledged as the first message carrying the subscription’s ID in meta under io.modelcontextprotocol/subscriptionId, and MUST NOT send any notification on the subscription before it.

basic/patterns/subscriptions#acknowledgment

Judged by subscriptions.acknowledgment-first.

SUBS-003

SHOULD · binds the client

The client SHOULD check the acknowledged filter against what it requested and handle any unsupported types gracefully.

basic/patterns/subscriptions#acknowledgment

Excluded. Comparing two things it already has and reacting well to the difference: both the comparison and “gracefully” are inside the client, and a client that never looked sends exactly the messages one that did would. The server’s side of the same exchange — honouring only what was requested — is SUBS-001 and is checked.

SUBS-004

MUST · binds the client

On stdio, where all messages share a single channel, clients MUST use this field to correlate notifications with their originating subscription.

basic/patterns/subscriptions#receiving-notifications

Excluded. The client’s own demultiplexing, which produces no message — the same clause TRAN-122 states from the stdio page, with the same reason. The wire-visible half, that the server tags every notification with io.modelcontextprotocol/subscriptionId, is BASE-039 and is checked.

SUBS-005

SHOULD · binds the server

The server tears it down (e.g., during shutdown) — it SHOULD send a successful subscriptions/listen response to signal a graceful end (see Graceful Closure), then close the stream.

basic/patterns/subscriptions#cancellation

Judged by subscriptions.graceful-close-result-shape.

SUBS-006

SHOULD · binds the server

When the server ends a subscription on its own initiative (for example, during shutdown), it SHOULD respond to the original subscriptions/listen request with a completion result before closing the stream.

basic/patterns/subscriptions#graceful-closure

Judged by subscriptions.graceful-close-result-shape.

SUBS-007

MUST · binds the client

On stdio, if the connection is terminated and then re-established, the client MUST re-send subscriptions/listen to re-establish its subscriptions — the server holds no subscription state across reconnections.

basic/patterns/subscriptions#graceful-closure

Excluded. The obligation begins after a re-established connection, which is a second session: the evidence would have to span two recordings, and nothing in either identifies the other as its predecessor. Same boundary as TRAN-127, which excludes the restart that precedes it.

CANC

CANC-001

SHOULD · binds the client

A client SHOULD send a cancellation notification to indicate that a request it previously issued should be terminated.

basic/patterns/cancellation

Excluded. The antecedent is the client’s wish to stop a request, which produces no message of its own: a session with no cancellation is indistinguishable from one where nothing was abandoned. The same boundary TRAN-123’s check records for the stdio half of this rule. What a cancellation looks like once sent is CANC-005’s.

CANC-002

MUST · binds the server

A server MUST send notifications/cancelled referencing a subscriptions/listen request ID when it tears down that subscription stream

basic/patterns/cancellation

Excluded. The antecedent — the server deciding to tear a subscription down — is internal, and a stream that simply stops is indistinguishable in a recording from one the client closed or the network lost, so a missing notification has no falsifier. The message itself, when sent, is accepted by the checks it would otherwise trip: meta.subscription-id-present (BASE-039) and subscriptions.only-requested-notifications (SUBS-001) both treat a notifications/cancelled whose requestId names a subscriptions/listen request as the stream’s own ending. Sending it for anything else is CANC-003’s and judged.

CANC-003

MUST NOT · binds the server

Servers MUST NOT send notifications/cancelled for any other purpose.

basic/patterns/cancellation

Judged by cancellation.server-cancels-only-subscriptions.

CANC-004

MUST · binds the server

The server MUST treat a client disconnect as cancellation of that request.

basic/patterns/cancellation#transport-specific-cancellation

Excluded. The cancellation page’s statement of TRAN-069, excluded for the same reason: how a server treats a closed stream is a decision, and the trace shows only that the transport ended. The observable consequence — anything sent for the request afterwards — is TRAN-070’s, judged by transport.no-messages-after-cancellation.

CANC-005

MUST · binds the client

The client MUST send a notifications/cancelled notification referencing the request ID.

basic/patterns/cancellation#transport-specific-cancellation

Judged by transport.cancel-notification-references-request.

CANC-006

SHOULD · binds the client and server

Implementations SHOULD establish timeouts for all sent requests, to prevent hung connections and resource exhaustion.

basic/patterns/cancellation#timeouts

Excluded. A timeout is a duration configured inside the sender, and checks may not consult time — the limit LIFE-015 records. A trace shows a request answered or not, never the deadline the sender held it to.

CANC-007

SHOULD · binds the client and server

When the request has not received a success or error response within the timeout period, the sender SHOULD cancel the request and stop waiting for a response.

basic/patterns/cancellation#timeouts

Excluded. The trigger is the sender’s own timeout expiring, which no message records and which checks may not compute from timestamps (LIFE-015’s limit). A cancellation that is sent is judged for its shape by CANC-005; whether a timeout prompted it is not on the wire.

CANC-008

SHOULD · binds the client and server

SDKs and other middleware SHOULD allow these timeouts to be configured on a per-request basis.

basic/patterns/cancellation#timeouts

Excluded. Binds an SDK’s programming interface — what its callers can configure — which has no protocol representation at all.

CANC-009

SHOULD · binds the client and server

However, implementations SHOULD always enforce a maximum timeout, regardless of progress notifications, to limit the impact of a misbehaving client or server.

basic/patterns/cancellation#timeouts

Excluded. A maximum timeout is a duration the sender enforces internally; checks may not consult time (LIFE-015), and a request still open when the recording ends says nothing about the deadline it would have hit.

CANC-010

MUST · binds the client and server

Cancellation notifications MUST only reference requests that: Were previously issued by the client; Are believed to still be in-progress

basic/patterns/cancellation#behavior-requirements

Excluded. Neither condition has a sound falsifier. A recording may begin mid-session, so a requestId the trace never saw issued may still have been — the reason transport.cancel-notification-references-request judges only that a request is named. “Believed to still be in-progress” is the sender’s belief, and the page’s own Timing Considerations make a cancellation that crosses the response on the wire conforming. The server-sent case is narrowed further by CANC-011, which is judged.

CANC-011

MUST · binds the server

Server-sent cancellation notifications MUST reference a subscriptions/listen request, to terminate that subscription stream

basic/patterns/cancellation#behavior-requirements

Judged by cancellation.server-cancels-only-subscriptions.

CANC-012

SHOULD · binds the server

Servers receiving cancellation notifications SHOULD: Stop processing the cancelled request; Free associated resources; Not send a response for the cancelled request

basic/patterns/cancellation#behavior-requirements

Excluded. Stopping work and freeing resources are internal. The one wire-visible item, not answering a cancelled request, is a MUST NOT on both transport pages and judged there at that strength: TRAN-124 (transport.no-messages-after-cancel-notification, stdio) and TRAN-070 (transport.no-messages-after-cancellation, Streamable HTTP). Judging it again here would report every such answer twice, once as a warning.

CANC-013

SHOULD · binds the client

The client SHOULD ignore any response to the cancelled request that arrives afterward

basic/patterns/cancellation#behavior-requirements

Excluded. Ignoring a response is the absence of a reaction inside the client; nothing it does with a late response reaches the wire.

CANC-014

MUST · binds the client and server

Both parties MUST handle these race conditions gracefully:

basic/patterns/cancellation#timing-considerations

Excluded. Graceful handling is internal; the race itself is what a recording shows, and the checks are built not to convict it — TRAN-124 reports a message after a cancellation only once the server has demonstrably read it, and the conforming stdio-cancellation-race trace pins that. A party that failed to handle the race would show it through some other clause’s violation, judged there.

CANC-015

SHOULD · binds the client and server

Both parties SHOULD log cancellation reasons for debugging

basic/patterns/cancellation#implementation-notes

Excluded. Logging is local diagnostics with no wire footprint.

CANC-016

SHOULD · binds the client

Application UIs SHOULD indicate when cancellation is requested

basic/patterns/cancellation#implementation-notes

Excluded. User-interface presentation, out by strategy rule 2: nothing a UI shows reaches the protocol.

CANC-017

SHOULD · binds the client and server

Invalid cancellation notifications SHOULD be ignored: Unknown request IDs; Already completed requests; Malformed notifications

basic/patterns/cancellation#error-handling

Excluded. Ignoring a notification is the absence of a reaction, which a trace cannot tell apart from the notification never mattering. The party that sent the invalid notification is judged instead, where it can be: CANC-005 for a cancellation naming no request, CANC-011 for a server cancelling anything but a subscription.

PROG

PROG-001

MUST · binds the client

Progress tokens MUST be a string or integer value

basic/patterns/progress#progress-flow

Judged by progress.token-type.

PROG-002

MUST · binds the client

Progress tokens can be chosen by the client using any means, but MUST be unique across all active requests.

basic/patterns/progress#progress-flow

Judged by progress.token-unique-in-flight.

PROG-003

MUST · binds the server

The progress value MUST increase with each notification, even if the total is unknown.

basic/patterns/progress#progress-flow

Judged by progress.value-increases.

PROG-004

SHOULD · binds the server

The message field SHOULD provide relevant human readable progress information.

basic/patterns/progress#progress-flow

Excluded. Whether a message is relevant and human readable is a judgment about its meaning, not its shape; any string satisfies the schema, and no mechanical reading of one decides relevance.

PROG-005

MUST · binds the server

Progress notifications MUST only reference tokens that: Were provided in an active request; Are associated with an in-progress operation

basic/patterns/progress#behavior-requirements

Judged by progress.no-progress-after-completion.

PROG-006

SHOULD · binds the client and server

Clients and servers SHOULD track active progress tokens

basic/patterns/progress#implementation-notes

Excluded. Tracking is bookkeeping inside each party. Its wire consequences are the MUST rows beside it, which are judged: a reused token (PROG-002) and progress for a finished request (PROG-005, PROG-008).

PROG-007

SHOULD · binds the client and server

Both parties SHOULD implement rate limiting to prevent flooding

basic/patterns/progress#implementation-notes

Excluded. A rate is messages per unit of time, and checks may not consult time — the limit LIFE-015 records. No count of notifications alone separates a flood from a long operation reporting often.

PROG-008

MUST · binds the server

Progress notifications MUST stop after completion

basic/patterns/progress#implementation-notes

Judged by progress.no-progress-after-completion.

CACH

CACH-001

MUST · binds the server

Servers MUST include caching hints on results with resultType: "complete" returned by the following operations: server/discover; tools/list; prompts/list; resources/list; resources/templates/list; resources/read

server/utilities/caching#cacheable-results

Judged by caching.hints-on-cacheable-results.

CACH-002

MUST NOT · binds the client

Clients MUST NOT serve a cached response for a request whose method or parameters differ from the request that produced it.

server/utilities/caching#cache-key

Excluded. Binds which cached response a client serves itself, and a cache hit is precisely the case where nothing reaches the wire. Serving from a cache produces no message: the client either sends a request or does not, and a session shows the request it sent, never the cached response it served instead.

CACH-003

MUST NOT · binds the client

Results produced by retrying a request through the multi round-trip requests mechanism—that is, requests carrying inputResponses or requestState—MUST NOT be cached, as they depend on inputs that are not part of the cache key.

server/utilities/caching#cache-key

Excluded. The prohibited act is caching, which happens inside the client and produces no message. Its server-side shadow is honoured by CACH-001’s check rather than left implicit: a complete result answering a request that carried inputResponses or requestState is not required to carry caching hints, because the specification does not ask a server to advertise freshness for a result nobody may cache.

CACH-004

SHOULD · binds the client

If ttlMs is 0, the response SHOULD be considered immediately stale.

server/utilities/caching#time-to-live-ttl-field

Excluded. Serving from a cache produces no message: the client either sends a request or does not, and a session shows the request it sent, never the cached response it served instead. The freshness rules are worse still — they are stated in terms of elapsed time, and checks may not consult time (LIFE-015).

CACH-005

SHOULD · binds the client

If ttlMs is positive, the client SHOULD consider the result fresh for that many milliseconds after receiving the response.

server/utilities/caching#time-to-live-ttl-field

Excluded. Serving from a cache produces no message: the client either sends a request or does not, and a session shows the request it sent, never the cached response it served instead. The freshness rules are worse still — they are stated in terms of elapsed time, and checks may not consult time (LIFE-015).

CACH-006

SHOULD · binds the client

If ttlMs is absent, clients SHOULD assume a default of 0 (immediately stale) and rely on their own caching heuristics or notifications.

server/utilities/caching#time-to-live-ttl-field

Excluded. Serving from a cache produces no message: the client either sends a request or does not, and a session shows the request it sent, never the cached response it served instead. The freshness rules are worse still — they are stated in terms of elapsed time, and checks may not consult time (LIFE-015).

CACH-007

SHOULD · binds the client

If ttlMs is negative, clients SHOULD ignore it and treat it as 0.

server/utilities/caching#time-to-live-ttl-field

Excluded. What a client concludes from a negative ttlMs stays inside it. The value itself is judged on the other side of the exchange: CACH-008 forbids a server from sending one.

CACH-008

MUST · binds the server

Servers MUST provide a ttlMs value that is >= 0.

server/utilities/caching#time-to-live-ttl-field

Judged by caching.ttl-non-negative.

CACH-009

SHOULD · binds the client

Once the TTL expires, the response is stale and the client SHOULD re-fetch on next access.

server/utilities/caching#freshness-calculation

Excluded. Serving from a cache produces no message: the client either sends a request or does not, and a session shows the request it sent, never the cached response it served instead. The freshness rules are worse still — they are stated in terms of elapsed time, and checks may not consult time (LIFE-015).

CACH-010

SHOULD NOT · binds the client

Clients SHOULD NOT treat TTL as a polling interval that triggers automatic background refetches.

server/utilities/caching#freshness-calculation

Excluded. Forbids a reason for sending requests, not the requests themselves: a poll and a genuine need look identical on the wire. Distinguishing them would mean inferring intent from inter-request timing, which checks may not consult.

CACH-011

MUST · binds the client

Implementations that do choose to poll MUST apply jitter and backoff.

server/utilities/caching#freshness-calculation

Excluded. Both the antecedent (this client is polling) and the obligation (its intervals are jittered and backed off) are properties of a request schedule, measurable only in elapsed time — which checks may not consult (LIFE-015) — and only across a run far longer than one recorded session.

CACH-019

MUST NOT · binds the client

Caches MUST NOT be shared across authorization contexts (e.g. a different access token requires a different cache).

server/utilities/caching#cache-scope-field

Excluded. A cache, and which authorization context may read from it, live inside the client or an intermediary: a hit serves a stored result without sending anything, so a cache shared across contexts produces no message. Nor does a trace carry the authorization context to compare: mcp-trace-capture records an allowlist of headers that never includes credentials, so no access token reaches the recording. Same boundary as CACH-002 (what a client may serve from its cache).

CACH-012

SHOULD · binds the client

When a cached page expires, the client SHOULD re-fetch that page using its cursor.

server/utilities/caching#interaction-with-pagination

Excluded. Serving from a cache produces no message: the client either sends a request or does not, and a session shows the request it sent, never the cached response it served instead. The freshness rules are worse still — they are stated in terms of elapsed time, and checks may not consult time (LIFE-015).

CACH-013

SHOULD · binds the client

Clients that require a consistent snapshot of the full list SHOULD re-fetch from the beginning (without a cursor).

server/utilities/caching#interaction-with-pagination

Excluded. The antecedent is a client requirement — that this client needs a consistent snapshot — which nothing on the wire declares. A client that paginates from a cursor is equally consistent with not needing one.

CACH-014

SHOULD · binds the client

If a cursor becomes invalid (e.g., the server returns an error for a previously valid cursor), the client SHOULD discard all cached pages and re-fetch from the beginning.

server/utilities/caching#interaction-with-pagination

Excluded. Discarding a cache is invisible, and the visible half — re-fetching from the beginning — is indistinguishable from an ordinary new list request. Serving from a cache produces no message: the client either sends a request or does not, and a session shows the request it sent, never the cached response it served instead.

CACH-015

MUST · binds the server

Servers MUST apply the same cacheScope to all response pages for a given list request.

server/utilities/caching#interaction-with-pagination

Judged by caching.page-scope-consistent.

CACH-016

MUST · binds the server

For example, if the first page of a tools/list response has cacheScope: "private", all subsequent pages for that request MUST also be "private".

server/utilities/caching#interaction-with-pagination

Judged by caching.page-scope-consistent.

CACH-017

MUST · binds the server

Servers MUST be aware that responses with a "public" cacheScope may be shared between callers even if the Result is coming from an authenticated endpoint.

server/utilities/caching#security-considerations

Excluded. Requires awareness. A server that has understood the sharing consequence and one that has not send the same bytes; what the clause is really asking for is the judgement CACH-018 then makes concrete, and that is excluded for its own reason.

CACH-018

MUST NOT · binds the server

MUST apply appropriate per-primitive access controls, and MUST NOT rely on cacheScope alone to prevent unauthorized access to primitives.

server/utilities/caching#security-considerations

Excluded. Access control happens before a response is composed, inside the server: a trace shows what one caller was given, never what a different caller would have been refused. Whether cacheScope was relied upon for that decision is a fact about the server’s code.

COMP

COMP-006

MUST · binds the client

Every request MUST include the required meta fields; see meta.

server/utilities/completion

Judged by meta.required-request-fields.

COMP-007

MUST · binds the server

Servers that support completions MUST declare the completions capability:

server/utilities/completion#capabilities

Judged by capabilities.completions-declared.

COMP-008

SHOULD · binds the server

Servers SHOULD return standard JSON-RPC errors for common failure cases: Method not found: -32601 (Capability not supported); Invalid prompt name: -32602 (Invalid params); Missing required arguments: -32602 (Invalid params); Internal errors: -32603 (Internal error)

server/utilities/completion#error-handling

Excluded. Each row’s antecedent is a server-side determination — that this prompt name is invalid, that a required argument is missing — and a trace shows the answer without the reason for it. An exchange ending in -32602 and one ending in a result are equally consistent with the clause, because nothing on the wire says which case the server was in.

COMP-009

SHOULD · binds the server

Servers SHOULD: Return suggestions sorted by relevance; Implement fuzzy matching where appropriate; Rate limit completion requests; Validate all inputs

server/utilities/completion#implementation-considerations

Excluded. Four quality properties, none of them a message shape: relevance ordering has no wire-observable definition, fuzzy matching is an algorithm choice, rate limiting is measured in elapsed time (LIFE-015), and validation that accepts leaves no trace.

COMP-010

SHOULD · binds the client

Clients SHOULD: Debounce rapid completion requests; Cache completion results where appropriate; Handle missing or partial results gracefully

server/utilities/completion#implementation-considerations

Excluded. Debouncing and caching are timing and cache-hit properties, both invisible for the reasons CACH-010 and CACH-002 record; “gracefully” describes what the client does after the response arrives, which is not a message.

COMP-011

MUST · binds the client and server

Implementations MUST: Validate all completion inputs; Implement appropriate rate limiting; Control access to sensitive suggestions; Prevent completion-based information disclosure

server/utilities/completion#security

Excluded. Security properties of an implementation, not of an exchange. A trace shows one completion answered for one caller; whether a different caller would have been refused, and whether the suggestions disclosed something they should not, are facts about the server’s data and policy rather than about the message.

PAGE

PAGE-006

MUST · binds the client

Every request MUST include the required meta fields; see meta.

server/utilities/pagination

Judged by meta.required-request-fields.

PAGE-007

MUST NOT · binds the client

The cursor is an opaque string token, representing a position in the result set; Page size is determined by the server, and clients MUST NOT assume a fixed page size

server/utilities/pagination#pagination-model

Excluded. Forbids an assumption. A client that assumed a fixed page size and one that did not send identical requests — the assumption only shows in how the client interprets what comes back, which is not on the wire.

PAGE-008

SHOULD · binds the server

Servers SHOULD: Provide stable cursors; Handle invalid cursors gracefully

server/utilities/pagination#implementation-guidelines

Excluded. Cursor stability is a property across time and sessions — the same cursor presented later must still resolve — which one recording cannot exhibit. The graceful half is stated concretely by PAGE-011, which is excluded at this revision for the reason it records.

PAGE-009

SHOULD · binds the client

Clients SHOULD: Treat a missing nextCursor as the end of results; Support both paginated and non-paginated flows

server/utilities/pagination#implementation-guidelines

Excluded. Both bind how a client reads a result it already has. Stopping because nextCursor was absent and stopping for any other reason produce the same session: no further request.

PAGE-010

MUST · binds the client

Clients MUST treat cursors as opaque tokens: Don’t make assumptions about cursor format; Don’t attempt to parse or modify cursors; Don’t make any determination based on cursor value other than whether a non-null value was provided (e.g. an empty string is a valid cursor and thus MUST NOT be treated as the end of results)

server/utilities/pagination#implementation-guidelines

Excluded. A recording’s witness for a cursor the client made up — one no result in the session issued — does not hold at this revision. 2026-07-28 drops 2025-11-25’s “Don’t persist cursors across sessions” and has no sessions to persist across; its caching page says “When a cached page expires, the client SHOULD re-fetch that page using its cursor”, a cursor issued before the recording began. A parsed or altered cursor is indistinguishable on the wire from such a one.

PAGE-011

SHOULD · binds the server

Invalid cursors SHOULD result in an error with code -32602 (Invalid params).

server/utilities/pagination#error-handling

Excluded. Whether a cursor is invalid is the server’s knowledge. At 2025-11-25 a cursor no result in the session issued witnesses it (cursors must not outlive a session, and 2025-11-25’s PAGE-003 judges that); at this revision a cursor issued before the recording began is a valid one the client should re-present (server/utilities/caching), so a server accepting it may be conforming. The everything server still rejects cursors its own scheme did not issue; the probe capture exercises that.

LOG

LOG-006

SHOULD NOT · binds the client and server

New implementations SHOULD NOT adopt it; existing implementations SHOULD migrate to logging to stderr for stdio transports, or to OpenTelemetry for structured observability.

server/utilities/logging

Excluded. Binds an adoption decision and a migration plan, neither of which is a message. A server that uses the feature is not thereby a new implementation, and a trace carries nothing that dates the code sending it.

LOG-007

MUST · binds the server

Servers that emit log message notifications MUST declare the logging capability:

server/utilities/logging#capabilities

Judged by capabilities.logging-declared.

LOG-008

MUST NOT · binds the server

The server MUST NOT emit notifications/message for a request that does not include this field.

server/utilities/logging#per-request-log-level

Judged by logging.level-requested.

LOG-009

MUST NOT · binds the server

notifications/message is request-scoped: the server MUST NOT deliver it on a subscriptions/listen stream or on any stream other than the one carrying the response to the request that set the log level.

server/utilities/logging#per-request-log-level

Judged by logging.not-on-subscription.

LOG-010

SHOULD · binds the server

If the io.modelcontextprotocol/logLevel value carried in a request’s meta is not a recognized log level, the server SHOULD reject that request with a standard JSON-RPC error: Invalid log level: -32602 (Invalid params); Internal errors: -32603 (Internal error)

server/utilities/logging#error-handling

Judged by logging.invalid-level-rejected.

LOG-011

SHOULD · binds the server

Servers SHOULD: Rate limit log messages; Include relevant context in data field; Use consistent logger names; Remove sensitive information

server/utilities/logging#implementation-considerations

Excluded. Rate limiting is measured in elapsed time (LIFE-015); “relevant context”, “consistent” logger names and “sensitive” information are judgements about the meaning of the payload, which this engine does not model.

LOG-012

MUST NOT · binds the server

Log messages MUST NOT contain: Credentials or secrets; Personal identifying information; Internal system details that could aid attacks

server/utilities/logging#security

Excluded. Requires classifying the meaning of arbitrary JSON. Whether a string is a credential, a person’s identity, or an internal detail that aids an attacker cannot be decided structurally, and a checker that guessed would report conforming servers while missing real leaks.

LOG-013

SHOULD · binds the client and server

Implementations SHOULD: Rate limit messages; Validate all data fields; Control log access; Monitor for sensitive content

server/utilities/logging#security

Excluded. Four operational practices of an implementation. Access control and monitoring happen outside the session entirely, and validation that accepts leaves nothing behind — the same reason MRTR-021 excludes.

TOOL

TOOL-016

MUST · binds the client

Every request MUST include the required meta fields; see meta.

server/tools

Judged by meta.required-request-fields.

TOOL-017

SHOULD · binds the client

For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny tool invocations.

server/tools#user-interaction-model

Excluded. Binds the application’s user interface — what it shows, when it asks, whether a person is there. None of it is a message, and MCP deliberately does not mandate an interaction model.

TOOL-018

SHOULD · binds the client

Applications SHOULD: Provide UI that makes clear which tools are being exposed to the AI model; Insert clear visual indicators when tools are invoked; Present confirmation prompts to the user for operations, to ensure a human is in the loop

server/tools#user-interaction-model

Excluded. Binds the application’s user interface — what it shows, when it asks, whether a person is there. None of it is a message, and MCP deliberately does not mandate an interaction model.

TOOL-019

MUST · binds the server

Servers that support tools MUST declare the tools capability:

server/tools#capabilities

Judged by capabilities.tools-declared.

TOOL-020

MUST · binds the server

Servers that declare the tools capability MUST respond to tools/list requests with the set of tools currently available to the requesting client.

server/tools#capabilities

Judged by capabilities.tools-list-implemented.

TOOL-021

MUST NOT · binds the server

This set MAY be empty and MAY change over time (see List Changed Notification), but MUST NOT vary per-connection or as a side effect of other requests on the connection.

server/tools#capabilities

Excluded. Binds the set across connections and across other requests — two properties that need more than one session, or a second request whose effect on the first is invisible. A single recording shows one list at one moment.

TOOL-022

SHOULD · binds the server

Servers SHOULD return tools in a deterministic order (i.e., the same ordering across requests when the underlying set of tools has not changed).

server/tools#capabilities

Judged by tools.deterministic-order.

TOOL-023

MUST · binds the client

Note that the JSON-RPC id MUST be different between the initial request and the retry.

server/tools#input-required-tool-results

Judged by mrtr.retry-id-differs.

TOOL-024

SHOULD · binds the server

When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification to clients that have opened a subscriptions/listen stream with toolsListChanged: true:

server/tools#list-changed-notification

Excluded. The antecedent is that the underlying set changed, which happens inside the server and reaches the wire only as the notification the clause is asking for.

TOOL-025

MUST · binds the server

JSON Schema defining expected parameters Follows the JSON Schema usage guidelines Defaults to 2020-12 if no $schema field is present MUST be a valid JSON Schema object (not null) For tools with no parameters, use one of these valid approaches:

server/tools#tool

Judged by tools.input-schema-object.

TOOL-026

MUST · binds the client

For trust & safety and security, clients MUST consider tool annotations to be untrusted unless they come from trusted servers.

server/tools#tool

Excluded. “Consider untrusted” is a disposition toward data already received, and “trusted servers” is the client’s own configuration. Neither reaches the wire.

TOOL-027

SHOULD · binds the server

Tool names SHOULD be between 1 and 128 characters in length (inclusive).

server/tools#tool-names

Judged by tools.name-length.

TOOL-028

SHOULD · binds the client and server

Tool names SHOULD be considered case-sensitive.

server/tools#tool-names

Excluded. States how names are to be compared, not how they may be spelled. Two implementations differing on case sensitivity send identical messages until they disagree about a collision, and TOOL-030 already judges the collision.

TOOL-029

SHOULD NOT · binds the server

The following SHOULD be the only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.) Tool names SHOULD NOT contain spaces, commas, or other special characters.

server/tools#tool-names

Judged by tools.name-charset.

TOOL-030

SHOULD · binds the server

Tool names SHOULD be unique within a server.

server/tools#tool-names

Judged by tools.name-unique.

TOOL-031

SHOULD · binds the client

Clients or proxies that aggregate tools from multiple servers MAY encounter naming collisions (for example, two servers each exposing a search tool) and SHOULD implement a disambiguation strategy such as prefixing tool names with a server identifier.

server/tools#tool-names

Excluded. Binds a client aggregating several servers. One recorded session is one server, so the collision the clause is about cannot appear in it, and neither can the disambiguation strategy.

TOOL-032

SHOULD NOT · binds the client

The server name (from serverInfo) is not guaranteed to be unique across servers and SHOULD NOT be relied upon for disambiguation.

server/tools#tool-names

Excluded. Forbids relying on serverInfo for disambiguation — a client-side decision about data it holds. Binds what the client does with what it received; the reasoning leaves no message behind.

TOOL-033

MUST NOT · binds the server

Constraints on x-mcp-header values: MUST NOT be empty; MUST match HTTP field-name token syntax (1*tchar, RFC 9110 Section 5.1); MUST NOT contain control characters, including carriage return (CR, \r) or line feed (LF, \n); MUST be case-insensitively unique among all x-mcp-header values in the inputSchema; MUST only be applied to parameters with primitive types (integer, string, boolean).

server/tools#x-mcp-header

Judged by transport.x-mcp-header-name-valid.

TOOL-034

MUST · binds the client

Constraints on x-mcp-header values: Integer values MUST be within the safe range for integers represented using IEEE754 double-precision floating point numbers (−253+1 to 253−1)

server/tools#x-mcp-header

Judged by tools.x-mcp-header-integer-range.

TOOL-035

MUST · binds the client

Rejection means the client MUST exclude the invalid tool from the result of tools/list.

server/tools#x-mcp-header

Judged by transport.x-mcp-header-invalid-tool-not-called.

TOOL-036

SHOULD · binds the client

Clients SHOULD log a warning when rejecting a tool definition, including the tool name and the reason for rejection.

server/tools#x-mcp-header

Excluded. Binds the client’s log, which is not part of the session.

TOOL-037

SHOULD NOT · binds the server

Server developers SHOULD NOT mark sensitive parameters (passwords, API keys, tokens, PII) with x-mcp-header, as header values are visible to network intermediaries.

server/tools#x-mcp-header

Excluded. Requires deciding whether a parameter is sensitive — a judgement about the meaning of a name and its values, which this engine does not model. Same boundary as LOG-012.

TOOL-038

SHOULD · binds the server

Servers that use embedded resources SHOULD implement the resources capability:

server/tools#embedded-resources

Judged by capabilities.embedded-resource-declared.

TOOL-039

SHOULD · binds the server

For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.

server/tools#structured-content

Judged by tools.structured-content-text.

TOOL-040

MUST · binds the server

Servers MUST provide structured results that conform to this schema.

server/tools#output-schema

Judged by tools.output-schema-structured-result.

TOOL-041

SHOULD · binds the client

Clients SHOULD validate structured results against this schema.

server/tools#output-schema

Excluded. Validation performed inside the client. A client that validated a structured result and one that did not send the same messages, and the clause does not say what a client does with a result that fails; the server half of the rule, TOOL-040, is judged.

TOOL-042

SHOULD · binds the client

Clients SHOULD provide tool execution errors to language models to enable self-correction.

server/tools#error-handling

Excluded. Binds what the client hands to a language model, which is downstream of the session entirely.

TOOL-043

MUST · binds the server

Servers MUST: Validate all tool inputs; Implement proper access controls; Rate limit tool invocations; Sanitize tool outputs

server/tools#security-considerations

Excluded. Four operational properties of the server — input validation, access control, rate limiting, output sanitization. A trace shows one call answered for one caller; whether another would have been refused, and whether the output was sanitized, are facts about the server’s code and data.

TOOL-044

SHOULD · binds the client

Clients SHOULD: Prompt for user confirmation on sensitive operations; Show tool inputs to the user before calling the server, to avoid malicious or accidental data exfiltration; Validate tool results before passing to LLM; Follow the $ref resolution requirements when validating tool inputs and outputs against inputSchema and outputSchema; Implement timeouts for tool calls; Log tool usage for audit purposes

server/tools#security-considerations

Excluded. Six client-side practices — confirmation prompts, showing inputs, validating results, $ref resolution, timeouts, audit logs. Each happens inside the client or in its UI; none is a message.

TOOL-045

MUST · binds the server

Constraints on x-mcp-header values: MUST only be applied to properties that are statically reachable from the schema root, as defined in Custom Headers from Tool Parameters, which also defines how header values are extracted from call arguments

server/tools#x-mcp-header

Judged by transport.x-mcp-header-statically-reachable.

TOOL-046

MUST · binds the client

Clients using the Streamable HTTP transport MUST reject tool definitions where any x-mcp-header value violates these constraints.

server/tools#x-mcp-header

Judged by transport.x-mcp-header-invalid-tool-not-called.

RES

RES-011

MUST · binds the client

Every request MUST include the required meta fields; see meta.

server/resources

Judged by meta.required-request-fields.

RES-012

MUST · binds the server

Servers that support resources MUST declare the resources capability:

server/resources#capabilities

Judged by capabilities.resources-declared.

RES-013

MUST · binds the server

Servers that declare the resources capability MUST respond to resources/list requests with the set of resources currently available to the requesting client.

server/resources#capabilities

Judged by capabilities.resources-list-implemented.

RES-014

MUST NOT · binds the server

This set MAY be empty and MAY change over time (see List Changed Notification), but MUST NOT vary per-connection or as a side effect of other requests on the connection.

server/resources#capabilities

Excluded. Binds the set across connections and across other requests — two properties that need more than one session, or a second request whose effect on the first is invisible. A single recording shows one list at one moment.

RES-015

SHOULD · binds the server

When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification:

server/resources#list-changed-notification

Excluded. The antecedent is that the underlying set changed, which happens inside the server and reaches the wire only as the notification the clause is asking for. A session with no notification is equally consistent with a list that never changed.

RES-016

SHOULD · binds the server

Servers SHOULD use this scheme only when the client is able to fetch and load the resource directly from the web on its own—that is, it doesn’t need to read the resource via the MCP server.

server/resources#https//

Excluded. Turns on whether the client can fetch the resource itself, which is a fact about the client and its network, not about any message. The server’s choice of scheme is visible; the condition that licenses it is not.

RES-017

SHOULD · binds the server

For other use cases, servers SHOULD prefer to use another URI scheme, or define a custom one, even if the server will itself be downloading resource contents over the internet.

server/resources#https//

Excluded. Design guidance about which scheme to prefer, resolved before a URI is ever minted. Whether “other use cases” applies is the server author’s judgement, and RES-018 checks the only part a trace can hold the result to: that a custom scheme is well-formed.

RES-018

MUST · binds the server

Custom URI schemes MUST be in accordance with RFC3986, taking the above guidance in to account.

server/resources#custom-uri-schemes

Judged by resources.uri-scheme-rfc3986.

RES-019

MUST · binds the server

If the requested resource does not exist, servers MUST return a JSON-RPC error with code -32602 (Invalid Params).

server/resources#error-handling

Excluded. The antecedent — that the requested resource does not exist — is the server’s own knowledge. An error and a result are both consistent with the clause, because nothing on the wire says which case the server was in. The one shape the clause rules out explicitly is checked, as RES-022.

RES-020

SHOULD · binds the server

Servers SHOULD return -32603 for internal errors.

server/resources#error-handling

Excluded. “Internal errors” names a class of server-side failure that no message declares membership of. Every error code is consistent with the clause unless the trace says what went wrong, and it never does.

RES-021

SHOULD · binds the client

For backwards compatibility, clients SHOULD also accept -32002 as a resource not found error, as earlier protocol versions used this code.

server/resources#error-handling

Excluded. Binds how a client interprets an error it received. Note the direction: BASE-058 forbids a 2026-07-28 server from emitting -32002, and this asks clients to keep accepting it from older ones — neither contradicts the other, and neither is visible in what the client sends.

RES-022

MUST NOT · binds the server

Servers MUST NOT return an empty contents array for a non-existent resource.

server/resources#error-handling

Judged by resources.read-contents-non-empty.

RES-023

MUST · binds the server

Servers MUST validate all resource URIs Access controls SHOULD be implemented for sensitive resources Binary data MUST be properly encoded Resource permissions SHOULD be checked before operations Servers MUST sanitize file paths to prevent directory traversal attacks when serving file:// resources

server/resources#security-considerations

Judged by resources.blob-base64.

PROM

PROM-011

MUST · binds the client

Every request MUST include the required meta fields; see meta.

server/prompts

Judged by meta.required-request-fields.

PROM-012

MUST · binds the server

Servers that support prompts MUST declare the prompts capability in their DiscoverResult:

server/prompts#capabilities

Judged by capabilities.prompts-declared.

PROM-013

MUST · binds the server

Servers that declare the prompts capability MUST respond to prompts/list requests with the set of prompts currently available to the requesting client.

server/prompts#capabilities

Judged by capabilities.prompts-list-implemented.

PROM-014

MUST NOT · binds the server

This set MAY be empty and MAY change over time (see List Changed Notification), but MUST NOT vary per-connection or as a side effect of other requests on the connection.

server/prompts#capabilities

Excluded. Binds the set across connections and across other requests — two properties that need more than one session, or a second request whose effect on the first is invisible. A single recording shows one list at one moment.

PROM-015

SHOULD · binds the server

When the list of available prompts changes, servers that declared the listChanged capability SHOULD send a notification to clients that have opened a subscriptions/listen stream with promptsListChanged: true:

server/prompts#list-changed-notification

Excluded. The antecedent is that the underlying set changed, which happens inside the server and reaches the wire only as the notification the clause is asking for. A session with no notification is equally consistent with a list that never changed.

PROM-016

MUST · binds the server

The image data MUST be base64-encoded and include a valid MIME type.

server/prompts#image-content

Judged by prompts.image-content-encoding.

PROM-017

MUST · binds the server

The audio data MUST be base64-encoded and include a valid MIME type.

server/prompts#audio-content

Judged by prompts.audio-content-encoding.

PROM-018

MUST · binds the server

Resources can contain either text or binary (blob) data and MUST include:

server/prompts#embedded-resources

Judged by prompts.embedded-resource-shape.

PROM-019

SHOULD · binds the server

Servers SHOULD return standard JSON-RPC errors for common failure cases:

server/prompts#error-handling

Excluded. Each row’s antecedent is a server-side determination — an invalid prompt name, a missing required argument — and the trace shows the answer without the reason. Same shape as COMP-008.

PROM-020

SHOULD · binds the client and server

Servers SHOULD validate prompt arguments before processing Clients SHOULD handle pagination for large prompt lists Both parties SHOULD respect capability negotiation

server/prompts#implementation-considerations

Judged by prompts.arguments-validated.

PROM-021

MUST · binds the client and server

Implementations MUST carefully validate all prompt inputs and outputs to prevent injection attacks or unauthorized access to resources.

server/prompts#security

Excluded. “Carefully validate to prevent injection attacks” is a property of the server’s handling, not of the message: a prompt that was validated and one that was not look identical when both are accepted. What a trace can hold an implementation to is the concrete shape rules, PROM-016 through PROM-020.

ELIC

ELIC-054

MUST NOT · binds the server

Servers MUST NOT use form mode elicitation to request sensitive information such as passwords, API keys, access tokens, or payment credentials

client/elicitation#user-interaction-model

Excluded. Whether a requested field is “sensitive information” — the page’s own definition is “secrets and credentials that grant access or authorize transactions” — is a judgement about meaning, not syntax: a schema property named token may be a display preference and one named answer may collect a password. A check would have to guess from field names, and a guess convicts conforming servers. The server’s author decides it; nothing in the message settles it.

ELIC-055

MUST · binds the server

Servers MUST use URL mode for interactions involving such sensitive information

client/elicitation#user-interaction-model

Excluded. The counterpart of the form mode prohibition, and blocked by the same judgement: that an interaction “involves such sensitive information” is a fact about what the user will be asked for, which no message states, so a form mode request cannot be shown to have needed URL mode.

ELIC-056

MUST · binds the client

MCP clients MUST: Provide UI that makes it clear which server is requesting information; Respect user privacy and provide clear decline and cancel options; For form mode, allow users to review and modify their responses before sending; For URL mode, clearly display the target domain/host and gather user consent before navigation to the target URL

client/elicitation#user-interaction-model

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-057

MUST · binds the client

Clients that support elicitation MUST declare the elicitation capability in _meta.io.modelcontextprotocol/clientCapabilities on each request:

client/elicitation#capabilities

Judged by elicitation.client-declares-capability.

ELIC-058

MUST · binds the client

Clients declaring the elicitation capability MUST support at least one mode (form or url).

client/elicitation#capabilities

Excluded. No declaration can violate it on the wire. The page makes an elicitation object naming neither mode “equivalent to declaring support for form mode only”, so every declared elicitation capability names at least one mode. What remains is whether the client can actually handle the mode it declared, which is internal: a client that cannot still returns a well-formed decline or cancel.

ELIC-059

MUST NOT · binds the server

Servers MUST NOT send elicitation requests with modes that are not supported by the client.

client/elicitation#capabilities

Judged by elicitation.mode-supported.

ELIC-060

MUST · binds the server

All elicitation requests MUST include the following parameters:

client/elicitation#elicitation-requests

Judged by elicitation.request-parameters.

ELIC-061

MUST · binds the client

Clients MUST treat requests without a mode field as form mode.

client/elicitation#elicitation-requests

Excluded. A reading, not an act, like MRTR-007: a client’s answers to a form mode request and to one without mode are the same messages (accept with content, decline, cancel), and content is optional in the result, so no answer shows which reading the client took. The server half — that such a request is form mode — is what elicitation.mode-supported and elicitation.form-schema-present apply.

ELIC-062

MUST · binds the server

Form mode elicitation requests MUST either specify mode: "form" or omit the mode field, and include these additional parameters:

client/elicitation#form-mode-elicitation-requests

Judged by elicitation.form-schema-present.

ELIC-063

SHOULD · binds the client

Clients that support defaults SHOULD pre-populate form fields with these values.

client/elicitation#requested-schema

Excluded. UI behaviour: pre-populating a form is what the user sees before submitting. The submitted content cannot show it — the user may change or clear every default — so no message carries the clause.

ELIC-064

MUST · binds the server

URL mode elicitation requests MUST specify mode: "url", a message, and include these additional parameters:

client/elicitation#url-mode-elicitation-requests

Judged by elicitation.url-parameters.

ELIC-065

MUST · binds the server

The url parameter MUST contain a valid URL.

client/elicitation#url-mode-elicitation-requests

Judged by elicitation.url-valid.

ELIC-066

SHOULD · binds the client

Clients SHOULD provide manual controls that let the user retry or cancel the original request (or otherwise resume interacting with the client).

client/elicitation#example-request-sensitive-data

Excluded. UI behaviour: the controls the client offers its user. The wire shows only whether a retry or cancellation was eventually sent, which a user may decline to do whatever controls exist.

ELIC-067

SHOULD NOT · binds the server

Servers SHOULD NOT assume that elicitation requests will always succeed

client/elicitation#error-handling

Excluded. Forbids an assumption. Its wire-visible consequence would be a server that breaks when an elicitation is declined, and the page leaves the server free to answer a declined retry with a result, an error or another round, so no answer is wrong on its face — the same reasoning as MRTR-014.

ELIC-068

MUST · binds the server

and MUST handle cases where the user declines or cancels the elicitation, or where the client fails to process the request.

client/elicitation#error-handling

Excluded. “Handle” names no observable outcome: the specification lets a server answer a declined, cancelled or failed elicitation with a result, an error or a further input_required round, and every one of those is handling. A server that fails to handle the case shows it only by breaking — a crash or a hang ends the recording rather than writing anything into it.

ELIC-069

MUST · binds the server

However, if state is stored, servers implementing elicitation MUST securely associate this state with individual users following the guidelines in the security best practices document. Specifically: State storage MUST be protected against unauthorized access; For remote MCP servers, user identification MUST be derived from credentials acquired via MCP authorization when possible (e.g. sub claim)

client/elicitation#statefulness

Excluded. Server-internal state handling. How a server stores per-user state, protects it and binds it to an identity happens behind its own storage and authorization, and the identity it binds to is established outside the session’s messages, by MCP authorization (whose pages this registry keeps out of scope, sources.json). Verified, where verified at all, by the server’s own security review and tests; at 2026-07-28 the state a client carries is requestState, opaque by construction (MRTR-008).

ELIC-070

MUST NOT · binds the server

MCP servers MUST NOT rely on URL mode elicitation to authorize users for themselves.

client/elicitation#url-mode-elicitation-for-oauth-flows

Excluded. Concerns what the server treats as authorization of its own users — an internal decision. A URL mode elicitation used to sign a user in to the server looks on the wire exactly like one used for a third-party grant; the difference is what the server does with the outcome.

ELIC-071

MUST NOT · binds the server

The third-party credentials MUST NOT transit through the MCP client

client/elicitation#understanding-the-distinction

Excluded. Third-party credentials are opaque strings whose provenance no message declares: a check cannot tell one in a result from any other string, and by design they never enter the session at all. The obligation is the server’s — credentials could only transit the client if the server sent them — and it is kept by the flow the server builds, outside the recording.

ELIC-072

MUST NOT · binds the server

The MCP server MUST NOT use the client’s credentials for the third-party service

client/elicitation#understanding-the-distinction

Excluded. Token passthrough happens between the server and the third-party service, outside the MCP session; the recording holds only the client–server exchange, in which the client’s credentials appear (as HTTP authorization) whether or not the server reuses them elsewhere.

ELIC-073

MUST · binds the server

The user MUST authorize the MCP server directly

client/elicitation#understanding-the-distinction

Excluded. By the clause’s own words the interaction “happens outside the MCP protocol, without involving the MCP client”: it is between the user’s browser and the server or a third-party authorization server, so a recording of the MCP session cannot contain it.

ELIC-074

MUST NOT · binds the server

The MCP server MUST NOT transmit credentials obtained through URL mode elicitation to the MCP client.

client/elicitation#understanding-the-distinction

Excluded. The same barrier as the transit clause: a credential obtained through URL mode elicitation is an opaque string whose provenance no message declares, so a check cannot tell one in a result from any other value the server returns.

ELIC-075

MUST · binds the server

Servers MUST bind elicitation requests to the client and user identity

client/elicitation#security-considerations

Excluded. Binding is server-internal state, and the identity it binds to is established by authorization outside the session’s messages, so no message shows whether an elicitation was bound to it.

ELIC-076

MUST · binds the client

Clients MUST provide clear indication of which server is requesting information

client/elicitation#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-077

SHOULD · binds the client

Clients SHOULD implement user approval controls

client/elicitation#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-078

SHOULD · binds the client

Clients SHOULD allow users to decline elicitation requests at any time

client/elicitation#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-079

SHOULD · binds the client

Clients SHOULD present elicitation requests in a way that makes it clear what information is being requested and why

client/elicitation#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-080

MUST NOT · binds the server

MCP servers requesting elicitation: MUST NOT include sensitive information about the end-user, including credentials, personally identifiable information, etc., in the URL sent to the client in a URL elicitation request.

client/elicitation#safe-url-handling

Excluded. Whether a URL’s path or query carries “sensitive information about the end-user” is a semantic judgement about opaque values — an identifier in a query may be a random nonce or an email address — so a check would convict on a guess. The page’s own examples put an elicitationId in the query, which is exactly the kind of value a pattern would misread.

ELIC-081

MUST NOT · binds the server

MCP servers requesting elicitation: MUST NOT provide a URL which is pre-authenticated to access a protected resource, as the URL could be used to impersonate the user by a malicious client.

client/elicitation#safe-url-handling

Excluded. Whether a URL grants access without further authentication is a property of the server it points to, observable only by dereferencing it — which a validator must not do (the client rules on the same page forbid even pre-fetching it), and which a recording does not contain.

ELIC-082

SHOULD NOT · binds the server

MCP servers requesting elicitation: SHOULD NOT include URLs intended to be clickable in any field of a form mode elicitation request.

client/elicitation#safe-url-handling

Excluded. Intent: the clause forbids URLs “intended to be clickable”, and a form’s message or a field’s description may mention a URL for reference. A check for URL-shaped text would convict a server for mentioning a link, and the intent itself is not on the wire.

ELIC-083

SHOULD · binds the server

MCP servers requesting elicitation: SHOULD use HTTPS URLs for non-development environments.

client/elicitation#safe-url-handling

Excluded. Its antecedent — that this is a non-development environment — is not in the trace. A loopback host suggests development without proving it, and a staging or internal host proves nothing either way, so an http URL is not by itself evidence of a violation. The scheme is recorded; the environment is not.

ELIC-084

MUST · binds the client

Clients implementing URL mode elicitation MUST handle URLs carefully to prevent users from unknowingly clicking malicious links.

client/elicitation#safe-url-handling

Excluded. Binds what the client does with a URL outside the MCP session — fetching it, opening it, displaying it — none of which is a message the session carries. The reference host never dereferences an elicitation URL: it answers URL mode from its interaction script and records the id (answer_elicitation, crates/mcp-reference-host/src/handler.rs).

ELIC-085

MUST NOT · binds the client

When handling URL mode elicitation requests, MCP clients: MUST NOT automatically pre-fetch the URL or any of its metadata.

client/elicitation#safe-url-handling

Excluded. Binds what the client does with a URL outside the MCP session — fetching it, opening it, displaying it — none of which is a message the session carries. The reference host never dereferences an elicitation URL: it answers URL mode from its interaction script and records the id (answer_elicitation, crates/mcp-reference-host/src/handler.rs).

ELIC-086

MUST NOT · binds the client

When handling URL mode elicitation requests, MCP clients: MUST NOT open the URL without explicit consent from the user.

client/elicitation#safe-url-handling

Excluded. Binds what the client does with a URL outside the MCP session — fetching it, opening it, displaying it — none of which is a message the session carries. The reference host never dereferences an elicitation URL: it answers URL mode from its interaction script and records the id (answer_elicitation, crates/mcp-reference-host/src/handler.rs).

ELIC-087

MUST · binds the client

When handling URL mode elicitation requests, MCP clients: MUST show the full URL to the user for examination before consent.

client/elicitation#safe-url-handling

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-088

MUST · binds the client

When handling URL mode elicitation requests, MCP clients: MUST open the URL provided by the server in a secure manner that does not enable the client or LLM to inspect the content or user inputs.

client/elicitation#safe-url-handling

Excluded. Binds what the client does with a URL outside the MCP session — fetching it, opening it, displaying it — none of which is a message the session carries. The reference host never dereferences an elicitation URL: it answers URL mode from its interaction script and records the id (answer_elicitation, crates/mcp-reference-host/src/handler.rs).

ELIC-089

SHOULD · binds the client

When handling URL mode elicitation requests, MCP clients: SHOULD highlight the domain of the URL to mitigate subdomain spoofing.

client/elicitation#safe-url-handling

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-090

SHOULD · binds the client

When handling URL mode elicitation requests, MCP clients: SHOULD have warnings for ambiguous/suspicious URIs (i.e., containing Punycode).

client/elicitation#safe-url-handling

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-091

SHOULD NOT · binds the client

When handling URL mode elicitation requests, MCP clients: SHOULD NOT render URLs as clickable in any field of an elicitation request, except for the url field in a URL elicitation request (with the restrictions detailed above).

client/elicitation#safe-url-handling

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-092

MUST NOT · binds the server

Servers MUST NOT rely on client-provided user identification without server verification, as this can be forged.

client/elicitation#identifying-the-user

Excluded. Server-internal: whether the server verified an identity before trusting it is a decision made with information — its authorization context — that the session’s messages do not carry.

ELIC-093

SHOULD · binds the server

Instead, servers SHOULD follow security best practices.

client/elicitation#identifying-the-user

Excluded. Points to a separate document whose practices are server-internal; nothing in the session’s messages shows which were followed.

ELIC-094

MUST NOT · binds the server

Servers MUST NOT request sensitive information (passwords, API keys, etc.) via form mode

client/elicitation#form-mode-security

Excluded. Whether a requested field is “sensitive information” — the page’s own definition is “secrets and credentials that grant access or authorize transactions” — is a judgement about meaning, not syntax: a schema property named token may be a display preference and one named answer may collect a password. A check would have to guess from field names, and a guess convicts conforming servers. The server’s author decides it; nothing in the message settles it.

ELIC-095

SHOULD · binds the client

Clients SHOULD validate all responses against the provided schema

client/elicitation#form-mode-security

Judged by elicitation.response-matches-schema.

ELIC-096

SHOULD · binds the server

Servers SHOULD validate received data matches the requested schema

client/elicitation#form-mode-security

Excluded. Binds an act of validation, as MRTR-021 does: the server’s outcome — accepting the data, re-asking, or failing — is the only thing that reaches the wire, and each is consistent with a server that validated and with one that did not.

ELIC-097

MUST · binds the server

The MCP Server MUST verify the identity of the user who opens the URL before accepting information.

client/elicitation#phishing

Excluded. Identity verification of the user who opens the URL happens between that user’s browser and the server, outside the MCP session; the recording carries the URL the server sent and the client’s accept, not who opened it.

ELIC-098

MUST · binds the server

To prevent this attack, the server MUST ensure that the user who started the elicitation request (the end-user who is accessing the server via the MCP client) is the same user who completes the authorization flow.

client/elicitation#phishing

Excluded. Identity verification of the user who opens the URL happens between that user’s browser and the server, outside the MCP session; the recording carries the URL the server sent and the client’s accept, not who opened it.

ELIC-099

MUST · binds the server

In all implementations, the server MUST ensure that the mechanism to determine the user’s identity is resilient to attacks where an attacker can modify the elicitation URL.

client/elicitation#phishing

Excluded. Identity verification of the user who opens the URL happens between that user’s browser and the server, outside the MCP session; the recording carries the URL the server sent and the client’s accept, not who opened it.

SAMP

SAMP-021

SHOULD NOT · binds the client and server

New implementations SHOULD NOT adopt it

client/sampling

Excluded. Whether an implementation is new is not in any message: a session using the feature is equally consistent with an existing implementation that has not yet migrated, which the sentence asks only to migrate.

SAMP-022

SHOULD · binds the client and server

existing implementations SHOULD migrate to integrating directly with LLM provider APIs.

client/sampling

Excluded. Asks for a migration over time, which one recording cannot show either way — a session that uses the feature may come from an implementation partway through migrating, and the feature remains in the specification until removal.

SAMP-023

SHOULD · binds the client

For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny sampling requests.

client/sampling#user-interaction-model

Excluded. Policy and UI: whether a human reviews sampling requests, and whether they can deny one, is the client application’s design. A request answered with a result may have been approved by a person or by policy; nothing in the answer says which.

SAMP-024

SHOULD · binds the client

Applications SHOULD: Provide UI that makes it easy and intuitive to review sampling requests; Allow users to view and edit prompts before sending; Present generated responses for review before delivery

client/sampling#user-interaction-model

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

SAMP-025

MUST · binds the client

Clients MUST declare support for tool use via the sampling.tools capability to receive tool-enabled sampling requests.

client/sampling#tools-in-sampling

Excluded. The precondition half of the sentence. A client that has not declared sampling.tools does nothing wrong by receiving a tool-enabled request — the server does, and the sentence’s other half (sampling.tools-declared) judges it. Nothing the client sends can break this half: the declaration is either made or not, and its absence becomes a violation only when such a request arrives, which is the server’s act.

SAMP-026

MUST NOT · binds the server

Servers MUST NOT send tool-enabled sampling requests to Clients that have not declared support for tool use via the sampling.tools capability.

client/sampling#tools-in-sampling

Judged by sampling.tools-declared.

SAMP-027

MUST · binds the client

Clients that support sampling MUST declare the sampling capability in _meta.io.modelcontextprotocol/clientCapabilities on each request:

client/sampling#capabilities

Judged by sampling.client-declares-capability.

SAMP-028

SHOULD · binds the server

Servers SHOULD avoid using these values (e.g. can just omit includeContext since it defaults to "none")

client/sampling#capabilities

Judged by sampling.include-context-avoided.

SAMP-029

SHOULD NOT · binds the server

and SHOULD NOT use them unless the client declares sampling.context capability.

client/sampling#capabilities

Judged by sampling.include-context-declared.

SAMP-030

MUST · binds the server

When a user message contains tool results (type: “tool_result”), it MUST contain ONLY tool results.

client/sampling#tool-result-messages

Judged by sampling.tool-results-only.

SAMP-031

MUST · binds the server

When using tool use in sampling, every assistant message containing ToolUseContent blocks MUST be followed by a user message that consists entirely of ToolResultContent blocks, with each tool use (e.g. with id: $id) matched by a corresponding tool result (with toolUseId: $id), before any other message.

client/sampling#tool-use-and-result-balance

Judged by sampling.tool-use-answered.

SAMP-032

MUST · binds the client

{mode: "required"}: Model MUST use at least one tool before completing

client/sampling#tool-choice-modes

Judged by sampling.tool-choice-required-honoured.

SAMP-033

MUST NOT · binds the client

{mode: "none"}: Model MUST NOT use any tools

client/sampling#tool-choice-modes

Judged by sampling.tool-choice-none-honoured.

SAMP-034

MUST · binds the client and server

Sampling messages MUST contain a role field of "user" or "assistant"; and a content field representing the message data.

client/sampling#messages

Judged by sampling.message-shape.

SAMP-035

SHOULD NOT · binds the client

The list of messages in a sampling request SHOULD NOT be retained between separate requests.

client/sampling#messages

Excluded. Retention is client-internal state: the next request’s messages are the server’s, and say nothing about what the client kept from the last one.

SAMP-036

MUST · binds the client

The client MUST respect the maxTokens parameter.

client/sampling#sampling-parameters

Excluded. A bound in the model’s tokens, and the tokenizer is the model’s: a validator cannot count the tokens of a result’s content without knowing which tokenizer produced it (the result’s model names the model, not its vocabulary), so an over-long answer cannot be shown.

SAMP-037

SHOULD · binds the client

Clients SHOULD implement user approval controls

client/sampling#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

SAMP-038

SHOULD · binds the client and server

Both parties SHOULD validate message content

client/sampling#security-considerations

Excluded. Validation is internal; its outcome is whatever each party sends next, and the shape of what they send is judged directly by the clauses that constrain it (tool-message balance, message roles).

SAMP-039

SHOULD · binds the client

Clients SHOULD respect model preference hints

client/sampling#security-considerations

Excluded. Hints are advisory and mapping them is the client’s choice (the page: clients “MAY map hints to equivalent models from different providers”); the result’s model names what was used, but no rule says which model a hint selects, so no answer contradicts one.

SAMP-040

SHOULD · binds the client

Clients SHOULD implement rate limiting

client/sampling#security-considerations

Excluded. A request schedule: rate limiting is measured in elapsed time, which checks may not consult (LIFE-015, the time boundary in 02-architecture.md), and its threshold is the client’s own policy.

SAMP-041

MUST · binds the client and server

Both parties MUST handle sensitive data appropriately

client/sampling#security-considerations

Excluded. “Appropriately” names no observable criterion, and which data is sensitive is a semantic judgement about content (see the elicitation pages’ sensitive-information clauses): no message can be shown to have mishandled it.

SAMP-042

MUST · binds the server

Servers MUST ensure that when replying to a stopReason: "toolUse", each ToolUseContent item is responded to with a ToolResultContent item with a matching toolUseId, and that the user message contains only tool results (no other content types)

client/sampling#security-considerations

Judged by sampling.tool-use-answered, sampling.tool-results-only.

SAMP-043

SHOULD · binds the client and server

Both parties SHOULD implement iteration limits for tool loops

client/sampling#security-considerations

Excluded. A limit on a loop’s length is a policy whose threshold the page does not set: any finite loop in a recording is consistent with some limit, and an unbounded one cannot be recorded.

ROOT

ROOT-011

SHOULD NOT · binds the client and server

New implementations SHOULD NOT adopt it

client/roots

Excluded. Whether an implementation is new is not in any message: a session using the feature is equally consistent with an existing implementation that has not yet migrated, which the sentence asks only to migrate.

ROOT-012

SHOULD · binds the client and server

existing implementations SHOULD migrate to passing directories or files via tool parameters, resource URIs, or server configuration.

client/roots

Excluded. Asks for a migration over time, which one recording cannot show either way — a session that uses the feature may come from an implementation partway through migrating, and the feature remains in the specification until removal.

ROOT-013

MUST · binds the client

Clients that support roots MUST declare the roots capability in _meta.io.modelcontextprotocol/clientCapabilities on each request:

client/roots#capabilities

Judged by roots.client-declares-capability.

ROOT-014

MUST · binds the client

uri: Unique identifier for the root. This MUST be a file:// URI in the current specification.

client/roots#root

Judged by roots.uri-file-scheme.

ROOT-015

MUST · binds the client

Clients MUST: Only expose roots with appropriate permissions; Validate all root URIs to prevent path traversal; Implement proper access controls; Monitor root accessibility

client/roots#security-considerations

Excluded. Client-internal policy: which directories the user permitted, how paths are validated and accessed, and whether they remain reachable are facts about the client’s host system, not its messages. The one part a message carries — the URI’s form — is judged by roots.uri-file-scheme; a file:// URI with .. segments is still well formed, and whether it escapes a permitted directory depends on the filesystem.

ROOT-016

SHOULD · binds the server

Servers SHOULD: Handle cases where roots become unavailable; Respect root boundaries during operations; Validate all paths against provided roots

client/roots#security-considerations

Excluded. Server-internal: what the server does with roots — keeping within their boundaries, validating paths against them, coping when they vanish or change, caching them — happens in its own operations on its host, which the MCP session does not record.

ROOT-017

SHOULD · binds the client

Clients SHOULD: Prompt users for consent before exposing roots to servers; Provide clear user interfaces for root management; Validate root accessibility before exposing; Monitor for root changes

client/roots#implementation-guidelines

Excluded. UI and internal monitoring: consent prompts and root-management screens are the client’s interface, and checking or watching roots’ accessibility happens on its filesystem. A recording carries only the roots it finally reported.

ROOT-018

SHOULD · binds the server

Servers SHOULD: Check for roots capability before usage

client/roots#implementation-guidelines

Judged by roots.capability-checked.

ROOT-019

SHOULD · binds the server

Servers SHOULD: Respect root boundaries in operations; Cache root information appropriately

client/roots#implementation-guidelines

Excluded. Server-internal: what the server does with roots — keeping within their boundaries, validating paths against them, coping when they vanish or change, caching them — happens in its own operations on its host, which the MCP session does not record.

AUTH

AUTH-086

SHOULD · binds the client and server

Implementations using an HTTP-based transport SHOULD conform to this specification.

basic/authorization/index#protocol-requirements

Excluded. Conformance to the pages as a whole is what their own entries judge, clause by clause; the sentence adds no property of a message beyond them. It binds only implementations that support authorization at all, which the page makes OPTIONAL. Same sentence as BASE-068 on the basic page.

AUTH-087

SHOULD NOT · binds the client and server

Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.

basic/authorization/index#protocol-requirements

Excluded. A stdio session has no HTTP layer for a challenge or a token header to appear in, so a recording cannot show the specification being followed there; retrieving credentials from the environment is process-local conduct with no wire footprint. Same sentence as BASE-068 on the basic page.

AUTH-088

MUST · binds the client and server

Implementations using alternative transports MUST follow established security best practices for their protocol.

basic/authorization/index#protocol-requirements

Excluded. Binds transports other than the two this revision defines, which no recording of an MCP session over stdio or Streamable HTTP carries, and names no practice a check could hold a message to.

AUTH-089

MUST · binds the server

Authorization servers MUST implement OAuth 2.1 with appropriate security measures for both confidential and public clients.

basic/authorization/index#overview

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-090

SHOULD · binds the client and server

Authorization servers and MCP clients SHOULD support OAuth Client ID Metadata Documents

basic/authorization/index#overview

Excluded. Support for a client registration mechanism is exercised between the client and the authorization server, off the MCP endpoint. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-091

MUST · binds the server

MCP servers MUST implement OAuth 2.0 Protected Resource Metadata (RFC9728).

basic/authorization/index#overview

Excluded. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-092

MUST · binds the client

MCP clients MUST use OAuth 2.0 Protected Resource Metadata for authorization server discovery.

basic/authorization/index#overview

Excluded. Which document a client used to find its authorization server is visible only in fetches off the MCP endpoint. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-093

MUST · binds the server

MCP authorization servers MUST provide at least one of the following discovery mechanisms:

basic/authorization/index#overview

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-094

MUST · binds the client

MCP clients MUST support both discovery mechanisms to obtain the information required to interact with the authorization server.

basic/authorization/index#overview

Excluded. Conduct inside the client’s OAuth flow (authorization server metadata discovery), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-095

MUST · binds the client and server

Implementations MUST follow the normative discovery requirements defined in Authorization Server Discovery.

basic/authorization/index#authorization-server-discovery

Excluded. Points at requirements stated elsewhere (the authorization-server-discovery page), each entered under its own id there; the sentence adds no obligation a recording could show apart from them.

AUTH-096

MUST · binds the client

Before initiating the authorization flow, MCP clients MUST obtain a client ID through one of three registration mechanisms: Client ID Metadata Documents, pre-registration, or Dynamic Client Registration, following the requirements and selection priority defined in Client Registration.

basic/authorization/index#client-registration

Excluded. Conduct inside the client’s OAuth flow (client registration), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-097

SHOULD · binds the server

MCP servers SHOULD include a scope parameter in the WWW-Authenticate header as defined in RFC 6750 Section 3 to indicate the scopes required for accessing the resource.

basic/authorization/index#scope-selection-strategy

Judged by auth.challenge-scope.

AUTH-098

MUST NOT · binds the client

Clients MUST NOT assume any particular set relationship between the challenged scope set and scopes_supported.

basic/authorization/index#scope-selection-strategy

Excluded. Conduct inside the client’s OAuth flow (the scope it requests, whatever it assumes about scopes_supported), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-099

MUST · binds the client

Clients MUST treat the scopes provided in the challenge as authoritative for the current operation.

basic/authorization/index#scope-selection-strategy

Excluded. Conduct inside the client’s OAuth flow (the scope it requests in its authorization request), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-100

SHOULD · binds the client

When re-authorizing, clients SHOULD include these scopes alongside any previously granted scopes to avoid losing permissions needed for other operations

basic/authorization/index#scope-selection-strategy

Excluded. Conduct inside the client’s OAuth flow (the scope of its re-authorization request), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-101

SHOULD · binds the server

Servers SHOULD strive for consistency in how they construct scope sets but they are not required to surface every dynamically issued scope through scopes_supported.

basic/authorization/index#scope-selection-strategy

Excluded. Striving for consistency is a design aim, not a property any one challenge has, and challenges for different operations legitimately name different scopes; scopes_supported lives in the Protected Resource Metadata document, which a recording does not carry.

AUTH-102

SHOULD · binds the client

When implementing authorization flows, MCP clients SHOULD follow the principle of least privilege by requesting only the scopes necessary for their intended operations.

basic/authorization/index#scope-selection-strategy

Excluded. Conduct inside the client’s OAuth flow (the scope of its authorization request), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-103

SHOULD · binds the client

During the initial authorization handshake, MCP clients SHOULD follow this priority order for scope selection:

basic/authorization/index#scope-selection-strategy

Excluded. Conduct inside the client’s OAuth flow (the scope of its authorization request), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-104

MUST · binds the client

Before redirecting the user-agent, the client MUST record the issuer value from the selected authorization server’s validated metadata document (see Authorization Server Metadata Discovery) and associate it with the same per-request record used to store the PKCE code verifier (and the state value, if used).

basic/authorization/index#authorization-response-validation

Excluded. Conduct inside the client’s OAuth flow (the issuer it records beside its PKCE verifier, which is the client’s own state), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-105

SHOULD · binds the server

MCP authorization servers SHOULD include the iss parameter in authorization responses, including error responses, as defined in RFC9207 Section 2.

basic/authorization/index#authorization-response-validation

Excluded. Authorization responses reach the client through the user’s browser. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-106

MUST · binds the server

Authorization servers that include the iss parameter MUST advertise this by setting authorization_response_iss_parameter_supported to true in their metadata (RFC9207 Section 2.3).

basic/authorization/index#authorization-response-validation

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-107

MUST · binds the client

On receiving the authorization response, MCP clients MUST apply the validation in RFC9207 Section 2.4 before transmitting the authorization code to any token endpoint:

basic/authorization/index#authorization-response-validation

Excluded. Conduct inside the client’s OAuth flow (the authorization response’s iss and the issuer recorded for it), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-108

SHOULD · binds the server

A future revision of this specification is expected to upgrade authorization server inclusion of iss from SHOULD to MUST.

basic/authorization/index#authorization-response-validation

Excluded. Names the two keywords to describe a future revision’s intent; it binds no party at this one. The obligation it refers to is the authorization server’s SHOULD entered above, excluded for the reason given there.

AUTH-109

MUST NOT · binds the client

After decoding the iss value from the application/x-www-form-urlencoded response per RFC 9207 Section 2.4, clients MUST NOT apply scheme or host case folding, default-port elision, trailing-slash, or percent-encoding normalization (RFC 3986 Sections 6.2.2-6.2.3) before comparison.

basic/authorization/index#authorization-response-validation

Excluded. Conduct inside the client’s OAuth flow (the authorization response’s iss and the issuer recorded for it), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-110

MUST NOT · binds the client

This validation applies equally to error responses - on mismatch the client MUST NOT act on or display error, error_description, or error_uri.

basic/authorization/index#authorization-response-validation

Excluded. Conduct inside the client’s OAuth flow (the authorization response’s iss and the issuer recorded for it), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records. Displaying an error is user interface besides.

AUTH-111

MUST · binds the client

MCP clients MUST implement Resource Indicators for OAuth 2.0 as defined in RFC 8707 to explicitly specify the target resource for which the token is being requested.

basic/authorization/index#resource-parameter-implementation

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-112

MUST · binds the client

The resource parameter: MUST be included in both authorization requests and token requests.; MUST identify the MCP server that the client intends to use the token with.; MUST use the canonical URI of the MCP server as defined in RFC 8707 Section 2.

basic/authorization/index#resource-parameter-implementation

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-113

SHOULD · binds the client

MCP clients SHOULD provide the most specific URI that they can for the MCP server they intend to access, following the guidance in RFC 8707.

basic/authorization/index#canonical-server-uri

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-114

SHOULD · binds the client and server

While the canonical form uses lowercase scheme and host components, implementations SHOULD accept uppercase scheme and host components for robustness and interoperability.

basic/authorization/index#canonical-server-uri

Excluded. Accepting a URI is a comparison made by whoever receives the resource parameter or validates a token’s audience — the authorization server, or the MCP server inside its validation — and neither comparison is a message on the MCP endpoint.

AUTH-115

SHOULD · binds the client and server

While both https://mcp.example.com/ (with trailing slash) and https://mcp.example.com (without trailing slash) are technically valid absolute URIs according to RFC 3986, implementations SHOULD consistently use the form without the trailing slash for better interoperability unless the trailing slash is semantically significant for the specific resource.

basic/authorization/index#canonical-server-uri

Excluded. The canonical URI’s spelling appears in the resource parameter of requests to the authorization server and in token audiences, neither of which a recording of the MCP endpoint carries; and an http event records no request target, so not even the endpoint URL the client used is there.

AUTH-116

MUST · binds the client

MCP clients MUST send this parameter regardless of whether authorization servers support it.

basic/authorization/index#canonical-server-uri

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-117

MUST · binds the client

Access token handling when making requests to MCP servers MUST conform to the requirements defined in OAuth 2.1 Section 5 “Resource Requests”.

basic/authorization/index#token-requirements

Excluded. Authorization is never in a trace: recorders keep headers by allowlist (RECORDED_HEADERS), and credentials stay off it by construction (docs/plan/05-security-model.md). Which requests carried a token, and which token, cannot be read back from a recording. The section’s other requirement, no token in the query string, has its own entry.

AUTH-118

MUST · binds the client

MCP client MUST use the Authorization request header field defined in OAuth 2.1 Section 5.1.1:

basic/authorization/index#token-requirements

Excluded. Authorization is never in a trace: recorders keep headers by allowlist (RECORDED_HEADERS), and credentials stay off it by construction (docs/plan/05-security-model.md). Which requests carried a token, and which token, cannot be read back from a recording.

AUTH-119

MUST · binds the client

Note that authorization MUST be included in every HTTP request from client to server.

basic/authorization/index#token-requirements

Excluded. Authorization is never in a trace: recorders keep headers by allowlist (RECORDED_HEADERS), and credentials stay off it by construction (docs/plan/05-security-model.md). Which requests carried a token, and which token, cannot be read back from a recording. A 401 to a request does not settle it either: an expired token draws the same answer as a missing one.

AUTH-120

MUST NOT · binds the client

Access tokens MUST NOT be included in the URI query string

basic/authorization/index#token-requirements

Excluded. An http event records the method, status and allowlisted headers, and no request target: neither the path nor the query string a request was sent to is in the trace, so a token in the URI cannot be seen. Recording the query to judge this would write into every trace the very credential the clause keeps out of URIs.

AUTH-121

MUST · binds the server

MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2.

basic/authorization/index#token-handling

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-122

MUST · binds the server

MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2.

basic/authorization/index#token-handling

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-123

MUST · binds the server

If validation fails, servers MUST respond according to OAuth 2.1 Section 5.3 error handling requirements.

basic/authorization/index#token-handling

Excluded. Whether validation failed is decided inside the server, against a token the recording never holds, and the sentence imports OAuth 2.1 Section 5.3 wholesale. What a recording can hold the response to is the page’s own statement of it: the status each declared error receives and the 401 for an invalid token, judged by auth.error-status and auth.invalid-token-401.

AUTH-124

MUST · binds the server

Invalid or expired tokens MUST receive a HTTP 401 response.

basic/authorization/index#token-handling

Judged by auth.invalid-token-401.

AUTH-125

MUST NOT · binds the client

MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server’s authorization server.

basic/authorization/index#token-handling

Excluded. Authorization is never in a trace: recorders keep headers by allowlist (RECORDED_HEADERS), and credentials stay off it by construction (docs/plan/05-security-model.md). Which requests carried a token, and which token, cannot be read back from a recording.

AUTH-126

MUST · binds the server

MCP servers MUST only accept tokens that are valid for use with their own resources.

basic/authorization/index#token-handling

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-127

MUST NOT · binds the server

MCP servers MUST NOT accept or transit any other tokens.

basic/authorization/index#token-handling

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens. Transiting a token is a request to an upstream service, outside the MCP session.

AUTH-128

MUST · binds the client

MCP Clients that desire refresh tokens: MUST keep refresh tokens confidential in transit and storage as specified in OAuth 2.1 Section 4.3; SHOULD include refresh_token in their grant_types client metadata; MUST NOT assume refresh tokens will be issued; the AS retains discretion

basic/authorization/index#refresh-tokens

Excluded. Refresh tokens are issued and redeemed at the authorization server’s token endpoint and kept in the client’s storage; client metadata is registered with the authorization server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-129

SHOULD NOT · binds the server

MCP Servers (Protected Resources) SHOULD NOT include offline_access in WWW-Authenticate scope or Protected Resource Metadata scopes_supported, as refresh tokens are not a resource requirement.

basic/authorization/index#refresh-tokens

Judged by auth.no-offline-access-challenge.

AUTH-130

MUST · binds the server

Servers MUST return appropriate HTTP status codes for authorization errors

basic/authorization/index#error-handling

Judged by auth.error-status.

AUTH-131

SHOULD · binds the server

When a client makes a request with an access token with insufficient scope during runtime operations, the server SHOULD respond with: HTTP 403 Forbidden status code (per RFC 6750 Section 3.1); WWW-Authenticate header with the Bearer scheme and additional parameters: error="insufficient_scope" - indicating the specific type of authorization failure; scope="required_scope1 required_scope2" - specifying the minimum scopes needed for the operation; resource_metadata - the URI of the Protected Resource Metadata document (for consistency with 401 responses)

basic/authorization/index#runtime-insufficient-scope-errors

Judged by auth.insufficient-scope-response.

AUTH-132

SHOULD · binds the server

Server Scope Management: When responding with insufficient scope errors, servers SHOULD include the scopes needed to satisfy the current operation in the scope parameter, consistent with RFC 6750 Section 3.1.

basic/authorization/index#runtime-insufficient-scope-errors

Judged by auth.insufficient-scope-names-scopes.

AUTH-133

SHOULD · binds the server

Whatever scope-inclusion strategy a server adopts, servers SHOULD include all scopes required for the current operation in a single challenge.

basic/authorization/index#runtime-insufficient-scope-errors

Judged by auth.single-challenge.

AUTH-134

SHOULD · binds the server

Servers SHOULD be consistent in their scope inclusion strategy to provide predictable behavior for clients.

basic/authorization/index#runtime-insufficient-scope-errors

Excluded. A strategy is a policy over every challenge the server issues, across sessions and users; one recording shows a few challenges for different operations, which legitimately name different scopes, and cannot tell an inconsistent strategy from a consistent one applied to different requests.

AUTH-135

SHOULD · binds the server

Servers SHOULD consider the user experience impact when determining which scopes to include in the response, as misconfigured scopes may require frequent user interaction.

basic/authorization/index#runtime-insufficient-scope-errors

Excluded. Binds the server’s deliberation, not a message: a challenge shows the scopes chosen, not whether their user experience impact was weighed.

AUTH-136

SHOULD · binds the client

Clients SHOULD respond to these errors by requesting a new access token with an increased set of scopes via a step-up authorization flow or handle the errors in other, appropriate ways.

basic/authorization/index#step-up-authorization-flow

Excluded. Conduct inside the client’s OAuth flow (the token request a step-up makes), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records. The clause also admits “other, appropriate ways”, which no message distinguishes.

AUTH-137

SHOULD · binds the client

Clients acting on behalf of a user SHOULD attempt the step-up authorization flow.

basic/authorization/index#step-up-authorization-flow

Excluded. Whether a client acts on behalf of a user or on its own (client_credentials) is a fact about its grant, which no MCP message states. Conduct inside the client’s OAuth flow (the step-up re-authorization), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-138

SHOULD · binds the client

Clients SHOULD implement retry limits and SHOULD track scope upgrade attempts to avoid repeated failures for the same resource and operation combination.

basic/authorization/index#step-up-authorization-flow

Excluded. A limit is a property of the client’s implementation, which a session that simply stops retrying satisfies whatever the limit is; the page fixes no number (“no more than a few times”) a recording could be held to, and the attempts tracked are re-authorizations off the MCP endpoint.

AUTH-139

MUST · binds the server

Servers MUST account for scope hierarchies, where a broader scope implies narrower ones, when deciding whether a token is sufficient for an operation.

basic/authorization/index#step-up-authorization-flow

Excluded. The decision is made inside the server from the scopes a token carries, and the token is never recorded; a challenge shows the outcome, not the scopes it was decided against.

AUTH-140

MUST · binds the client and server

Implementations of this specification MUST follow the normative security requirements in Security Considerations

basic/authorization/index#security-considerations

Excluded. Points at requirements stated elsewhere (the security-considerations page), each entered under its own id there; the sentence adds no obligation a recording could show apart from them.

AUTH-141

MUST · binds the server

MCP servers MUST implement the OAuth 2.0 Protected Resource Metadata (RFC9728) specification to indicate the locations of authorization servers.

basic/authorization/authorization-server-discovery#authorization-server-location

Excluded. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-142

MUST · binds the server

The Protected Resource Metadata document returned by the MCP server MUST include the authorization_servers field containing at least one authorization server.

basic/authorization/authorization-server-discovery#authorization-server-location

Excluded. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-143

MUST · binds the client

Clients MUST maintain separate registration state (client credentials, tokens) per authorization server and MUST NOT assume that credentials valid for one authorization server will be accepted by another.

basic/authorization/authorization-server-discovery#authorization-server-location

Excluded. Registration state is the client’s own storage, and the credentials it holds are presented to authorization servers off the MCP endpoint — or, as tokens, in the Authorization header a recording never keeps.

AUTH-144

MUST · binds the server

MCP servers MUST implement one of the following discovery mechanisms to provide authorization server location information to MCP clients:

basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirements

Excluded. A disjunction whose second branch a recording cannot witness. A 401 whose challenge carries no resource_metadata is conforming when the server serves the well-known document instead, and that document is not in the trace: The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-145

MUST · binds the client

MCP clients MUST support both discovery mechanisms and use the resource metadata URL from the parsed WWW-Authenticate headers when present; otherwise, they MUST fall back to constructing and requesting the well-known URIs in the order listed above.

basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirements

Excluded. Which URLs the client requested next, and in what order, are fetches off the MCP endpoint. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-146

MUST · binds the client

MCP clients MUST be able to parse WWW-Authenticate headers and respond appropriately to HTTP 401 Unauthorized responses from the MCP server.

basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirements

Excluded. Conduct inside the client’s OAuth flow (parsing the challenge, then the discovery, authorization and token requests that are the appropriate response to a 401), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-147

MUST · binds the client

To handle different issuer URL formats and ensure interoperability with both OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0 specifications, MCP clients MUST attempt multiple well-known endpoints when discovering authorization server metadata.

basic/authorization/authorization-server-discovery#authorization-server-metadata-discovery

Excluded. Requests to the authorization server’s well-known endpoints, and what the client does with the documents they return. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-148

MUST · binds the client

For issuer URLs with path components (e.g., https://auth.example.com/tenant1), clients MUST try endpoints in the following priority order:

basic/authorization/authorization-server-discovery#authorization-server-metadata-discovery

Excluded. Requests to the authorization server’s well-known endpoints, and what the client does with the documents they return. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-149

MUST · binds the client

For issuer URLs without path components (e.g., https://auth.example.com), clients MUST try:

basic/authorization/authorization-server-discovery#authorization-server-metadata-discovery

Excluded. Requests to the authorization server’s well-known endpoints, and what the client does with the documents they return. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-150

MUST · binds the client

After retrieving a metadata document, MCP clients MUST validate it as required by RFC8414 Section 3.3 or OpenID Connect Discovery Section 4.3: the issuer value in the document MUST be identical to the issuer identifier used to construct the well-known URL. If they differ, the client MUST NOT use the metadata. For example, a document fetched from https://attacker.example/.well-known/oauth-authorization-server that contains "issuer": "https://honest.example" MUST be rejected.

basic/authorization/authorization-server-discovery#authorization-server-metadata-discovery

Excluded. Requests to the authorization server’s well-known endpoints, and what the client does with the documents they return. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-151

SHOULD · binds the client

Clients supporting all options SHOULD use the following priority order:

basic/authorization/client-registration

Excluded. Conduct inside the client’s OAuth flow (choosing a registration mechanism), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-152

SHOULD · binds the client and server

MCP clients and authorization servers SHOULD support OAuth Client ID Metadata Documents as specified in OAuth Client ID Metadata Document for client registration.

basic/authorization/client-registration#client-id-metadata-documents

Excluded. Support for a client registration mechanism is exercised between the client and the authorization server, off the MCP endpoint. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-153

MUST · binds the client and server

MCP implementations supporting Client ID Metadata Documents MUST follow the requirements specified in OAuth Client ID Metadata Document.

basic/authorization/client-registration#implementation-requirements

Excluded. Points at requirements stated elsewhere (the Client ID Metadata Document draft, and the two lists that follow on this page), each entered under its own id there; the sentence adds no obligation a recording could show apart from them.

AUTH-154

MUST · binds the client

Clients MUST host their metadata document at an HTTPS URL following RFC requirements; The client_id URL MUST use the “https” scheme and contain a path component, e.g. https://example.com/client.json; The metadata document MUST include at least the following properties: client_id, client_name, redirect_uris; Clients MUST ensure the client_id value in the metadata matches the document URL exactly

basic/authorization/client-registration#implementation-requirements

Excluded. The metadata document is hosted by the client and fetched by the authorization server; neither request touches the MCP endpoint, and the client_id travels in the authorization request. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-155

MUST · binds the server

For Authorization Servers: SHOULD fetch metadata documents when encountering URL-formatted client_ids; MUST validate that the fetched document’s client_id matches the URL exactly; SHOULD cache metadata respecting HTTP cache headers; MUST validate redirect URIs presented in an authorization request against those in the metadata document; MUST validate the document structure is valid JSON and contains required fields; SHOULD follow the security considerations in Section 6 of Client ID Metadata Document and in Client ID Metadata Document Security

basic/authorization/client-registration#implementation-requirements

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-156

SHOULD · binds the client

MCP clients SHOULD check for this capability and MAY fall back to Dynamic Client Registration or pre-registration if unavailable.

basic/authorization/client-registration#advertising-cimd-support

Excluded. Conduct inside the client’s OAuth flow (reading authorization server metadata and choosing a registration mechanism), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-157

SHOULD · binds the client

MCP clients SHOULD support an option for static client credentials such as those supplied by a pre-registration flow.

basic/authorization/client-registration#pre-registration

Excluded. A configuration option of the client, exercised against the authorization server; no MCP message shows whether the option exists.

AUTH-158

MUST · binds the client

MCP clients MUST specify an appropriate application_type during Dynamic Client Registration.

basic/authorization/client-registration#application-type-and-redirect-uri-constraints

Excluded. Conduct inside the client’s OAuth flow (Dynamic Client Registration with the authorization server), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-159

SHOULD · binds the client

Native applications (desktop applications, mobile apps, CLI tools, and locally-hosted web applications accessed via localhost) SHOULD use application_type: "native"; Web applications (remote browser-based applications served from a non-local host) SHOULD use application_type: "web"

basic/authorization/client-registration#application-type-and-redirect-uri-constraints

Excluded. Conduct inside the client’s OAuth flow (Dynamic Client Registration with the authorization server), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records. Which kind of application the client is, is not stated anywhere a recording reaches either.

AUTH-160

MUST · binds the client

MCP clients MUST be prepared to handle registration failures due to redirect URI constraints when authorization servers implement OIDC.

basic/authorization/client-registration#application-type-and-redirect-uri-constraints

Excluded. Conduct inside the client’s OAuth flow (Dynamic Client Registration with the authorization server), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-161

SHOULD · binds the client

When a registration request is rejected, clients SHOULD surface a meaningful error to the user or developer.

basic/authorization/client-registration#application-type-and-redirect-uri-constraints

Excluded. Conduct inside the client’s OAuth flow (Dynamic Client Registration with the authorization server), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records. Surfacing an error is user interface besides.

AUTH-162

MUST · binds the client

Clients that use pre-registered credentials, or persist client credentials obtained via Dynamic Client Registration, MUST associate those credentials with the specific authorization server that issued them, keyed by the authorization server’s issuer identifier.

basic/authorization/client-registration#authorization-server-binding

Excluded. Client credentials are the client’s own stored state, presented to authorization servers off the MCP endpoint. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-163

MUST NOT · binds the client

When the authorization server changes (detected via updated protected resource metadata), clients MUST NOT reuse client credentials from a different authorization server and MUST re-register with the new authorization server.

basic/authorization/client-registration#authorization-server-binding

Excluded. Client credentials are the client’s own stored state, presented to authorization servers off the MCP endpoint. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct. The change itself is detected in Protected Resource Metadata, which a recording does not carry.

AUTH-164

SHOULD · binds the client

If the authorization server indicated by protected resource metadata no longer matches the one the credentials were registered with, clients SHOULD surface an error rather than silently attempting to use mismatched credentials.

basic/authorization/client-registration#authorization-server-binding

Excluded. Client credentials are the client’s own stored state, presented to authorization servers off the MCP endpoint. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct. Surfacing an error is user interface besides.

AUTH-165

MUST · binds the client and server

This document outlines security requirements that implementers MUST consider when building MCP clients and servers.

basic/authorization/security-considerations

Excluded. Points at requirements stated elsewhere (the sections of this page), each entered under its own id there; the sentence adds no obligation a recording could show apart from them.

AUTH-166

MUST · binds the client and server

Additionally, implementors MUST follow OAuth 2.1 security best practices as outlined in OAuth 2.1 Section 7. “Security Considerations”.

basic/authorization/security-considerations

Excluded. Imports OAuth 2.1 Section 7 wholesale. Its observable consequences for an MCP exchange are the challenge clauses this page states itself, judged under their own ids; the rest is implementation conduct (storage, transport configuration, the OAuth flow) with no footprint on the MCP endpoint.

AUTH-167

MUST · binds the client

MCP clients MUST include the resource parameter in authorization and token requests as specified in the Resource Parameter Implementation section

basic/authorization/security-considerations#token-audience-binding-and-validation

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-168

MUST · binds the server

MCP servers MUST validate that tokens presented to them were specifically issued for their use

basic/authorization/security-considerations#token-audience-binding-and-validation

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-169

MUST · binds the client and server

Clients and servers MUST implement secure token storage and follow OAuth best practices, as outlined in OAuth 2.1, Section 7.1.

basic/authorization/security-considerations#token-theft

Excluded. Token storage is local state of each party, and a token is never recorded: nothing on the wire shows how either party keeps one.

AUTH-170

SHOULD · binds the server

Authorization servers SHOULD issue short-lived access tokens to reduce the impact of leaked tokens.

basic/authorization/security-considerations#token-theft

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-171

MUST · binds the server

For public clients, authorization servers MUST rotate refresh tokens as described in OAuth 2.1 Section 4.3.1 “Token Endpoint Extension”.

basic/authorization/security-considerations#token-theft

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-172

MUST · binds the client and server

Implementations MUST follow OAuth 2.1 Section 1.5 “Communication Security”.

basic/authorization/security-considerations#communication-security

Excluded. Imports OAuth 2.1 Section 1.5 wholesale. Its observable consequences for an MCP exchange are the challenge clauses this page states itself, judged under their own ids; the rest is implementation conduct (storage, transport configuration, the OAuth flow) with no footprint on the MCP endpoint.

AUTH-173

MUST · binds the server

All authorization server endpoints MUST be served over HTTPS.

basic/authorization/security-considerations#communication-security

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-174

MUST · binds the client

All redirect URIs MUST be either localhost or use HTTPS.

basic/authorization/security-considerations#communication-security

Excluded. Redirect URIs are registered with the authorization server and travel in authorization requests through the user’s browser. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-175

MUST · binds the client

To mitigate this, MCP clients MUST implement PKCE according to OAuth 2.1 Section 7.5.2 and MUST verify PKCE support before proceeding with authorization.

basic/authorization/security-considerations#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-176

MUST · binds the client

MCP clients MUST use the S256 code challenge method when technically capable, as required by OAuth 2.1 Section 4.1.1.

basic/authorization/security-considerations#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-177

MUST · binds the client

Since OAuth 2.1 and PKCE specifications do not define a mechanism for clients to discover PKCE support, MCP clients MUST rely on authorization server metadata to verify this capability:

basic/authorization/security-considerations#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-178

MUST · binds the client

OAuth 2.0 Authorization Server Metadata: If code_challenge_methods_supported is absent, the authorization server does not support PKCE and MCP clients MUST refuse to proceed.

basic/authorization/security-considerations#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-179

MUST · binds the client

MCP clients MUST verify the presence of code_challenge_methods_supported in the provider metadata response. If the field is absent, MCP clients MUST refuse to proceed.

basic/authorization/security-considerations#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-180

MUST · binds the server

Authorization servers providing OpenID Connect Discovery 1.0 MUST include code_challenge_methods_supported in their metadata to ensure MCP compatibility.

basic/authorization/security-considerations#authorization-code-protection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-181

MUST · binds the client

MCP clients MUST have redirect URIs registered with the authorization server.

basic/authorization/security-considerations#open-redirection

Excluded. Conduct inside the client’s OAuth flow (client registration), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-182

MUST · binds the server

Authorization servers MUST validate exact redirect URIs against pre-registered values to prevent redirection attacks.

basic/authorization/security-considerations#open-redirection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-183

SHOULD · binds the client

MCP clients SHOULD use and verify state parameters in the authorization code flow and discard any results that do not include or have a mismatch with the original state.

basic/authorization/security-considerations#open-redirection

Excluded. Conduct inside the client’s OAuth flow (the authorization code flow’s state), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-184

MUST · binds the server

Authorization servers MUST take precautions to prevent redirecting user agents to untrusted URI’s, following suggestions laid out in OAuth 2.1 Section 7.12.2

basic/authorization/security-considerations#open-redirection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-185

SHOULD · binds the server

Authorization servers SHOULD only automatically redirect the user agent if it trusts the redirection URI.

basic/authorization/security-considerations#open-redirection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-186

MUST · binds the server

When implementing Client ID Metadata Documents, authorization servers MUST consider the security implications detailed in OAuth Client ID Metadata Document, Section 6.

basic/authorization/security-considerations#client-id-metadata-document-security

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-187

SHOULD · binds the server

Authorization servers fetching metadata documents SHOULD consider Server-Side Request Forgery (SSRF) risks

basic/authorization/security-considerations#authorization-server-abuse-protection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-188

MUST · binds the server

Authorization servers: SHOULD display additional warnings for localhost-only redirect URIs; MUST clearly display the redirect URI hostname during authorization

basic/authorization/security-considerations#localhost-redirect-uri-risks

Excluded. What the authorization server shows the user during authorization is consent UI in the user’s browser. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-189

MUST · binds the server

MCP proxy servers using static client IDs MUST obtain user consent for each dynamically registered client before forwarding to third-party authorization servers (which may require additional consent).

basic/authorization/security-considerations#confused-deputy-problem

Excluded. Consent is obtained in the user’s browser, and the forwarding is the proxy server’s own OAuth flow with a third-party authorization server; neither is a message on the MCP endpoint the recording captures.

AUTH-190

MUST · binds the server

MCP servers MUST validate access tokens before processing the request, ensuring the access token is issued specifically for the MCP server, and take all necessary steps to ensure no data is returned to unauthorized parties.

basic/authorization/security-considerations#access-token-privilege-restriction

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens. A result returned to an unauthorized party would need the request’s credential to show, and that is the header a recording never keeps.

AUTH-191

MUST · binds the server

A MCP server MUST follow the guidelines in OAuth 2.1 - Section 5.2 to validate inbound tokens.

basic/authorization/security-considerations#access-token-privilege-restriction

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-192

MUST · binds the server

MCP servers MUST only accept tokens specifically intended for themselves and MUST reject tokens that do not include them in the audience claim or otherwise verify that they are the intended recipient of the token.

basic/authorization/security-considerations#access-token-privilege-restriction

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-193

MUST NOT · binds the server

The MCP server MUST NOT pass through the token it received from the MCP client.

basic/authorization/security-considerations#access-token-privilege-restriction

Excluded. Passthrough is a request from the MCP server to an upstream API, outside the MCP session; the recording holds the client–server exchange only, and not even the token the client sent (Authorization is never recorded).

AUTH-194

MUST · binds the client

MCP clients MUST implement and use the resource parameter as defined in RFC 8707 - Resource Indicators for OAuth 2.0 to explicitly specify the target resource for which the token is being requested.

basic/authorization/security-considerations#access-token-privilege-restriction

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

Requirements — 2025-11-25

Every normative clause of the 2025-11-25 specification that the validator’s registry carries — 348 in all, 102 judged from a recorded trace and 246 excluded with the reason below. A report names a clause by the ID used as each heading here; each entry quotes the specification verbatim and links to the published text. mcp-trace-validator requirements --revision 2025-11-25 --format json prints the same data as JSON.

BASE

BASE-001

MUST · binds the client and server

Requests MUST include a string or integer ID.

basic#requests

Judged by base.request-id-type.

BASE-002

MUST NOT · binds the client and server

Unlike base JSON-RPC, the ID MUST NOT be null.

basic#requests

Judged by base.request-id-not-null.

BASE-003

MUST NOT · binds the client and server

The request ID MUST NOT have been previously used by the requestor within the same session.

basic#requests

Judged by base.request-id-unique.

BASE-004

MUST · binds the client and server

Result responses MUST include the same ID as the request they correspond to.

basic#result-responses

Judged by base.result-id-matches.

BASE-005

MUST NOT · binds the client and server

Notifications MUST NOT include an ID.

basic#notifications

Judged by base.notification-no-id.

BASE-006

MUST · binds the client and server

Error responses MUST include an error field with a code and message.

basic#error-responses

Judged by base.error-shape.

BASE-007

MUST · binds the client and server

Error codes MUST be integers.

basic#error-responses

Judged by base.error-code-integer.

BASE-008

MUST · binds the client and server

All messages between MCP clients and servers MUST follow the JSON-RPC 2.0 specification.

basic#messages

Judged by base.jsonrpc-version.

BASE-009

MUST · binds the client and server

Error responses MUST include the same ID as the request they correspond to (except in error cases where the ID could not be read due a malformed request).

basic#error-responses

Judged by base.error-id-matches.

BASE-010

MUST · binds the client and server

Result responses MUST include a result field.

basic#result-responses

Judged by base.result-field.

BASE-011

MUST NOT · binds the client and server

The receiver MUST NOT send a response.

basic#notifications

Excluded. A response to a notification carries no correlating ID, so it is indistinguishable on the wire from any other unsolicited response; unsolicited responses are flagged by base.result-id-matches and base.error-id-matches under BASE-004/BASE-009.

BASE-012

MUST · binds the client and server

All implementations MUST support the base protocol and lifecycle management components.

basic

Excluded. The umbrella clause the BASE and LIFE areas mechanize requirement-by-requirement; it has no observable beyond its constituents.

BASE-013

MUST · binds the client and server

Clients and servers MUST support JSON Schema 2020-12 for schemas without an explicit $schema field

basic#implementation-requirements

Excluded. Dialect support is validation behavior inside each implementation; a trace shows schemas exchanged, not how the receiver processed them.

BASE-014

MUST · binds the client and server

Clients and servers MUST validate schemas according to their declared or default dialect.

basic#implementation-requirements

Excluded. Whether validation happened, and against which dialect, is implementation-internal; validation outcomes need ground truth about inputs that a trace does not carry.

BASE-015

MUST · binds the client and server

They MUST handle unsupported dialects gracefully by returning an appropriate error indicating the dialect is not supported.

basic#implementation-requirements

Excluded. Judging graceful handling requires knowing the receiver’s supported-dialect set, which is not in the trace.

BASE-016

SHOULD · binds the client and server

Clients and servers SHOULD document which schema dialects they support

basic#implementation-requirements

Excluded. Documentation lives outside the protocol session entirely.

BASE-017

MUST · binds the client and server

Schemas MUST be valid according to their declared or default dialect

basic#schema-validation

Judged by base.schemas-valid-for-dialect.

BASE-018

MUST NOT · binds the client and server

Certain key names are reserved by MCP for protocol-level metadata, as specified below; implementations MUST NOT make assumptions about values at these keys.

basic#_meta

Excluded. What an implementation assumes is internal; only conduct that follows from an assumption could surface, and attributing it requires the assumption.

BASE-019

MUST · binds the client and server

If specified, MUST be a series of labels separated by dots (.), followed by a slash (/); Labels MUST start with a letter and end with a letter or digit; interior characters can be letters, digits, or hyphens (-)

basic#_meta

Judged by base.meta-key-format.

BASE-020

MUST · binds the client and server

Unless empty, MUST begin and end with an alphanumeric character ([a-z0-9A-Z]); MAY contain hyphens (-), underscores (_), dots (.), and alphanumerics in between

basic#_meta

Judged by base.meta-key-format.

BASE-021

SHOULD · binds the client and server

Implementations SHOULD use reverse DNS notation (e.g., com.example/ rather than example.com/).

basic#_meta

Excluded. Whether a syntactically valid prefix is reverse-DNS for its author cannot be decided without knowing the author’s domain.

BASE-022

MUST · binds the client

Clients that support rendering icons MUST support at least the following MIME types:

basic#icons

Excluded. Icon rendering support is client-internal capability with no wire footprint; the conditional premise (that the client renders icons) is equally invisible.

BASE-023

MUST · binds the client

Consumers of icon metadata MUST take appropriate security precautions when handling icons to prevent compromise:

basic#icons

Excluded. The umbrella for consumer-side icon hygiene (fetch policy, size limits, origin checks) — all consumer-internal conduct after the recorded session ends.

BASE-024

MUST · binds the client

Clients MUST reject icon URIs that use unsafe schemes and redirects, such as javascript:, file:, ftp:, ws:, or local app URI schemes.

basic#icons

Excluded. Rejection is client-side handling after receipt; the trace records the icon metadata served, not what the client did with it.

BASE-082

MUST · binds the client and server

Supported dialects: Implementations MUST support at least 2020-12 and SHOULD document which additional dialects they support

basic#schema-dialect

Excluded. Restates BASE-013’s dialect floor and BASE-016’s documentation SHOULD as the section’s third numbered rule, unconditionally where BASE-013 scopes itself to schemas without an explicit $schema (the same restatement shape as TRAN-049 to TRAN-024). Excluded for BASE-013’s reason: dialect support is validation behavior inside each implementation, and a trace shows the schemas exchanged, not how the receiver processed them.

BASE-085

SHOULD NOT · binds the client and server

Implementations using an HTTP-based transport SHOULD conform to this specification, whereas implementations using STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.

basic#auth

Excluded. Credential retrieval from the environment is process-local conduct with no wire footprint, and the HTTP half defers to the authorization specification, whose clauses are entered on its own page: AUTH-001 and AUTH-002 restate this sentence there, and the challenge clauses a recording can judge carry checks. One entry for the sentence’s SHOULD and SHOULD NOT, as BASE-068 enters it at 2026-07-28.

BASE-086

SHOULD · binds the client and server

Implementors are RECOMMENDED to use JSON Schema 2020-12.

basic#schema-dialect

Judged by base.schemas-use-2020-12.

BASE-087

SHOULD · binds the client

image/png - PNG images (safe, universal compatibility) image/jpeg (and image/jpg) - JPEG images (safe, universal compatibility) Clients that support rendering icons SHOULD also support:

basic#icons

Excluded. Extends BASE-022’s client-internal rendering-support premise: neither the support nor its absence reaches the wire, and the premise itself (that the client renders icons) is equally invisible.

LIFE

LIFE-001

MUST · binds the client

The initialization phase MUST be the first interaction between client and server.

basic/lifecycle#initialization

Judged by lifecycle.first-interaction-initialize.

LIFE-002

MUST · binds the client

The client MUST initiate this phase by sending an initialize request containing: Protocol version supported; Client capabilities; Client implementation information

basic/lifecycle#initialization

Judged by lifecycle.initialize-params.

LIFE-003

MUST · binds the client

After successful initialization, the client MUST send an initialized notification to indicate it is ready to begin normal operations

basic/lifecycle#initialization

Judged by lifecycle.initialized-notification.

LIFE-004

SHOULD NOT · binds the client

The client SHOULD NOT send requests other than pings before the server has responded to the initialize request.

basic/lifecycle#initialization

Judged by lifecycle.client-requests-before-init-response.

LIFE-005

SHOULD NOT · binds the server

The server SHOULD NOT send requests other than pings and logging before receiving the initialized notification.

basic/lifecycle#initialization

Judged by lifecycle.server-requests-before-initialized.

LIFE-006

MUST · binds the server

If the server supports the requested protocol version, it MUST respond with the same version. Otherwise, the server MUST respond with another protocol version it supports.

basic/lifecycle#version-negotiation

Judged by lifecycle.initialize-result-version.

LIFE-007

MUST · binds the client

In the initialize request, the client MUST send a protocol version it supports.

basic/lifecycle#version-negotiation

Judged by lifecycle.initialize-protocol-version.

LIFE-008

MUST · binds the client and server

Both parties MUST: Respect the negotiated protocol version

basic/lifecycle#operation

Excluded. After negotiation, neither stdio nor HTTP message payloads carry a per-message protocol version, so respect is not observable in message traffic. The one wire artifact — the HTTP MCP-Protocol-Version header — is judged under TRAN-017 and TRAN-018.

LIFE-009

MUST · binds the client and server

Both parties MUST: Only use capabilities that were successfully negotiated

basic/lifecycle#operation

Judged by lifecycle.negotiated-capabilities-only.

LIFE-010

MUST · binds the server

The server MUST respond with its own capabilities and information:

basic/lifecycle#initialization

Judged by lifecycle.initialize-result-shape.

LIFE-011

SHOULD · binds the client

This SHOULD be the latest version supported by the client.

basic/lifecycle#version-negotiation

Excluded. The client’s supported-version set is internal state with no wire footprint; the trace shows the version sent, not the versions available.

LIFE-012

SHOULD · binds the server

This SHOULD be the latest version supported by the server.

basic/lifecycle#version-negotiation

Excluded. The server’s supported-version set is internal state with no wire footprint; the trace shows the version answered, not the versions available.

LIFE-013

SHOULD · binds the client

If the client does not support the version in the server’s response, it SHOULD disconnect.

basic/lifecycle#version-negotiation

Excluded. Whether the client supports the answered version is client-internal; a continued session is only evidence of support if the premise is known. TRAN-018 warns on the adjacent observable (a mismatched MCP-Protocol-Version header).

LIFE-014

SHOULD · binds the client

For the stdio transport, the client SHOULD initiate shutdown by: First, closing the input stream to the child process (the server); Waiting for the server to exit, or sending SIGTERM if the server does not exit within a reasonable time; Sending SIGKILL if the server does not exit within a reasonable time after SIGTERM

basic/lifecycle#shutdown

Excluded. Stdin closure, signals, and process exit are host-OS actions outside the trace event vocabulary (transport-open/close/abort record that a transport ended, not who ended it or how).

LIFE-015

SHOULD · binds the client and server

Implementations SHOULD establish timeouts for all sent requests, to prevent hung connections and resource exhaustion.

basic/lifecycle#timeouts

Excluded. Checks may not consult time: trace timestamps are informational by design (determinism rule, 02-architecture.md), so timing conduct cannot be judged.

LIFE-016

SHOULD · binds the client and server

When the request has not received a success or error response within the timeout period, the sender SHOULD issue a cancellation notification for that request and stop waiting for a response.

basic/lifecycle#timeouts

Excluded. Checks may not consult time: trace timestamps are informational by design (determinism rule, 02-architecture.md), so timing conduct cannot be judged.

LIFE-017

SHOULD · binds the client and server

However, implementations SHOULD always enforce a maximum timeout, regardless of progress notifications, to limit the impact of a misbehaving client or server.

basic/lifecycle#timeouts

Excluded. Checks may not consult time: trace timestamps are informational by design (determinism rule, 02-architecture.md), so timing conduct cannot be judged.

LIFE-018

MUST · binds the client

If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server.

basic/lifecycle#version-negotiation

Judged by transport.protocol-version-header.

LIFE-019

SHOULD · binds the client and server

SDKs and other middleware SHOULD allow these timeouts to be configured on a per-request basis.

basic/lifecycle#timeouts

Excluded. Binds an SDK’s programming interface — what its callers can configure — which has no protocol representation at all.

LIFE-020

SHOULD · binds the client and server

Implementations SHOULD be prepared to handle these error cases: Protocol version mismatch; Failure to negotiate required capabilities; Request timeouts

basic/lifecycle#error-handling

Excluded. Preparedness is a property of the implementation, not of any session it took part in: a trace that happens to contain a version mismatch shows one case handled, and a trace without one evidences nothing. Where the handling has a wire form it is judged under the clause that states it, such as the version negotiation rules.

TRAN

TRAN-001

MUST NOT · binds the client and server

Messages are delimited by newlines, and MUST NOT contain embedded newlines.

basic/transports#stdio

Excluded. Framing is enforced at capture time: the JSON Lines trace format cannot represent an embedded-newline violation, so a parseable trace is evidence the rule held. Violations surface as capture-time parse failures, not as trace findings.

TRAN-002

MUST · binds the server

Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks

basic/transports#security-warning

Excluded. Judging Origin validation requires the server’s configured allowlist, which recorded traces do not carry. Enforced instead as default-on policy in mcp-everything-server and exercised by its transport integration tests (roadmap M2).

TRAN-003

MUST · binds the client and server

JSON-RPC messages MUST be UTF-8 encoded.

basic/transports

Excluded. A violation cannot be judged from a trace: trace documents are UTF-8 by construction, so recorded messages prove nothing was mis-encoded among what was recorded — and absence is not evidence, because capture tooling may drop undecodable bytes (mcp-everything-server’s tap skips non-UTF-8 SSE chunks and unparseable POST bodies). No offline check is sound.

TRAN-004

MUST NOT · binds the server

The server MUST NOT write anything to its stdout that is not a valid MCP message.

basic/transports#stdio

Judged by transport.stdio-server-output-valid.

TRAN-005

MUST NOT · binds the client

The client MUST NOT write anything to the server’s stdin that is not a valid MCP message.

basic/transports#stdio

Judged by transport.stdio-client-input-valid.

TRAN-006

MUST · binds the server

The server MUST provide a single HTTP endpoint path (hereafter referred to as the MCP endpoint) that supports both POST and GET methods.

basic/transports#streamable-http

Excluded. Recorded HTTP events carry status and conformance-relevant headers, not request lines, so endpoint topology is invisible in a trace. Enforced by mcp-everything-server’s transport integration tests (roadmap M2).

TRAN-007

MUST · binds the server

If the Origin header is present and invalid, servers MUST respond with HTTP 403 Forbidden.

basic/transports#security-warning

Excluded. Judging whether an Origin value is invalid requires the server’s configured allowlist, which recorded traces do not carry (the TRAN-002 rationale). Enforced as default-on policy in mcp-everything-server with 403 tests (roadmap M2).

TRAN-008

SHOULD · binds the server

When running locally, servers SHOULD bind only to localhost (127.0.0.1) rather than all network interfaces (0.0.0.0)

basic/transports#security-warning

Excluded. Socket binding is deployment state with no trace footprint. mcp-everything-server defaults to a loopback bind (the –bind clap default in src/main.rs), asserted by default_bind_is_loopback in tests/cli.rs.

TRAN-009

SHOULD · binds the server

Servers SHOULD implement proper authentication for all connections

basic/transports#security-warning

Excluded. Whether authentication is properly implemented is not decidable from one session’s headers: the client’s credential is never recorded, and the server’s validation of it is internal. What a recording does show — the challenge a protected server answers with — is judged under the authorization page’s own clauses (AUTH-014, AUTH-045, AUTH-049–AUTH-051).

TRAN-010

SHOULD · binds the server

The session ID SHOULD be globally unique and cryptographically secure (e.g., a securely generated UUID, a JWT, or a cryptographic hash).

basic/transports#session-management

Excluded. Global uniqueness and entropy of an identifier cannot be established from the single sample a trace contains. mcp-everything-server’s ids are pinned to the v4-UUID shape (OS RNG) and distinctness by session_ids_are_version_4_uuids_and_distinct in tests/http_policy.rs.

TRAN-011

MUST · binds the server

The session ID MUST only contain visible ASCII characters (ranging from 0x21 to 0x7E).

basic/transports#session-management

Judged by transport.session-id-visible-ascii.

TRAN-012

MUST · binds the client

The client MUST handle the session ID in a secure manner, see Session Hijacking mitigations for more details.

basic/transports#session-management

Excluded. Secure handling (storage, exposure, logging discipline) is client-internal behavior with no wire footprint beyond the echo requirement judged under TRAN-013.

TRAN-013

MUST · binds the client

If an MCP-Session-Id is returned by the server during initialization, clients using the Streamable HTTP transport MUST include it in the MCP-Session-Id header on all of their subsequent HTTP requests.

basic/transports#session-management

Judged by transport.session-id-echoed.

TRAN-014

SHOULD · binds the server

Servers that require a session ID SHOULD respond to requests without an MCP-Session-Id header (other than initialization) with HTTP 400 Bad Request.

basic/transports#session-management

Excluded. Whether a server requires a session ID is server configuration a trace does not carry; without it, a non-400 answer to a session-less request is not evidence either way.

TRAN-015

MUST · binds the server

The server MAY terminate the session at any time, after which it MUST respond to requests containing that session ID with HTTP 404 Not Found.

basic/transports#session-management

Excluded. Session termination is server-internal state; a trace cannot distinguish a terminated session (404 required) from a live one (404 forbidden).

TRAN-016

MUST · binds the client

When a client receives HTTP 404 in response to a request containing an MCP-Session-Id, it MUST start a new session by sending a new InitializeRequest without a session ID attached.

basic/transports#session-management

Excluded. A 404 is attributed to the request it answered when the recording’s order is unambiguous, but not to the moment the client read it: a recorder logs the status as it passes, and the client may already have sent further requests on the old session. Requests after the 404 therefore do not show the client ignored it, and a client that stops instead of reconnecting sends nothing to judge.

TRAN-017

MUST · binds the client

If using HTTP, the client MUST include the MCP-Protocol-Version: <protocol-version> HTTP header on all subsequent requests to the MCP server, allowing the MCP server to respond based on the MCP protocol version.

basic/transports#protocol-version-header

Judged by transport.protocol-version-header.

TRAN-018

SHOULD · binds the client

The protocol version sent by the client SHOULD be the one negotiated during initialization.

basic/transports#protocol-version-header

Judged by transport.protocol-version-negotiated.

TRAN-019

SHOULD · binds the server

For backwards compatibility, if the server does not receive an MCP-Protocol-Version header, and has no other way to identify the version - for example, by relying on the protocol version negotiated during initialization - the server SHOULD assume protocol version 2025-03-26.

basic/transports#protocol-version-header

Excluded. Which version a server assumed is internal state; subsequent traffic does not reveal it.

TRAN-020

MUST · binds the server

If the server receives a request with an invalid or unsupported MCP-Protocol-Version, it MUST respond with 400 Bad Request.

basic/transports#protocol-version-header

Judged by transport.unsupported-version-header-rejected.

TRAN-021

SHOULD NOT · binds the client

The client MAY capture, forward, or ignore the server’s stderr output and SHOULD NOT assume stderr output indicates error conditions.

basic/transports#stdio

Excluded. How a client interprets stderr is internal, and stderr bytes are not part of the protocol trace (the trace records MCP messages and transport lifecycle, not diagnostics channels).

TRAN-022

SHOULD · binds the client

Clients SHOULD support stdio whenever possible.

basic/transports

Excluded. A client’s supported-transport surface is a property of the implementation, not of any one session; a streamable-HTTP trace says nothing about whether stdio was available.

TRAN-023

MUST · binds the client and server

Implementers who choose to support custom transports MUST ensure they preserve the JSON-RPC message format and lifecycle requirements defined by MCP.

basic/transports#custom-transports

Excluded. The trace vocabulary covers stdio and streamable HTTP; custom transports are outside the capture universe by construction (TransportKind is closed per revision).

TRAN-024

MUST · binds the client

Every JSON-RPC message sent from the client MUST be a new HTTP POST request to the MCP endpoint.

basic/transports#sending-messages-to-the-server

Excluded. Adds to TRAN-049’s verb obligation that each message ride a new POST to the MCP endpoint. A POST body is one JSON value, which a recorder writes as one payload, so more than one message in a POST is a batch array — TRAN-026 judges that. What remains — that the request was new rather than reused, and was addressed to the MCP endpoint — leaves no trace: the endpoint is not recorded.

TRAN-025

MUST · binds the client

The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types.

basic/transports#sending-messages-to-the-server

Judged by transport.client-post-accept-header.

TRAN-026

MUST · binds the client

The body of the POST request MUST be a single JSON-RPC request, notification, or response.

basic/transports#sending-messages-to-the-server

Judged by transport.http-post-single-message.

TRAN-027

MUST · binds the server

If the server accepts the input, the server MUST return HTTP status code 202 Accepted with no body.

basic/transports#sending-messages-to-the-server

Judged by transport.accepted-input-status.

TRAN-028

MUST · binds the server

If the server cannot accept the input, it MUST return an HTTP error status code (e.g., 400 Bad Request).

basic/transports#sending-messages-to-the-server

Excluded. Whether input was acceptable is server-internal judgment, and the trace schema records HTTP request and response observations as independent events without request/response correlation, so which response answered which POST is not decidable offline; the official suite exercises this live and the conformance gate runs it.

TRAN-029

MUST · binds the server

If the input is a JSON-RPC request, the server MUST either return Content-Type: text/event-stream, to initiate an SSE stream, or Content-Type: application/json, to return one JSON object.

basic/transports#sending-messages-to-the-server

Judged by transport.success-content-type.

TRAN-030

MUST · binds the client

The client MUST support both these cases.

basic/transports#sending-messages-to-the-server

Excluded. What a client could have handled is client-internal capability; a trace shows the case that occurred, not the cases supported.

TRAN-031

SHOULD · binds the server

The server SHOULD immediately send an SSE event consisting of an event ID and an empty data field in order to prime the client to reconnect (using that event ID as Last-Event-ID).

basic/transports#sending-messages-to-the-server

Excluded. SSE framing is consumed at capture: the tap reassembles data: lines into messages and drops event IDs and retry fields, so SSE-frame-level conduct is invisible in a trace.

TRAN-032

SHOULD · binds the server

If the server does close the connection prior to terminating the SSE stream, it SHOULD send an SSE event with a standard retry field before closing the connection.

basic/transports#sending-messages-to-the-server

Excluded. SSE framing is consumed at capture: the tap reassembles data: lines into messages and drops event IDs and retry fields, so SSE-frame-level conduct is invisible in a trace.

TRAN-033

MUST · binds the client

The client MUST respect the retry field, waiting the given number of milliseconds before attempting to reconnect.

basic/transports#sending-messages-to-the-server

Excluded. Checks may not consult time: trace timestamps are informational by design (determinism rule, 02-architecture.md), so timing conduct cannot be judged.

TRAN-034

SHOULD · binds the server

The SSE stream SHOULD eventually include a JSON-RPC response for the JSON-RPC request sent in the POST body.

basic/transports#sending-messages-to-the-server

Excluded. Which HTTP stream delivered a message is not part of the trace vocabulary — events form a single capture-ordered sequence — so per-stream conduct cannot be judged from a recorded trace.

TRAN-035

SHOULD · binds the server

These messages SHOULD relate to the originating client request.

basic/transports#sending-messages-to-the-server

Excluded. Relatedness is a semantic judgment over message content, and which HTTP stream delivered a message is not part of the trace vocabulary — events form a single capture-ordered sequence — so per-stream conduct cannot be judged from a recorded trace.

TRAN-036

SHOULD · binds the server

After the JSON-RPC response has been sent, the server SHOULD terminate the SSE stream.

basic/transports#sending-messages-to-the-server

Excluded. Which HTTP stream delivered a message is not part of the trace vocabulary — events form a single capture-ordered sequence — so per-stream conduct cannot be judged from a recorded trace.

TRAN-037

SHOULD NOT · binds the server

Disconnection SHOULD NOT be interpreted as the client cancelling its request.

basic/transports#sending-messages-to-the-server

Excluded. How the server interpreted a disconnect is internal state; only conduct that follows from a misinterpretation could surface, and attributing it requires the interpretation.

TRAN-038

SHOULD · binds the client

To cancel, the client SHOULD explicitly send an MCP CancelledNotification.

basic/transports#sending-messages-to-the-server

Excluded. A wish to cancel is client intent with no observable of its own; a session without a cancellation is not evidence one was wanted.

TRAN-039

MUST · binds the client

The client MUST include an Accept header, listing text/event-stream as a supported content type.

basic/transports#listening-for-messages-from-the-server

Judged by transport.client-get-accept-header.

TRAN-040

MUST · binds the server

The server MUST either return Content-Type: text/event-stream in response to this HTTP GET, or else return HTTP 405 Method Not Allowed, indicating that the server does not offer an SSE stream at this endpoint.

basic/transports#listening-for-messages-from-the-server

Judged by transport.get-content-type.

TRAN-041

SHOULD · binds the server

These messages SHOULD be unrelated to any concurrently-running JSON-RPC request from the client.

basic/transports#listening-for-messages-from-the-server

Excluded. Unrelatedness is a semantic judgment over message content, and which HTTP stream delivered a message is not part of the trace vocabulary — events form a single capture-ordered sequence — so per-stream conduct cannot be judged from a recorded trace.

TRAN-042

MUST NOT · binds the server

The server MUST NOT send a JSON-RPC response on the stream unless resuming a stream associated with a previous client request.

basic/transports#listening-for-messages-from-the-server

Excluded. Which HTTP stream delivered a message is not part of the trace vocabulary — events form a single capture-ordered sequence — so per-stream conduct cannot be judged from a recorded trace.

TRAN-043

MUST · binds the server

The server MUST send each of its JSON-RPC messages on only one of the connected streams; that is, it MUST NOT broadcast the same message across multiple streams.

basic/transports#multiple-connections

Excluded. Which HTTP stream delivered a message is not part of the trace vocabulary — events form a single capture-ordered sequence — so per-stream conduct cannot be judged from a recorded trace. A duplicated message in the capture order is also indistinguishable from a legitimately repeated notification.

TRAN-044

MUST · binds the server

If present, the ID MUST be globally unique across all streams within that session—or all streams with that specific client, if session management is not in use.

basic/transports#resumability-and-redelivery

Excluded. SSE framing is consumed at capture: the tap reassembles data: lines into messages and drops event IDs and retry fields, so SSE-frame-level conduct is invisible in a trace.

TRAN-045

SHOULD · binds the server

Event IDs SHOULD encode sufficient information to identify the originating stream, enabling the server to correlate a Last-Event-ID to the correct stream.

basic/transports#resumability-and-redelivery

Excluded. SSE framing is consumed at capture: the tap reassembles data: lines into messages and drops event IDs and retry fields, so SSE-frame-level conduct is invisible in a trace.

TRAN-046

SHOULD · binds the client

If the client wishes to resume after a disconnection (whether due to network failure or server-initiated closure), it SHOULD issue an HTTP GET to the MCP endpoint, and include the Last-Event-ID header to indicate the last event ID it received.

basic/transports#resumability-and-redelivery

Excluded. Conditional on client intent (the wish to resume): a session that never disconnects owes nothing, and a recording cannot show a client wishing. When a resumption does occur its method and last-event-id header are both recorded, but absence proves nothing.

TRAN-047

MUST NOT · binds the server

The server MUST NOT replay messages that would have been delivered on a different stream.

basic/transports#resumability-and-redelivery

Excluded. Which HTTP stream delivered a message is not part of the trace vocabulary — events form a single capture-ordered sequence — so per-stream conduct cannot be judged from a recorded trace.

TRAN-048

SHOULD · binds the client

Clients that no longer need a particular session (e.g., because the user is leaving the client application) SHOULD send an HTTP DELETE to the MCP endpoint with the MCP-Session-Id header, to explicitly terminate the session.

basic/transports#session-management

Excluded. Conditional on client intent (no longer needing the session). A teardown DELETE is recorded when it happens — corpus/good/http-session.jsonl carries one — but a session that ends without one may simply not have finished with it, so absence is not a violation.

TRAN-049

MUST · binds the client

The client MUST use HTTP POST to send JSON-RPC messages to the MCP endpoint.

basic/transports#sending-messages-to-the-server

Judged by transport.client-messages-use-post.

TRAN-138

SHOULD · binds the client

The client SHOULD then “poll” the SSE stream by attempting to reconnect.

basic/transports#sending-messages-to-the-server

Excluded. The obligation arises when the server closes the connection without terminating the SSE stream, and that is connection- and SSE-frame-level conduct the capture consumes (TRAN-032’s reason), so no trace shows when it arose. A later reconnecting GET is equally consistent with any other reason to open one, as for TRAN-046.

TRAN-139

SHOULD · binds the server

If the server closes the connection without terminating the stream, it SHOULD follow the same polling behavior as described for POST requests: sending a retry field and allowing the client to reconnect.

basic/transports#listening-for-messages-from-the-server

Excluded. SSE framing is consumed at capture: the tap reassembles data: lines into messages and drops event IDs and retry fields, so SSE-frame-level conduct is invisible in a trace. TRAN-032 is the same rule for streams a POST opened.

TRAN-140

SHOULD · binds the client and server

Custom transports SHOULD document their specific connection establishment and message exchange patterns to aid interoperability.

basic/transports#custom-transports

Excluded. Binds a transport’s documentation, not its messages: the artefact the clause asks for is a document, and no session can be evidence about whether one exists. Custom transports are outside the capture universe besides (TRAN-023).

PING

PING-001

MUST · binds the client and server

The receiver MUST respond promptly with an empty response:

basic/utilities/ping#behavior-requirements

Judged by ping.empty-result.

PING-002

SHOULD · binds the client and server

Implementations SHOULD periodically issue pings to detect connection health

basic/utilities/ping#implementation-considerations

Excluded. “Periodically” is a rate over wall-clock time, and checks may not consult time — the limit LIFE-015 records. A recording would have to show the interval between pings against the length of the session, and it carries neither: a session with no ping and one with few are both what a conforming implementation produces over a short enough connection.

PING-003

SHOULD · binds the client and server

The frequency of pings SHOULD be configurable

basic/utilities/ping#implementation-considerations

Excluded. Binds the implementation’s configuration surface — what its operator can set — which has no protocol representation (LIFE-019’s boundary). A recording shows the pings one configuration produced, never whether another was available.

PING-004

SHOULD · binds the client and server

Timeouts SHOULD be appropriate for the network environment

basic/utilities/ping#implementation-considerations

Excluded. A timeout is a duration held inside the sender, and checks may not consult time (LIFE-015). A recording would have to carry the deadline the sender held a ping to and the network it was chosen for; it shows a ping answered or not, and nothing of either.

PING-005

SHOULD · binds the client and server

Excessive pinging SHOULD be avoided to reduce network overhead

basic/utilities/ping#implementation-considerations

Excluded. “Excessive” is a rate — pings per unit of time — and checks may not consult time (LIFE-015). No count of pings alone separates a flood from a long session checking its connection, the same limit PROG-018 records for rate limiting.

PING-006

SHOULD · binds the client and server

Timeouts SHOULD be treated as connection failures

basic/utilities/ping#error-handling

Excluded. The antecedent, a ping timing out, is the sender’s own deadline expiring, which no message records and checks may not compute from timestamps (LIFE-015). The consequence, treating the connection as failed, is at most a transport-close lifecycle event, which says that a transport ended and not why; a recording cannot tell a connection dropped for a timeout from one closed at the end of a session.

PING-007

SHOULD · binds the client and server

Implementations SHOULD log ping failures for diagnostics

basic/utilities/ping#error-handling

Excluded. Logging is local diagnostics with no wire footprint (CANC-024’s boundary): nothing an implementation writes to its own log is a protocol message, so no recording contains it.

PROG

PROG-009

MUST · binds the client and server

Progress tokens MUST be a string or integer value

basic/utilities/progress#progress-flow

Judged by progress.token-string-or-integer.

PROG-010

MUST · binds the client and server

Progress tokens can be chosen by the sender using any means, but MUST be unique across all active requests.

basic/utilities/progress#progress-flow

Judged by progress.token-unique-among-active-requests.

PROG-011

MUST · binds the client and server

The progress value MUST increase with each notification, even if the total is unknown.

basic/utilities/progress#progress-flow

Judged by progress.value-strictly-increases.

PROG-012

SHOULD · binds the client and server

The message field SHOULD provide relevant human readable progress information.

basic/utilities/progress#progress-flow

Excluded. Whether a message is relevant and human readable is a judgment about its meaning, not its shape; any string satisfies the schema, and no mechanical reading of one decides relevance. The same sentence is PROG-004 at 2026-07-28, excluded for the same reason.

PROG-013

MUST · binds the client and server

Progress notifications MUST only reference tokens that: Were provided in an active request; Are associated with an in-progress operation

basic/utilities/progress#behavior-requirements

Judged by progress.token-issued-in-session, progress.stops-after-completion.

PROG-014

MUST · binds the client and server

For task-augmented requests, the progressToken provided in the original request MUST continue to be used for progress notifications throughout the task’s lifetime, even after the CreateTaskResult has been returned.

basic/utilities/progress#behavior-requirements

Judged by progress.task-token-unchanged.

PROG-015

MUST · binds the client and server

Progress notifications for tasks MUST use the same progressToken that was provided in the initial task-augmented request

basic/utilities/progress#behavior-requirements

Judged by progress.task-token-unchanged.

PROG-016

MUST · binds the client and server

Progress notifications for tasks MUST stop after the task reaches a terminal status (completed, failed, or cancelled)

basic/utilities/progress#behavior-requirements

Judged by progress.stops-after-completion.

PROG-017

SHOULD · binds the client and server

Senders and receivers SHOULD track active progress tokens

basic/utilities/progress#implementation-notes

Excluded. Tracking is bookkeeping inside each party, and a recording carries no message that shows a token being tracked or forgotten. Its wire consequences are the MUST rows beside it, which are judged: a token reused while its request is active (PROG-010), a token no request provided and progress for a finished request (PROG-013, PROG-016, PROG-019).

PROG-018

SHOULD · binds the client and server

Both parties SHOULD implement rate limiting to prevent flooding

basic/utilities/progress#implementation-notes

Excluded. A rate is messages per unit of time, and checks may not consult time — the limit LIFE-015 records. No count of notifications alone separates a flood from a long operation reporting often.

PROG-019

MUST · binds the client and server

Progress notifications MUST stop after completion

basic/utilities/progress#implementation-notes

Judged by progress.stops-after-completion.

CANC

CANC-018

MUST · binds the client and server

Cancellation notifications MUST only reference requests that: Were previously issued in the same direction; Are believed to still be in-progress

basic/utilities/cancellation#behavior-requirements

Judged by cancellation.references-issued-request.

CANC-019

MUST NOT · binds the client

The initialize request MUST NOT be cancelled by clients

basic/utilities/cancellation#behavior-requirements

Judged by cancellation.initialize-not-cancelled.

CANC-020

MUST · binds the client and server

For task-augmented requests, the tasks/cancel request MUST be used instead of the notifications/cancelled notification.

basic/utilities/cancellation#behavior-requirements

Judged by cancellation.task-cancelled-by-request.

CANC-021

SHOULD · binds the client and server

Receivers of cancellation notifications SHOULD: Stop processing the cancelled request; Free associated resources; Not send a response for the cancelled request

basic/utilities/cancellation#behavior-requirements

Judged by cancellation.no-response-after-cancellation.

CANC-022

SHOULD · binds the client and server

The sender of the cancellation notification SHOULD ignore any response to the request that arrives afterward

basic/utilities/cancellation#behavior-requirements

Excluded. Ignoring a response is the absence of a reaction inside the sender. A recording would have to carry a message the sender sends because it acted on the late response, and the protocol defines none: nothing a party does with a response it received reaches the wire.

CANC-023

MUST · binds the client and server

Both parties MUST handle these race conditions gracefully:

basic/utilities/cancellation#timing-considerations

Excluded. Graceful handling is internal; the race itself is what a recording shows, and the checks are built not to convict it — cancellation.no-response-after-cancellation reports an answer to a cancelled request only once the receiver has demonstrably read the cancellation, and the conforming stdio-cancellation trace pins the answer that crosses one. A recording would have to carry a message the specification defines as ungraceful handling, and it defines none; a party that failed to handle the race would show it through some other clause’s violation, judged there.

CANC-024

SHOULD · binds the client and server

Both parties SHOULD log cancellation reasons for debugging

basic/utilities/cancellation#implementation-notes

Excluded. Logging is local diagnostics with no wire footprint: nothing an implementation writes to its own log is a protocol message, so no recording contains it.

CANC-025

SHOULD · binds the client

Application UIs SHOULD indicate when cancellation is requested

basic/utilities/cancellation#implementation-notes

Excluded. User-interface presentation, out by strategy rule 2: what an application shows its user is not a protocol message, so no recording carries it.

CANC-026

SHOULD · binds the client and server

Invalid cancellation notifications SHOULD be ignored: Unknown request IDs; Already completed requests; Malformed notifications

basic/utilities/cancellation#error-handling

Excluded. Ignoring a notification is the absence of a reaction, which a trace cannot tell apart from the notification never mattering: a recording would have to carry a message the receiver sends because it did not ignore the notification, and the protocol defines none (answering a notification is forbidden anyway, BASE-011). The party that sent the invalid notification is judged instead, where it can be: CANC-018 for a cancellation naming a request its sender never issued in the session, CANC-019 for one naming initialize.

TOOL

TOOL-001

MUST · binds the server

Servers that support tools MUST declare the tools capability:

server/tools#capabilities

Judged by tools.capability-declared.

TOOL-002

SHOULD · binds the server · only once server.tools.listChanged is negotiated

When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification:

server/tools#list-changed-notification

Excluded. The trigger condition — the tool list actually changing — is server-internal state a recorded trace cannot evidence, so absence of the notification is never proof of violation.

TOOL-003

MUST · binds the server · only once server.tools is negotiated

inputSchema: JSON Schema defining expected parameters; MUST be a valid JSON Schema object (not null)

server/tools#tool

Judged by tools.input-schema-object.

TOOL-004

MUST · binds the client

For trust & safety and security, clients MUST consider tool annotations to be untrusted unless they come from trusted servers.

server/tools#tool

Excluded. How a client weighs annotations is internal trust policy with no wire footprint.

TOOL-005

SHOULD · binds the server · only once server.tools is negotiated

Tool names SHOULD be between 1 and 128 characters in length (inclusive).

server/tools#tool-names

Judged by tools.name-length.

TOOL-006

SHOULD · binds the server · only once server.tools is negotiated

The following SHOULD be the only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.)

server/tools#tool-names

Judged by tools.name-charset.

TOOL-007

SHOULD NOT · binds the server · only once server.tools is negotiated

Tool names SHOULD NOT contain spaces, commas, or other special characters.

server/tools#tool-names

Judged by tools.name-charset.

TOOL-008

SHOULD · binds the server · only once server.tools is negotiated

Tool names SHOULD be unique within a server.

server/tools#tool-names

Judged by tools.name-unique.

TOOL-009

SHOULD · binds the server · only once server.tools is negotiated

Servers that use embedded resources SHOULD implement the resources capability:

server/tools#embedded-resources

Judged by tools.embedded-resource-capability.

TOOL-010

SHOULD · binds the server · only once server.tools is negotiated

For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.

server/tools#structured-content

Judged by tools.structured-content-text.

TOOL-011

MUST · binds the server · only once server.tools is negotiated

If an output schema is provided: Servers MUST provide structured results that conform to this schema.

server/tools#output-schema

Judged by tools.output-schema-structured-result.

TOOL-012

MUST · binds the server · only once server.tools is negotiated

Servers MUST: Validate all tool inputs; Implement proper access controls; Rate limit tool invocations; Sanitize tool outputs

server/tools#security-considerations

Excluded. Input validation, access control, rate limiting, and output sanitization are server-internal enforcement without a deterministic wire signature. Of the four duties, mcp-everything-server implements and tests input validation (tests/roundtrip.rs: tool_call_with_missing_required_argument_is_a_protocol_error and neighbors); access control beyond the transport-layer Host/Origin gate, rate limiting, and output sanitization are not in its scope — it serves fixed synthetic fixtures to a loopback-only harness.

TOOL-013

SHOULD · binds the client and server · only once server.tools is negotiated

Tool names SHOULD be considered case-sensitive.

server/tools#tool-names

Excluded. Name-comparison semantics are consumer-internal; tools.name-unique (TOOL-008) applies case-sensitive comparison in accordance with this clause.

TOOL-014

SHOULD · binds the client · only once server.tools is negotiated

Clients SHOULD validate structured results against this schema.

server/tools#output-schema

Excluded. Client-side validation has no wire footprint; the server-side half of the contract is TOOL-011’s check.

TOOL-015

SHOULD · binds the client · only once server.tools is negotiated

For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny tool invocations.

server/tools#user-interaction-model

Excluded. A host application’s approval UX is outside the protocol session; a trace shows invocations that happened, not the approvals that gated them.

TOOL-047

SHOULD · binds the client

Applications SHOULD: Provide UI that makes clear which tools are being exposed to the AI model; Insert clear visual indicators when tools are invoked; Present confirmation prompts to the user for operations, to ensure a human is in the loop

server/tools#user-interaction-model

Excluded. Binds the application’s user interface — what it shows, when it asks, whether a person is there. None of it is a message, and MCP deliberately does not mandate an interaction model.

TOOL-048

SHOULD · binds the client

Clients SHOULD provide tool execution errors to language models to enable self-correction.

server/tools#error-handling

Excluded. Binds what the client hands to a language model, which is downstream of the session entirely.

TOOL-049

SHOULD · binds the client

Clients SHOULD: Prompt for user confirmation on sensitive operations; Show tool inputs to the user before calling the server, to avoid malicious or accidental data exfiltration; Validate tool results before passing to LLM; Implement timeouts for tool calls; Log tool usage for audit purposes

server/tools#security-considerations

Excluded. Five client-side practices — confirmation prompts, showing inputs, validating results, timeouts, audit logs. Each happens inside the client or in its UI; none is a message.

RES

RES-001

MUST · binds the server

Servers that support resources MUST declare the resources capability:

server/resources#capabilities

Judged by resources.capability-declared.

RES-002

SHOULD · binds the server · only once server.resources.listChanged is negotiated

When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification:

server/resources#list-changed-notification

Excluded. The trigger condition — the resource list actually changing — is server-internal state a recorded trace cannot evidence.

RES-003

SHOULD · binds the server · only once server.resources is negotiated

Servers SHOULD use this scheme only when the client is able to fetch and load the resource directly from the web on its own—that is, it doesn’t need to read the resource via the MCP server.

server/resources#https//

Excluded. Whether the client can fetch a URI on its own is a property of the client’s environment, not of the recorded session.

RES-004

MUST · binds the server · only once server.resources is negotiated

Custom URI schemes MUST be in accordance with RFC3986, taking the above guidance in to account.

server/resources#custom-uri-schemes

Judged by resources.uri-scheme-rfc3986.

RES-005

MUST · binds the server

Servers MUST validate all resource URIs

server/resources#security-considerations

Excluded. URI validation is server-internal enforcement; a trace shows the URIs exchanged, not whether the server vetted them. mcp-everything-server’s read path is fail-closed — unknown or malformed URIs resolve to None — asserted by template_rejects_malformed_instantiations (src/resources.rs) and the unknown-URI protocol-error assertion in resources_surface_matches_the_scenarios (tests/roundtrip.rs).

RES-006

MUST · binds the server · only once server.resources is negotiated

Binary data MUST be properly encoded

server/resources#security-considerations

Judged by resources.blob-base64.

RES-007

SHOULD · binds the server · only once server.resources is negotiated

For other use cases, servers SHOULD prefer to use another URI scheme, or define a custom one, even if the server will itself be downloading resource contents over the internet.

server/resources#https//

Excluded. The premise — whether the client could fetch the URI directly — is a property of the client’s environment, not of the session (the RES-003 argument).

RES-008

SHOULD · binds the server · only once server.resources is negotiated

Servers SHOULD return standard JSON-RPC errors for common failure cases: Resource not found: -32002; Internal errors: -32603

server/resources#error-handling

Excluded. Whether a failure case actually occurred (the resource was absent, an internal fault happened) is server-side ground truth a trace does not carry.

RES-009

SHOULD · binds the server

Access controls SHOULD be implemented for sensitive resources

server/resources#security-considerations

Excluded. Which resources are sensitive and what controls guard them is deployment policy with no per-session observable.

RES-010

SHOULD · binds the server

Resource permissions SHOULD be checked before operations

server/resources#security-considerations

Excluded. Permission checks are server-internal enforcement; a trace shows the operations served, not the checks that admitted them.

PROM

PROM-001

MUST · binds the server

Servers that support prompts MUST declare the prompts capability during initialization:

server/prompts#capabilities

Judged by prompts.capability-declared.

PROM-002

SHOULD · binds the server · only once server.prompts.listChanged is negotiated

When the list of available prompts changes, servers that declared the listChanged capability SHOULD send a notification:

server/prompts#list-changed-notification

Excluded. The trigger condition — the prompt list actually changing — is server-internal state a recorded trace cannot evidence.

PROM-003

MUST · binds the server · only once server.prompts is negotiated

The image data MUST be base64-encoded and include a valid MIME type.

server/prompts#image-content

Judged by prompts.image-content-encoding.

PROM-004

MUST · binds the server · only once server.prompts is negotiated

The audio data MUST be base64-encoded and include a valid MIME type.

server/prompts#audio-content

Judged by prompts.audio-content-encoding.

PROM-005

MUST · binds the server · only once server.prompts is negotiated

Resources can contain either text or binary (blob) data and MUST include: A valid resource URI; The appropriate MIME type; Either text content or base64-encoded blob data

server/prompts#embedded-resources

Judged by prompts.embedded-resource-shape.

PROM-006

MUST · binds the client and server

Implementations MUST carefully validate all prompt inputs and outputs to prevent injection attacks or unauthorized access to resources.

server/prompts#security

Excluded. Validation thoroughness is implementation-internal; a trace shows the prompt traffic, not the vetting applied to it.

PROM-007

SHOULD · binds the server · only once server.prompts is negotiated

Servers SHOULD return standard JSON-RPC errors for common failure cases: Invalid prompt name: -32602 (Invalid params); Missing required arguments: -32602 (Invalid params); Internal errors: -32603 (Internal error)

server/prompts#error-handling

Excluded. Whether a failure case actually occurred (the name was invalid, an argument was missing) is server-side ground truth a trace does not carry.

PROM-008

SHOULD · binds the server · only once server.prompts is negotiated

Servers SHOULD validate prompt arguments before processing

server/prompts#implementation-considerations

Judged by prompts.arguments-validated.

PROM-009

SHOULD · binds the client · only once server.prompts is negotiated

Clients SHOULD handle pagination for large prompt lists

server/prompts#implementation-considerations

Excluded. Client-side pagination handling is internal; a session whose lists fit one page exercises nothing.

PROM-010

SHOULD · binds the client and server

Both parties SHOULD respect capability negotiation

server/prompts#implementation-considerations

Excluded. Mechanized at the protocol level by LIFE-009’s negotiated-capabilities-only check; this restatement adds no separately observable conduct.

LOG

LOG-001

MUST · binds the server

Servers that emit log message notifications MUST declare the logging capability:

server/utilities/logging#capabilities

Judged by logging.capability-declared.

LOG-002

MUST NOT · binds the server · only once server.logging is negotiated

Log messages MUST NOT contain: Credentials or secrets; Personal identifying information; Internal system details that could aid attacks

server/utilities/logging#security

Excluded. Deciding whether free-form log data contains secrets or PII is semantic content analysis, not structural validation; offline heuristics would produce unsound, unfalsifiable verdicts — a fixed pattern list is deterministic but proves nothing about what it misses.

LOG-003

SHOULD · binds the server · only once server.logging is negotiated

Servers SHOULD return standard JSON-RPC errors for common failure cases: Invalid log level: -32602 (Invalid params); Configuration errors: -32603 (Internal error)

server/utilities/logging#error-handling

Excluded. Whether a failure case actually occurred (the level was invalid for this server, configuration failed) is server-side ground truth a trace does not carry.

LOG-004

SHOULD · binds the server · only once server.logging is negotiated

Servers SHOULD: Rate limit log messages; Include relevant context in data field; Use consistent logger names; Remove sensitive information

server/utilities/logging#implementation-considerations

Excluded. Rate policy, context relevance, naming consistency, and redaction are semantic internals (the LOG-002 argument).

LOG-005

SHOULD · binds the client and server · only once server.logging is negotiated

Implementations SHOULD: Rate limit messages; Validate all data fields; Control log access; Monitor for sensitive content

server/utilities/logging#security

Excluded. Rate limiting, validation, access control, and monitoring are implementation-internal enforcement without trace-level observables.

COMP

COMP-001

MUST · binds the server

Servers that support completions MUST declare the completions capability:

server/utilities/completion#capabilities

Judged by completion.capability-declared.

COMP-002

MUST · binds the client and server

Implementations MUST: Validate all completion inputs; Implement appropriate rate limiting; Control access to sensitive suggestions; Prevent completion-based information disclosure

server/utilities/completion#security

Excluded. Input validation, rate limiting, and access control are implementation-internal enforcement without a deterministic wire signature.

COMP-003

SHOULD · binds the server · only once server.completions is negotiated

Servers SHOULD return standard JSON-RPC errors for common failure cases: Method not found: -32601 (Capability not supported); Invalid prompt name: -32602 (Invalid params); Missing required arguments: -32602 (Invalid params); Internal errors: -32603 (Internal error)

server/utilities/completion#error-handling

Excluded. Whether a failure case actually occurred is server-side ground truth a trace does not carry.

COMP-004

SHOULD · binds the server · only once server.completions is negotiated

Servers SHOULD: Return suggestions sorted by relevance; Implement fuzzy matching where appropriate; Rate limit completion requests; Validate all inputs

server/utilities/completion#implementation-considerations

Excluded. Relevance ordering, fuzziness, rate policy, and input vetting are semantic internals without trace-level observables.

COMP-005

SHOULD · binds the client · only once server.completions is negotiated

Clients SHOULD: Debounce rapid completion requests; Cache completion results where appropriate; Handle missing or partial results gracefully

server/utilities/completion#implementation-considerations

Excluded. Debouncing and caching are client-internal and timing-shaped; checks may not consult time (determinism rule).

PAGE

PAGE-001

MUST NOT · binds the client

Page size is determined by the server, and clients MUST NOT assume a fixed page size

server/utilities/pagination#pagination-model

Excluded. What a client assumes is internal state; the assumption has no wire footprint (MCP pagination requests carry no size parameter to inspect).

PAGE-002

MUST · binds the client

Clients MUST treat cursors as opaque tokens: Don’t make assumptions about cursor format; Don’t attempt to parse or modify cursors; Don’t persist cursors across sessions

server/utilities/pagination#implementation-guidelines

Judged by pagination.cursor-opacity.

PAGE-003

SHOULD · binds the server

Invalid cursors SHOULD result in an error with code -32602 (Invalid params).

server/utilities/pagination#error-handling

Judged by pagination.invalid-cursor-rejected.

PAGE-004

SHOULD · binds the server

Servers SHOULD: Provide stable cursors; Handle invalid cursors gracefully

server/utilities/pagination#implementation-guidelines

Excluded. Cursor stability is cross-request server behavior over hidden state; gracefulness on invalid cursors requires knowing a cursor was invalid (the PAGE-003 argument).

PAGE-005

SHOULD · binds the client

Clients SHOULD: Treat a missing nextCursor as the end of results; Support both paginated and non-paginated flows

server/utilities/pagination#implementation-guidelines

Excluded. How a client interprets a missing cursor, and what flows it supports, is client-internal; the wire-visible cursor discipline is PAGE-002’s check.

ELIC

ELIC-001

MUST NOT · binds the server

Servers MUST NOT use form mode elicitation to request sensitive information such as passwords, API keys, access tokens, or payment credentials

client/elicitation#user-interaction-model

Excluded. Whether a requested field is “sensitive information” — the page’s own definition is “secrets and credentials that grant access or authorize transactions” — is a judgement about meaning, not syntax: a schema property named token may be a display preference and one named answer may collect a password. A check would have to guess from field names, and a guess convicts conforming servers. The server’s author decides it; nothing in the message settles it.

ELIC-002

MUST · binds the server

Servers MUST use URL mode for interactions involving such sensitive information

client/elicitation#user-interaction-model

Excluded. The counterpart of the form mode prohibition, and blocked by the same judgement: that an interaction “involves such sensitive information” is a fact about what the user will be asked for, which no message states, so a form mode request cannot be shown to have needed URL mode.

ELIC-003

MUST · binds the client

MCP clients MUST: Provide UI that makes it clear which server is requesting information; Respect user privacy and provide clear decline and cancel options; For form mode, allow users to review and modify their responses before sending; For URL mode, clearly display the target domain/host and gather user consent before navigation to the target URL

client/elicitation#user-interaction-model

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-004

MUST · binds the client

Clients that support elicitation MUST declare the elicitation capability during initialization:

client/elicitation#capabilities

Judged by elicitation.client-declares-capability.

ELIC-005

MUST · binds the client

Clients declaring the elicitation capability MUST support at least one mode (form or url).

client/elicitation#capabilities

Excluded. No declaration can violate it on the wire. The page makes an elicitation object naming neither mode “equivalent to declaring support for form mode only”, so every declared elicitation capability names at least one mode. What remains is whether the client can actually handle the mode it declared, which is internal: a client that cannot still returns a well-formed decline or cancel.

ELIC-006

MUST NOT · binds the server

Servers MUST NOT send elicitation requests with modes that are not supported by the client.

client/elicitation#capabilities

Judged by elicitation.mode-supported.

ELIC-007

MUST · binds the server

All elicitation requests MUST include the following parameters:

client/elicitation#elicitation-requests

Judged by elicitation.request-parameters.

ELIC-008

MUST · binds the client

Clients MUST treat requests without a mode field as form mode.

client/elicitation#elicitation-requests

Excluded. A reading, not an act, like MRTR-007: a client’s answers to a form mode request and to one without mode are the same messages (accept with content, decline, cancel), and content is optional in the result, so no answer shows which reading the client took. The server half — that such a request is form mode — is what elicitation.mode-supported and elicitation.form-schema-present apply.

ELIC-009

MUST · binds the server

Form mode elicitation requests MUST either specify mode: "form" or omit the mode field, and include these additional parameters:

client/elicitation#form-mode-elicitation-requests

Judged by elicitation.form-schema-present.

ELIC-010

SHOULD · binds the client

Clients that support defaults SHOULD pre-populate form fields with these values.

client/elicitation#requested-schema

Excluded. UI behaviour: pre-populating a form is what the user sees before submitting. The submitted content cannot show it — the user may change or clear every default — so no message carries the clause.

ELIC-011

MUST · binds the server

URL mode elicitation requests MUST specify mode: "url", a message, and include these additional parameters:

client/elicitation#url-mode-elicitation-requests

Judged by elicitation.url-parameters-with-id.

ELIC-012

MUST · binds the server

The url parameter MUST contain a valid URL.

client/elicitation#url-mode-elicitation-requests

Judged by elicitation.url-valid.

ELIC-013

MUST · binds the server

Servers sending notifications: MUST only send the notification to the client that initiated the elicitation request.

client/elicitation#completion-notifications-for-url-mode-elicitation

Excluded. Which client initiated an elicitation is a fact across connections, and a recording holds one session. A completion sent to the wrong client appears in that client’s recording as a notification for an id its session never established — the observation elicitation.complete-notification-id judges under the clause that names the id, because the one recording cannot say which of the two rules was broken.

ELIC-014

MUST · binds the server

Servers sending notifications: MUST include the elicitationId established in the original elicitation/create request.

client/elicitation#completion-notifications-for-url-mode-elicitation

Judged by elicitation.complete-notification-id.

ELIC-015

MUST · binds the client

Clients: MUST ignore notifications referencing unknown or already-completed IDs.

client/elicitation#completion-notifications-for-url-mode-elicitation

Excluded. Ignoring leaves no trace: a client that ignored a notification for an unknown or completed id and one that acted on it internally send the same messages afterwards.

ELIC-016

SHOULD · binds the client

Clients: SHOULD still provide manual controls that let the user retry or cancel the original request (or otherwise resume interacting with the client) if the notification never arrives.

client/elicitation#completion-notifications-for-url-mode-elicitation

Excluded. UI behaviour: the controls the client offers its user. The wire shows only whether a retry or cancellation was eventually sent, which a user may decline to do whatever controls exist.

ELIC-017

MUST NOT · binds the server

The server MUST NOT return this error except when URL mode elicitation is required.

client/elicitation#url-elicitation-required-error

Excluded. Its condition — that URL mode elicitation is required before the request can proceed — is the server’s own determination, which no message states; a -32042 cannot be shown to be unwarranted.

ELIC-018

MUST · binds the server

The error MUST include a list of elicitations that are required to complete before the original can be retried.

client/elicitation#url-elicitation-required-error

Judged by elicitation.url-required-error-lists.

ELIC-019

MUST · binds the server

Any elicitations returned in the error MUST be URL mode elicitations and have an elicitationId property.

client/elicitation#url-elicitation-required-error

Judged by elicitation.url-required-error-url-mode.

ELIC-020

MUST · binds the server

Servers MUST return standard JSON-RPC errors for common failure cases: When a request cannot be processed until an elicitation is completed: -32042 (URLElicitationRequiredError)

client/elicitation#error-handling

Excluded. Its antecedent — that a request cannot be processed until an elicitation completes — is the server’s determination, and the page lets it ask by other means (it “MAY return” the error, or may send the elicitation while the request is pending). A server that chose another way sends no error, and nothing in the trace says it should have.

ELIC-021

MUST · binds the client

Clients MUST return standard JSON-RPC errors for common failure cases: Server sends an elicitation/create request with a mode not declared in client capabilities: -32602 (Invalid params)

client/elicitation#error-handling

Judged by elicitation.undeclared-mode-rejected.

ELIC-022

MUST · binds the server

Servers implementing elicitation MUST securely associate this state with individual users following the guidelines in the security best practices document. Specifically: State MUST NOT be associated with session IDs alone; State storage MUST be protected against unauthorized access; For remote MCP servers, user identification MUST be derived from credentials acquired via MCP authorization when possible (e.g. sub claim)

client/elicitation#statefulness

Excluded. Server-internal state handling. How a server stores per-user state, protects it and binds it to an identity happens behind its own storage and authorization, and the identity it binds to is established outside the session’s messages, by MCP authorization (whose pages this registry keeps out of scope, sources.json). Verified, where verified at all, by the server’s own security review and tests; at 2026-07-28 the state a client carries is requestState, opaque by construction (MRTR-008).

ELIC-023

MUST NOT · binds the server

MCP servers MUST NOT rely on URL mode elicitation to authorize users for themselves.

client/elicitation#url-mode-elicitation-for-oauth-flows

Excluded. Concerns what the server treats as authorization of its own users — an internal decision. A URL mode elicitation used to sign a user in to the server looks on the wire exactly like one used for a third-party grant; the difference is what the server does with the outcome.

ELIC-024

MUST NOT · binds the server

The third-party credentials MUST NOT transit through the MCP client

client/elicitation#understanding-the-distinction

Excluded. Third-party credentials are opaque strings whose provenance no message declares: a check cannot tell one in a result from any other string, and by design they never enter the session at all. The obligation is the server’s — credentials could only transit the client if the server sent them — and it is kept by the flow the server builds, outside the recording.

ELIC-025

MUST NOT · binds the server

The MCP server MUST NOT use the client’s credentials for the third-party service

client/elicitation#understanding-the-distinction

Excluded. Token passthrough happens between the server and the third-party service, outside the MCP session; the recording holds only the client–server exchange, in which the client’s credentials appear (as HTTP authorization) whether or not the server reuses them elsewhere.

ELIC-026

MUST · binds the server

The user MUST authorize the MCP server directly

client/elicitation#understanding-the-distinction

Excluded. By the clause’s own words the interaction “happens outside the MCP protocol, without involving the MCP client”: it is between the user’s browser and the server or a third-party authorization server, so a recording of the MCP session cannot contain it.

ELIC-027

MUST NOT · binds the server

The MCP server MUST NOT transmit credentials obtained through URL mode elicitation to the MCP client.

client/elicitation#understanding-the-distinction

Excluded. The same barrier as the transit clause: a credential obtained through URL mode elicitation is an opaque string whose provenance no message declares, so a check cannot tell one in a result from any other value the server returns.

ELIC-028

MUST · binds the server

Servers MUST bind elicitation requests to the client and user identity

client/elicitation#security-considerations

Excluded. Binding is server-internal state, and the identity it binds to is established by authorization outside the session’s messages, so no message shows whether an elicitation was bound to it.

ELIC-029

MUST · binds the client

Clients MUST provide clear indication of which server is requesting information

client/elicitation#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-030

SHOULD · binds the client

Clients SHOULD implement user approval controls

client/elicitation#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-031

SHOULD · binds the client

Clients SHOULD allow users to decline elicitation requests at any time

client/elicitation#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-032

SHOULD · binds the client

Clients SHOULD implement rate limiting

client/elicitation#security-considerations

Excluded. A request schedule: rate limiting is measured in elapsed time, which checks may not consult (LIFE-015, the time boundary in 02-architecture.md), and its threshold is the client’s own policy.

ELIC-033

SHOULD · binds the client

Clients SHOULD present elicitation requests in a way that makes it clear what information is being requested and why

client/elicitation#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-034

MUST NOT · binds the server

MCP servers requesting elicitation: MUST NOT include sensitive information about the end-user, including credentials, personal identifiable information, etc., in the URL sent to the client in a URL elicitation request.

client/elicitation#safe-url-handling

Excluded. Whether a URL’s path or query carries “sensitive information about the end-user” is a semantic judgement about opaque values — an identifier in a query may be a random nonce or an email address — so a check would convict on a guess. The page’s own examples put an elicitationId in the query, which is exactly the kind of value a pattern would misread.

ELIC-035

MUST NOT · binds the server

MCP servers requesting elicitation: MUST NOT provide a URL which is pre-authenticated to access a protected resource, as the URL could be used to impersonate the user by a malicious client.

client/elicitation#safe-url-handling

Excluded. Whether a URL grants access without further authentication is a property of the server it points to, observable only by dereferencing it — which a validator must not do (the client rules on the same page forbid even pre-fetching it), and which a recording does not contain.

ELIC-036

SHOULD NOT · binds the server

MCP servers requesting elicitation: SHOULD NOT include URLs intended to be clickable in any field of a form mode elicitation request.

client/elicitation#safe-url-handling

Excluded. Intent: the clause forbids URLs “intended to be clickable”, and a form’s message or a field’s description may mention a URL for reference. A check for URL-shaped text would convict a server for mentioning a link, and the intent itself is not on the wire.

ELIC-037

SHOULD · binds the server

MCP servers requesting elicitation: SHOULD use HTTPS URLs for non-development environments.

client/elicitation#safe-url-handling

Excluded. Its antecedent — that this is a non-development environment — is not in the trace. A loopback host suggests development without proving it, and a staging or internal host proves nothing either way, so an http URL is not by itself evidence of a violation. The scheme is recorded; the environment is not.

ELIC-038

MUST · binds the client

Clients implementing URL mode elicitation MUST handle URLs carefully to prevent users from unknowingly clicking malicious links.

client/elicitation#safe-url-handling

Excluded. Binds what the client does with a URL outside the MCP session — fetching it, opening it, displaying it — none of which is a message the session carries. The reference host never dereferences an elicitation URL: it answers URL mode from its interaction script and records the id (answer_elicitation, crates/mcp-reference-host/src/handler.rs).

ELIC-039

MUST NOT · binds the client

When handling URL mode elicitation requests, MCP clients: MUST NOT automatically pre-fetch the URL or any of its metadata.

client/elicitation#safe-url-handling

Excluded. Binds what the client does with a URL outside the MCP session — fetching it, opening it, displaying it — none of which is a message the session carries. The reference host never dereferences an elicitation URL: it answers URL mode from its interaction script and records the id (answer_elicitation, crates/mcp-reference-host/src/handler.rs).

ELIC-040

MUST NOT · binds the client

When handling URL mode elicitation requests, MCP clients: MUST NOT open the URL without explicit consent from the user.

client/elicitation#safe-url-handling

Excluded. Binds what the client does with a URL outside the MCP session — fetching it, opening it, displaying it — none of which is a message the session carries. The reference host never dereferences an elicitation URL: it answers URL mode from its interaction script and records the id (answer_elicitation, crates/mcp-reference-host/src/handler.rs).

ELIC-041

MUST · binds the client

When handling URL mode elicitation requests, MCP clients: MUST show the full URL to the user for examination before consent.

client/elicitation#safe-url-handling

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-042

MUST · binds the client

When handling URL mode elicitation requests, MCP clients: MUST open the URL provided by the server in a secure manner that does not enable the client or LLM to inspect the content or user inputs.

client/elicitation#safe-url-handling

Excluded. Binds what the client does with a URL outside the MCP session — fetching it, opening it, displaying it — none of which is a message the session carries. The reference host never dereferences an elicitation URL: it answers URL mode from its interaction script and records the id (answer_elicitation, crates/mcp-reference-host/src/handler.rs).

ELIC-043

SHOULD · binds the client

When handling URL mode elicitation requests, MCP clients: SHOULD highlight the domain of the URL to mitigate subdomain spoofing.

client/elicitation#safe-url-handling

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-044

SHOULD · binds the client

When handling URL mode elicitation requests, MCP clients: SHOULD have warnings for ambiguous/suspicious URIs (i.e., containing Punycode).

client/elicitation#safe-url-handling

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-045

SHOULD NOT · binds the client

When handling URL mode elicitation requests, MCP clients: SHOULD NOT render URLs as clickable in any field of an elicitation request, except for the url field in a URL elicitation request (with the restrictions detailed above).

client/elicitation#safe-url-handling

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

ELIC-046

MUST NOT · binds the server

Servers MUST NOT rely on client-provided user identification without server verification, as this can be forged.

client/elicitation#identifying-the-user

Excluded. Server-internal: whether the server verified an identity before trusting it is a decision made with information — its authorization context — that the session’s messages do not carry.

ELIC-047

SHOULD · binds the server

Instead, servers SHOULD follow security best practices.

client/elicitation#identifying-the-user

Excluded. Points to a separate document whose practices are server-internal; nothing in the session’s messages shows which were followed.

ELIC-048

MUST NOT · binds the server

Servers MUST NOT request sensitive information (passwords, API keys, etc.) via form mode

client/elicitation#form-mode-security

Excluded. Whether a requested field is “sensitive information” — the page’s own definition is “secrets and credentials that grant access or authorize transactions” — is a judgement about meaning, not syntax: a schema property named token may be a display preference and one named answer may collect a password. A check would have to guess from field names, and a guess convicts conforming servers. The server’s author decides it; nothing in the message settles it.

ELIC-049

SHOULD · binds the client

Clients SHOULD validate all responses against the provided schema

client/elicitation#form-mode-security

Judged by elicitation.response-matches-schema.

ELIC-050

SHOULD · binds the server

Servers SHOULD validate received data matches the requested schema

client/elicitation#form-mode-security

Excluded. Binds an act of validation, as MRTR-021 does: the server’s outcome — accepting the data, re-asking, or failing — is the only thing that reaches the wire, and each is consistent with a server that validated and with one that did not.

ELIC-051

MUST · binds the server

The MCP Server MUST verify the identity of the user who opens the URL before accepting information.

client/elicitation#phishing

Excluded. Identity verification of the user who opens the URL happens between that user’s browser and the server, outside the MCP session; the recording carries the URL the server sent and the client’s accept, not who opened it.

ELIC-052

MUST · binds the server

To prevent this attack, the server MUST ensure that the user who started the elicitation request (the end-user who is accessing the server via the MCP client) is the same user who completes the authorization flow.

client/elicitation#phishing

Excluded. Identity verification of the user who opens the URL happens between that user’s browser and the server, outside the MCP session; the recording carries the URL the server sent and the client’s accept, not who opened it.

ELIC-053

MUST · binds the server

In all implementations, the server MUST ensure that the mechanism to determine the user’s identity is resilient to attacks where an attacker can modify the elicitation URL.

client/elicitation#phishing

Excluded. Identity verification of the user who opens the URL happens between that user’s browser and the server, outside the MCP session; the recording carries the URL the server sent and the client’s accept, not who opened it.

SAMP

SAMP-001

SHOULD · binds the client

For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny sampling requests.

client/sampling#user-interaction-model

Excluded. Policy and UI: whether a human reviews sampling requests, and whether they can deny one, is the client application’s design. A request answered with a result may have been approved by a person or by policy; nothing in the answer says which.

SAMP-002

SHOULD · binds the client

Applications SHOULD: Provide UI that makes it easy and intuitive to review sampling requests; Allow users to view and edit prompts before sending; Present generated responses for review before delivery

client/sampling#user-interaction-model

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

SAMP-003

MUST · binds the client

Clients MUST declare support for tool use via the sampling.tools capability to receive tool-enabled sampling requests.

client/sampling#tools-in-sampling

Excluded. The precondition half of the sentence. A client that has not declared sampling.tools does nothing wrong by receiving a tool-enabled request — the server does, and the sentence’s other half (sampling.tools-declared) judges it. Nothing the client sends can break this half: the declaration is either made or not, and its absence becomes a violation only when such a request arrives, which is the server’s act.

SAMP-004

MUST NOT · binds the server

Servers MUST NOT send tool-enabled sampling requests to Clients that have not declared support for tool use via the sampling.tools capability.

client/sampling#tools-in-sampling

Judged by sampling.tools-declared.

SAMP-005

MUST · binds the client

Clients that support sampling MUST declare the sampling capability during initialization:

client/sampling#capabilities

Judged by sampling.client-declares-capability.

SAMP-006

SHOULD · binds the server

Servers SHOULD avoid using these values (e.g. can just omit includeContext since it defaults to "none")

client/sampling#capabilities

Judged by sampling.include-context-avoided.

SAMP-007

SHOULD NOT · binds the server

and SHOULD NOT use them unless the client declares sampling.context capability.

client/sampling#capabilities

Judged by sampling.include-context-declared.

SAMP-008

MUST · binds the server

When a user message contains tool results (type: “tool_result”), it MUST contain ONLY tool results.

client/sampling#tool-result-messages

Judged by sampling.tool-results-only.

SAMP-009

MUST · binds the server

When using tool use in sampling, every assistant message containing ToolUseContent blocks MUST be followed by a user message that consists entirely of ToolResultContent blocks, with each tool use (e.g. with id: $id) matched by a corresponding tool result (with toolUseId: $id), before any other message.

client/sampling#tool-use-and-result-balance

Judged by sampling.tool-use-answered.

SAMP-010

MUST · binds the client

{mode: "required"}: Model MUST use at least one tool before completing

client/sampling#tool-choice-modes

Judged by sampling.tool-choice-required-honoured.

SAMP-011

MUST NOT · binds the client

{mode: "none"}: Model MUST NOT use any tools

client/sampling#tool-choice-modes

Judged by sampling.tool-choice-none-honoured.

SAMP-012

SHOULD · binds the client

Clients SHOULD return errors for common failure cases: Tool result missing in request: -32602 (Invalid params); Tool results mixed with other content: -32602 (Invalid params)

client/sampling#error-handling

Judged by sampling.malformed-tool-messages-rejected.

SAMP-013

SHOULD · binds the client

Clients SHOULD return errors for common failure cases: User rejected sampling request: -1

client/sampling#error-handling

Excluded. Its antecedent — that the user rejected the request — happens in the client’s interface. An error with another code may be some other failure, and a result may follow the user’s approval, so no answer is wrong without knowing what the user did.

SAMP-014

SHOULD · binds the client

Clients SHOULD implement user approval controls

client/sampling#security-considerations

Excluded. Binds the client’s user interface — what it shows the user, offers them, or lets them do — and none of that reaches the wire: a recording carries the client’s messages, not its screens. The reference host this project ships has no interactive interface at all (it answers from its interaction script, crates/mcp-reference-host/src/handler.rs), so the clause is a host-implementation property, reviewed in the host, never judged from a trace.

SAMP-015

SHOULD · binds the client and server

Both parties SHOULD validate message content

client/sampling#security-considerations

Excluded. Validation is internal; its outcome is whatever each party sends next, and the shape of what they send is judged directly by the clauses that constrain it (tool-message balance, message roles).

SAMP-016

SHOULD · binds the client

Clients SHOULD respect model preference hints

client/sampling#security-considerations

Excluded. Hints are advisory and mapping them is the client’s choice (the page: clients “MAY map hints to equivalent models from different providers”); the result’s model names what was used, but no rule says which model a hint selects, so no answer contradicts one.

SAMP-017

SHOULD · binds the client

Clients SHOULD implement rate limiting

client/sampling#security-considerations

Excluded. A request schedule: rate limiting is measured in elapsed time, which checks may not consult (LIFE-015, the time boundary in 02-architecture.md), and its threshold is the client’s own policy.

SAMP-018

MUST · binds the client and server

Both parties MUST handle sensitive data appropriately

client/sampling#security-considerations

Excluded. “Appropriately” names no observable criterion, and which data is sensitive is a semantic judgement about content (see the elicitation pages’ sensitive-information clauses): no message can be shown to have mishandled it.

SAMP-019

MUST · binds the server

Servers MUST ensure that when replying to a stopReason: "toolUse", each ToolUseContent item is responded to with a ToolResultContent item with a matching toolUseId, and that the user message contains only tool results (no other content types)

client/sampling#security-considerations

Judged by sampling.tool-use-answered, sampling.tool-results-only.

SAMP-020

SHOULD · binds the client and server

Both parties SHOULD implement iteration limits for tool loops

client/sampling#security-considerations

Excluded. A limit on a loop’s length is a policy whose threshold the page does not set: any finite loop in a recording is consistent with some limit, and an unbounded one cannot be recorded.

ROOT

ROOT-001

MUST · binds the client

Clients that support roots MUST declare the roots capability during initialization:

client/roots#capabilities

Judged by roots.client-declares-capability.

ROOT-002

MUST · binds the client

When roots change, clients that support listChanged MUST send a notification:

client/roots#root-list-changes

Excluded. The antecedent — that the client’s roots changed — happens inside the client. A recording sees only the notification, so a session without one cannot be told from one whose roots never changed.

ROOT-003

MUST · binds the client

uri: Unique identifier for the root. This MUST be a file:// URI in the current specification.

client/roots#root

Judged by roots.uri-file-scheme.

ROOT-004

SHOULD · binds the client

Clients SHOULD return standard JSON-RPC errors for common failure cases: Client does not support roots: -32601 (Method not found)

client/roots#error-handling

Judged by roots.unsupported-rejected.

ROOT-005

SHOULD · binds the client

Clients SHOULD return standard JSON-RPC errors for common failure cases: Internal errors: -32603

client/roots#error-handling

Excluded. Its antecedent — an internal error — is the client’s own determination; another error code may be some other failure, so no answer is wrong on its face.

ROOT-006

MUST · binds the client

Clients MUST: Only expose roots with appropriate permissions; Validate all root URIs to prevent path traversal; Implement proper access controls; Monitor root accessibility

client/roots#security-considerations

Excluded. Client-internal policy: which directories the user permitted, how paths are validated and accessed, and whether they remain reachable are facts about the client’s host system, not its messages. The one part a message carries — the URI’s form — is judged by roots.uri-file-scheme; a file:// URI with .. segments is still well formed, and whether it escapes a permitted directory depends on the filesystem.

ROOT-007

SHOULD · binds the server

Servers SHOULD: Handle cases where roots become unavailable; Respect root boundaries during operations; Validate all paths against provided roots

client/roots#security-considerations

Excluded. Server-internal: what the server does with roots — keeping within their boundaries, validating paths against them, coping when they vanish or change, caching them — happens in its own operations on its host, which the MCP session does not record.

ROOT-008

SHOULD · binds the client

Clients SHOULD: Prompt users for consent before exposing roots to servers; Provide clear user interfaces for root management; Validate root accessibility before exposing; Monitor for root changes

client/roots#implementation-guidelines

Excluded. UI and internal monitoring: consent prompts and root-management screens are the client’s interface, and checking or watching roots’ accessibility happens on its filesystem. A recording carries only the roots it finally reported.

ROOT-009

SHOULD · binds the server

Servers SHOULD: Check for roots capability before usage

client/roots#implementation-guidelines

Judged by roots.capability-checked.

ROOT-010

SHOULD · binds the server

Servers SHOULD: Handle root list changes gracefully; Respect root boundaries in operations; Cache root information appropriately

client/roots#implementation-guidelines

Excluded. Server-internal: what the server does with roots — keeping within their boundaries, validating paths against them, coping when they vanish or change, caching them — happens in its own operations on its host, which the MCP session does not record.

AUTH

AUTH-001

SHOULD · binds the client and server

Implementations using an HTTP-based transport SHOULD conform to this specification.

basic/authorization#protocol-requirements

Excluded. Conformance to the page as a whole is what its own entries judge, clause by clause; the sentence adds no property of a message beyond them. It binds only implementations that support authorization at all, which the page makes OPTIONAL. Same sentence as BASE-085 on the basic page.

AUTH-002

SHOULD NOT · binds the client and server

Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.

basic/authorization#protocol-requirements

Excluded. A stdio session has no HTTP layer for a challenge or a token header to appear in, so a recording cannot show the specification being followed there; retrieving credentials from the environment is process-local conduct with no wire footprint. Same sentence as BASE-085 on the basic page.

AUTH-003

MUST · binds the client and server

Implementations using alternative transports MUST follow established security best practices for their protocol.

basic/authorization#protocol-requirements

Excluded. Binds transports other than the two this revision defines, which no recording of an MCP session over stdio or Streamable HTTP carries, and names no practice a check could hold a message to.

AUTH-004

MUST · binds the server

Authorization servers MUST implement OAuth 2.1 with appropriate security measures for both confidential and public clients.

basic/authorization#overview

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-005

SHOULD · binds the client and server

Authorization servers and MCP clients SHOULD support OAuth Client ID Metadata Documents

basic/authorization#overview

Excluded. Support for a client registration mechanism is exercised between the client and the authorization server, off the MCP endpoint. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-006

MUST · binds the server

MCP servers MUST implement OAuth 2.0 Protected Resource Metadata (RFC9728).

basic/authorization#overview

Excluded. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-007

MUST · binds the client

MCP clients MUST use OAuth 2.0 Protected Resource Metadata for authorization server discovery.

basic/authorization#overview

Excluded. Which document a client used to find its authorization server is visible only in fetches off the MCP endpoint. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-008

MUST · binds the server

MCP authorization servers MUST provide at least one of the following discovery mechanisms:

basic/authorization#overview

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-009

MUST · binds the client

MCP clients MUST support both discovery mechanisms to obtain the information required to interact with the authorization server.

basic/authorization#overview

Excluded. Conduct inside the client’s OAuth flow (authorization server metadata discovery), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-010

MUST · binds the server

MCP servers MUST implement the OAuth 2.0 Protected Resource Metadata (RFC9728) specification to indicate the locations of authorization servers.

basic/authorization#authorization-server-location

Excluded. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-011

MUST · binds the server

The Protected Resource Metadata document returned by the MCP server MUST include the authorization_servers field containing at least one authorization server.

basic/authorization#authorization-server-location

Excluded. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-012

MUST · binds the server

MCP servers MUST implement one of the following discovery mechanisms to provide authorization server location information to MCP clients:

basic/authorization#protected-resource-metadata-discovery-requirements

Excluded. A disjunction whose second branch a recording cannot witness. A 401 whose challenge carries no resource_metadata is conforming when the server serves the well-known document instead, and that document is not in the trace: The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-013

MUST · binds the client

MCP clients MUST support both discovery mechanisms and use the resource metadata URL from the parsed WWW-Authenticate headers when present; otherwise, they MUST fall back to constructing and requesting the well-known URIs in the order listed above.

basic/authorization#protected-resource-metadata-discovery-requirements

Excluded. Which URLs the client requested next, and in what order, are fetches off the MCP endpoint. The Protected Resource Metadata document is not on the MCP endpoint, and a recording cannot show it. The resource_metadata URL names the server’s own address, which mcp-trace-capture does not rewrite, so a client fetches the document past the proxy; the well-known fallback is built from the proxy’s origin, and the proxy maps every path under the upstream’s (/.well-known/... reaches /mcp/.well-known/...), where no server serves it. Where a fetch does pass through, its http event records no request target, so its JSON body cannot be told from any other.

AUTH-014

SHOULD · binds the server

MCP servers SHOULD include a scope parameter in the WWW-Authenticate header as defined in RFC 6750 Section 3 to indicate the scopes required for accessing the resource.

basic/authorization#protected-resource-metadata-discovery-requirements

Judged by auth.challenge-scope.

AUTH-015

MUST NOT · binds the client

Clients MUST NOT assume any particular set relationship between the challenged scope set and scopes_supported.

basic/authorization#protected-resource-metadata-discovery-requirements

Excluded. Conduct inside the client’s OAuth flow (the scope it requests, whatever it assumes about scopes_supported), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-016

MUST · binds the client

Clients MUST treat the scopes provided in the challenge as authoritative for satisfying the current request.

basic/authorization#protected-resource-metadata-discovery-requirements

Excluded. Conduct inside the client’s OAuth flow (the scope it requests in its authorization request), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-017

SHOULD · binds the server

Servers SHOULD strive for consistency in how they construct scope sets but they are not required to surface every dynamically issued scope through scopes_supported.

basic/authorization#protected-resource-metadata-discovery-requirements

Excluded. Striving for consistency is a design aim, not a property any one challenge has, and challenges for different operations legitimately name different scopes; scopes_supported lives in the Protected Resource Metadata document, which a recording does not carry.

AUTH-018

MUST · binds the client

MCP clients MUST be able to parse WWW-Authenticate headers and respond appropriately to HTTP 401 Unauthorized responses from the MCP server.

basic/authorization#protected-resource-metadata-discovery-requirements

Excluded. Conduct inside the client’s OAuth flow (parsing the challenge, then the discovery, authorization and token requests that are the appropriate response to a 401), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-019

SHOULD · binds the client

If the scope parameter is absent, clients SHOULD apply the fallback behavior defined in the Scope Selection Strategy section.

basic/authorization#protected-resource-metadata-discovery-requirements

Excluded. Conduct inside the client’s OAuth flow (the scope it requests in its authorization request), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-020

MUST · binds the client

To handle different issuer URL formats and ensure interoperability with both OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0 specifications, MCP clients MUST attempt multiple well-known endpoints when discovering authorization server metadata.

basic/authorization#authorization-server-metadata-discovery

Excluded. Requests to the authorization server’s well-known endpoints. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-021

MUST · binds the client

For issuer URLs with path components (e.g., https://auth.example.com/tenant1), clients MUST try endpoints in the following priority order:

basic/authorization#authorization-server-metadata-discovery

Excluded. Requests to the authorization server’s well-known endpoints. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-022

MUST · binds the client

For issuer URLs without path components (e.g., https://auth.example.com), clients MUST try:

basic/authorization#authorization-server-metadata-discovery

Excluded. Requests to the authorization server’s well-known endpoints. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-023

SHOULD · binds the client

Clients supporting all options SHOULD follow the following priority order:

basic/authorization#client-registration-approaches

Excluded. Conduct inside the client’s OAuth flow (choosing a registration mechanism), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-024

SHOULD · binds the client and server

MCP clients and authorization servers SHOULD support OAuth Client ID Metadata Documents as specified in OAuth Client ID Metadata Document.

basic/authorization#client-id-metadata-documents

Excluded. Support for a client registration mechanism is exercised between the client and the authorization server, off the MCP endpoint. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-025

MUST · binds the client and server

MCP implementations supporting Client ID Metadata Documents MUST follow the requirements specified in OAuth Client ID Metadata Document.

basic/authorization#implementation-requirements

Excluded. Points at requirements stated elsewhere (the Client ID Metadata Document draft, and the two lists that follow on this page), each entered under its own id there; the sentence adds no obligation a recording could show apart from them.

AUTH-026

MUST · binds the client

Clients MUST host their metadata document at an HTTPS URL following RFC requirements; The client_id URL MUST use the “https” scheme and contain a path component, e.g. https://example.com/client.json; The metadata document MUST include at least the following properties: client_id, client_name, redirect_uris; Clients MUST ensure the client_id value in the metadata matches the document URL exactly

basic/authorization#implementation-requirements

Excluded. The metadata document is hosted by the client and fetched by the authorization server; neither request touches the MCP endpoint, and the client_id travels in the authorization request. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-027

MUST · binds the server

For Authorization Servers: SHOULD fetch metadata documents when encountering URL-formatted client_ids; MUST validate that the fetched document’s client_id matches the URL exactly; SHOULD cache metadata respecting HTTP cache headers; MUST validate redirect URIs presented in an authorization request against those in the metadata document; MUST validate the document structure is valid JSON and contains required fields; SHOULD follow the security considerations in Section 6 of Client ID Metadata Document

basic/authorization#implementation-requirements

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-028

SHOULD · binds the client

MCP clients SHOULD check for this capability and MAY fall back to Dynamic Client Registration or pre-registration if unavailable.

basic/authorization#discovery

Excluded. Conduct inside the client’s OAuth flow (reading authorization server metadata and choosing a registration mechanism), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-029

SHOULD · binds the client

MCP clients SHOULD support an option for static client credentials such as those supplied by a preregistration flow.

basic/authorization#preregistration

Excluded. A configuration option of the client, exercised against the authorization server; no MCP message shows whether the option exists.

AUTH-030

SHOULD · binds the client

When implementing authorization flows, MCP clients SHOULD follow the principle of least privilege by requesting only the scopes necessary for their intended operations.

basic/authorization#scope-selection-strategy

Excluded. Conduct inside the client’s OAuth flow (the scope of its authorization request), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-031

SHOULD · binds the client

During the initial authorization handshake, MCP clients SHOULD follow this priority order for scope selection:

basic/authorization#scope-selection-strategy

Excluded. Conduct inside the client’s OAuth flow (the scope of its authorization request), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-032

MUST · binds the client

MCP clients MUST implement Resource Indicators for OAuth 2.0 as defined in RFC 8707 to explicitly specify the target resource for which the token is being requested.

basic/authorization#resource-parameter-implementation

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-033

MUST · binds the client

The resource parameter: MUST be included in both authorization requests and token requests.; MUST identify the MCP server that the client intends to use the token with.; MUST use the canonical URI of the MCP server as defined in RFC 8707 Section 2.

basic/authorization#resource-parameter-implementation

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-034

SHOULD · binds the client

MCP clients SHOULD provide the most specific URI that they can for the MCP server they intend to access, following the guidance in RFC 8707.

basic/authorization#canonical-server-uri

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-035

SHOULD · binds the client and server

While the canonical form uses lowercase scheme and host components, implementations SHOULD accept uppercase scheme and host components for robustness and interoperability.

basic/authorization#canonical-server-uri

Excluded. Accepting a URI is a comparison made by whoever receives the resource parameter or validates a token’s audience — the authorization server, or the MCP server inside its validation — and neither comparison is a message on the MCP endpoint.

AUTH-036

SHOULD · binds the client and server

While both https://mcp.example.com/ (with trailing slash) and https://mcp.example.com (without trailing slash) are technically valid absolute URIs according to RFC 3986, implementations SHOULD consistently use the form without the trailing slash for better interoperability unless the trailing slash is semantically significant for the specific resource.

basic/authorization#canonical-server-uri

Excluded. The canonical URI’s spelling appears in the resource parameter of requests to the authorization server and in token audiences, neither of which a recording of the MCP endpoint carries; and an http event records no request target, so not even the endpoint URL the client used is there.

AUTH-037

MUST · binds the client

MCP clients MUST send this parameter regardless of whether authorization servers support it.

basic/authorization#canonical-server-uri

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-038

MUST · binds the client

Access token handling when making requests to MCP servers MUST conform to the requirements defined in OAuth 2.1 Section 5 “Resource Requests”.

basic/authorization#token-requirements

Excluded. Authorization is never in a trace: recorders keep headers by allowlist (RECORDED_HEADERS), and credentials stay off it by construction (docs/plan/05-security-model.md). Which requests carried a token, and which token, cannot be read back from a recording. The section’s other requirement, no token in the query string, has its own entry.

AUTH-039

MUST · binds the client

MCP client MUST use the Authorization request header field defined in OAuth 2.1 Section 5.1.1:

basic/authorization#token-requirements

Excluded. Authorization is never in a trace: recorders keep headers by allowlist (RECORDED_HEADERS), and credentials stay off it by construction (docs/plan/05-security-model.md). Which requests carried a token, and which token, cannot be read back from a recording.

AUTH-040

MUST · binds the client

Note that authorization MUST be included in every HTTP request from client to server, even if they are part of the same logical session.

basic/authorization#token-requirements

Excluded. Authorization is never in a trace: recorders keep headers by allowlist (RECORDED_HEADERS), and credentials stay off it by construction (docs/plan/05-security-model.md). Which requests carried a token, and which token, cannot be read back from a recording. A 401 to a request in mid-session does not settle it either: an expired token draws the same answer as a missing one.

AUTH-041

MUST NOT · binds the client

Access tokens MUST NOT be included in the URI query string

basic/authorization#token-requirements

Excluded. An http event records the method, status and allowlisted headers, and no request target: neither the path nor the query string a request was sent to is in the trace, so a token in the URI cannot be seen. Recording the query to judge this would write into every trace the very credential the clause keeps out of URIs.

AUTH-042

MUST · binds the server

MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2.

basic/authorization#token-handling

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-043

MUST · binds the server

MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2.

basic/authorization#token-handling

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-044

MUST · binds the server

If validation fails, servers MUST respond according to OAuth 2.1 Section 5.3 error handling requirements.

basic/authorization#token-handling

Excluded. Whether validation failed is decided inside the server, against a token the recording never holds, and the sentence imports OAuth 2.1 Section 5.3 wholesale. What a recording can hold the response to is the page’s own statement of it: the status each declared error receives and the 401 for an invalid token, judged by auth.error-status and auth.invalid-token-401.

AUTH-045

MUST · binds the server

Invalid or expired tokens MUST receive a HTTP 401 response.

basic/authorization#token-handling

Judged by auth.invalid-token-401.

AUTH-046

MUST NOT · binds the client

MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server’s authorization server.

basic/authorization#token-handling

Excluded. Authorization is never in a trace: recorders keep headers by allowlist (RECORDED_HEADERS), and credentials stay off it by construction (docs/plan/05-security-model.md). Which requests carried a token, and which token, cannot be read back from a recording.

AUTH-047

MUST · binds the server

MCP servers MUST only accept tokens that are valid for use with their own resources.

basic/authorization#token-handling

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-048

MUST NOT · binds the server

MCP servers MUST NOT accept or transit any other tokens.

basic/authorization#token-handling

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens. Transiting a token is a request to an upstream service, outside the MCP session.

AUTH-049

MUST · binds the server

Servers MUST return appropriate HTTP status codes for authorization errors

basic/authorization#error-handling

Judged by auth.error-status.

AUTH-050

SHOULD · binds the server

When a client makes a request with an access token with insufficient scope during runtime operations, the server SHOULD respond with: HTTP 403 Forbidden status code (per RFC 6750 Section 3.1); WWW-Authenticate header with the Bearer scheme and additional parameters: error="insufficient_scope" - indicating the specific type of authorization failure; scope="required_scope1 required_scope2" - specifying the minimum scopes needed for the operation; resource_metadata - the URI of the Protected Resource Metadata document (for consistency with 401 responses)

basic/authorization#runtime-insufficient-scope-errors

Judged by auth.insufficient-scope-response.

AUTH-051

SHOULD · binds the server

Server Scope Management: When responding with insufficient scope errors, servers SHOULD include the scopes needed to satisfy the current request in the scope parameter.

basic/authorization#runtime-insufficient-scope-errors

Judged by auth.insufficient-scope-names-scopes.

AUTH-052

SHOULD · binds the server

Servers SHOULD be consistent in their scope inclusion strategy to provide predictable behavior for clients.

basic/authorization#runtime-insufficient-scope-errors

Excluded. A strategy is a policy over every challenge the server issues, across sessions and users; one recording shows a few challenges for different operations, which legitimately name different scopes, and cannot tell an inconsistent strategy from a consistent one applied to different requests.

AUTH-053

SHOULD · binds the server

Servers SHOULD consider the user experience impact when determining which scopes to include in the response, as misconfigured scopes may require frequent user interaction.

basic/authorization#runtime-insufficient-scope-errors

Excluded. Binds the server’s deliberation, not a message: a challenge shows the scopes chosen, not whether their user experience impact was weighed.

AUTH-054

SHOULD · binds the client

Clients SHOULD respond to these errors by requesting a new access token with an increased set of scopes via a step-up authorization flow or handle the errors in other, appropriate ways.

basic/authorization#step-up-authorization-flow

Excluded. Conduct inside the client’s OAuth flow (the token request a step-up makes), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records. The clause also admits “other, appropriate ways”, which no message distinguishes.

AUTH-055

SHOULD · binds the client

Clients acting on behalf of a user SHOULD attempt the step-up authorization flow.

basic/authorization#step-up-authorization-flow

Excluded. Whether a client acts on behalf of a user or on its own (client_credentials) is a fact about its grant, which no MCP message states. Conduct inside the client’s OAuth flow (the step-up re-authorization), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-056

SHOULD · binds the client

Clients SHOULD implement retry limits and SHOULD track scope upgrade attempts to avoid repeated failures for the same resource and operation combination.

basic/authorization#step-up-authorization-flow

Excluded. A limit is a property of the client’s implementation, which a session that simply stops retrying satisfies whatever the limit is; the page fixes no number (“no more than a few times”) a recording could be held to, and the attempts tracked are re-authorizations off the MCP endpoint.

AUTH-057

MUST · binds the client and server

Implementations MUST follow OAuth 2.1 security best practices as laid out in OAuth 2.1 Section 7. “Security Considerations”.

basic/authorization#security-considerations

Excluded. Imports OAuth 2.1 Section 7 wholesale. Its observable consequences for an MCP exchange are the challenge clauses this page states itself, judged under their own ids; the rest is implementation conduct (storage, transport configuration, the OAuth flow) with no footprint on the MCP endpoint.

AUTH-058

MUST · binds the client

MCP clients MUST include the resource parameter in authorization and token requests as specified in the Resource Parameter Implementation section

basic/authorization#token-audience-binding-and-validation

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-059

MUST · binds the server

MCP servers MUST validate that tokens presented to them were specifically issued for their use

basic/authorization#token-audience-binding-and-validation

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-060

MUST · binds the client and server

Clients and servers MUST implement secure token storage and follow OAuth best practices, as outlined in OAuth 2.1, Section 7.1.

basic/authorization#token-theft

Excluded. Token storage is local state of each party, and a token is never recorded: nothing on the wire shows how either party keeps one.

AUTH-061

SHOULD · binds the server

Authorization servers SHOULD issue short-lived access tokens to reduce the impact of leaked tokens.

basic/authorization#token-theft

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-062

MUST · binds the server

For public clients, authorization servers MUST rotate refresh tokens as described in OAuth 2.1 Section 4.3.1 “Token Endpoint Extension”.

basic/authorization#token-theft

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-063

MUST · binds the client and server

Implementations MUST follow OAuth 2.1 Section 1.5 “Communication Security”.

basic/authorization#communication-security

Excluded. Imports OAuth 2.1 Section 1.5 wholesale. Its observable consequences for an MCP exchange are the challenge clauses this page states itself, judged under their own ids; the rest is implementation conduct (storage, transport configuration, the OAuth flow) with no footprint on the MCP endpoint.

AUTH-064

MUST · binds the server

All authorization server endpoints MUST be served over HTTPS.

basic/authorization#communication-security

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-065

MUST · binds the client

All redirect URIs MUST be either localhost or use HTTPS.

basic/authorization#communication-security

Excluded. Redirect URIs are registered with the authorization server and travel in authorization requests through the user’s browser. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-066

MUST · binds the client

To mitigate this, MCP clients MUST implement PKCE according to OAuth 2.1 Section 7.5.2 and MUST verify PKCE support before proceeding with authorization.

basic/authorization#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-067

MUST · binds the client

MCP clients MUST use the S256 code challenge method when technically capable, as required by OAuth 2.1 Section 4.1.1.

basic/authorization#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-068

MUST · binds the client

Since OAuth 2.1 and PKCE specifications do not define a mechanism for clients to discover PKCE support, MCP clients MUST rely on authorization server metadata to verify this capability:

basic/authorization#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-069

MUST · binds the client

OAuth 2.0 Authorization Server Metadata: If code_challenge_methods_supported is absent, the authorization server does not support PKCE and MCP clients MUST refuse to proceed.

basic/authorization#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-070

MUST · binds the client

MCP clients MUST verify the presence of code_challenge_methods_supported in the provider metadata response. If the field is absent, MCP clients MUST refuse to proceed.

basic/authorization#authorization-code-protection

Excluded. Conduct inside the client’s OAuth flow (PKCE and the authorization server metadata it is verified against), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-071

MUST · binds the server

Authorization servers providing OpenID Connect Discovery 1.0 MUST include code_challenge_methods_supported in their metadata to ensure MCP compatibility.

basic/authorization#authorization-code-protection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-072

MUST · binds the client

MCP clients MUST have redirect URIs registered with the authorization server.

basic/authorization#open-redirection

Excluded. Conduct inside the client’s OAuth flow (client registration), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-073

MUST · binds the server

Authorization servers MUST validate exact redirect URIs against pre-registered values to prevent redirection attacks.

basic/authorization#open-redirection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-074

SHOULD · binds the client

MCP clients SHOULD use and verify state parameters in the authorization code flow and discard any results that do not include or have a mismatch with the original state.

basic/authorization#open-redirection

Excluded. Conduct inside the client’s OAuth flow (the authorization code flow’s state), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

AUTH-075

MUST · binds the server

Authorization servers MUST take precautions to prevent redirecting user agents to untrusted URI’s, following suggestions laid out in OAuth 2.1 Section 7.12.2

basic/authorization#open-redirection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-076

SHOULD · binds the server

Authorization servers SHOULD only automatically redirect the user agent if it trusts the redirection URI.

basic/authorization#open-redirection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-077

MUST · binds the server

When implementing Client ID Metadata Documents, authorization servers MUST consider the security implications detailed in OAuth Client ID Metadata Document, Section 6.

basic/authorization#client-id-metadata-document-security

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-078

SHOULD · binds the server

Authorization servers fetching metadata documents SHOULD consider Server-Side Request Forgery (SSRF) risks

basic/authorization#authorization-server-abuse-protection

Excluded. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-079

MUST · binds the server

Authorization servers: SHOULD display additional warnings for localhost-only redirect URIs; MUST clearly display the redirect URI hostname during authorization

basic/authorization#localhost-redirect-uri-risks

Excluded. What the authorization server shows the user during authorization is consent UI in the user’s browser. Binds the authorization server, the separate OAuth party the page’s Roles section places outside the MCP server. A client reaches it directly, never through the MCP endpoint, so a recording of the MCP exchanges holds none of its conduct.

AUTH-080

MUST · binds the server

MCP proxy servers using static client IDs MUST obtain user consent for each dynamically registered client before forwarding to third-party authorization servers (which may require additional consent).

basic/authorization#confused-deputy-problem

Excluded. Consent is obtained in the user’s browser, and the forwarding is the proxy server’s own OAuth flow with a third-party authorization server; neither is a message on the MCP endpoint the recording captures.

AUTH-081

MUST · binds the server

MCP servers MUST validate access tokens before processing the request, ensuring the access token is issued specifically for the MCP server, and take all necessary steps to ensure no data is returned to unauthorized parties.

basic/authorization#access-token-privilege-restriction

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens. A result returned to an unauthorized party would need the request’s credential to show, and that is the header a recording never keeps.

AUTH-082

MUST · binds the server

A MCP server MUST follow the guidelines in OAuth 2.1 - Section 5.2 to validate inbound tokens.

basic/authorization#access-token-privilege-restriction

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-083

MUST · binds the server

MCP servers MUST only accept tokens specifically intended for themselves and MUST reject tokens that do not include them in the audience claim or otherwise verify that they are the intended recipient of the token.

basic/authorization#access-token-privilege-restriction

Excluded. Token validation is decided inside the server, against a token the recording never holds (Authorization is not recorded). What the server says about the outcome is its challenge, judged where the page states what it must carry: the error-handling table and the 401 for invalid tokens.

AUTH-084

MUST NOT · binds the server

The MCP server MUST NOT pass through the token it received from the MCP client.

basic/authorization#access-token-privilege-restriction

Excluded. Passthrough is a request from the MCP server to an upstream API, outside the MCP session; the recording holds the client–server exchange only, and not even the token the client sent (Authorization is never recorded).

AUTH-085

MUST · binds the client

MCP clients MUST implement and use the resource parameter as defined in RFC 8707 - Resource Indicators for OAuth 2.0 to explicitly specify the target resource for which the token is being requested.

basic/authorization#access-token-privilege-restriction

Excluded. Conduct inside the client’s OAuth flow (the resource parameter of its authorization and token requests), which runs between the client, the user’s browser and the authorization server, off the MCP endpoint. A recording of the MCP exchanges carries none of it; the only trace the flow leaves there is the token in Authorization, which capture never records.

Extensions — 2026-07-28

The clauses of the official extensions attached to 2026-07-28 — 149 in all, 60 judged from a recorded trace and 89 excluded with the reason below. An extension is optional by definition, so its clauses never change a report’s verdict or exit status: a report lists them under extensions, each extension with a verdict of its own, the way the official suite runs an extension’s scenarios under not_scored: extension (ADR-0023). Each entry quotes the extension’s own specification and links to where its repository publishes it.

io.modelcontextprotocol/tasks

TASK-001

MUST NOT · binds the server

A server MUST NOT return CreateTaskResult to a client that did not include the extension capability on its request, regardless of prior declarations.

specification/2026-07-28/tasks#capability-negotiation

Judged by tasks.create-requires-declaration.

TASK-002

MUST · binds the client

A client that has negotiated this extension MUST be prepared to handle either CallToolResult or CreateTaskResult in response to any supported request it issues.

specification/2026-07-28/tasks#capability-negotiation

Excluded. Being prepared is a property of the client’s code. What a recording could show — a client that receives a CreateTaskResult and never polls the task — is also how a client conforms when its user abandons the call or the capture ends first, so the wire cannot tell an unprepared client from either.

TASK-003

MUST · binds the client

A client that receives CreateTaskResult in response to an unsupported request type MUST interpret this as an invalid response to the request.

specification/2026-07-28/tasks#capability-negotiation

Excluded. How a client interprets a response is internal: the protocol defines no message for rejecting an invalid response, so a client that treated the task result as invalid and one that accepted it can send exactly the same thing next.

TASK-004

MUST · binds the server

If a server is unable to service a request to a client that does not declare this extension capability without returning CreateTaskResult, the server MUST return an error with the code -32021 (Missing Required Client Capability), indicating the required extension in the error response

specification/2026-07-28/tasks#capability-negotiation

Excluded. Its antecedent — that the server cannot serve this request without creating a task — is the server’s own judgment, which no message states: a server that can serve the call answers it normally, so a non-declaring client’s call answered with a result conforms whichever way the server could have gone. The -32021 error itself, when sent, is the core clause BASE-035 judges (meta.missing-capability-error).

TASK-005

SHOULD · binds the client and server

This specification may be extended to support tasks over other request types in the future; implementations SHOULD be designed to accommodate additional request types in future revisions of this specification.

specification/2026-07-28/tasks#supported-methods

Excluded. A design property: what an implementation is ready to accept from a later revision is in its code, not in anything it sends today.

TASK-006

MUST · binds the server

Servers MUST set resultType to "task" when returning a CreateTaskResult so that clients can distinguish it from a standard result.

specification/2026-07-28/tasks#polymorphic-results

Judged by tasks.create-result-discriminator.

TASK-007

MUST NOT · binds the server

Servers MUST NOT set resultType to "task" on result types other than CreateTaskResult.

specification/2026-07-28/tasks#polymorphic-results

Judged by tasks.task-result-type-only-on-create.

TASK-008

MUST NOT · binds the server

failed: The request failed due to a JSON-RPC error during execution. The task will include the error field with the JSON-RPC error details. This status MUST NOT be used for non-JSON-RPC errors.

specification/2026-07-28/tasks#task-status

Judged by tasks.failed-not-tool-result.

TASK-009

MUST NOT · binds the server

A server MUST NOT return CreateTaskResult until the task is durably created — that is, until a tasks/get for the returned taskId would resolve.

specification/2026-07-28/tasks#task-creation

Excluded. Durable creation shows only as its failure — a tasks/get for the returned id that does not resolve — and that answer is also the conforming one once the task’s TTL has elapsed (the page: “It is compliant behavior for a server to return an error stating the task cannot be found if it has purged an expired task”). Telling the two apart needs the time between creation and poll, and checks never consult an event’s ts.

TASK-010

MUST · binds the server

In eventually-consistent environments, the server MUST wait for consistency before responding.

specification/2026-07-28/tasks#task-creation

Excluded. How the server’s storage reaches consistency is internal; its one observable consequence is TASK-009’s, excluded for the reason recorded there.

TASK-011

SHOULD · binds the server

Server implementations that use multi round-trip requests in conjunction with task creation (for example, a tool that requires elicitation over InputRequiredResult before creating a task) SHOULD resolve all MRTR exchanges synchronously before responding with a CreateTaskResult.

specification/2026-07-28/tasks#task-creation

Excluded. Every input-required round of the original request necessarily precedes the CreateTaskResult that ends it, so every recording of a server running such rounds shows them resolved first; how else the server could have resolved them is not a message.

TASK-012

SHOULD · binds the client

Clients SHOULD respect the pollIntervalMs provided in responses when determining polling frequency.

specification/2026-07-28/tasks#task-polling

Excluded. Polling frequency is the time between requests, and checks never consult an event’s ts: it is optional, informational, and stamped by the recorder’s clock rather than the client’s.

TASK-013

SHOULD · binds the client

Clients SHOULD continue polling until the task reaches a terminal status or until invoking tasks/cancel.

specification/2026-07-28/tasks#task-polling

Excluded. A recording ends when its capture stops: a task with no terminal status in the trace shows a capture that ended as often as a client that stopped, and nothing on the wire tells the two apart.

TASK-014

SHOULD · binds the client

Clients SHOULD persist task IDs to durable storage so that polling can resume after a crash or restart.

specification/2026-07-28/tasks#task-polling

Excluded. Durable storage is the client’s own, and it shows only across a crash or restart, which a recording of one session does not span.

TASK-015

MUST · binds the server

Upon receiving a tasks/get request, the server MUST check the status of the task and respond accordingly: If the status is working, the server MUST return a a Task object with status working.; If the status is input_required, the server MUST return a Task object with status input_required and an inputRequests field defined in Multi Round-Trip Requests.; If the status is completed, the server MUST return a Task object with status completed and a result field containing the final result of the task.; If the status is cancelled, the server MUST return a Task object with status cancelled.; If the status is failed, the server MUST return a Task object with status failed and the error that occurred during execution.

specification/2026-07-28/tasks#response

Judged by tasks.task-fields, tasks.input-required-carries-requests, tasks.completed-carries-result, tasks.failed-carries-error.

TASK-016

MUST · binds the server

The inputRequests field MUST contain all outstanding requests from the server to the client that need to be fulfilled before the task can proceed.

specification/2026-07-28/tasks#response

Excluded. Which requests are outstanding is the server’s own state: a tasks/get that lists fewer requests than the task is waiting on looks exactly like one that lists them all.

TASK-017

MUST · binds the server

The resultType field MUST be set to "complete" on this object as it is the standard result shape for the tasks/get request.

specification/2026-07-28/tasks#response

Judged by tasks.results-complete.

TASK-018

SHOULD · binds the client

When a client observes a tasks/get response (or notifications/tasks notification) with status: "input_required", the client SHOULD fulfill the outstanding requests in inputRequests by sending one or more tasks/update requests with corresponding inputResponses.

specification/2026-07-28/tasks#task-update-requests

Excluded. Fulfilling an input request needs its user or model to answer, and the recording may end before they do: an unanswered input_required task shows a capture that stopped as often as a client that did not respond.

TASK-019

SHOULD · binds the client

After sending a tasks/update, the client SHOULD continue observing the task’s status via polling (tasks/get) or notifications (notifications/tasks) until it reaches a terminal state.

specification/2026-07-28/tasks#task-update-requests

Excluded. A recording ends when its capture stops: a task with no terminal status in the trace shows a capture that ended as often as a client that stopped, and nothing on the wire tells the two apart.

TASK-020

MUST · binds the client

Clients MUST treat each entry in inputRequests as they would the equivalent standalone server-to-client request — for example, an elicitation request surfaced via inputRequests is subject to the same trust model and user-facing behavior as a direct elicitation/create request.

specification/2026-07-28/tasks#task-update-requests

Excluded. Binds how the host presents and trusts a request a task carries — consent and user interface, which strategy rule 2 keeps out: none of it is a message to the server.

TASK-021

SHOULD · binds the client

Clients SHOULD deduplicate inputRequests keys across consecutive polls to avoid presenting the same request to the user or model more than once.

specification/2026-07-28/tasks#task-update-requests

Excluded. Deduplication here is presentation: receiving the same key on the next poll is the documented snapshot behaviour, and whether the user saw the request twice is user interface. Answering one key twice, which a recording does show, is TASK-026.

TASK-022

MUST · binds the server

Each request key in inputRequests MUST be unique over the lifetime of a single task.

specification/2026-07-28/tasks#task-update-requests

Judged by tasks.input-request-keys-stable.

TASK-023

MUST NOT · binds the server

A server MUST NOT reuse a key for a subsequent server-to-client request after a response for that key has been delivered, and MUST NOT use the same key to refer to two distinct requests over a task’s lifetime.

specification/2026-07-28/tasks#task-update-requests

Judged by tasks.input-request-keys-stable.

TASK-024

MUST · binds the server

On success, the server MUST acknowledge the request with an empty result.

specification/2026-07-28/tasks#response-1

Judged by tasks.ack-empty.

TASK-025

SHOULD · binds the server

Servers SHOULD return a JSON-RPC error if the taskId does not correspond to a known task.

specification/2026-07-28/tasks#response-1

Excluded. Whether a taskId is known is the server’s own state: a task created before the recording began, or one already purged after its TTL (“It is compliant behavior for a server to return an error stating the task cannot be found if it has purged an expired task”), is indistinguishable in a trace from one that never existed, so no answer to an unfamiliar id convicts the server.

TASK-026

SHOULD · binds the client

Clients SHOULD track inputRequests keys to avoid responding to requests more than once.

specification/2026-07-28/tasks#response-1

Judged by tasks.update-answers-once.

TASK-027

SHOULD · binds the server

A server SHOULD ignore any inputResponses responses mapped to a key that is not currently outstanding for the task — including keys that were never issued, keys that have already been answered, and keys whose corresponding request has been superseded.

specification/2026-07-28/tasks#response-1

Excluded. Which keys are outstanding is the server’s own state (TASK-016), and an ignored response leaves no trace of having been ignored: the acknowledgement is the same either way.

TASK-028

MUST · binds the server

The resultType field MUST be set to "complete" on UpdateTaskResult as it is the standard result shape for the tasks/update request.

specification/2026-07-28/tasks#response-1

Judged by tasks.results-complete.

TASK-029

MUST NOT · binds the client

The notifications/cancelled notification MUST NOT be used for task cancellation.

specification/2026-07-28/tasks#task-cancellation

Judged by tasks.no-cancelled-notification.

TASK-030

MUST · binds the server

On success, the server MUST acknowledge the request with an empty result.

specification/2026-07-28/tasks#response-2

Judged by tasks.ack-empty.

TASK-031

SHOULD · binds the server

Servers SHOULD return a JSON-RPC error if the taskId does not correspond to a known task.

specification/2026-07-28/tasks#response-2

Excluded. Whether a taskId is known is the server’s own state: a task created before the recording began, or one already purged after its TTL (“It is compliant behavior for a server to return an error stating the task cannot be found if it has purged an expired task”), is indistinguishable in a trace from one that never existed, so no answer to an unfamiliar id convicts the server.

TASK-032

MUST · binds the server

The resultType field MUST be set to "complete" on CancelTaskResult as it is the standard result shape for the tasks/cancel request.

specification/2026-07-28/tasks#response-2

Judged by tasks.results-complete.

TASK-033

MUST · binds the server

If a client requests task status notifications but does not declare the io.modelcontextprotocol/tasks extension capability, the server MUST return a JSON-RPC error specifying the missing capabilities

specification/2026-07-28/tasks#task-status-notifications

Judged by tasks.listen-requires-declaration.

TASK-034

MUST NOT · binds the server

notifications/progress and notifications/message notifications MUST NOT be sent on the subscriptions/listen stream for a task, and are not supported on tasks in general in this specification.

specification/2026-07-28/tasks#task-status-notifications

Judged by tasks.no-progress-on-task-stream.

TASK-035

MUST · binds the client

When tasks/get, tasks/update, or tasks/cancel is sent over the Streamable HTTP transport, the client MUST set the Mcp-Name header to the value of params.taskId.

specification/2026-07-28/tasks#streamable-http-routing-headers

Judged by tasks.mcp-name-header.

TASK-036

MUST · binds the server

Servers MUST return standard JSON-RPC errors for the following protocol error cases: Invalid or nonexistent taskId: -32602 (Invalid params); Servers MUST return this error for tasks/get.

specification/2026-07-28/tasks#protocol-errors

Excluded. Whether a taskId is known is the server’s own state: a task created before the recording began, or one already purged after its TTL (“It is compliant behavior for a server to return an error stating the task cannot be found if it has purged an expired task”), is indistinguishable in a trace from one that never existed, so no answer to an unfamiliar id convicts the server.

TASK-037

SHOULD · binds the server

Servers SHOULD return this error for tasks/update and tasks/cancel.

specification/2026-07-28/tasks#protocol-errors

Excluded. Whether a taskId is known is the server’s own state: a task created before the recording began, or one already purged after its TTL (“It is compliant behavior for a server to return an error stating the task cannot be found if it has purged an expired task”), is indistinguishable in a trace from one that never existed, so no answer to an unfamiliar id convicts the server.

TASK-038

MUST · binds the server

Missing required client capabilities: -32021 (Missing Required Client Capability); Servers MUST return this error for non-declaring clients requesting task notifications on subscriptions/listen.

specification/2026-07-28/tasks#protocol-errors

Judged by tasks.listen-requires-declaration.

TASK-039

MUST · binds the server

Servers MUST return this error for non-declaring clients issuing tasks/get, tasks/update, and tasks/cancel requests.

specification/2026-07-28/tasks#protocol-errors

Judged by tasks.methods-require-declaration.

TASK-040

SHOULD · binds the server

Servers SHOULD provide informative error messages to describe the cause of errors.

specification/2026-07-28/tasks#protocol-errors

Excluded. Informativeness is a judgment about prose; any non-empty message passes a mechanical test of it, so a check would only ever restate the core clause that error messages exist.

TASK-041

SHOULD · binds the server

The tasks/get response SHOULD include a statusMessage field with diagnostic information about the failure

specification/2026-07-28/tasks#task-execution-errors

Judged by tasks.failed-status-message.

TASK-042

MUST · binds the server

and MUST include the error field with the JSON-RPC error.

specification/2026-07-28/tasks#task-execution-errors

Judged by tasks.failed-carries-error.

TASK-043

MUST NOT · binds the server

The failed status MUST NOT be used to represent non-JSON-RPC errors, such as a tool result that completed with isError: true.

specification/2026-07-28/tasks#task-execution-errors

Judged by tasks.failed-not-tool-result.

TASK-044

MUST · binds the server

Errors within the context of a protocol method result MUST use the completed status with the error details in the result field.

specification/2026-07-28/tasks#task-execution-errors

Judged by tasks.failed-not-tool-result, tasks.completed-carries-result.

TASK-045

MUST · binds the server

If the underlying request resulted in a JSON-RPC error, the task uses failed status and the error field MUST contain that JSON-RPC error.

specification/2026-07-28/tasks#task-execution-errors

Judged by tasks.failed-carries-error.

TASK-046

MUST · binds the server

If the request completed with a result (even if isError: true for tool results), the task uses completed status and the result field MUST contain that result.

specification/2026-07-28/tasks#task-execution-errors

Judged by tasks.completed-carries-result.

TASK-047

MUST · binds the server

Servers MUST generate them with sufficient entropy that a third party cannot enumerate or guess them.

specification/2026-07-28/tasks#security-considerations

Excluded. Entropy is a property of the generator, not of one identifier: a recording holds a handful of ids, and an unguessable one and a predictable one can be spelled alike.

TASK-048

MUST · binds the server

Servers MUST perform authentication and authorization checks on each task-related request to ensure that the client has permission to access a task.

specification/2026-07-28/tasks#security-considerations

Excluded. Authorization is decided inside the server against a credential no recording carries: Authorization is never recorded (ADR-0022, fact 1).

TASK-049

MUST · binds the client

Hosts MUST apply the same trust model to these payloads as they would to standard elicitation/sampling requests.

specification/2026-07-28/tasks#security-considerations

Excluded. Binds how the host presents and trusts a request a task carries — consent and user interface, which strategy rule 2 keeps out: none of it is a message to the server.

io.modelcontextprotocol/skills

SKILL-001

MUST · binds the client

Every request MUST include the required request metadata in params._meta.

specification/stable/skills.mdx

Excluded. Restates the core rule the revision’s own registry judges on every request (BASE-030, meta.required-request-fields). Judged there once, it is not judged again here, where a request missing its metadata would read as a skills defect in a session that never touched the extension.

SKILL-002

MUST · binds the server

A server that declares this extension MUST implement skills/list and skills/get.

specification/stable/skills.mdx

Judged by skills.list-implemented, skills.get-implemented.

SKILL-003

MUST · binds the server

A server that additionally declares directoryRead: true MUST implement resources/directory/read.

specification/stable/skills.mdx

Judged by skills.directory-read-implemented.

SKILL-004

MUST NOT · binds the client

Clients MUST NOT call resources/directory/read against a server that has not declared directoryRead: true.

specification/stable/skills.mdx

Judged by skills.directory-read-declared.

SKILL-005

MUST · binds the server

A server declaring this extension therefore MUST also declare the resources capability.

specification/stable/skills.mdx

Judged by skills.declares-resources.

SKILL-006

MUST · binds the server

A skill served over MCP MUST conform to the Agent Skills specification.

specification/stable/skills.mdx

Excluded. The umbrella over the format rules this page goes on to state: those it states with their own keyword are entered and judged one by one (SKILL-007, SKILL-008, SKILL-013). The rest of the Agent Skills specification is another document, outside this registry’s page set.

SKILL-007

MUST · binds the server

Every skill MUST contain a SKILL.md file at its root.

specification/stable/skills.mdx

Judged by skills.entry-uri-is-skill-md.

SKILL-008

MUST · binds the server

SKILL.md MUST begin with YAML frontmatter containing at minimum the name and description fields as defined by the Agent Skills specification.

specification/stable/skills.mdx

Judged by skills.skill-md-frontmatter.

SKILL-009

MUST · binds the client

In the event that the Agent Skills specification changes in a backwards incompatible way, clients MUST honor any backwards compatibility mechanisms provided by the Agent Skills specification

specification/stable/skills.mdx

Excluded. Binds a client’s response to a future incompatible revision of another specification; no such revision exists, and what a client honours of one is in its code, not in what it sends.

SKILL-010

SHOULD · binds the client

and SHOULD continue to support the Agent Skills specification as it existed prior to any incompatible change.

specification/stable/skills.mdx

Excluded. As SKILL-009: support for a format revision is a property of client code.

SKILL-011

SHOULD · binds the server

Servers SHOULD use the skill:// URI scheme

specification/stable/skills.mdx

Judged by skills.skill-scheme.

SKILL-012

MUST · binds the server

The final segment of <skill-path> MUST equal the skill’s name as declared in its SKILL.md frontmatter.

specification/stable/skills.mdx

Judged by skills.final-segment-is-name.

SKILL-013

MUST · binds the server

The final segment, being the skill name, MUST satisfy the Agent Skills specification’s naming rules.

specification/stable/skills.mdx

Judged by skills.name-follows-naming-rules.

SKILL-014

SHOULD · binds the server

The first segment occupies the URI authority component and SHOULD be a valid reg-name per RFC 3986.

specification/stable/skills.mdx

Judged by skills.authority-reg-name.

SKILL-015

SHOULD · binds the server

Any other prefix segments SHOULD be valid URI path segments.

specification/stable/skills.mdx

Judged by skills.prefix-segments.

SKILL-016

MUST NOT · binds the client

The authority component carries no special semantics under this extension, and clients MUST NOT attempt DNS or network resolution of it.

specification/stable/skills.mdx

Excluded. Name resolution happens off the MCP connection: a recording of the session carries none of the client’s DNS queries or other network traffic.

SKILL-017

MUST NOT · binds the client

A host MUST NOT conclude that a resource is a skill merely because its URI carries a particular scheme.

specification/stable/skills.mdx

Excluded. What a host concludes about a resource is internal. The only act on the wire it could lead to — reading the resource — is permitted whether or not the host takes it for a skill (the page: reading is transport, not a load).

SKILL-018

SHOULD · binds the server

mimeType SHOULD be text/markdown.

specification/stable/skills.mdx

Judged by skills.skill-md-mime-type.

SKILL-019

SHOULD · binds the server

name SHOULD be set from the name field of the SKILL.md YAML frontmatter.

specification/stable/skills.mdx

Judged by skills.skill-md-resource-name.

SKILL-020

SHOULD · binds the server

description SHOULD be set from the description field of the SKILL.md YAML frontmatter.

specification/stable/skills.mdx

Judged by skills.skill-md-resource-description.

SKILL-021

SHOULD · binds the client and server

When _meta keys are used for skill resources, implementations SHOULD use the io.modelcontextprotocol.skills/ reverse-domain prefix.

specification/stable/skills.mdx

Excluded. Binds the keys that expose frontmatter fields, and nothing on the wire says which _meta keys do: a key under another prefix on a skill resource may carry any other metadata, which the base protocol permits.

SKILL-022

MUST NOT · binds the client

A nested SKILL.md read this way is ordinary markdown, and hosts MUST NOT act on its frontmatter.

specification/stable/skills.mdx

Excluded. Binds what the host lets the model execute and which permissions it grants, decided inside the host; a tool the model then runs on the host is not an MCP message to this server.

SKILL-023

MUST · binds the client

Hosts MUST preserve both halves wherever a skill is recorded or addressed, including the registry, persisted approvals, the cache, and any tool or path through which the model reaches the skill.

specification/stable/skills.mdx

Excluded. Binds the host’s own records — its skill registry, persisted approvals and cache — which never cross the MCP connection; and a recording holds one session with one server, so the server identity that would tell two same-URI skills apart is never in question within it.

SKILL-024

MUST NOT · binds the client

Hosts MUST NOT key any of these on the uri alone.

specification/stable/skills.mdx

Excluded. Binds the host’s own records — its skill registry, persisted approvals and cache — which never cross the MCP connection; and a recording holds one session with one server, so the server identity that would tell two same-URI skills apart is never in question within it.

SKILL-025

MUST · binds the client

Any path at which a host materializes skill content, whether a cache directory or a virtual mount, MUST encode the server identity as well as the uri, so that same-URI skills from different servers land at distinct paths and the originating server is recoverable from the path.

specification/stable/skills.mdx

Excluded. Binds the host’s own records — its skill registry, persisted approvals and cache — which never cross the MCP connection; and a recording holds one session with one server, so the server identity that would tell two same-URI skills apart is never in question within it.

SKILL-026

MUST · binds the server

The frontmatter object MUST be identical in content to the frontmatter of the SKILL.md it describes.

specification/stable/skills.mdx

Judged by skills.frontmatter-identical.

SKILL-027

MUST · binds the server

The final <skill-path> segment of the entry’s uri MUST equal frontmatter.name, per Resource Mapping.

specification/stable/skills.mdx

Judged by skills.final-segment-is-name.

SKILL-028

SHOULD · binds the client and server

Implementations SHOULD ignore keys under this prefix that they do not recognize.

specification/stable/skills.mdx

Excluded. Ignoring a metadata key leaves nothing on the wire.

SKILL-029

SHOULD · binds the server

Within a server’s listing, names SHOULD be unique, but they are not guaranteed to be.

specification/stable/skills.mdx

Judged by skills.listing-names-unique.

SKILL-030

MUST NOT · binds the client

Hosts MUST NOT assume name uniqueness.

specification/stable/skills.mdx

Excluded. Binds how the host resolves skill names against other servers and its own filesystem skills: a recording holds one session with one server and none of the host’s other origins, and name resolution produces no message.

SKILL-031

MUST · binds the client

When two entries in one listing collide on name, hosts MUST disambiguate them (for example by their distinguishing path segments) rather than silently discarding or preferring one.

specification/stable/skills.mdx

Excluded. Binds how the host resolves skill names against other servers and its own filesystem skills: a recording holds one session with one server and none of the host’s other origins, and name resolution produces no message.

SKILL-032

MUST · binds the client

When skills from different origins collide on name, hosts MUST resolve the name within a per-origin namespace, identifying servers by a host-assigned label.

specification/stable/skills.mdx

Excluded. Binds how the host resolves skill names against other servers and its own filesystem skills: a recording holds one session with one server and none of the host’s other origins, and name resolution produces no message.

SKILL-033

MUST NOT · binds the client

An MCP-served skill MUST NOT silently shadow, or be silently substituted for, a same-named skill from any other origin, whether another server or the host’s own filesystem skills.

specification/stable/skills.mdx

Excluded. Binds how the host resolves skill names against other servers and its own filesystem skills: a recording holds one session with one server and none of the host’s other origins, and name resolution produces no message.

SKILL-034

MUST · binds the server

resources is REQUIRED on every skill entry and takes one of two forms: an array of SkillResource objects enumerating the skill’s files (SKILL.md and every supporting file), or the string "dynamic".

specification/stable/skills.mdx

Judged by skills.entry-resources-form.

SKILL-035

MUST · binds the server

When resources is an array, it MUST be complete. It lists every file of the skill, each exactly once, including an entry whose uri equals the skill’s top-level uri.

specification/stable/skills.mdx

Judged by skills.manifest-lists-skill-md.

SKILL-036

MUST · binds the server

Each uri MUST be the skill’s SKILL.md or a file within the skill’s directory.

specification/stable/skills.mdx

Judged by skills.manifest-within-skill.

SKILL-037

MUST · binds the server

Each entry MUST carry size, the length in bytes of the file’s raw content (the same bytes the digest covers).

specification/stable/skills.mdx

Judged by skills.manifest-entry-size.

SKILL-038

MUST · binds the server

When a skill’s content is generated dynamically, such that stable digests cannot be published, the server MUST set resources to the string "dynamic" instead of an array.

specification/stable/skills.mdx

Excluded. Its antecedent — content generated so that stable digests cannot be published — is the server’s own knowledge. An array of digests and the string "dynamic" are both valid forms, and SKILL-034 judges that one of them is present.

SKILL-039

MUST NOT · binds the client

An entry with no resources at all, or with any value other than an array or "dynamic", is invalid, and hosts MUST NOT load it.

specification/stable/skills.mdx

Excluded. Loading is the host placing content in the model’s context, which no message marks: a resources/read of the skill’s SKILL.md is transport, not a load (the page says so), so a read after an invalid entry convicts nothing. The server’s half — an entry without a valid resources — is SKILL-034.

SKILL-040

SHOULD · binds the server

Hosts MAY decline to load such skills, and server authors SHOULD expect that some hosts will.

specification/stable/skills.mdx

Excluded. An expectation held by a server’s author is not behaviour.

SKILL-041

MUST · binds the client

Hosts MUST support skills up to and including these limits

specification/stable/skills.mdx

Excluded. The capacity of the host: a recording shows what was served and read, not what the host could have accepted.

SKILL-042

SHOULD NOT · binds the server

Servers SHOULD NOT serve a skill that exceeds either limit.

specification/stable/skills.mdx

Judged by skills.within-limits.

SKILL-043

SHOULD · binds the client

A host that declines a skill on this basis SHOULD tell the user why rather than fail silently on a later read.

specification/stable/skills.mdx

Excluded. Binds what the host shows its user, which strategy rule 2 keeps out: none of it is a message.

SKILL-044

MUST · binds the server

A server declaring this extension MUST implement the skills/list method, which returns entries for the skills it serves.

specification/stable/skills.mdx

Judged by skills.list-implemented.

SKILL-045

MUST · binds the server

The resultType field MUST be set to "complete" on ListSkillsResult, as it is the standard result shape for the skills/list request.

specification/stable/skills.mdx

Judged by skills.list-result-complete.

SKILL-046

MUST NOT · binds the client

Hosts MUST NOT treat an empty or partial listing as proof that a server has no skills, and MUST support loading a skill given only its URI

specification/stable/skills.mdx

Excluded. How a host reasons from a listing, and whether it can load a skill by URI alone, are host capabilities; a session in which it never does either is conforming, and one in which it does looks like any skills/get.

SKILL-047

MUST · binds the server

ListSkillsResult extends CacheableResult, so ttlMs and cacheScope are REQUIRED, as they are on tools/list and resources/list, and carry the same semantics.

specification/stable/skills.mdx

Judged by skills.list-cacheable.

SKILL-048

MUST · binds the server

A server declaring this extension MUST implement the skills/get method, which returns the entry for a single skill named by the URI of its SKILL.md.

specification/stable/skills.mdx

Judged by skills.get-implemented.

SKILL-049

MUST · binds the server

The resultType field MUST be set to "complete" on GetSkillResult, as it is the standard result shape for the skills/get request.

specification/stable/skills.mdx

Judged by skills.get-result-complete.

SKILL-050

MUST · binds the client

params.uri MUST be the URI of a skill’s SKILL.md.

specification/stable/skills.mdx

Judged by skills.get-uri-names-skill-md.

SKILL-051

MUST · binds the server

If the URI does not identify a skill the server serves, the server MUST return error -32602 (Invalid params), the same code resources/read uses for unknown resources.

specification/stable/skills.mdx

Judged by skills.get-unknown-invalid-params.

SKILL-052

MUST · binds the server

A server MUST answer for every skill it serves, whether or not that skill appears in its skills/list result.

specification/stable/skills.mdx

Excluded. What a server serves is its own state at the moment of the call: a skill listed earlier and refused later may have been withdrawn in between, which the page allows (an entry is a point-in-time snapshot), so a refusal convicts nothing.

SKILL-053

MUST · binds the server

GetSkillResult extends CacheableResult, so ttlMs and cacheScope are REQUIRED, as they are on resources/read, and carry the same semantics.

specification/stable/skills.mdx

Judged by skills.get-cacheable.

SKILL-054

MUST NOT · binds the client

Hosts MUST NOT treat a resources/read of a SKILL.md that arrives by any other route as a load.

specification/stable/skills.mdx

Excluded. Whether a host treats a read as a load is internal: the page itself says a read is transport and a load is the host placing content in the model’s context, which no message marks.

SKILL-055

SHOULD · binds the client

Content obtained that way is ordinary resource content, and a host that returns it to the model SHOULD do so as it would any other resource read, not as a loaded skill.

specification/stable/skills.mdx

Excluded. Binds how the host treats skill content inside the model’s context — its trust, its provenance tags, its authority, how it is presented — which strategy rule 2 keeps out: none of it is a message to the server.

SKILL-056

MUST · binds the client

When a host retrieves a file listed in a skill’s resources, it MUST verify the content against that entry’s digest and size.

specification/stable/skills.mdx

Excluded. Verification runs inside the host: comparing bytes against a digest, a size or the entry’s frontmatter produces no message, and the page itself says its only wire trace is the skills/get or skills/list refresh after a failure, which looks like any other call.

SKILL-057

MUST NOT · binds the client

In all cases, hosts MUST NOT use the unverified content.

specification/stable/skills.mdx

Excluded. Verification runs inside the host: comparing bytes against a digest, a size or the entry’s frontmatter produces no message, and the page itself says its only wire trace is the skills/get or skills/list refresh after a failure, which looks like any other call.

SKILL-058

MUST · binds the client

While acting on a skill, a host MUST resolve reads of the skill’s files only to URIs listed in the held entry’s resources.

specification/stable/skills.mdx

Excluded. Binds reads made while the host is acting on a skill, and the acting window is host state: the same resources/read is permitted outside it (a generic resource read, a resource browser), and nothing on the wire marks which one a read was.

SKILL-059

MUST · binds the client

A host MUST treat a read of an unlisted file within the skill as a verification failure equivalent to a digest mismatch.

specification/stable/skills.mdx

Excluded. Verification runs inside the host: comparing bytes against a digest, a size or the entry’s frontmatter produces no message, and the page itself says its only wire trace is the skills/get or skills/list refresh after a failure, which looks like any other call.

SKILL-060

MUST · binds the client

After fetching a SKILL.md for which the host holds an entry (digest-verified when the entry’s resources is an array, unverifiable when it is "dynamic"), hosts MUST parse its YAML frontmatter and compare it field-by-field against the entry’s frontmatter.

specification/stable/skills.mdx

Excluded. Verification runs inside the host: comparing bytes against a digest, a size or the entry’s frontmatter produces no message, and the page itself says its only wire trace is the skills/get or skills/list refresh after a failure, which looks like any other call.

SKILL-061

MUST · binds the client

Any discrepancy MUST be treated as a verification failure equivalent to a digest mismatch, and the skill MUST NOT be loaded.

specification/stable/skills.mdx

Excluded. Verification runs inside the host: comparing bytes against a digest, a size or the entry’s frontmatter produces no message, and the page itself says its only wire trace is the skills/get or skills/list refresh after a failure, which looks like any other call.

SKILL-062

MUST NOT · binds the client

Hosts MUST NOT retrieve a skill’s files ahead of need, whether on connection, on listing, or at approval.

specification/stable/skills.mdx

Excluded. Need is the model’s or the user’s request to load or read, which no recording carries: a read right after a listing is prefetching or a load the user asked for, and the wire is the same. The official suite judges this clause in a scripted scenario in which nothing asked for a load (sep-2640-client-no-prefetch), which a recording of an ordinary session cannot reproduce.

SKILL-063

SHOULD · binds the client

Hosts SHOULD cache what they do retrieve.

specification/stable/skills.mdx

Excluded. Binds the host’s disk cache — where it lives, who may write it, how its bytes are checked and when they are removed — none of which crosses the MCP connection.

SKILL-064

MUST NOT · binds the client

Hosts MUST NOT treat a digest match as a security boundary.

specification/stable/skills.mdx

Excluded. A security policy inside the host; trusting or distrusting content produces no message.

SKILL-065

MUST · binds the server

The resultType field MUST be set to "complete" on ReadResourceDirectoryResult, as it is the standard result shape for the resources/directory/read request.

specification/stable/skills.mdx

Judged by skills.directory-result-complete.

SKILL-066

MUST · binds the server

If the URI does not exist, or exists but is not a directory resource, the server MUST return error -32602 (Invalid params).

specification/stable/skills.mdx

Judged by skills.directory-read-invalid-params.

SKILL-100

MUST · binds the server

ReadResourceDirectoryResult extends CacheableResult, so ttlMs and cacheScope are REQUIRED, as they are on resources/list, and carry the same semantics.

specification/stable/skills.mdx

Judged by skills.directory-cacheable.

SKILL-067

MUST · binds the server

A server that declares directoryRead MUST support the method for every directory within the skill namespaces it serves as individual files.

specification/stable/skills.mdx

Judged by skills.directory-read-every-directory.

SKILL-068

MUST NOT · binds the client

While acting on the skill under the held entry, the host MUST NOT read the newly listed child (an unlisted file is a verification failure) and MUST NOT surface it to the model as a file of the skill.

specification/stable/skills.mdx

Excluded. Binds reads made while the host is acting on a skill, and the acting window is host state: the same resources/read is permitted outside it (a generic resource read, a resource browser), and nothing on the wire marks which one a read was.

SKILL-069

SHOULD · binds the client

Hosts SHOULD present this sequence as a skill that has changed and needs re-approval, rather than as a read error.

specification/stable/skills.mdx

Excluded. Binds what the host shows its user, which strategy rule 2 keeps out: none of it is a message.

SKILL-070

MUST NOT · binds the client

Hosts MUST NOT treat the directory result as extending the manifest.

specification/stable/skills.mdx

Excluded. Binds reads made while the host is acting on a skill, and the acting window is host state: the same resources/read is permitted outside it (a generic resource read, a resource browser), and nothing on the wire marks which one a read was.

SKILL-071

MUST · binds the server

Servers MUST return standard JSON-RPC errors for the following protocol error cases: skills/get with a uri that does not identify a skill the server serves: -32602 (Invalid params).; resources/directory/read with a uri that does not exist or is not a directory resource: -32602 (Invalid params).; resources/read of a skill file the server does not serve: -32602 (Invalid params), per the base Resources specification.; Internal errors: -32603 (Internal error).

specification/stable/skills.mdx

Judged by skills.get-unknown-invalid-params, skills.directory-read-invalid-params.

SKILL-072

SHOULD · binds the server

Servers SHOULD provide informative error messages to describe the cause of errors.

specification/stable/skills.mdx

Excluded. Informativeness is a judgment about prose; any non-empty message passes a mechanical test of it.

SKILL-073

MUST · binds the client

Hosts MUST treat MCP-served skill content as untrusted model input, subject to the same prompt-injection defenses applied to any server-provided text.

specification/stable/skills.mdx

Excluded. Binds how the host treats skill content inside the model’s context — its trust, its provenance tags, its authority, how it is presented — which strategy rule 2 keeps out: none of it is a message to the server.

SKILL-074

MUST · binds the client

Hosts MUST treat MCP-served skills as a higher-risk surface than remote tool invocation.

specification/stable/skills.mdx

Excluded. Binds how the host treats skill content inside the model’s context — its trust, its provenance tags, its authority, how it is presented — which strategy rule 2 keeps out: none of it is a message to the server.

SKILL-075

MUST · binds the client

Hosts MUST tag MCP-served skill content with its originating server identity at the point it enters model context.

specification/stable/skills.mdx

Excluded. Binds how the host treats skill content inside the model’s context — its trust, its provenance tags, its authority, how it is presented — which strategy rule 2 keeps out: none of it is a message to the server.

SKILL-076

MUST NOT · binds the client

Hosts MUST NOT present an MCP-served skill to the model as indistinguishable from a local filesystem skill.

specification/stable/skills.mdx

Excluded. Binds how the host treats skill content inside the model’s context — its trust, its provenance tags, its authority, how it is presented — which strategy rule 2 keeps out: none of it is a message to the server.

SKILL-077

MUST NOT · binds the client

Hosts MUST NOT allow MCP-served skill content to cause host-side code execution without explicit per-skill user approval.

specification/stable/skills.mdx

Excluded. Binds what the host lets the model execute and which permissions it grants, decided inside the host; a tool the model then runs on the host is not an MCP message to this server.

SKILL-078

MUST · binds the client

Hosts MUST ignore or approval-gate (a).

specification/stable/skills.mdx

Excluded. Binds what the host lets the model execute and which permissions it grants, decided inside the host; a tool the model then runs on the host is not an MCP message to this server.

SKILL-079

MUST · binds the client

Hosts MUST apply the same approval gate to code-execution tool calls issued while the model is acting on an MCP-served skill.

specification/stable/skills.mdx

Excluded. Binds what the host lets the model execute and which permissions it grants, decided inside the host; a tool the model then runs on the host is not an MCP message to this server.

SKILL-080

MUST · binds the client

Hosts MUST bind such reads to the skill’s originating server.

specification/stable/skills.mdx

Excluded. Concerns reads across two servers, or the labels the host gives them: a recording holds one session with one server, so neither the second server nor the host’s labels are in it.

SKILL-081

MUST NOT · binds the client

A skill served by server A MUST NOT cause a resources/read against server B.

specification/stable/skills.mdx

Excluded. Concerns reads across two servers, or the labels the host gives them: a recording holds one session with one server, so neither the second server nor the host’s labels are in it.

SKILL-082

MUST · binds the client

Hosts MUST identify servers by a host-assigned label, not the server’s self-reported serverInfo.name.

specification/stable/skills.mdx

Excluded. Concerns reads across two servers, or the labels the host gives them: a recording holds one session with one server, so neither the second server nor the host’s labels are in it.

SKILL-083

MUST · binds the client

Any cross-origin read MUST be gated behind explicit per-call user approval naming both servers.

specification/stable/skills.mdx

Excluded. Concerns reads across two servers, or the labels the host gives them: a recording holds one session with one server, so neither the second server nor the host’s labels are in it.

SKILL-084

MUST · binds the client

Hosts MUST resolve skill names within a per-origin namespace, identifying servers by a host-assigned label.

specification/stable/skills.mdx

Excluded. Binds how the host resolves skill names against other servers and its own filesystem skills: a recording holds one session with one server and none of the host’s other origins, and name resolution produces no message.

SKILL-085

MUST NOT · binds the client

Hosts MUST NOT let an MCP-served skill silently shadow, replace, or intercept invocations of a same-named skill from any other origin, including the host’s filesystem skills.

specification/stable/skills.mdx

Excluded. Binds how the host resolves skill names against other servers and its own filesystem skills: a recording holds one session with one server and none of the host’s other origins, and name resolution produces no message.

SKILL-086

SHOULD · binds the client

Hosts SHOULD surface collisions to the user.

specification/stable/skills.mdx

Excluded. Binds what the host shows its user, which strategy rule 2 keeps out: none of it is a message.

SKILL-087

MUST NOT · binds the client

Hosts MUST NOT honor frontmatter fields that widen the model’s tool or filesystem permissions when the skill arrives over MCP.

specification/stable/skills.mdx

Excluded. Binds what the host lets the model execute and which permissions it grants, decided inside the host; a tool the model then runs on the host is not an MCP message to this server.

SKILL-088

MUST · binds the client

In particular, the Agent Skills allowed-tools field MUST be ignored for MCP-origin skills unless the user has explicitly approved that grant for that skill.

specification/stable/skills.mdx

Excluded. Binds what the host lets the model execute and which permissions it grants, decided inside the host; a tool the model then runs on the host is not an MCP message to this server.

SKILL-089

MUST NOT · binds the client

Hosts MUST NOT treat skill resources as higher-authority than other context.

specification/stable/skills.mdx

Excluded. Binds how the host treats skill content inside the model’s context — its trust, its provenance tags, its authority, how it is presented — which strategy rule 2 keeps out: none of it is a message to the server.

SKILL-090

SHOULD · binds the client

Hosts SHOULD indicate which server a skill originates from when presenting it.

specification/stable/skills.mdx

Excluded. Binds what the host shows its user, which strategy rule 2 keeps out: none of it is a message.

SKILL-091

SHOULD · binds the client

Hosts SHOULD let users inspect a skill’s content before it is loaded into model context.

specification/stable/skills.mdx

Excluded. Binds what the host shows its user, which strategy rule 2 keeps out: none of it is a message.

SKILL-092

MUST · binds the client

When a host persists any per-skill user approval, it MUST be bound to the entry’s resources set (every uri and digest) observed at the moment of approval.

specification/stable/skills.mdx

Excluded. Approval, its persistence and its revocation are host state and user interaction; a re-prompt is not a message, so a recording cannot tell a host that revoked an approval from one that never had one.

SKILL-093

MUST · binds the client

If a subsequent entry for that skill, from skills/list or skills/get, advertises a different set, whether a file was rotated, added, or removed, the host MUST treat the prior approval as revoked and re-prompt before loading or executing.

specification/stable/skills.mdx

Excluded. Approval, its persistence and its revocation are host state and user interaction; a re-prompt is not a message, so a recording cannot tell a host that revoked an approval from one that never had one.

SKILL-094

MUST NOT · binds the client

Hosts MAY decline to load it, and MUST NOT treat a persisted approval as covering whatever content the server currently serves.

specification/stable/skills.mdx

Excluded. Approval, its persistence and its revocation are host state and user interaction; a re-prompt is not a message, so a recording cannot tell a host that revoked an approval from one that never had one.

SKILL-095

SHOULD · binds the client

Hosts SHOULD cache verified skill content locally, populated on demand as files are read rather than in bulk.

specification/stable/skills.mdx

Excluded. Binds the host’s disk cache — where it lives, who may write it, how its bytes are checked and when they are removed — none of which crosses the MCP connection.

SKILL-096

MUST · binds the client

Hosts that cache skill content on disk MUST do one of the following for every file served from the cache.

specification/stable/skills.mdx

Excluded. Binds the host’s disk cache — where it lives, who may write it, how its bytes are checked and when they are removed — none of which crosses the MCP connection.

SKILL-097

MUST · binds the client

Hosts that cache MCP-served skill content on disk MUST do so in a location excluded from every filesystem-skill discovery path.

specification/stable/skills.mdx

Excluded. Binds the host’s disk cache — where it lives, who may write it, how its bytes are checked and when they are removed — none of which crosses the MCP connection.

SKILL-098

MUST · binds the client

Hosts MUST treat content loaded from that location as having arrived over MCP for all purposes of the no-implicit-local-execution rule above, including after host restart and after the originating server is disconnected.

specification/stable/skills.mdx

Excluded. Binds the host’s disk cache — where it lives, who may write it, how its bytes are checked and when they are removed — none of which crosses the MCP connection.

SKILL-099

SHOULD · binds the client

Hosts SHOULD remove a server’s cached skill content when the user removes that server.

specification/stable/skills.mdx

Excluded. Binds the host’s disk cache — where it lives, who may write it, how its bytes are checked and when they are removed — none of which crosses the MCP connection.

Glossary

The terms this book and the tools’ output use, in the sense they use them.

Agreement check. The project’s calibration: on every CI run the official conformance suite drives the reference server and host, the sessions are recorded, and the validator’s verdicts on them are compared with the suite’s. A disagreement nobody has explained fails the build.

Capture. Recording a session as a trace. mcp-trace-capture does it for any implementation; the reference server and host can also record their own sessions.

Capability gate. The capability a clause depends on, such as server.tools. A gated clause applies only to a session that negotiated that capability; in a session that did not, it is not applicable.

Check. One mechanical test in the validator, with an ID such as base.request-id-unique. A clause is judged by one or more checks. Every check is shown to fail on at least one committed violation trace.

Clause (or requirement). One normative sentence of the specification — a MUST, MUST NOT, SHOULD or SHOULD NOT — entered in the registry under a stable ID such as BASE-003. The book’s reference pages list every one. A MAY permits rather than binds, so no trace can break one and the built-in registries enter none: requirements --level may is accepted, for a custom --registry that has some, and lists nothing otherwise.

Corpus. The committed traces the validator is tested against: conforming sessions, one violating trace per check, and recordings of real sessions, each with a golden report. See the corpus chapter.

Event. One line of a trace: a protocol message, an HTTP exchange’s metadata, or a transport opening or closing. See the trace format.

Excluded. A clause no recorded trace can judge — it is about an implementation’s internals, its user interface, or something that leaves nothing on the wire. The registry gives the reason for each.

Finding. One concrete violation, addressed to a clause and, where possible, to the event (seq) that broke it.

Golden report. The committed expected report for a corpus trace. A change in the validator’s output on any corpus trace is a test failure until the golden is regenerated and the difference reviewed.

Level. A clause’s RFC 2119 strength. A broken MUST or MUST NOT is a fail; an unfollowed SHOULD or SHOULD NOT is a warn, which fails the run only under --strict.

Not applicable. The outcome of a capability-gated clause in a session that never negotiated the capability.

Not observed. The outcome of a clause whose subject never appeared in the session — nothing was paginated, no error was returned. It is not a pass: the session had no opportunity to break it.

Registry. The specification as data: every clause of a revision with its ID, level, the party it binds, its verbatim quote and either its checks or its exclusion. The registry set holds one registry per revision.

Revision. A dated version of the MCP specification: 2026-07-28 (current) or 2025-11-25. The validator judges the revision a session declares, unless --revision names one.

Reference server, reference host. mcp-everything-server and mcp-reference-host: an MCP server and client built on the official Rust SDK, which pass the official suite and are used to calibrate the validator.

seq. An event’s position in its trace, assigned by the recorder when it observed the event. It is the only ordering the validator uses.

Session. One conversation between a client and a server, from opening to closing. The validator judges each trace as one session.

Trace. A recorded session in the trace format: JSON Lines, one event per line.

Verdict. A report’s conclusion: pass, pass with warnings, fail, or unsupported (the build cannot judge a clause its registry names — a mismatched installation). A run over several traces that could not judge one of them — unreadable, malformed, of a revision this build does not know, or judging nothing — reads incomplete, whatever the others say. The exit status carries it.