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.xon 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 unreleasedmainbranch 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
| Crate | What it is |
|---|---|
mcp-conformance-core | The spec as data: the requirement registry, the trace schema, and RFC 8785 canonical JSON. Serde-only; no protocol SDK. |
mcp-trace-capture | The recorder: a stdio wrapper and HTTP reverse proxy that write a validator-ready trace of any MCP session, linking no SDK. |
mcp-trace-validator | The deterministic judgment engine and its CLI (human / JSON / JUnit / SARIF reports, documented exit codes). |
mcp-everything-server | A 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-host | A 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-25and2026-07-28together, 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-28and2025-11-25under 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:
| Outcome | Meaning |
|---|---|
pass | The session carried traffic the clause binds to, and every check of it found nothing wrong. |
fail | A MUST or MUST NOT clause was broken. Each finding says where. |
warn | A SHOULD or SHOULD NOT clause was not followed. Warnings do not fail the run unless you pass --strict. |
not observed | The 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 applicable | The clause applies only after a capability was negotiated, and this session did not negotiate it. |
excluded | The 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. |
unsupported | This 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
-oan 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 ismcp-trace.jsonlin 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-conformanceis onmain, not yet released (ADR-0021). Build it from a checkout withcargo 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
FAILUREor aWARNINGfails it. - The clause report — one validator report per recorded session, as
mcp-trace-validator validateprints 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-conformanceis onmain, not yet released (ADR-0021). Build it from a checkout withcargo 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 observedtopass. That is progress, not news, andwatchsays nothing. - A verdict about the future. Right after the
initializeresponse, the trace so far has noinitializednotification, soLIFE-003fails — 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 atools/callor the prompt of aprompts/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.versionwhen not2.0;network.transport(pipefor stdio,tcpfor HTTP) withnetwork.protocol.nameandnetwork.protocol.versionfor HTTP;server.addressandserver.port(the upstream) for a client’s request over HTTP;gen_ai.tool.namewithgen_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.typeandrpc.response.status_codeto the JSON-RPC code and the span status toERROR, 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 withisError: truesetserror.type = tool_error; a request the session ended before answering,unanswered. - Findings are span events named
mcp_conformance.finding, withmcp_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 itsseq, 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
| Outcome | What to do |
|---|---|
fail | A MUST or MUST NOT was broken. Triage each finding (below). |
warn | A SHOULD or SHOULD NOT was not followed. Decide whether the deviation is deliberate; --strict makes warnings fail the run. |
pass | Nothing. The session carried traffic the clause binds to, and no check found a fault in it. |
not observed | Nothing 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 applicable | The clause depends on a capability this session did not negotiate. A session that negotiates it judges the clause. |
excluded | No 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. |
unsupported | The 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
- Find the event.
seq 3is the event whoseseqfield is3— in a trace written bymcp-trace-capture, the line afterseq 2. Many findings name a second event (at seq 0): the earlier one the offending event conflicts with.--format sarifputs each finding on its trace line in a SARIF viewer. - 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 atseq(JSON carries it as the finding’sparty, and each row’sactoris 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-25prints the clause in full. When the other side of the session is not yours to fix — the official suite’s client, say —--party servercounts only the server’s findings toward the verdict and exit status, and the verdict line says(server findings only). - Read the clause where it is published. The
spec:line is the registry’s verbatim quote, andsee: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. - 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;--revisionnames 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-hostdrive it with--sweep(and--cancel,--subscribe,--log-levelat2026-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.
| Term | Finds the recordings that… |
|---|---|
method:tools/call | sent or received that method |
tool:echo, prompt:greeting, resource:test://static-text | called, got, read or listed it |
session:3f2a… | carried that Mcp-Session-Id (or are that library entry) |
code:-32602 | hold a JSON-RPC error with that code |
req:BASE-003 | have a finding against that requirement |
server:everything, tag:nightly | were recorded against that server, or carry that tag |
verdict:fail | have 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:
| Action | In the page | From the command line |
|---|---|---|
| Import a trace, an mcpsnoop capture or an MCP Inspector export | Import… | library import FILE [--from mcpsnoop|inspector|trace] [--transport stdio|streamable-http] |
| Tag | Edit tags | library tag ID --tag T… |
| Expire old recordings | Retention… | library prune --retain-days N [--apply] |
| Delete | Delete | library delete ID… |
| Export (tar of traces and sidecars; each recording once, at most 100 recordings and 192 MiB) | Export | library export ID… -o FILE |
| Redact | Redact… | 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
HostorMcp-Session-Id, response headers butContent-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/inspector2.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"
| Input | Meaning |
|---|---|
capture | stdio or http; empty (the default) records nothing |
server | A 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 |
upstream | http: 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) |
client | A 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-dir | Where 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 want | Use |
|---|---|
| Fail on any broken MUST | the 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 report | fail-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
-
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. -
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.txtThe same command with the old binary gives
findings-old.txt;diffthe two. A row in one and not the other is a verdict change the table explains. The report’swire_schemamember (onmainsince0.6.0) is judged beside the rows; compare itsoutcometoo. -
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, orcargo binstall [email protected]for the attested prebuilt binary;- the GitHub Action at a tag or commit (
uses: tomtom215/[email protected]), or withversion: 0.6.0to 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 after0.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 says | Cause and fix |
|---|---|
the document begins with a UTF-8 byte-order mark | An 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 Lines | A pretty-printed file or a JSON array. The message names the jq command that converts it. |
blank line | JSON 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 limit | A 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 seq | Two 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:
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-validatordoes not depend onrmcp. 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:
messageevents hold the JSON-RPC payload (as parsed JSON: every value, not its whitespace, member order, or the spelling of its numbers);httpevents record the conformance-relevant headers, a response’s status, and a client request’smethod— Streamable HTTP binds different obligations toPOST,GET, andDELETE, so a clause addressed to one of them is judged only where the recording says which it was; andlifecycleevents mark the transport’s moments:transport-open(it became usable),transport-close(it closed in an orderly way) andtransport-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-25 | 2026-07-28 | |
|---|---|---|
| Registry entries | 348 | 501 |
| Judged by a named check | 102 | 181 |
| Carrying a documented exclusion | 246 | 320 |
| Shipped in every build | yes | yes |
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 readspassthenabsent.DISC-001— theserver/discoverprobe that replaces the handshake — is a clause the newer revision adds, and this session carried nothing it binds to, so it readsnot-observedrather than counting as a pass.BASE-030— every request must carry anio.modelcontextprotocol/clientCapabilities_metamember — is a clause the newer revision adds that this session breaks. It readsfail, and it is the migration work this trace has actually discovered.BASE-045— the narrowed request-ID rule — readspass, 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 everycargo 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:
| Reads | Means |
|---|---|
absent | The clause does not exist at that revision. |
excluded | It exists, but no recorded trace can judge it — the reason is stated per entry. |
not-applicable | It exists and is judgeable, but it is gated on a capability this session never negotiated. |
not-observed | It 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’s2026-07-28scenarios 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 verdictpass, at the revision each declares:validatewith no flags passes all of them but one, the session whose server answersinitializewith2024-11-05(stdio-server-negotiates-2024-11-05), whichvalidaterefuses 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 revision2026-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/— the2025-11-25counterpart: the official SDKs’ conformance servers recorded byinterop/matrix.sh, judged against that revision’s registry, likewise pinned without an asserted verdict. Their2026-07-28sessions sit indraft/captured/beside the others. See The official SDKs’ conformance servers below.golden/— the byte-pinned expected report for every trace, withgolden/draft/holding the2026-07-28corpus’s. Regenerate only viacargo xtask blessand 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-25 | 2026-07-28 | |
|---|---|---|
| Goldens | 125 | 238 |
| Excluded rows the ledger holds | 246 | 320 |
| Distinct not-observed sets across them | 86 | 151 |
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/
| Trace | Exercises |
|---|---|
http-session.jsonl | Streamable 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.jsonl | Every 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.jsonl | Every 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.jsonl | Handshake plus ping, tools/list, tools/call over stdio |
stdio-minimal-init.jsonl | Smallest conformant session: the three-message handshake |
stdio-request-unanswered-at-close.jsonl | A 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.jsonl | The 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.jsonl | A 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.jsonl | Two 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.jsonl | Progress 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.jsonl | A 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.jsonl | Cancellation 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.jsonl | A 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.jsonl | A 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).
| Trace | Falsifies |
|---|---|
base-001-request-id-boolean.jsonl | BASE-001 |
base-002-request-id-null.jsonl | BASE-002 |
base-003-request-id-reuse.jsonl | BASE-003 |
base-004-request-answered-twice-cross-flavor.jsonl | BASE-004 (the second of an error+result double-answer) |
base-004-result-unknown-id.jsonl | BASE-004 |
base-005-notification-with-id.jsonl | BASE-005 |
base-006-error-missing-message.jsonl | BASE-006 |
base-007-error-code-float.jsonl | BASE-007 |
base-008-jsonrpc-version.jsonl | BASE-008 |
base-009-error-unknown-id.jsonl | BASE-009 |
base-010-response-without-result.jsonl | BASE-010 |
base-019-meta-key-bad-prefix.jsonl | BASE-019, BASE-020 (shared base.meta-key-format check) |
comp-001-capability-undeclared.jsonl | COMP-001 |
life-001-first-message-not-initialize.jsonl | LIFE-001 |
life-002-initialize-missing-protocolversion.jsonl | LIFE-002 |
life-003-missing-initialized.jsonl | LIFE-003 |
life-004-client-request-before-init-response.jsonl | LIFE-004 |
life-005-server-request-before-initialized.jsonl | LIFE-005 |
life-006-result-version-invalid.jsonl | LIFE-006 |
life-007-initialize-protocolversion-not-string.jsonl | LIFE-007 |
life-009-undeclared-capability-use.jsonl | LIFE-009 |
life-010-initialize-result-missing-capabilities.jsonl | LIFE-010 |
log-001-capability-undeclared.jsonl | LOG-001 |
page-002-cursor-never-issued.jsonl | PAGE-002 |
page-002-fabricated-cursor-rejected.jsonl | PAGE-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.jsonl | PROM-001 |
prom-003-image-data-invalid.jsonl | PROM-003 (and, since 2026-10-24, the wire schema’s byte format on the same message) |
prom-004-audio-data-invalid.jsonl | PROM-004 (and, since 2026-10-24, the wire schema’s byte format on the same message) |
prom-005-embedded-resource-malformed.jsonl | PROM-005 |
prom-008-required-argument-unvalidated.jsonl | PROM-008 |
res-001-capability-undeclared.jsonl | RES-001 |
res-004-uri-bad-scheme.jsonl | RES-004 |
res-006-blob-not-base64.jsonl | RES-006 (and, since 2026-10-24, the wire schema’s byte format on the same message) |
tool-001-capability-undeclared.jsonl | TOOL-001 |
tool-003-input-schema-null.jsonl | TOOL-003 |
tool-005-name-length.jsonl | TOOL-005 |
tool-006-name-charset.jsonl | TOOL-006 |
tool-008-name-duplicate.jsonl | TOOL-008 |
tool-009-embedded-resource-without-capability.jsonl | TOOL-009 |
tool-010-structured-without-text.jsonl | TOOL-010 |
tool-011-output-schema-no-structured-result.jsonl | TOOL-011 |
tran-004-stdout-invalid-message.jsonl | TRAN-004 |
tran-005-stdin-invalid-message.jsonl | TRAN-005 |
tran-011-session-id-invisible-ascii.jsonl | TRAN-011 |
tran-013-session-id-not-echoed.jsonl | TRAN-013 |
tran-017-protocol-version-header-missing.jsonl | TRAN-017 |
tran-018-protocol-version-mismatched.jsonl | TRAN-018 |
tran-018-declined-version-header-refused-400.jsonl | TRAN-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.jsonl | TRAN-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.jsonl | TRAN-025 (a POST with no Accept header at all) |
tran-025-accept-header-json-missing.jsonl | TRAN-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.jsonl | TRAN-039 (a standalone-stream GET offering application/json only) |
tran-049-message-not-posted.jsonl | TRAN-049 (a ping sent by PUT after a clean handshake) |
tran-026-http-post-batch.jsonl | TRAN-026 (a batch array POSTed after a clean handshake) |
tran-027-notification-answered-200.jsonl | TRAN-027 (the notifications/initialized POST answered 200 instead of 202 Accepted) |
tran-029-content-type-unexpected.jsonl | TRAN-029 (an initialize request answered 200 with Content-Type: text/html) |
tran-029-error-without-content-type.jsonl | TRAN-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.jsonl | TRAN-040 (a standalone-stream GET answered 200 with application/json rather than text/event-stream or 405) |
base-017-input-schema-invalid.jsonl | BASE-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.jsonl | BASE-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.jsonl | TOOL-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.jsonl | Wire schema: a resources/list result whose resource has no uri, which Resource requires |
wire-schema-log-level-not-in-enum.jsonl | Wire schema: a notifications/message at level chatty, outside LoggingLevel |
wire-schema-content-block-unknown-type.jsonl | Wire schema: a tools/call result whose content block has type: "txt", matching no ContentBlock member |
wire-schema-tool-image-data-not-base64.jsonl | Wire 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.jsonl | ELIC-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.jsonl | ELIC-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.jsonl | ELIC-007: a form elicitation with no message. The wire schema rejects the same request |
elic-009-form-without-schema.jsonl | ELIC-009: a form elicitation with no requestedSchema; the wire schema too |
elic-011-url-without-elicitation-id.jsonl | ELIC-011: a URL mode elicitation with no elicitationId, which this revision’s table requires; the wire schema too |
elic-012-url-without-scheme.jsonl | ELIC-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.jsonl | ELIC-014: a completion notification naming e-99 after the session’s only URL elicitation established e-42 |
elic-018-url-required-error-without-list.jsonl | ELIC-018: a -32042 whose data lists no elicitations; the wire schema too |
elic-019-url-required-error-form-mode.jsonl | ELIC-019: a -32042 listing a form mode elicitation; the wire schema too |
elic-021-undeclared-mode-accepted.jsonl | ELIC-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.jsonl | ELIC-049, a warning: accept with {"city": 75001} against a schema typing city as a string |
samp-004-tools-without-sampling-tools.jsonl | SAMP-004: tools sent to a client declaring plain sampling (LIFE-009 reads the same sub-capability) |
samp-005-answered-without-capability.jsonl | SAMP-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.jsonl | SAMP-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.jsonl | SAMP-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.jsonl | SAMP-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.jsonl | SAMP-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.jsonl | SAMP-010: toolChoice required answered with text and no tool use |
samp-011-none-tool-choice-ignored.jsonl | SAMP-011: toolChoice none answered with a tool use |
samp-012-malformed-tool-messages-answered.jsonl | SAMP-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.jsonl | ROOT-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.jsonl | ROOT-003: a root listed as https://example.com/repos/trips |
root-004-unsupported-answered-internal-error.jsonl | ROOT-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.jsonl | ROOT-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.jsonl | AUTH-014, a warning: the 401 challenge carries resource_metadata but no scope |
auth-045-invalid-token-answered-403.jsonl | AUTH-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.jsonl | AUTH-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.jsonl | AUTH-050, a warning: a 403 insufficient_scope challenge naming its scopes but no resource_metadata |
auth-051-insufficient-scope-names-no-scope.jsonl | AUTH-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.jsonl | PING-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.jsonl | PROG-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.jsonl | PROG-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.jsonl | PROG-011: progress 50 then 10 for one token |
prog-013-progress-token-never-issued.jsonl | PROG-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.jsonl | PROG-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.jsonl | PROG-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.jsonl | PROG-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.jsonl | CANC-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.jsonl | CANC-019: the client cancels its own initialize request before the server answers it |
canc-020-task-cancelled-by-notification.jsonl | CANC-020: notifications/cancelled naming the tools/call a CreateTaskResult answered; a task is cancelled with tasks/cancel |
canc-021-response-after-cancellation-read.jsonl | CANC-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.
| Field | official-suite-2026-07-28-scenarios.jsonl | official-suite-2026-07-28-stateless.jsonl |
|---|---|---|
| Client | The 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 |
| Server | mcp-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 content | mcp-everything-server --protocol-version 2026-07-28, its stateless mode, same build |
| Recorded by | mcp-everything-server’s tap, during cargo xtask draft-readiness, 2026-10-13 | same run, second leg |
| Contents | 99 events / 24 POST exchanges | 99 events / 24 POST exchanges |
| Our verdict | 70 pass, 0 fail, 1 warn, 110 not observed, 320 excluded | 71 pass, 0 fail, 0 warn, 110 not observed, 320 excluded |
| The official runner’s verdict, that run | 42 passing / 0 failing | 42 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
| Field | reference-host-2026-07-28-stdio.jsonl |
|---|---|
| Client | This workspace’s mcp-reference-host, on rmcp’s stateless client lifecycle |
| Server | mcp-everything-server --transport stdio --protocol-version 2026-07-28 |
| Recorded by | The host’s own --trace-dir capture, during cargo xtask draft-capture, 2026-10-13 |
| Contents | server/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 verdict | 109 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
| Field | reference-host-2026-07-28-http.jsonl |
|---|---|
| Client | The same mcp-reference-host, driving the identical session over Streamable HTTP |
| Server | mcp-everything-server --transport http --protocol-version 2026-07-28 |
| Recorded by | The server’s tap, during cargo xtask draft-capture, 2026-10-13 |
| Contents | 195 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 verdict | 125 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
| Field | probe-2026-07-28-http.jsonl |
|---|---|
| Client | mcp-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 |
| Server | mcp-everything-server --transport http --protocol-version 2026-07-28 |
| Recorded by | The server’s tap, during cargo xtask draft-capture, 2026-10-13 |
| Contents | A _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 verdict | Judged 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.
| Capture | Judged | pass | fail | warn | Not observed |
|---|---|---|---|---|---|
official-suite-2026-07-28-scenarios | 71 | 70 | 0 | 1 | 110 |
official-suite-2026-07-28-stateless | 71 | 71 | 0 | 0 | 110 |
probe-2026-07-28-http | 85 | 73 | 11 | 1 | 96 |
reference-host-2026-07-28-http | 125 | 125 | 0 | 0 | 56 |
reference-host-2026-07-28-stdio | 109 | 109 | 0 | 0 | 72 |
sdk-go-2026-07-28 | 83 | 78 | 4 | 1 | 98 |
sdk-python-2026-07-28 | 65 | 65 | 0 | 0 | 116 |
sdk-rust-2026-07-28 | 74 | 73 | 0 | 1 | 107 |
sdk-typescript-v2-2026-07-28 | 72 | 72 | 0 | 0 | 109 |
| Union | 148 | 33 |
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.
| Field | Value |
|---|---|
| Servers | TypeScript 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 |
| Clients | 2025-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 by | mcp-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-verified | Weekly: 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 |
| Trace | Session |
|---|---|
sdk-typescript-2025-11-25.jsonl | The 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.jsonl | The TypeScript v2 server, the same session, answered through its legacy (handshake) path |
sdk-python-2025-11-25.jsonl | The Python server, the same session |
sdk-go-2025-11-25.jsonl | The 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.jsonl | The Rust server, the same session |
sdk-typescript-v2-2026-07-28.jsonl | The 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.jsonl | The Python server at 2026-07-28: server/discover, then tools, resources and prompts with the per-request _meta envelope |
sdk-go-2026-07-28.jsonl | The 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.jsonl | The 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.
| Trace | Exercises |
|---|---|
stateless-session.jsonl | Conformant 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.jsonl | Two 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.jsonl | A 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.jsonl | The 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.jsonl | good/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.jsonl | A 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.jsonl | A 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.jsonl | A 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.jsonl | A 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.jsonl | A 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.jsonl | Request _meta omits io.modelcontextprotocol/clientCapabilities (BASE-030) |
base-030-malformed-request-id-reused-correctly.jsonl | A 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.jsonl | Over 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.jsonl | A 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.jsonl | The 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.jsonl | A 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.jsonl | Server answers a _meta-incomplete request with a result instead of -32602 (BASE-031) |
base-031-reused-id-malformed-answered.jsonl | The 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.jsonl | A 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.jsonl | Server returns input_required asking for elicitation/create the request never declared (BASE-034) |
base-034-input-request-for-undeclared-mode.jsonl | The 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.jsonl | A 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.jsonl | Notification on a subscriptions/listen stream with no io.modelcontextprotocol/subscriptionId (BASE-039) |
base-039-subscription-notification-mistagged.jsonl | A 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.jsonl | traceparent that is not W3C Trace Context shaped (BASE-040) |
base-040-traceparent-version-ff.jsonl | traceparent 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.jsonl | Request 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.jsonl | Result omits the resultType SEP-2322 requires (BASE-048) |
base-052-null-id-error-then-id-reused.jsonl | A 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.jsonl | Error 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.jsonl | Error code -32055: inside the MCP-reserved sub-range but undefined (BASE-057) |
base-058-withdrawn-error-code.jsonl | Error code -32002, withdrawn by this revision (BASE-058) |
base-060-app-code-in-reserved-range.jsonl | Application-defined -32500 placed inside the JSON-RPC reserved range (BASE-060) |
streamable-http-session.jsonl | Conformant 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.jsonl | Two 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.jsonl | Two 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.jsonl | A 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.jsonl | A 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.jsonl | A 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.jsonl | A 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.jsonl | The 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.jsonl | A 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.jsonl | A 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.jsonl | Progress 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.jsonl | Per-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.jsonl | The 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.jsonl | The 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.jsonl | POST carries neither Mcp-Method nor Mcp-Name (TRAN-058) |
tran-058-mcp-name-mismatch-refused-without-id.jsonl | A 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.jsonl | Client 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.jsonl | A notifications/cancelled POST answered 200 instead of 202 Accepted (TRAN-061) |
tran-066-independent-server-request.jsonl | Server 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.jsonl | SSE response omits X-Accel-Buffering: no (TRAN-068, SHOULD → warn) |
tran-068-sse-media-type-mixed-case.jsonl | The 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.jsonl | Server 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.jsonl | POST request without MCP-Protocol-Version (TRAN-071) |
tran-072-protocol-version-header-mismatched.jsonl | Header 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.jsonl | The 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.jsonl | Header 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.jsonl | The 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.jsonl | The 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.jsonl | The 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.jsonl | A 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.jsonl | The 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.jsonl | Non-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.jsonl | tools/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.jsonl | One 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.jsonl | A plain value that matches the sentinel pattern, carried verbatim rather than encoded (TRAN-092) |
tran-096-invalid-param-header-accepted.jsonl | A 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.jsonl | The 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.jsonl | Mcp-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.jsonl | Mcp-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.jsonl | The 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.jsonl | HeaderMismatch returned with HTTP 500 rather than 400 (TRAN-098, TRAN-102 — one rule stated in two sections) |
tran-132-null-parameter-header-sent.jsonl | A 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.jsonl | The 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.jsonl | The 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.jsonl | region 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.jsonl | A 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.jsonl | Mcp-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.jsonl | The 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.jsonl | tools/call answered though discovery declared no tools capability (TOOL-019) |
tool-020-declared-tools-list-unimplemented.jsonl | tools declared, but tools/list refused with -32601 (TOOL-020) |
tool-022-tools-list-order-changes.jsonl | Two tools/list results with the same tools in a different order (TOOL-022) |
tool-034-mirrored-integer-out-of-range.jsonl | An 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.jsonl | A tools/call result embedding a resource with no resources capability declared (TOOL-038) |
res-012-resources-undeclared.jsonl | resources/read answered though discovery declared no resources capability (RES-012) |
res-013-declared-resources-list-unimplemented.jsonl | resources declared, but resources/list refused with -32601 (RES-013) |
res-022-read-empty-contents.jsonl | A 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.jsonl | prompts/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.jsonl | prompts declared, but prompts/list refused with -32601 (PROM-013) |
comp-007-completions-undeclared.jsonl | completion/complete answered though the server/discover result declared no completions capability (COMP-007) |
log-007-logging-undeclared.jsonl | notifications/message emitted though discovery declared no logging capability (LOG-007) |
log-008-log-without-requested-level.jsonl | A log notification in a session where no request set io.modelcontextprotocol/logLevel (LOG-008) |
log-008-log-for-request-without-level.jsonl | The 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.jsonl | A log notification tagged with a subscription id, so travelling on a subscription’s stream (LOG-009) |
log-009-log-with-no-request-in-flight.jsonl | A 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.jsonl | A request declaring log level verbose, served rather than rejected with -32602 (LOG-010) |
cach-001-cacheable-result-without-hints.jsonl | A complete tools/list result with no ttlMs caching hint (CACH-001) |
cach-001-ttl-without-cache-scope.jsonl | A 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.jsonl | ttlMs: -1, which servers must never provide (CACH-008) |
cach-015-page-scope-changes.jsonl | A 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.jsonl | A prompts/list_changed on a subscription whose filter asked only for tools-list changes (SUBS-001) |
subs-001-cancelled-for-another-request.jsonl | A 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.jsonl | A notification ahead of notifications/subscriptions/acknowledged on the same subscription id (SUBS-002) |
subs-002-reused-id-first-opening-unacknowledged.jsonl | Subscription 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.jsonl | A 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.jsonl | input_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.jsonl | inputRequests asking for tools/list, which is not an ElicitRequest, CreateMessageRequest or ListRootsRequest (MRTR-006) |
mrtr-011-input-required-empty.jsonl | input_required carrying neither inputRequests nor requestState, opening a round that cannot be completed (MRTR-011) |
mrtr-015-retry-without-input-responses.jsonl | Retry 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.jsonl | Retry 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.jsonl | The 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.jsonl | Retry invents a requestState for a round that issued none (MRTR-018) |
mrtr-019-retry-reuses-id.jsonl | Retry 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.jsonl | A prompts/get carrying the requestState a tools/call round issued (MRTR-020) |
mrtr-024-shortfall-answered-with-error.jsonl | Retry 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.jsonl | notifications/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.jsonl | Server 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.jsonl | A 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.jsonl | A request opting into progress with progressToken: 1.5, a number but not an integer (PROG-001). |
prog-002-progress-token-reused-in-flight.jsonl | Two 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.jsonl | Progress 50 then 10 for one token (PROG-003). |
prog-008-progress-after-completion.jsonl | A 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.jsonl | server/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.jsonl | A 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.jsonl | A 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.jsonl | After 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.jsonl | Client 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.jsonl | BASE-073 (the same invalid inputSchema, at this revision) |
base-088-input-schema-draft-07.jsonl | BASE-088, a warning (the same valid draft-07 inputSchema as base-086-…, at this revision) |
tran-141-mcp-name-header-missing.jsonl | A 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.jsonl | TOOL-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.jsonl | TRAN-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.jsonl | Over 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.jsonl | The 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.jsonl | TRAN-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.jsonl | TOOL-040 (structuredContent missing conditions, which the tool’s outputSchema requires) |
wire-schema-resource-missing-uri.jsonl | Wire 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.jsonl | Wire schema: cacheScope: "shared", which is neither public nor private |
wire-schema-content-block-unknown-type.jsonl | Wire schema: a tools/call result whose content block has type: "txt", matching no ContentBlock member |
wire-schema-tool-image-data-not-base64.jsonl | Wire 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.jsonl | A 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.jsonl | ELIC-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.jsonl | ELIC-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.jsonl | ELIC-060: an elicitation with no message; the wire schema too |
elic-062-form-without-schema.jsonl | ELIC-062: form mode with no requestedSchema; the wire schema too |
elic-064-url-mode-without-url.jsonl | ELIC-064: URL mode with no url; the wire schema too |
elic-065-url-not-a-valid-url.jsonl | ELIC-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.jsonl | ELIC-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.jsonl | SAMP-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.jsonl | SAMP-027: a retry declaring only elicitation answers the sampling request its call drew |
samp-028-include-context-deprecated.jsonl | SAMP-028, a warning: includeContext thisServer with sampling.context declared — SAMP-029’s pass path |
samp-029-include-context-undeclared.jsonl | SAMP-029, a warning: includeContext allServers without sampling.context; SAMP-028 warns too |
samp-030-tool-results-mixed.jsonl | SAMP-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.jsonl | SAMP-031: a tool use followed by a plain text message; SAMP-042 restates it |
samp-032-required-tool-choice-ignored.jsonl | SAMP-032: toolChoice required answered with text |
samp-033-none-tool-choice-ignored.jsonl | SAMP-033: toolChoice none answered with a tool use |
samp-034-message-without-role.jsonl | SAMP-034: a sampling message with no role; the wire schema too |
root-013-answered-without-capability.jsonl | ROOT-013: a retry declaring only sampling answers roots/list |
root-014-root-not-file-uri.jsonl | ROOT-014: a root listed as https://example.com/repos/trips |
root-018-asked-without-capability.jsonl | ROOT-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.jsonl | AUTH-097, a warning: the 401 challenge carries resource_metadata but no scope |
auth-124-invalid-token-answered-403.jsonl | AUTH-124: error="invalid_token" sent with 403; AUTH-130 fails too, as for auth-045 at 2025-11-25 |
auth-129-offline-access-challenged.jsonl | AUTH-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.jsonl | AUTH-130: error="insufficient_scope" sent with 401; AUTH-131 warns too |
auth-131-insufficient-scope-without-resource-metadata.jsonl | AUTH-131, a warning: an insufficient_scope challenge without resource_metadata |
auth-132-insufficient-scope-names-no-scope.jsonl | AUTH-132, a warning: an insufficient_scope challenge with no scope; AUTH-131 warns too |
auth-133-scope-challenged-incrementally.jsonl | AUTH-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.
| Trace | Exercises |
|---|---|
tasks-session.jsonl | Conformant 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.jsonl | Conformant 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.jsonl | TASK-001: a tools/call whose request does not declare the extension is answered with a task |
task-006-task-without-result-type.jsonl | TASK-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.jsonl | TASK-007: resultType: "task" on a result with no taskId |
task-015-get-result-not-a-task.jsonl | TASK-015: a tasks/get result with status running and no timestamps or ttlMs |
task-015-input-required-without-requests.jsonl | TASK-015: an input_required task without inputRequests |
task-017-get-result-type-not-complete.jsonl | TASK-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.jsonl | TASK-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.jsonl | TASK-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.jsonl | TASK-026, a warning: a second tasks/update answering a key an acknowledged one already answered |
task-029-task-cancelled-by-notification.jsonl | TASK-029: notifications/cancelled naming the tools/call a task answered |
task-033-task-listen-without-declaration.jsonl | TASK-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.jsonl | TASK-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.jsonl | TASK-035: a tasks/get POSTed without Mcp-Name |
task-039-get-without-declaration.jsonl | TASK-039: a non-declaring client’s tasks/get answered with the task |
task-041-failed-without-status-message.jsonl | TASK-041, a warning: a failed task without statusMessage |
task-042-failed-without-error.jsonl | TASK-042: a failed task without error. TASK-015 (its fifth item) and TASK-045 state the same rule |
task-043-tool-error-reported-failed.jsonl | TASK-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.jsonl | TASK-046: a completed task without result. TASK-015 (its third item) and TASK-044 state the same rule |
skills-session.jsonl | Conformant 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.jsonl | SKILL-003: directoryRead: true declared, resources/directory/read answered -32601 |
skill-004-directory-read-undeclared.jsonl | SKILL-004: the client calls resources/directory/read on a server that declared no directoryRead |
skill-005-skills-without-resources.jsonl | SKILL-005: the extension declared without the resources capability |
skill-007-entry-uri-not-skill-md.jsonl | SKILL-007: an entry whose uri is a README.md |
skill-008-skill-md-without-frontmatter.jsonl | SKILL-008: a SKILL.md read back without YAML frontmatter |
skill-011-skill-under-other-scheme.jsonl | SKILL-011, a warning: a skill served under github:// |
skill-012-path-not-ending-in-name.jsonl | SKILL-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.jsonl | SKILL-013: a skill named PDF--Processing |
skill-014-authority-not-reg-name.jsonl | SKILL-014, a warning: a first path segment with a space |
skill-015-prefix-segment-invalid.jsonl | SKILL-015, a warning: a prefix segment containing | |
skill-018-skill-md-not-markdown.jsonl | SKILL-018, a warning: a SKILL.md listed as text/plain |
skill-019-resource-name-not-skill-name.jsonl | SKILL-019, a warning: a SKILL.md listed under the name SKILL.md |
skill-020-resource-description-differs.jsonl | SKILL-020, a warning: a SKILL.md listed with a description its frontmatter does not give |
skill-026-entry-frontmatter-differs.jsonl | SKILL-026: the file read back has the manifest’s digest but a different description from the entry’s frontmatter |
skill-029-listing-names-collide.jsonl | SKILL-029, a warning: two skills in one listing named pdf-processing |
skill-034-entry-without-resources.jsonl | SKILL-034: an entry without resources |
skill-035-manifest-without-skill-md.jsonl | SKILL-035: a manifest that does not list the skill’s own SKILL.md |
skill-036-manifest-file-outside-skill.jsonl | SKILL-036: a manifest listing skill://shared/LICENSE |
skill-037-manifest-entry-without-size.jsonl | SKILL-037: a manifest entry with a digest and no size |
skill-042-skill-beyond-limits.jsonl | SKILL-042, a warning: a manifest totalling 16,777,217 bytes, one over the limit |
skill-044-list-not-implemented.jsonl | SKILL-044: skills/list answered -32601 by a declaring server. SKILL-002 states the same rule |
skill-045-list-result-type-not-complete.jsonl | SKILL-045: a skills/list result marked resultType: "partial" |
skill-047-list-without-cache-fields.jsonl | SKILL-047: a skills/list result without ttlMs and cacheScope |
skill-048-get-not-implemented.jsonl | SKILL-048: skills/get answered -32601 by a declaring server. SKILL-002 states the same rule |
skill-049-get-result-type-not-complete.jsonl | SKILL-049: a skills/get result marked resultType: "partial" |
skill-050-get-names-a-directory.jsonl | SKILL-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.jsonl | SKILL-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.jsonl | SKILL-053: a skills/get result without ttlMs and cacheScope |
skill-065-directory-result-type-not-complete.jsonl | SKILL-065: a resources/directory/read result marked resultType: "partial" |
skill-066-directory-read-of-a-file-answered.jsonl | SKILL-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.jsonl | SKILL-067: resources/directory/read of templates/, a directory the manifest puts a file under, refused |
skill-100-directory-result-without-cache-fields.jsonl | SKILL-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:
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 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:
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.
| SDK | Pinned release | Revision | Verdict | pass | fail | warn | not observed | Wire schema | Evidence |
|---|---|---|---|---|---|---|---|---|---|
| TypeScript v1 | @modelcontextprotocol/sdk 1.32.1 | 2025-11-25 | pass-with-warnings | 39 | 0 | 1 | 62 | pass (18 messages) | report · trace |
| TypeScript v1 | @modelcontextprotocol/sdk 1.32.1 | 2026-07-28 | not served | — | — | — | — | — | — |
| TypeScript v2 | @modelcontextprotocol/server 2.3.1 | 2025-11-25 | pass | 43 | 0 | 0 | 59 | pass (18 messages) | report · trace |
| TypeScript v2 | @modelcontextprotocol/server 2.3.1 | 2026-07-28 | pass | 72 | 0 | 0 | 109 | pass (14 messages) | report · trace |
| Python | mcp 2.3.0 | 2025-11-25 | pass | 39 | 0 | 0 | 63 | pass (17 messages) | report · trace |
| Python | mcp 2.3.0 | 2026-07-28 | pass | 65 | 0 | 0 | 116 | pass (14 messages) | report · trace |
| Go | go-sdk v1.8.0 | 2025-11-25 | pass | 43 | 0 | 0 | 59 | pass (17 messages) | report · trace |
| Go | go-sdk v1.8.0 | 2026-07-28 | fail | 78 | 4 | 1 | 98 | pass (14 messages) | report · trace |
| Rust | rmcp 3.7.0 | 2025-11-25 | pass | 42 | 0 | 0 | 60 | pass (17 messages) | report · trace |
| Rust | rmcp 3.7.0 | 2026-07-28 | pass-with-warnings | 73 | 0 | 1 | 107 | pass (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 forelicitation/create, which needs theelicitationcapability, but the request’sclientCapabilitiesdid not declare it - Go,
2026-07-28—TRAN-068(SHOULD, warn, 6 findings): SSE response does not carryX-Accel-Buffering: no - Go,
2026-07-28—MRTR-012(MUST NOT, fail, 1 finding): server asked forelicitation/create, which needs theelicitationcapability, but the request’sclientCapabilitiesdid not declare it - Go,
2026-07-28—MRTR-013(MUST NOT, fail, 1 finding): server asked forelicitation/create, which needs theelicitationcapability, but the request’sclientCapabilitiesdid 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’sclientCapabilitiesdoes not declareelicitationat all - Rust,
2026-07-28—BASE-037(SHOULD, warn, 5 findings): result answeringtools/listcarries noio.modelcontextprotocol/serverInfoin 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 directory | Traces |
|---|---|
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 trace | mcpsnoop does not | |
|---|---|---|
our verdict fail | 83 | 139 |
our verdict pass or pass-with-warnings | 4 | 132 |
Traces by how the two readings relate:
| Relation | Traces |
|---|---|
| both clean | 44 |
| agree | 79 |
| beyond mcpsnoop | 222 |
| divergent | 13 |
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.
| # | Kind | Subject | Traces | Class | Right | Resolution |
|---|---|---|---|---|---|---|
| 1 | mcpsnoop-only | invalid-frame | violations/base-007-error-code-float | granularity | both | none: the same defect on the same frame, reported at different granularity |
| 2 | mcpsnoop-only | request-id-in-flight | draft/violations/base-052-null-id-error-then-id-reused | mcpsnoop-defect | ours | upstream: mcpsnoop-null-id-answer |
| 3 | mcpsnoop-only | server-rejected-header-mismatch | draft/violations/tran-098-header-mismatch-without-400, draft/violations/tran-133-omitted-null-header-rejected, draft/violations/tran-135-omitted-absent-header-rejected | mcpsnoop-defect | ours | upstream: mcpsnoop-trusts-header-mismatch-rejection |
| 4 | ours-only | BASE-030 | draft/violations/base-031-malformed-meta-answered-with-result, draft/violations/base-032-invalid-params-not-http-400 | revision-attribution | both | none: the revision is stated to our validator out of band and is absent from the trace |
| 5 | ours-only | MRTR-002 | draft/violations/base-031-malformed-meta-answered-with-result, draft/violations/base-032-invalid-params-not-http-400 | revision-attribution | both | none: the revision is stated to our validator out of band and is absent from the trace |
| 6 | ours-only | TOOL-016 | draft/violations/base-031-malformed-meta-answered-with-result, draft/violations/base-032-invalid-params-not-http-400 | revision-attribution | both | none: the revision is stated to our validator out of band and is absent from the trace |
| 7 | ours-only | BASE-030 | draft/violations/vers-008-initialize-error-without-versions, draft/violations/base-030-legacy-initialize-negotiated-2026-07-28 | spec-ambiguity | undecided | 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 | ours-only | MRTR-002 | draft/violations/vers-008-initialize-error-without-versions, draft/violations/base-030-legacy-initialize-negotiated-2026-07-28 | spec-ambiguity | undecided | open: the same decision as BASE-030 on these traces |
| 9 | ours-only | TOOL-016 | draft/violations/vers-008-initialize-error-without-versions, draft/violations/base-030-legacy-initialize-negotiated-2026-07-28 | spec-ambiguity | undecided | open: the same decision as BASE-030 on these traces |
| 10 | ours-only | LIFE-018 | violations/tran-017-protocol-version-header-missing | mcpsnoop-defect | ours | upstream: mcpsnoop-version-header-2025-11-25 |
| 11 | ours-only | TRAN-017 | violations/tran-017-protocol-version-header-missing | mcpsnoop-defect | ours | upstream: mcpsnoop-version-header-2025-11-25 |
| 12 | mcpsnoop-only | response-unmatched | good/http-origin-refused-without-id, draft/good/http-origin-refused-without-id | mcpsnoop-defect | ours | upstream: mcpsnoop-origin-refusal-unmatched |
| 13 | mcpsnoop-only | progress-after-completion | good/stdio-task-progress | mcpsnoop-defect | ours | 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) |
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 RequestHTTP 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 inmeta; 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
metafields; seemeta. — 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
metafields; seemeta. — 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 inmeta; 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
initializerequest, 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
metafields; seemeta. — 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
metafields; seemeta. — 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
progressTokenprovided in the original request MUST continue to be used for progress notifications throughout the task’s lifetime, even after theCreateTaskResulthas been returned. — source
PROG-016 (MUST): Progress notifications for tasks MUST stop after the task reaches a terminal status (
completed,failed, orcancelled) — 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.
| Rule | Template | 2025-11-25 | 2026-07-28 | Context | Fired | Reading |
|---|---|---|---|---|---|---|
jsonrpc-version | jsonrpc must be 2.0 | BASE-008 | BASE-042 | body | 1 | A frame whose jsonrpc member is not "2.0": one of the JSON-RPC 2.0 rules every message must follow. |
invalid-frame | frame on the protocol channel is not valid JSON-RPC | BASE-008, TRAN-004, TRAN-005 | BASE-042, TRAN-117, TRAN-118 | body | 3 | A 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-type | request id has type * MCP requires a string or integer id | BASE-001 | BASE-043 | body | 1 | A request id that is neither a string nor an integer. |
request-id-null | request id is null; MCP requires a string or integer id | BASE-001, BASE-002 | BASE-043, BASE-044 | body | 1 | A 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-flight | request reuses an id already in flight | BASE-003 | BASE-045 | pairing | 2 | A 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-unmatched | response id has no matching request | BASE-004, BASE-009 | BASE-046, BASE-052 | pairing | 10 | A response whose id names no outstanding request: the clauses requiring a response to carry the id of the request it answers. |
response-duplicate | duplicate response for the same id | BASE-004, BASE-009 | BASE-046, BASE-052 | pairing | 1 | A second response for an id already answered, which corresponds to no outstanding request. |
response-without-result-or-error | response has neither result nor error | BASE-010 | BASE-047 | body | 1 | A response carrying neither result nor error. |
response-with-result-and-error | response has both result and error | BASE-008 | BASE-042 | body | 0 | A response carrying both result and error, which JSON-RPC 2.0 forbids. Template read from the binary; no corpus trace exercises it. |
result-type-missing | result is missing required resultType | — | BASE-048 | body | 1 | A result without resultType. The field does not exist at 2025-11-25. |
client-sent-response | client sent a JSON-RPC response, which * no longer defines | — | TRAN-060, TRAN-119, BASE-084 | body | 1 | A client sending a JSON-RPC response at 2026-07-28, where a server sends no requests to answer. |
server-sent-request | server sent a JSON-RPC request *, which * replaced with the MRTR pattern | — | BASE-084, MRTR-001, TRAN-066, TRAN-120 | body | 1 | A server-initiated JSON-RPC request at 2026-07-28, which replaced them with multi round-trip requests. |
request-meta-missing | request _meta is missing required io.modelcontextprotocol/ | — | BASE-030, MRTR-002, TOOL-016 | body | 6 | A request without one of the _meta fields 2026-07-28 requires on every request. |
protocol-version-header-missing | required routing header MCP-Protocol-Version is missing | LIFE-018, TRAN-017 | TRAN-071 | http | 1 | A POST without the MCP-Protocol-Version header where the negotiated revision requires it. |
method-name-header-missing | required routing header Mcp-Method is missingrequired routing header Mcp-Name is missing | — | TRAN-058 | http | 2 | A POST without its Mcp-Method or Mcp-Name request metadata header. |
param-header-missing | required routing header Mcp-Param-* is missing for body parameter | — | TRAN-079, TRAN-130 | http | 4 | A tools/call carrying a parameter its tool marks with x-mcp-header, sent without the mirrored Mcp-Param-* header. |
protocol-version-header-disagrees | MCP-Protocol-Version header * disagrees with _meta protocolVersion | — | TRAN-072 | http | 7 | An MCP-Protocol-Version header that differs from the body’s _meta protocol version. |
method-header-disagrees | routing header Mcp-Method * disagrees with body method | — | TRAN-058 | http | 0 | An Mcp-Method header that is not the body’s method. Template read from the binary; no corpus trace exercises it. |
name-header-disagrees | routing header Mcp-Name * disagrees with body name | — | TRAN-058, TRAN-077, TRAN-092 | http | 3 | An 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-disagrees | routing header Mcp-Param-* disagrees with body parameter | — | TRAN-079, TRAN-086, TRAN-087, TRAN-089, TRAN-092 | http | 4 | An 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-value | routing header Mcp-Param-* is present but body parameter * is absent or null | — | TRAN-132, TRAN-134 | http | 2 | An Mcp-Param-* header sent although the body parameter is null or absent. |
batch-routing-headers | routing headers are invalid on a JSON-RPC batch | TRAN-026 | TRAN-059 | http | 0 | Routing 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-mismatch | a 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-134 | http | 10 | The 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-type | the response to a request * the transport * application/json or text/event-stream | TRAN-029 | TRAN-063 | http | 4 | A POST carrying a request answered with a Content-Type other than application/json or text/event-stream, or with none. |
cacheable-without-ttl | cacheable * result declares no ttlMs, which * requires | — | CACH-001 | body | 9 | A complete result of a cacheable operation without ttlMs. CACH-001 asks for caching hints; mcpsnoop checks ttlMs and not cacheScope. |
cache-scope-across-pages | cacheScope * on a later page of * disagrees with | — | CACH-015, CACH-016 | body | 1 | Pages of one list request carrying different cacheScope values. |
cache-scope-value | cacheScope is *, and * defines only public and private | — | wire-schema | body | 1 | A 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-ttl | ttlMs is *, and * requires a value of zero or more | — | CACH-008 | body | 1 | A negative ttlMs. |
input-required-empty | InputRequiredResult carries neither inputRequests nor requestState | — | MRTR-011 | body | 1 | An InputRequiredResult with neither inputRequests nor requestState. |
input-required-on-method | InputRequiredResult on *, which * allows only on | — | MRTR-004 | body | 1 | An InputRequiredResult answering a method that may not return one. |
input-request-method | inputRequests names *, and * allows only | — | MRTR-006 | body | 1 | An inputRequests entry that is not an elicitation, sampling or roots request. |
input-request-undeclared | inputRequests asks for * while the request declared no | — | BASE-034, MRTR-012, MRTR-013 | body | 3 | An 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-changed | MRTR retry changed requestState | — | MRTR-016, MRTR-003, MRTR-017 | body | 2 | A retry echoing a requestState other than the one the server sent. |
request-state-missing | MRTR retry is missing requestState | — | MRTR-016 | body | 0 | A retry leaving out the requestState the server sent. Template read from the binary; no corpus trace exercises it. |
request-state-invented | MRTR retry invented requestState | — | MRTR-018 | body | 1 | A retry carrying a requestState the server never sent. |
subscription-before-ack | arrived on subscription * before its acknowledgment | — | SUBS-002 | body | 2 | A subscription notification sent before the subscription’s acknowledgment. |
subscription-untagged | carries no io.modelcontextprotocol/subscriptionId | — | BASE-039 | body | 1 | A 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-unrequested | was not requested by subscription | — | SUBS-001 | body | 1 | A 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-increasing | progress for token * went from * requires it to increase | PROG-011 | PROG-003 | body | 2 | A progress value that does not increase. |
progress-after-completion | progress notification for token * arrived after its request completed | PROG-019 | PROG-008 | body | 4 | A progress notification after its request was answered. |
server-cancellation | server cancelled *, and * lets a server cancel only a subscriptions/listen request | — | CANC-003, CANC-011 | body | 2 | A server-sent notifications/cancelled naming anything but a subscriptions/listen request. |
input-schema-not-object | advertises an inputSchema that is not a JSON object | TOOL-003 | TOOL-025 | body | 1 | A tool whose inputSchema is not a JSON Schema object. |
x-mcp-header-chain | declares an x-mcp-header under *, which * does not allow in the chain | — | TRAN-082, TRAN-137, TOOL-045 | body | 5 | An x-mcp-header on a property not reachable through properties alone. |
x-mcp-header-repeated | declares x-mcp-header * on more than one property | — | TRAN-080, TOOL-033 | body | 1 | One x-mcp-header name on two properties of a tool: one of the constraints TRAN-080 lists. |
task-for-undeclaring-client | a task handle uses the io.modelcontextprotocol/tasks extension, which the client never advertised | — | TASK-001, VERS-005 | body | 1 | A 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
| Trace | Revision | Ours | We flag | mcpsnoop | Rules fired | Set aside | Relation |
|---|---|---|---|---|---|---|---|
good/http-authorization | 2025-11-25 | pass | — | silent | — | — | both clean |
good/http-origin-refused-without-id | 2025-11-25 | pass | — | flags | response-unmatched | — | divergent |
good/http-session | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-cancellation | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-client-features | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-feature-session | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-full-session | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-minimal-init | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-progress | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-request-unanswered-at-close | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-requests-partly-unanswered | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-server-negotiates-2024-11-05 | 2025-11-25 | pass | — | silent | — | — | both clean |
good/stdio-task-progress | 2025-11-25 | pass | — | flags | progress-after-completion | — | divergent |
good/stdio-tool-image-base64-padding | 2025-11-25 | pass | — | silent | — | — | both clean |
violations/auth-014-challenge-without-scope | 2025-11-25 | pass-with-warnings | AUTH-014 | silent | — | — | beyond mcpsnoop |
violations/auth-045-invalid-token-answered-403 | 2025-11-25 | fail | AUTH-045, AUTH-049 | silent | — | — | beyond mcpsnoop |
violations/auth-049-insufficient-scope-answered-401 | 2025-11-25 | fail | AUTH-049, AUTH-050 | silent | — | — | beyond mcpsnoop |
violations/auth-050-insufficient-scope-without-resource-metadata | 2025-11-25 | pass-with-warnings | AUTH-050 | silent | — | — | beyond mcpsnoop |
violations/auth-051-insufficient-scope-names-no-scope | 2025-11-25 | pass-with-warnings | AUTH-050, AUTH-051 | silent | — | — | beyond mcpsnoop |
violations/base-001-request-id-boolean | 2025-11-25 | fail | BASE-001, wire-schema | flags | request-id-type | — | agree |
violations/base-002-request-id-null | 2025-11-25 | fail | BASE-001, BASE-002, wire-schema | flags | request-id-null | — | agree |
violations/base-003-request-id-reuse | 2025-11-25 | fail | BASE-003 | silent | — | — | beyond mcpsnoop |
violations/base-004-request-answered-twice-cross-flavor | 2025-11-25 | fail | BASE-004, PING-001 | flags | response-duplicate | — | agree |
violations/base-004-result-unknown-id | 2025-11-25 | fail | BASE-004 | flags | response-unmatched | — | agree |
violations/base-005-notification-with-id | 2025-11-25 | fail | BASE-005, LIFE-003 | silent | — | — | beyond mcpsnoop |
violations/base-006-error-missing-message | 2025-11-25 | fail | BASE-006, wire-schema | silent | — | — | beyond mcpsnoop |
violations/base-007-error-code-float | 2025-11-25 | fail | BASE-007, wire-schema | flags | invalid-frame | — | divergent |
violations/base-008-jsonrpc-version | 2025-11-25 | fail | BASE-008, wire-schema | flags | jsonrpc-version | — | agree |
violations/base-009-error-unknown-id | 2025-11-25 | fail | BASE-009 | flags | response-unmatched | — | agree |
violations/base-010-response-without-result | 2025-11-25 | fail | BASE-008, BASE-010, TRAN-004, wire-schema | flags | response-without-result-or-error | — | agree |
violations/base-017-input-schema-invalid | 2025-11-25 | fail | BASE-017 | silent | — | — | beyond mcpsnoop |
violations/base-019-meta-key-bad-prefix | 2025-11-25 | fail | BASE-019, BASE-020 | silent | — | — | beyond mcpsnoop |
violations/base-086-input-schema-draft-07 | 2025-11-25 | pass-with-warnings | BASE-086 | silent | — | — | beyond mcpsnoop |
violations/canc-018-cancellation-of-unknown-request | 2025-11-25 | fail | CANC-018 | silent | — | — | beyond mcpsnoop |
violations/canc-019-initialize-cancelled | 2025-11-25 | fail | CANC-019 | silent | — | — | beyond mcpsnoop |
violations/canc-020-task-cancelled-by-notification | 2025-11-25 | fail | CANC-020 | silent | — | — | beyond mcpsnoop |
violations/canc-021-response-after-cancellation-read | 2025-11-25 | pass-with-warnings | CANC-021 | silent | — | — | beyond mcpsnoop |
violations/comp-001-capability-undeclared | 2025-11-25 | fail | COMP-001, LIFE-009 | silent | — | — | beyond mcpsnoop |
violations/elic-004-answered-without-capability | 2025-11-25 | fail | ELIC-004, ELIC-006, LIFE-009 | silent | — | — | beyond mcpsnoop |
violations/elic-006-url-mode-undeclared | 2025-11-25 | fail | ELIC-006, LIFE-009 | silent | — | — | beyond mcpsnoop |
violations/elic-007-request-without-message | 2025-11-25 | fail | ELIC-007, wire-schema | silent | — | — | beyond mcpsnoop |
violations/elic-009-form-without-schema | 2025-11-25 | fail | ELIC-009, wire-schema | silent | — | — | beyond mcpsnoop |
violations/elic-011-url-without-elicitation-id | 2025-11-25 | fail | ELIC-011, wire-schema | silent | — | — | beyond mcpsnoop |
violations/elic-012-url-without-scheme | 2025-11-25 | fail | ELIC-012 | silent | — | — | beyond mcpsnoop |
violations/elic-014-complete-names-unknown-id | 2025-11-25 | fail | ELIC-014 | silent | — | — | beyond mcpsnoop |
violations/elic-018-url-required-error-without-list | 2025-11-25 | fail | ELIC-018, wire-schema | silent | — | — | beyond mcpsnoop |
violations/elic-019-url-required-error-form-mode | 2025-11-25 | fail | ELIC-019, wire-schema | silent | — | — | beyond mcpsnoop |
violations/elic-021-undeclared-mode-accepted | 2025-11-25 | fail | ELIC-006, ELIC-021, LIFE-009 | silent | — | — | beyond mcpsnoop |
violations/elic-049-accepted-content-violates-schema | 2025-11-25 | pass-with-warnings | ELIC-049 | silent | — | — | beyond mcpsnoop |
violations/life-001-first-message-not-initialize | 2025-11-25 | fail | LIFE-001, LIFE-004 | silent | — | — | beyond mcpsnoop |
violations/life-002-initialize-missing-protocolversion | 2025-11-25 | fail | LIFE-002, LIFE-007, wire-schema | silent | — | — | beyond mcpsnoop |
violations/life-003-missing-initialized | 2025-11-25 | fail | LIFE-003 | silent | — | — | beyond mcpsnoop |
violations/life-004-client-request-before-init-response | 2025-11-25 | pass-with-warnings | LIFE-004 | silent | — | — | beyond mcpsnoop |
violations/life-005-server-request-before-initialized | 2025-11-25 | pass-with-warnings | LIFE-005 | silent | — | — | beyond mcpsnoop |
violations/life-006-result-version-invalid | 2025-11-25 | fail | LIFE-006 | silent | — | — | beyond mcpsnoop |
violations/life-007-initialize-protocolversion-not-string | 2025-11-25 | fail | LIFE-002, LIFE-007, wire-schema | silent | — | — | beyond mcpsnoop |
violations/life-009-undeclared-capability-use | 2025-11-25 | fail | LIFE-009 | silent | — | — | beyond mcpsnoop |
violations/life-010-initialize-result-missing-capabilities | 2025-11-25 | fail | LIFE-010, wire-schema | silent | — | — | beyond mcpsnoop |
violations/log-001-capability-undeclared | 2025-11-25 | fail | LIFE-009, LOG-001 | silent | — | — | beyond mcpsnoop |
violations/page-002-cursor-never-issued | 2025-11-25 | fail | PAGE-002, PAGE-003 | silent | — | — | beyond mcpsnoop |
violations/page-002-fabricated-cursor-rejected | 2025-11-25 | fail | PAGE-002 | silent | — | — | beyond mcpsnoop |
violations/ping-001-ping-answered-with-content | 2025-11-25 | fail | PING-001 | silent | — | — | beyond mcpsnoop |
violations/prog-009-progress-token-float | 2025-11-25 | fail | PROG-009, wire-schema | silent | — | — | beyond mcpsnoop |
violations/prog-010-progress-token-reused-in-flight | 2025-11-25 | fail | PROG-010 | silent | — | — | beyond mcpsnoop |
violations/prog-011-progress-value-decreases | 2025-11-25 | fail | PROG-011 | flags | progress-not-increasing | — | agree |
violations/prog-013-progress-token-never-issued | 2025-11-25 | fail | PROG-013 | silent | — | — | beyond mcpsnoop |
violations/prog-015-task-progress-token-differs | 2025-11-25 | fail | PROG-013, PROG-014, PROG-015 | silent | — | — | beyond mcpsnoop |
violations/prog-016-task-progress-after-terminal-status | 2025-11-25 | fail | PROG-013, PROG-016, PROG-019 | flags | progress-after-completion | — | agree |
violations/prog-019-progress-after-completion | 2025-11-25 | fail | PROG-013, PROG-016, PROG-019 | flags | progress-after-completion | — | agree |
violations/prom-001-capability-undeclared | 2025-11-25 | fail | LIFE-009, PROM-001 | silent | — | — | beyond mcpsnoop |
violations/prom-003-image-data-invalid | 2025-11-25 | fail | PROM-003, wire-schema | silent | — | — | beyond mcpsnoop |
violations/prom-004-audio-data-invalid | 2025-11-25 | fail | PROM-004, wire-schema | silent | — | — | beyond mcpsnoop |
violations/prom-005-embedded-resource-malformed | 2025-11-25 | fail | PROM-005, wire-schema | silent | — | — | beyond mcpsnoop |
violations/prom-008-required-argument-unvalidated | 2025-11-25 | pass-with-warnings | PROM-008 | silent | — | — | beyond mcpsnoop |
violations/res-001-capability-undeclared | 2025-11-25 | fail | LIFE-009, RES-001 | silent | — | — | beyond mcpsnoop |
violations/res-004-uri-bad-scheme | 2025-11-25 | fail | RES-004 | silent | — | — | beyond mcpsnoop |
violations/res-006-blob-not-base64 | 2025-11-25 | fail | RES-006, wire-schema | silent | — | — | beyond mcpsnoop |
violations/root-001-answered-without-capability | 2025-11-25 | fail | LIFE-009, ROOT-001, ROOT-004, ROOT-009 | silent | — | — | beyond mcpsnoop |
violations/root-003-root-not-file-uri | 2025-11-25 | fail | ROOT-003 | silent | — | — | beyond mcpsnoop |
violations/root-004-unsupported-answered-internal-error | 2025-11-25 | fail | LIFE-009, ROOT-004, ROOT-009 | silent | — | — | beyond mcpsnoop |
violations/root-009-asked-without-capability | 2025-11-25 | fail | LIFE-009, ROOT-009 | silent | — | — | beyond mcpsnoop |
violations/samp-004-tools-without-sampling-tools | 2025-11-25 | fail | LIFE-009, SAMP-004 | silent | — | — | beyond mcpsnoop |
violations/samp-005-answered-without-capability | 2025-11-25 | fail | LIFE-009, SAMP-005 | silent | — | — | beyond mcpsnoop |
violations/samp-006-include-context-deprecated | 2025-11-25 | pass-with-warnings | SAMP-006 | silent | — | — | beyond mcpsnoop |
violations/samp-007-include-context-undeclared | 2025-11-25 | pass-with-warnings | SAMP-006, SAMP-007 | silent | — | — | beyond mcpsnoop |
violations/samp-008-tool-results-mixed | 2025-11-25 | fail | SAMP-008, SAMP-009, SAMP-019 | silent | — | — | beyond mcpsnoop |
violations/samp-009-tool-use-unanswered | 2025-11-25 | fail | SAMP-009, SAMP-019 | silent | — | — | beyond mcpsnoop |
violations/samp-010-required-tool-choice-ignored | 2025-11-25 | fail | SAMP-010 | silent | — | — | beyond mcpsnoop |
violations/samp-011-none-tool-choice-ignored | 2025-11-25 | fail | SAMP-011 | silent | — | — | beyond mcpsnoop |
violations/samp-012-malformed-tool-messages-answered | 2025-11-25 | fail | SAMP-009, SAMP-012, SAMP-019 | silent | — | — | beyond mcpsnoop |
violations/tool-001-capability-undeclared | 2025-11-25 | fail | LIFE-009, TOOL-001 | silent | — | — | beyond mcpsnoop |
violations/tool-003-input-schema-null | 2025-11-25 | fail | BASE-017, TOOL-003, wire-schema | flags | input-schema-not-object | — | agree |
violations/tool-005-name-length | 2025-11-25 | pass-with-warnings | TOOL-005 | silent | — | — | beyond mcpsnoop |
violations/tool-006-name-charset | 2025-11-25 | pass-with-warnings | TOOL-006, TOOL-007 | silent | — | — | beyond mcpsnoop |
violations/tool-008-name-duplicate | 2025-11-25 | pass-with-warnings | TOOL-008 | silent | — | — | beyond mcpsnoop |
violations/tool-009-embedded-resource-without-capability | 2025-11-25 | pass-with-warnings | TOOL-009 | silent | — | — | beyond mcpsnoop |
violations/tool-010-structured-without-text | 2025-11-25 | pass-with-warnings | TOOL-010 | silent | — | — | beyond mcpsnoop |
violations/tool-011-output-schema-no-structured-result | 2025-11-25 | fail | TOOL-011 | silent | — | — | beyond mcpsnoop |
violations/tool-011-structured-content-not-conforming | 2025-11-25 | fail | TOOL-011 | silent | — | — | beyond mcpsnoop |
violations/tran-004-stdout-invalid-message | 2025-11-25 | fail | BASE-008, TRAN-004, wire-schema | flags | invalid-frame | — | agree |
violations/tran-005-stdin-invalid-message | 2025-11-25 | fail | BASE-008, TRAN-005, wire-schema | flags | invalid-frame | — | agree |
violations/tran-011-session-id-invisible-ascii | 2025-11-25 | fail | TRAN-011 | silent | — | — | beyond mcpsnoop |
violations/tran-013-session-id-not-echoed | 2025-11-25 | fail | TRAN-013 | silent | — | — | beyond mcpsnoop |
violations/tran-017-protocol-version-header-missing | 2025-11-25 | fail | LIFE-018, TRAN-017 | silent | — | — | divergent |
violations/tran-018-declined-version-header-refused-400 | 2025-11-25 | pass-with-warnings | TRAN-018 | silent | — | — | beyond mcpsnoop |
violations/tran-018-protocol-version-mismatched | 2025-11-25 | pass-with-warnings | TRAN-018 | silent | — | — | beyond mcpsnoop |
violations/tran-020-declined-version-header-accepted | 2025-11-25 | fail | TRAN-018, TRAN-020 | silent | — | — | beyond mcpsnoop |
violations/tran-025-accept-header-json-missing | 2025-11-25 | fail | TRAN-025 | silent | — | — | beyond mcpsnoop |
violations/tran-025-accept-header-missing | 2025-11-25 | fail | TRAN-025 | silent | — | — | beyond mcpsnoop |
violations/tran-026-http-post-batch | 2025-11-25 | fail | BASE-008, TRAN-026, wire-schema | silent | — | — | beyond mcpsnoop |
violations/tran-027-notification-answered-200 | 2025-11-25 | fail | TRAN-027 | silent | — | — | beyond mcpsnoop |
violations/tran-029-content-type-unexpected | 2025-11-25 | fail | TRAN-029 | flags | response-content-type | — | agree |
violations/tran-029-error-without-content-type | 2025-11-25 | fail | TRAN-029 | flags | response-content-type | — | agree |
violations/tran-039-get-accept-header-missing | 2025-11-25 | fail | TRAN-039 | silent | — | — | beyond mcpsnoop |
violations/tran-040-get-answered-json | 2025-11-25 | fail | TRAN-040 | silent | — | — | beyond mcpsnoop |
violations/tran-049-message-not-posted | 2025-11-25 | fail | TRAN-049 | silent | — | — | beyond mcpsnoop |
violations/wire-schema-content-block-unknown-type | 2025-11-25 | fail | wire-schema | silent | — | — | beyond mcpsnoop |
violations/wire-schema-log-level-not-in-enum | 2025-11-25 | fail | wire-schema | silent | — | — | beyond mcpsnoop |
violations/wire-schema-resource-missing-uri | 2025-11-25 | fail | wire-schema | silent | — | — | beyond mcpsnoop |
violations/wire-schema-tool-image-data-not-base64 | 2025-11-25 | fail | wire-schema | silent | — | — | beyond mcpsnoop |
draft/good/caching-ttl-written-as-float | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/client-features-mrtr | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/http-authorization | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/http-close-after-another-response | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/http-concurrent-posts | 2026-07-28 | pass | — | silent | — | method-name-header-missing, protocol-version-header-missing | both clean |
draft/good/http-dual-era-modern-request-first | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/http-origin-refused-without-id | 2026-07-28 | pass | — | flags | response-unmatched | — | divergent |
draft/good/http-stream-closed-by-client | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/http-upstream-abort | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/http-x-mcp-header-omitted | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/input-required-id-reused | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/logging-per-request | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/mrtr-parallel-rounds | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/mrtr-retry-round-before-recording | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/resource-read-existing-empty | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/skills-session | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/stateless-session | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/stdio-cancellation-race | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/stdio-cancelled-id-reused | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/stdio-discover-preferred-version-unsupported | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/stdio-progress | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/stdio-x-mcp-header-large-integer | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/streamable-http-session | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/subscription-id-reused-after-close | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/subscription-server-teardown | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/tasks-http | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/tasks-session | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/good/traceparent-higher-version | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/violations/auth-097-challenge-without-scope | 2026-07-28 | pass-with-warnings | AUTH-097 | silent | — | — | beyond mcpsnoop |
draft/violations/auth-124-invalid-token-answered-403 | 2026-07-28 | fail | AUTH-124, AUTH-130 | silent | — | — | beyond mcpsnoop |
draft/violations/auth-129-offline-access-challenged | 2026-07-28 | pass-with-warnings | AUTH-129 | silent | — | — | beyond mcpsnoop |
draft/violations/auth-130-insufficient-scope-answered-401 | 2026-07-28 | fail | AUTH-130, AUTH-131 | silent | — | — | beyond mcpsnoop |
draft/violations/auth-131-insufficient-scope-without-resource-metadata | 2026-07-28 | pass-with-warnings | AUTH-131 | silent | — | — | beyond mcpsnoop |
draft/violations/auth-132-insufficient-scope-names-no-scope | 2026-07-28 | pass-with-warnings | AUTH-131, AUTH-132 | silent | — | — | beyond mcpsnoop |
draft/violations/auth-133-scope-challenged-incrementally | 2026-07-28 | pass-with-warnings | AUTH-133 | silent | — | — | beyond mcpsnoop |
draft/violations/base-030-legacy-initialize-negotiated-2026-07-28 | 2026-07-28 | fail | BASE-030, COMP-006, DISC-002, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, TRAN-128 | silent | — | — | divergent |
draft/violations/base-030-malformed-request-id-reused-correctly | 2026-07-28 | fail | BASE-030, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schema | flags | request-meta-missing | — | agree |
draft/violations/base-030-malformed-request-refused-without-id | 2026-07-28 | fail | BASE-030, BASE-052, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schema | flags | request-meta-missing, response-unmatched | — | agree |
draft/violations/base-030-request-meta-missing-required-field | 2026-07-28 | fail | BASE-030, BASE-031, CACH-001, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schema | flags | cacheable-without-ttl, request-meta-missing | — | agree |
draft/violations/base-030-unsupported-version-refused-as-invalid-params | 2026-07-28 | fail | BASE-030, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schema | flags | request-meta-missing | — | agree |
draft/violations/base-031-malformed-meta-answered-with-result | 2026-07-28 | fail | BASE-030, BASE-031, CACH-001, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schema | silent | — | — | divergent |
draft/violations/base-031-reused-id-malformed-answered | 2026-07-28 | fail | BASE-030, BASE-031, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schema | flags | request-meta-missing | — | agree |
draft/violations/base-032-invalid-params-not-http-400 | 2026-07-28 | fail | BASE-030, BASE-032, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, wire-schema | silent | — | — | divergent |
draft/violations/base-033-request-without-client-info | 2026-07-28 | pass-with-warnings | BASE-033 | silent | — | — | beyond mcpsnoop |
draft/violations/base-034-input-request-for-undeclared-capability | 2026-07-28 | fail | BASE-034, ELIC-059, MRTR-012, MRTR-013, wire-schema | flags | input-request-undeclared | — | agree |
draft/violations/base-034-input-request-for-undeclared-mode | 2026-07-28 | fail | BASE-034, ELIC-059, MRTR-012, MRTR-013, SAMP-026 | silent | — | — | beyond mcpsnoop |
draft/violations/base-035-missing-capability-error-without-capabilities | 2026-07-28 | fail | BASE-035, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/base-036-missing-capability-not-http-400 | 2026-07-28 | fail | BASE-035, BASE-036, TRAN-063, wire-schema | flags | response-content-type | method-name-header-missing, protocol-version-header-missing | agree |
draft/violations/base-037-result-without-server-info | 2026-07-28 | pass-with-warnings | BASE-037 | silent | — | — | beyond mcpsnoop |
draft/violations/base-039-subscription-notification-mistagged | 2026-07-28 | fail | BASE-039 | silent | — | — | beyond mcpsnoop |
draft/violations/base-039-subscription-notification-untagged | 2026-07-28 | fail | BASE-039, wire-schema | flags | subscription-untagged | — | agree |
draft/violations/base-040-malformed-traceparent | 2026-07-28 | fail | BASE-040, CACH-001, wire-schema | flags | cacheable-without-ttl | — | agree |
draft/violations/base-040-traceparent-version-ff | 2026-07-28 | fail | BASE-040 | silent | — | — | beyond mcpsnoop |
draft/violations/base-045-request-id-reused-in-flight | 2026-07-28 | fail | BASE-045, CACH-001, wire-schema | flags | cacheable-without-ttl, request-id-in-flight | — | agree |
draft/violations/base-048-result-without-result-type | 2026-07-28 | fail | BASE-048, wire-schema | flags | result-type-missing | — | agree |
draft/violations/base-052-null-id-error-then-id-reused | 2026-07-28 | fail | BASE-052, wire-schema | flags | request-id-in-flight, response-unmatched | — | divergent |
draft/violations/base-057-undefined-reserved-error-code | 2026-07-28 | fail | BASE-057 | silent | — | — | beyond mcpsnoop |
draft/violations/base-058-withdrawn-error-code | 2026-07-28 | fail | BASE-058, BASE-083, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/base-060-app-code-in-reserved-range | 2026-07-28 | pass-with-warnings | BASE-060 | silent | — | — | beyond mcpsnoop |
draft/violations/base-073-input-schema-invalid | 2026-07-28 | fail | BASE-073 | silent | — | — | beyond mcpsnoop |
draft/violations/base-083-legacy-error-code | 2026-07-28 | pass-with-warnings | BASE-083 | silent | — | — | beyond mcpsnoop |
draft/violations/base-088-input-schema-draft-07 | 2026-07-28 | pass-with-warnings | BASE-088 | silent | — | — | beyond mcpsnoop |
draft/violations/cach-001-cacheable-result-without-hints | 2026-07-28 | fail | CACH-001, wire-schema | flags | cacheable-without-ttl | — | agree |
draft/violations/cach-001-ttl-without-cache-scope | 2026-07-28 | fail | CACH-001, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/cach-008-negative-ttl | 2026-07-28 | fail | CACH-008, wire-schema | flags | negative-ttl | — | agree |
draft/violations/cach-015-page-scope-changes | 2026-07-28 | fail | CACH-015, CACH-016 | flags | cache-scope-across-pages | — | agree |
draft/violations/canc-003-server-cancels-tool-call | 2026-07-28 | fail | CANC-003, CANC-011 | flags | server-cancellation | — | agree |
draft/violations/comp-007-completions-undeclared | 2026-07-28 | fail | COMP-007 | silent | — | — | beyond mcpsnoop |
draft/violations/disc-001-server-discover-method-not-found | 2026-07-28 | fail | DISC-001, VERS-003 | silent | — | — | beyond mcpsnoop |
draft/violations/disc-002-dual-era-client-skips-probe | 2026-07-28 | pass-with-warnings | DISC-002, TRAN-128 | silent | — | — | beyond mcpsnoop |
draft/violations/disc-003-discover-result-without-server-info | 2026-07-28 | pass-with-warnings | BASE-037, DISC-003 | silent | — | — | beyond mcpsnoop |
draft/violations/elic-057-answered-without-capability | 2026-07-28 | fail | ELIC-057 | silent | — | — | beyond mcpsnoop |
draft/violations/elic-059-url-mode-undeclared | 2026-07-28 | fail | BASE-034, ELIC-059, MRTR-012, MRTR-013 | silent | — | — | beyond mcpsnoop |
draft/violations/elic-060-request-without-message | 2026-07-28 | fail | ELIC-060, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/elic-062-form-without-schema | 2026-07-28 | fail | ELIC-062, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/elic-064-url-mode-without-url | 2026-07-28 | fail | ELIC-064, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/elic-065-url-not-a-valid-url | 2026-07-28 | fail | ELIC-065 | silent | — | — | beyond mcpsnoop |
draft/violations/elic-095-accepted-content-violates-schema | 2026-07-28 | pass-with-warnings | ELIC-095 | silent | — | — | beyond mcpsnoop |
draft/violations/log-007-logging-undeclared | 2026-07-28 | fail | LOG-007, TOOL-019, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/log-008-log-for-request-without-level | 2026-07-28 | fail | LOG-008 | silent | — | — | beyond mcpsnoop |
draft/violations/log-008-log-without-requested-level | 2026-07-28 | fail | LOG-008, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/log-009-log-on-subscription-stream | 2026-07-28 | fail | LOG-009, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/log-009-log-with-no-request-in-flight | 2026-07-28 | fail | LOG-009 | silent | — | — | beyond mcpsnoop |
draft/violations/log-010-unrecognized-log-level-accepted | 2026-07-28 | fail | LOG-010, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/mrtr-004-input-required-on-unsupported-method | 2026-07-28 | fail | MRTR-004 | flags | input-required-on-method | — | agree |
draft/violations/mrtr-006-input-request-unknown-method | 2026-07-28 | fail | MRTR-006, wire-schema | flags | input-request-method | — | agree |
draft/violations/mrtr-011-input-required-empty | 2026-07-28 | fail | MRTR-011 | flags | input-required-empty | — | agree |
draft/violations/mrtr-015-retry-without-input-responses | 2026-07-28 | fail | MRTR-015 | silent | — | — | beyond mcpsnoop |
draft/violations/mrtr-016-parallel-retry-alters-state | 2026-07-28 | fail | MRTR-003, MRTR-016, MRTR-017 | flags | request-state-changed | — | agree |
draft/violations/mrtr-016-request-state-not-echoed | 2026-07-28 | fail | MRTR-003, MRTR-016, MRTR-017 | flags | request-state-changed | — | agree |
draft/violations/mrtr-018-unsolicited-request-state | 2026-07-28 | fail | MRTR-018 | flags | request-state-invented | — | agree |
draft/violations/mrtr-019-retry-reuses-id | 2026-07-28 | fail | MRTR-019, TOOL-023 | silent | — | — | beyond mcpsnoop |
draft/violations/mrtr-020-request-state-on-another-method | 2026-07-28 | fail | MRTR-020 | silent | — | — | beyond mcpsnoop |
draft/violations/mrtr-024-shortfall-answered-with-error | 2026-07-28 | fail | MRTR-015, MRTR-024 | silent | — | — | beyond mcpsnoop |
draft/violations/prog-001-progress-token-not-integer | 2026-07-28 | fail | PROG-001, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/prog-002-progress-token-reused-in-flight | 2026-07-28 | fail | PROG-002 | silent | — | — | beyond mcpsnoop |
draft/violations/prog-003-progress-value-decreases | 2026-07-28 | fail | PROG-003 | flags | progress-not-increasing | — | agree |
draft/violations/prog-008-progress-after-completion | 2026-07-28 | fail | PROG-005, PROG-008 | flags | progress-after-completion | — | agree |
draft/violations/prom-012-prompts-undeclared | 2026-07-28 | fail | PROM-012 | silent | — | — | beyond mcpsnoop |
draft/violations/prom-013-declared-prompts-list-unimplemented | 2026-07-28 | fail | PROM-013 | silent | — | — | beyond mcpsnoop |
draft/violations/res-012-resources-undeclared | 2026-07-28 | fail | RES-012 | silent | — | — | beyond mcpsnoop |
draft/violations/res-013-declared-resources-list-unimplemented | 2026-07-28 | fail | RES-013 | silent | — | — | beyond mcpsnoop |
draft/violations/res-022-read-empty-contents | 2026-07-28 | fail | RES-022 | silent | — | — | beyond mcpsnoop |
draft/violations/root-013-answered-without-capability | 2026-07-28 | fail | ROOT-013 | silent | — | — | beyond mcpsnoop |
draft/violations/root-014-root-not-file-uri | 2026-07-28 | fail | ROOT-014 | silent | — | — | beyond mcpsnoop |
draft/violations/root-018-asked-without-capability | 2026-07-28 | fail | BASE-034, MRTR-012, MRTR-013, ROOT-018 | flags | input-request-undeclared | — | agree |
draft/violations/samp-026-tools-without-sampling-tools | 2026-07-28 | fail | BASE-034, MRTR-012, MRTR-013, SAMP-026 | silent | — | — | beyond mcpsnoop |
draft/violations/samp-027-answered-without-capability | 2026-07-28 | fail | SAMP-027 | silent | — | — | beyond mcpsnoop |
draft/violations/samp-028-include-context-deprecated | 2026-07-28 | pass-with-warnings | SAMP-028 | silent | — | — | beyond mcpsnoop |
draft/violations/samp-029-include-context-undeclared | 2026-07-28 | pass-with-warnings | SAMP-028, SAMP-029 | silent | — | — | beyond mcpsnoop |
draft/violations/samp-030-tool-results-mixed | 2026-07-28 | fail | SAMP-030, SAMP-031, SAMP-042 | silent | — | — | beyond mcpsnoop |
draft/violations/samp-031-tool-use-unanswered | 2026-07-28 | fail | SAMP-031, SAMP-042 | silent | — | — | beyond mcpsnoop |
draft/violations/samp-032-required-tool-choice-ignored | 2026-07-28 | fail | SAMP-032 | silent | — | — | beyond mcpsnoop |
draft/violations/samp-033-none-tool-choice-ignored | 2026-07-28 | fail | SAMP-033 | silent | — | — | beyond mcpsnoop |
draft/violations/samp-034-message-without-role | 2026-07-28 | fail | SAMP-034, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/skill-003-directory-read-not-implemented | 2026-07-28 | pass | SKILL-003 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-004-directory-read-undeclared | 2026-07-28 | pass | SKILL-004 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-005-skills-without-resources | 2026-07-28 | pass | SKILL-005 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-007-entry-uri-not-skill-md | 2026-07-28 | pass | SKILL-007 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-008-skill-md-without-frontmatter | 2026-07-28 | pass | SKILL-008 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-011-skill-under-other-scheme | 2026-07-28 | pass | SKILL-011 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-012-path-not-ending-in-name | 2026-07-28 | pass | SKILL-012, SKILL-027 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-013-name-breaks-naming-rules | 2026-07-28 | pass | SKILL-013 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-014-authority-not-reg-name | 2026-07-28 | pass | SKILL-014 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-015-prefix-segment-invalid | 2026-07-28 | pass | SKILL-015 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-018-skill-md-not-markdown | 2026-07-28 | pass | SKILL-018 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-019-resource-name-not-skill-name | 2026-07-28 | pass | SKILL-019 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-020-resource-description-differs | 2026-07-28 | pass | SKILL-020 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-026-entry-frontmatter-differs | 2026-07-28 | pass | SKILL-026 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-029-listing-names-collide | 2026-07-28 | pass | SKILL-029 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-034-entry-without-resources | 2026-07-28 | pass | SKILL-034 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-035-manifest-without-skill-md | 2026-07-28 | pass | SKILL-035 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-036-manifest-file-outside-skill | 2026-07-28 | pass | SKILL-036 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-037-manifest-entry-without-size | 2026-07-28 | pass | SKILL-037 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-042-skill-beyond-limits | 2026-07-28 | pass | SKILL-042 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-044-list-not-implemented | 2026-07-28 | pass | SKILL-002, SKILL-044 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-045-list-result-type-not-complete | 2026-07-28 | pass | SKILL-045 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-047-list-without-cache-fields | 2026-07-28 | pass | SKILL-047 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-048-get-not-implemented | 2026-07-28 | pass | SKILL-002, SKILL-048 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-049-get-result-type-not-complete | 2026-07-28 | pass | SKILL-049 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-050-get-names-a-directory | 2026-07-28 | pass | SKILL-050 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-051-unknown-skill-wrong-error | 2026-07-28 | pass | SKILL-051, SKILL-071 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-053-get-without-cache-fields | 2026-07-28 | pass | SKILL-053 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-065-directory-result-type-not-complete | 2026-07-28 | pass | SKILL-065 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-066-directory-read-of-a-file-answered | 2026-07-28 | pass | SKILL-066, SKILL-071 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-067-skill-directory-refused | 2026-07-28 | pass | SKILL-067 | silent | — | — | beyond mcpsnoop |
draft/violations/skill-100-directory-result-without-cache-fields | 2026-07-28 | pass | SKILL-100 | silent | — | — | beyond mcpsnoop |
draft/violations/subs-001-cancelled-for-another-request | 2026-07-28 | fail | CANC-003, CANC-011, SUBS-001 | flags | server-cancellation | — | agree |
draft/violations/subs-001-unrequested-notification-type | 2026-07-28 | fail | SUBS-001, wire-schema | flags | subscription-unrequested | — | agree |
draft/violations/subs-002-notification-before-acknowledgment | 2026-07-28 | fail | SUBS-002, wire-schema | flags | subscription-before-ack | — | agree |
draft/violations/subs-002-reused-id-first-opening-unacknowledged | 2026-07-28 | fail | SUBS-002 | flags | subscription-before-ack | — | agree |
draft/violations/subs-006-graceful-close-result-not-empty | 2026-07-28 | pass-with-warnings | SUBS-005, SUBS-006 | silent | — | — | beyond mcpsnoop |
draft/violations/task-001-task-for-undeclaring-client | 2026-07-28 | pass | TASK-001 | flags | task-for-undeclaring-client | — | agree |
draft/violations/task-006-task-without-result-type | 2026-07-28 | fail | TASK-006, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/task-007-task-result-type-without-task | 2026-07-28 | pass | TASK-007 | silent | — | — | beyond mcpsnoop |
draft/violations/task-015-get-result-not-a-task | 2026-07-28 | pass | TASK-015 | silent | — | — | beyond mcpsnoop |
draft/violations/task-015-input-required-without-requests | 2026-07-28 | pass | TASK-015 | silent | — | — | beyond mcpsnoop |
draft/violations/task-017-get-result-type-not-complete | 2026-07-28 | pass | TASK-017, TASK-028, TASK-032 | silent | — | — | beyond mcpsnoop |
draft/violations/task-022-input-request-key-reused | 2026-07-28 | pass | TASK-022, TASK-023 | silent | — | — | beyond mcpsnoop |
draft/violations/task-024-update-ack-not-empty | 2026-07-28 | pass | TASK-024, TASK-030 | silent | — | — | beyond mcpsnoop |
draft/violations/task-026-input-answered-twice | 2026-07-28 | pass | TASK-026 | silent | — | — | beyond mcpsnoop |
draft/violations/task-029-task-cancelled-by-notification | 2026-07-28 | pass | TASK-029 | silent | — | — | beyond mcpsnoop |
draft/violations/task-033-task-listen-without-declaration | 2026-07-28 | pass | TASK-033, TASK-038 | silent | — | — | beyond mcpsnoop |
draft/violations/task-034-progress-on-task-stream | 2026-07-28 | fail | LOG-007, LOG-008, LOG-009, SUBS-001, TASK-034 | silent | — | — | beyond mcpsnoop |
draft/violations/task-035-mcp-name-missing | 2026-07-28 | pass | TASK-035 | silent | — | — | beyond mcpsnoop |
draft/violations/task-039-get-without-declaration | 2026-07-28 | pass | TASK-039 | silent | — | — | beyond mcpsnoop |
draft/violations/task-041-failed-without-status-message | 2026-07-28 | pass | TASK-041 | silent | — | — | beyond mcpsnoop |
draft/violations/task-042-failed-without-error | 2026-07-28 | pass | TASK-015, TASK-042, TASK-045 | silent | — | — | beyond mcpsnoop |
draft/violations/task-043-tool-error-reported-failed | 2026-07-28 | pass | TASK-008, TASK-043, TASK-044 | silent | — | — | beyond mcpsnoop |
draft/violations/task-046-completed-without-result | 2026-07-28 | pass | TASK-015, TASK-044, TASK-046 | silent | — | — | beyond mcpsnoop |
draft/violations/tool-019-tools-undeclared | 2026-07-28 | fail | TOOL-019 | silent | — | — | beyond mcpsnoop |
draft/violations/tool-020-declared-tools-list-unimplemented | 2026-07-28 | fail | TOOL-020 | silent | — | — | beyond mcpsnoop |
draft/violations/tool-022-tools-list-order-changes | 2026-07-28 | pass-with-warnings | TOOL-022 | silent | — | — | beyond mcpsnoop |
draft/violations/tool-034-mirrored-integer-out-of-range | 2026-07-28 | fail | TOOL-034, TRAN-081 | silent | — | method-name-header-missing, param-header-missing, protocol-version-header-missing | beyond mcpsnoop |
draft/violations/tool-038-embedded-resource-undeclared | 2026-07-28 | pass-with-warnings | TOOL-038 | silent | — | — | beyond mcpsnoop |
draft/violations/tool-040-structured-content-not-conforming | 2026-07-28 | fail | TOOL-040 | silent | — | — | beyond mcpsnoop |
draft/violations/tool-045-x-mcp-header-in-defs | 2026-07-28 | fail | TOOL-045, TRAN-082, TRAN-137 | flags | x-mcp-header-chain | method-name-header-missing, protocol-version-header-missing | agree |
draft/violations/tran-058-mcp-name-mismatch-refused-without-id | 2026-07-28 | fail | BASE-052, TRAN-058, TRAN-141, wire-schema | flags | name-header-disagrees, response-unmatched | — | agree |
draft/violations/tran-058-request-metadata-headers-missing | 2026-07-28 | fail | TRAN-058, TRAN-141 | flags | method-name-header-missing | — | agree |
draft/violations/tran-060-client-posts-a-response | 2026-07-28 | fail | BASE-046, CACH-001, TRAN-060, TRAN-119, wire-schema | flags | cacheable-without-ttl, client-sent-response, response-unmatched | — | agree |
draft/violations/tran-061-notification-answered-200 | 2026-07-28 | fail | TRAN-061 | silent | — | — | beyond mcpsnoop |
draft/violations/tran-063-error-without-content-type | 2026-07-28 | fail | TRAN-063 | flags | response-content-type | — | agree |
draft/violations/tran-066-independent-server-request | 2026-07-28 | fail | BASE-084, MRTR-001, TRAN-066, TRAN-120 | flags | server-sent-request | — | agree |
draft/violations/tran-068-sse-media-type-mixed-case | 2026-07-28 | pass-with-warnings | TRAN-068 | silent | — | — | beyond mcpsnoop |
draft/violations/tran-068-sse-without-accel-buffering | 2026-07-28 | pass-with-warnings | TRAN-068 | silent | — | — | beyond mcpsnoop |
draft/violations/tran-070-message-after-stream-close | 2026-07-28 | fail | TRAN-070 | silent | — | — | beyond mcpsnoop |
draft/violations/tran-071-protocol-version-header-missing | 2026-07-28 | fail | CACH-001, TRAN-071, wire-schema | flags | cacheable-without-ttl, protocol-version-header-missing | — | agree |
draft/violations/tran-072-mismatch-rejected-without-id | 2026-07-28 | fail | BASE-052, TRAN-072, wire-schema | flags | protocol-version-header-disagrees, response-unmatched | — | agree |
draft/violations/tran-072-protocol-version-header-mismatched | 2026-07-28 | fail | TRAN-072 | flags | protocol-version-header-disagrees, server-rejected-header-mismatch | — | agree |
draft/violations/tran-072-unsupported-body-version-header-mismatch | 2026-07-28 | fail | TRAN-072 | flags | protocol-version-header-disagrees, server-rejected-header-mismatch | — | agree |
draft/violations/tran-072-unsupported-body-version-refused-as-unsupported | 2026-07-28 | fail | TRAN-072 | flags | protocol-version-header-disagrees | — | agree |
draft/violations/tran-073-header-mismatch-not-rejected | 2026-07-28 | fail | CACH-001, TRAN-072, TRAN-073, wire-schema | flags | cacheable-without-ttl, protocol-version-header-disagrees | — | agree |
draft/violations/tran-073-mismatch-refused-without-id | 2026-07-28 | fail | BASE-052, TRAN-072, TRAN-073, wire-schema | flags | protocol-version-header-disagrees, response-unmatched | — | agree |
draft/violations/tran-074-unsupported-version-accepted | 2026-07-28 | fail | CACH-001, TOOL-019, TRAN-074, VERS-001, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/tran-074-unsupported-version-without-400 | 2026-07-28 | fail | TRAN-074 | silent | — | — | beyond mcpsnoop |
draft/violations/tran-074-unsupported-version-without-supported-list | 2026-07-28 | fail | TRAN-074, VERS-001, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/tran-075-method-not-found-not-404 | 2026-07-28 | fail | TRAN-075 | silent | — | — | beyond mcpsnoop |
draft/violations/tran-077-mcp-name-unencoded | 2026-07-28 | fail | CACH-001, TRAN-077, TRAN-086, TRAN-087, wire-schema | flags | cacheable-without-ttl | — | agree |
draft/violations/tran-077-param-header-value-rejected | 2026-07-28 | fail | TRAN-077, TRAN-086, TRAN-087 | flags | server-rejected-header-mismatch | — | agree |
draft/violations/tran-079-missing-param-header-rejected | 2026-07-28 | fail | TRAN-079, TRAN-130 | flags | param-header-missing, server-rejected-header-mismatch | — | agree |
draft/violations/tran-079-param-header-value-not-mirrored | 2026-07-28 | fail | TRAN-079 | flags | param-header-disagrees, server-rejected-header-mismatch | — | agree |
draft/violations/tran-079-x-mcp-header-not-mirrored | 2026-07-28 | fail | TRAN-079, TRAN-130, TRAN-136 | flags | param-header-missing | — | agree |
draft/violations/tran-080-x-mcp-header-name-invalid | 2026-07-28 | fail | TOOL-033, TRAN-080 | flags | x-mcp-header-repeated | — | agree |
draft/violations/tran-082-invalid-tool-excluded-valid-called | 2026-07-28 | fail | TOOL-045, TRAN-082, TRAN-137 | flags | x-mcp-header-chain | — | agree |
draft/violations/tran-082-x-mcp-header-under-any-of | 2026-07-28 | fail | TOOL-045, TRAN-082, TRAN-137 | flags | x-mcp-header-chain | method-name-header-missing, protocol-version-header-missing | agree |
draft/violations/tran-083-invalid-tool-called | 2026-07-28 | fail | TOOL-035, TOOL-045, TOOL-046, TRAN-082, TRAN-083, TRAN-084, TRAN-137 | flags | x-mcp-header-chain | — | agree |
draft/violations/tran-089-sentinel-markers-miscased | 2026-07-28 | fail | TRAN-089 | flags | param-header-disagrees, server-rejected-header-mismatch | — | agree |
draft/violations/tran-092-sentinel-pattern-unencoded | 2026-07-28 | fail | CACH-001, TRAN-092, wire-schema | flags | cacheable-without-ttl, name-header-disagrees | — | agree |
draft/violations/tran-096-invalid-param-header-accepted | 2026-07-28 | fail | TRAN-077, TRAN-086, TRAN-087, TRAN-096 | silent | — | — | beyond mcpsnoop |
draft/violations/tran-097-header-body-mismatch-accepted | 2026-07-28 | fail | TRAN-079, TRAN-097, TRAN-100, TRAN-131 | flags | param-header-disagrees | — | agree |
draft/violations/tran-097-integer-header-different-number | 2026-07-28 | fail | TRAN-079, TRAN-097, TRAN-100, TRAN-131 | flags | param-header-disagrees | — | agree |
draft/violations/tran-097-mismatch-after-concurrent-posts | 2026-07-28 | fail | TRAN-058, TRAN-097, TRAN-100, TRAN-131, TRAN-141 | flags | name-header-disagrees | method-name-header-missing, protocol-version-header-missing | agree |
draft/violations/tran-098-header-mismatch-without-400 | 2026-07-28 | fail | TRAN-098, TRAN-102 | flags | server-rejected-header-mismatch | — | divergent |
draft/violations/tran-123-cancellation-without-request-id | 2026-07-28 | fail | CANC-005, TRAN-123, wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/tran-124-message-after-cancel-notification | 2026-07-28 | fail | TRAN-124 | silent | — | — | beyond mcpsnoop |
draft/violations/tran-132-null-parameter-header-sent | 2026-07-28 | fail | TRAN-132 | flags | param-header-without-value | — | agree |
draft/violations/tran-133-omitted-null-header-rejected | 2026-07-28 | fail | TRAN-133 | flags | server-rejected-header-mismatch | — | divergent |
draft/violations/tran-134-absent-parameter-header-sent | 2026-07-28 | fail | TRAN-134 | flags | param-header-without-value | — | agree |
draft/violations/tran-135-omitted-absent-header-rejected | 2026-07-28 | fail | TRAN-135 | flags | server-rejected-header-mismatch | — | divergent |
draft/violations/tran-136-missing-param-header-accepted | 2026-07-28 | fail | TRAN-079, TRAN-130, TRAN-136 | flags | param-header-missing | — | agree |
draft/violations/tran-137-x-mcp-header-under-items | 2026-07-28 | fail | TOOL-045, TRAN-082, TRAN-137 | flags | x-mcp-header-chain | method-name-header-missing, protocol-version-header-missing | agree |
draft/violations/tran-141-mcp-name-header-missing | 2026-07-28 | fail | TRAN-058, TRAN-141 | flags | method-name-header-missing | — | agree |
draft/violations/vers-002-retry-with-unsupported-version | 2026-07-28 | pass-with-warnings | VERS-002 | silent | — | — | beyond mcpsnoop |
draft/violations/vers-004-extension-identifier-without-prefix | 2026-07-28 | fail | VERS-004 | silent | — | — | beyond mcpsnoop |
draft/violations/vers-008-initialize-error-without-versions | 2026-07-28 | fail | BASE-030, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, VERS-008 | silent | — | — | divergent |
draft/violations/wire-schema-cache-scope-not-in-enum | 2026-07-28 | fail | wire-schema | flags | cache-scope-value | — | agree |
draft/violations/wire-schema-content-block-unknown-type | 2026-07-28 | fail | wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/wire-schema-resource-missing-uri | 2026-07-28 | fail | wire-schema | silent | — | — | beyond mcpsnoop |
draft/violations/wire-schema-tool-image-data-not-base64 | 2026-07-28 | fail | wire-schema | silent | — | — | beyond mcpsnoop |
draft/captured/official-suite-2026-07-28-scenarios | 2026-07-28 | pass-with-warnings | BASE-037 | silent | — | — | beyond mcpsnoop |
draft/captured/official-suite-2026-07-28-stateless | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/captured/probe-2026-07-28-http | 2026-07-28 | fail | BASE-030, BASE-033, COMP-006, MRTR-002, PAGE-006, PROM-011, RES-011, TOOL-016, TRAN-071, TRAN-072, TRAN-079, TRAN-130, wire-schema | flags | param-header-missing, protocol-version-header-disagrees, request-meta-missing, server-rejected-header-mismatch | — | agree |
draft/captured/reference-host-2026-07-28-http | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/captured/reference-host-2026-07-28-stdio | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/captured/sdk-go-2026-07-28 | 2026-07-28 | fail | BASE-034, ELIC-059, MRTR-012, MRTR-013, TRAN-068 | flags | input-request-undeclared | — | agree |
draft/captured/sdk-python-2026-07-28 | 2026-07-28 | pass | — | silent | — | — | both clean |
draft/captured/sdk-rust-2026-07-28 | 2026-07-28 | pass-with-warnings | BASE-037 | silent | — | — | beyond mcpsnoop |
draft/captured/sdk-typescript-v2-2026-07-28 | 2026-07-28 | pass | — | 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.
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.
Judged by base.jsonrpc-version.
BASE-043
MUST · binds the client and server
Requests MUST include a string or integer ID.
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.
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.
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.
Judged by base.result-id-matches.
BASE-047
MUST · binds the client and server
Result responses MUST include a
resultfield.
Judged by base.result-field.
BASE-048
MUST · binds the client and server
The
resultMUST include aresultTypefield to indicate the type of the result.
Judged by base.result-type-present.
BASE-049
MUST · binds the client
The set of supported
ResultTypevalues MUST be created from the set defined in the core protocol and include any additional values of supported extensions that are advertised via capabilities.
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
resultTypeof any value unrecognized by the client MUST be considered invalid.
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 absentresultTypeas"complete".
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).
Judged by base.error-id-matches.
BASE-053
MUST · binds the client and server
Error responses MUST include an
errorfield with acodeandmessage.
Judged by base.error-shape.
BASE-054
MUST · binds the client and server
Error codes MUST be integers.
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
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.
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.
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.
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:
Judged by base.error-code-withdrawn.
BASE-059
SHOULD · binds the client
Clients SHOULD still accept
-32002from servers implementing earlier versions.
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 (
-32768to-32000); the remainder of the integer space is available for application-defined errors.
Judged by base.error-code-application-range.
BASE-061
MUST NOT · binds the client and server
The receiver MUST NOT send a response.
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.
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).
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.
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.
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.
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.
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.
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:
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
$schemafield 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
Judged by base.schemas-valid-for-dialect.
BASE-074
MUST NOT · binds the client and server
Implementations MUST NOT automatically dereference
$refvalues that resolve to a network URI.
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.
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
$refSHOULD be rejected rather than silently treated as permissive.
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.
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.
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 (/).
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 (
-).
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 thanexample.com/).
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]).
Judged by base.meta-key-format.
BASE-030
MUST · binds the client
Client requests carry the following
io.modelcontextprotocol/*fields inmeta; fields marked as required MUST be included on every request.
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).
Judged by meta.missing-required-field-rejected.
BASE-032
MUST · binds the server
On HTTP, the response status MUST be
400 Bad Request.
Judged by meta.missing-required-field-http-status.
BASE-033
SHOULD · binds the client
Clients SHOULD include
io.modelcontextprotocol/clientInfoon every request unless specifically configured not to do so.
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.
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 aMissingRequiredClientCapabilityError(-32021) whosedata.requiredCapabilitieslists the missing capabilities.
Judged by meta.missing-capability-error.
BASE-036
MUST · binds the server
On HTTP, the response status MUST be
400 Bad Request.
Judged by meta.missing-capability-http-status.
BASE-037
SHOULD · binds the server
Servers SHOULD include the following
io.modelcontextprotocol/*field in every result’smeta, unless specifically configured not to do so, to identify themselves without relying on any prior connection state:
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.
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 includeio.modelcontextprotocol/subscriptionIdinmetaso the client can correlate the notification with the originating subscription request.
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.
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:
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(andimage/jpg) - JPEG images (safe, universal compatibility) Clients that support rendering icons SHOULD also support:
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:
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.
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.
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
Originheader 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
Originheader 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
Acceptheader listing bothapplication/jsonandtext/event-streamas 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 Acceptedwith 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) orContent-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: noheader 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-Versionheader.
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-28The header value MUST match theio.modelcontextprotocol/protocolVersionfield carried in the request body’smeta.
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 Requestand aHeaderMismatchJSON-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 Requestand 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 Foundand 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-Namesource 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-headeris 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-headerannotations, 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-headervalues: 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 allx-mcp-headervalues in theinputSchema; 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-headervalues: 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-headervalue 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-NameorMcp-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/callrequest 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
HeaderMismatcherror because requiredMcp-Param-*headers are missing or do not match the body, the client SHOULD calltools/listto check for changes to the tool’sinputSchema, 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-headervalues: MUST only be applied to properties that are statically reachable from the schema root: reachable via a chain consisting solely ofpropertieskeys.
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 RequestHTTP 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.0and42are 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 Requestand 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-NameandMcp-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-Versionheader 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 use400for [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/discoverlets a client query a server’s supported protocol versions, capabilities, and identity before sending any other requests. Servers MUST implement it.
Judged by discover.implemented.
DISC-002
SHOULD · binds the client
A client that supports both modern (per-request
meta) and legacy (initializehandshake) servers SHOULD sendserver/discoverfirst; see stdio: Backward Compatibility for the fallback rules.
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
serverInfois 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
UnsupportedProtocolVersionErrorlisting 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
supportedlist 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
metakey 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
initializerequest, 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/discoverfirst 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.
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
stderroutput and SHOULD NOT assumestderroutput indicates error conditions.
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
stdoutthat is not a valid MCP message.
Judged by transport.stdio-server-output-valid.
TRAN-118
MUST NOT · binds the client
The client MUST NOT write anything to the server’s
stdinthat is not a valid MCP message.
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.
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/subscriptionIdfield inmeta; 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/cancellednotification 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
initializehandshake SHOULD probe with [server/discover][server-discover] before sending any other request, setting its preferred modern version inmeta.
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-
initializerequests with implementation-defined errors (commonly-32601or-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
initializeand would process an era-ambiguous method (such astools/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, orelicitation/create) using the MRTR pattern.
Judged by transport.no-independent-server-requests.
MRTR-002
MUST · binds the client
Every request MUST include the required
metafields; seemeta.
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
InputRequiredResultresponses on any other client requests.
basic/patterns/mrtr#supported-requests
Judged by mrtr.input-required-supported-methods.
MRTR-005
MUST · binds the server
inputRequestskeys 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
inputRequestsvalues are request objects that MUST be one ofElicitRequest,CreateMessageRequest, orListRootsRequest
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
requestStatefield, servers MUST treatrequestStateas 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
requestStateinfluences 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
requestStatepayload 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
requestStatemust 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
inputRequestsorrequestStatein everyInputRequiredResultresponse.
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
inputRequeststhat 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 anyelicitation/createrequests in theinputRequestsfield.
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
inputRequestsor 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
InputRequiredResultthat contains theinputRequestsfield, 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
InputRequiredResultcontains therequestStatefield, 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
requestStatecontents.
basic/patterns/mrtr#client-requirements-basic-workflow
Judged by mrtr.request-state-echoed.
MRTR-018
MUST NOT · binds the client
If the
InputRequiredResultdoes not contain arequestStatefield, 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
idMUST 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
InputResponsesobject 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
InputResponsesobject, 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 newInputRequiredResultrequesting 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/acknowledgedas the first message carrying the subscription’s ID inmetaunderio.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/listenresponse 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/listenrequest 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/listento 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.
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/cancelledreferencing asubscriptions/listenrequest ID when it tears down that subscription stream
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/cancelledfor any other purpose.
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/cancellednotification 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/listenrequest, 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
progressvalue 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
messagefield 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
inputResponsesorrequestState—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
ttlMsis0, 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
ttlMsis 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
ttlMsis absent, clients SHOULD assume a default of0(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
ttlMsis negative, clients SHOULD ignore it and treat it as0.
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
ttlMsvalue 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
cacheScopeto 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/listresponse hascacheScope: "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"cacheScopemay 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
cacheScopealone 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
metafields; seemeta.
Judged by meta.required-request-fields.
COMP-007
MUST · binds the server
Servers that support completions MUST declare the
completionscapability:
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
metafields; seemeta.
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
nextCursoras 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
stderrfor stdio transports, or to OpenTelemetry for structured observability.
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
loggingcapability:
server/utilities/logging#capabilities
Judged by capabilities.logging-declared.
LOG-008
MUST NOT · binds the server
The server MUST NOT emit
notifications/messagefor 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/messageis request-scoped: the server MUST NOT deliver it on asubscriptions/listenstream 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/logLevelvalue carried in a request’smetais 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
metafields; seemeta.
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
toolscapability:
Judged by capabilities.tools-declared.
TOOL-020
MUST · binds the server
Servers that declare the
toolscapability MUST respond totools/listrequests with the set of tools currently available to the requesting client.
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.
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).
Judged by tools.deterministic-order.
TOOL-023
MUST · binds the client
Note that the JSON-RPC
idMUST 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
listChangedcapability SHOULD send a notification to clients that have opened asubscriptions/listenstream withtoolsListChanged: 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
$schemafield is present MUST be a valid JSON Schema object (notnull) For tools with no parameters, use one of these valid approaches:
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.
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).
Judged by tools.name-length.
TOOL-028
SHOULD · binds the client and server
Tool names SHOULD be considered case-sensitive.
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.
Judged by tools.name-charset.
TOOL-030
SHOULD · binds the server
Tool names SHOULD be unique within a server.
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
searchtool) and SHOULD implement a disambiguation strategy such as prefixing tool names with a server identifier.
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(fromserverInfo) is not guaranteed to be unique across servers and SHOULD NOT be relied upon for disambiguation.
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-headervalues: 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 allx-mcp-headervalues in theinputSchema; MUST only be applied to parameters with primitive types (integer, string, boolean).
Judged by transport.x-mcp-header-name-valid.
TOOL-034
MUST · binds the client
Constraints on
x-mcp-headervalues: Integer values MUST be within the safe range for integers represented using IEEE754 double-precision floating point numbers (−253+1 to 253−1)
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.
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.
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.
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
resourcescapability:
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.
Judged by tools.output-schema-structured-result.
TOOL-041
SHOULD · binds the client
Clients SHOULD validate structured results against this 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.
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
$refresolution requirements when validating tool inputs and outputs againstinputSchemaandoutputSchema; 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-headervalues: 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
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-headervalue violates these constraints.
Judged by transport.x-mcp-header-invalid-tool-not-called.
RES
RES-011
MUST · binds the client
Every request MUST include the required
metafields; seemeta.
Judged by meta.required-request-fields.
RES-012
MUST · binds the server
Servers that support resources MUST declare the
resourcescapability:
Judged by capabilities.resources-declared.
RES-013
MUST · binds the server
Servers that declare the
resourcescapability MUST respond toresources/listrequests with the set of resources currently available to the requesting client.
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.
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
listChangedcapability 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.
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.
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
-32603for 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
-32002as 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
contentsarray 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
metafields; seemeta.
Judged by meta.required-request-fields.
PROM-012
MUST · binds the server
Servers that support prompts MUST declare the
promptscapability in theirDiscoverResult:
Judged by capabilities.prompts-declared.
PROM-013
MUST · binds the server
Servers that declare the
promptscapability MUST respond toprompts/listrequests with the set of prompts currently available to the requesting client.
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.
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
listChangedcapability SHOULD send a notification to clients that have opened asubscriptions/listenstream withpromptsListChanged: 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.
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.
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:
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.
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
elicitationcapability in_meta.io.modelcontextprotocol/clientCapabilitieson each request:
client/elicitation#capabilities
Judged by elicitation.client-declares-capability.
ELIC-058
MUST · binds the client
Clients declaring the
elicitationcapability MUST support at least one mode (formorurl).
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
modefield 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 themodefield, 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", amessage, and include these additional parameters:
client/elicitation#url-mode-elicitation-requests
Judged by elicitation.url-parameters.
ELIC-065
MUST · binds the server
The
urlparameter 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.
subclaim)
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
urlfield 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.
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.
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.
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
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.
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.toolscapability 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.toolscapability.
client/sampling#tools-in-sampling
Judged by sampling.tools-declared.
SAMP-027
MUST · binds the client
Clients that support sampling MUST declare the
samplingcapability in_meta.io.modelcontextprotocol/clientCapabilitieson each request:
Judged by sampling.client-declares-capability.
SAMP-028
SHOULD · binds the server
Servers SHOULD avoid using these values (e.g. can just omit
includeContextsince it defaults to"none")
Judged by sampling.include-context-avoided.
SAMP-029
SHOULD NOT · binds the server
and SHOULD NOT use them unless the client declares
sampling.contextcapability.
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
ToolUseContentblocks MUST be followed by a user message that consists entirely ofToolResultContentblocks, with each tool use (e.g. withid: $id) matched by a corresponding tool result (withtoolUseId: $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
rolefield of"user"or"assistant"; and acontentfield representing the message data.
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.
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
maxTokensparameter.
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", eachToolUseContentitem is responded to with aToolResultContentitem with a matchingtoolUseId, 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
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.
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
rootscapability in_meta.io.modelcontextprotocol/clientCapabilitieson each request:
Judged by roots.client-declares-capability.
ROOT-014
MUST · binds the client
uri: Unique identifier for the root. This MUST be afile://URI in the current specification.
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
scopeparameter in theWWW-Authenticateheader 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
issuervalue 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 thestatevalue, 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
issparameter 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
issparameter MUST advertise this by settingauthorization_response_iss_parameter_supportedtotruein 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
issfrom 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
issvalue from theapplication/x-www-form-urlencodedresponse 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, orerror_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
resourceparameter: 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) andhttps://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_tokenin theirgrant_typesclient 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_accessinWWW-Authenticatescope or Protected Resource Metadatascopes_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 Forbiddenstatus code (per RFC 6750 Section 3.1);WWW-Authenticateheader with theBearerscheme 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
scopeparameter, 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_serversfield 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:
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-Authenticateheaders when present; otherwise, they MUST fall back to constructing and requesting the well-known URIs in the order listed above.
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-Authenticateheaders and respond appropriately toHTTP 401 Unauthorizedresponses from the MCP server.
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
issuervalue 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 fromhttps://attacker.example/.well-known/oauth-authorization-serverthat 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_idURL 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 theclient_idvalue 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_idmatches 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_typeduring 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 useapplication_type: "native"; Web applications (remote browser-based applications served from a non-local host) SHOULD useapplication_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
issueridentifier.
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
resourceparameter 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
localhostor 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
S256code 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_supportedis 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_supportedin 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_supportedin 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
resourceparameter 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.
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.
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.
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.
Judged by base.result-id-matches.
BASE-005
MUST NOT · binds the client and server
Notifications MUST NOT include an ID.
Judged by base.notification-no-id.
BASE-006
MUST · binds the client and server
Error responses MUST include an
errorfield with acodeandmessage.
Judged by base.error-shape.
BASE-007
MUST · binds the client and server
Error codes MUST be integers.
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.
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).
Judged by base.error-id-matches.
BASE-010
MUST · binds the client and server
Result responses MUST include a
resultfield.
Judged by base.result-field.
BASE-011
MUST NOT · binds the client and server
The receiver MUST NOT send a response.
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.
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
$schemafield
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
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.
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 (-)
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
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 thanexample.com/).
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:
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:
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.
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
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.
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.
Judged by base.schemas-use-2020-12.
BASE-087
SHOULD · binds the client
image/png- PNG images (safe, universal compatibility)image/jpeg(andimage/jpg) - JPEG images (safe, universal compatibility) Clients that support rendering icons SHOULD also support:
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
initializerequest 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
initializednotification 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
initializerequest.
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
initializednotification.
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
initializerequest, 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
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
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
SIGTERMif the server does not exit within a reasonable time; SendingSIGKILLif the server does not exit within a reasonable time afterSIGTERM
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.
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.
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.
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.
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.
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
Originheader 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.
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
stdoutthat is not a valid MCP message.
Judged by transport.stdio-server-output-valid.
TRAN-005
MUST NOT · binds the client
The client MUST NOT write anything to the server’s
stdinthat is not a valid MCP message.
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
Originheader 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-Idis returned by the server during initialization, clients using the Streamable HTTP transport MUST include it in theMCP-Session-Idheader 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-Idheader (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 newInitializeRequestwithout 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-Versionheader, 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 version2025-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 with400 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
stderroutput and SHOULD NOT assumestderroutput indicates error conditions.
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.
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
Acceptheader, listing bothapplication/jsonandtext/event-streamas 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, orContent-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
datafield in order to prime the client to reconnect (using that event ID asLast-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
retryfield 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
retryfield, 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
Acceptheader, listingtext/event-streamas 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-streamin 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-IDto 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-IDheader 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-Idheader, 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
retryfield 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
progressvalue 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
messagefield 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
progressTokenprovided in the original request MUST continue to be used for progress notifications throughout the task’s lifetime, even after theCreateTaskResulthas 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
progressTokenthat 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, orcancelled)
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
initializerequest 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/cancelrequest MUST be used instead of thenotifications/cancellednotification.
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
toolscapability:
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
listChangedcapability 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 (notnull)
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.
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).
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 (.)
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.
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.
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
resourcescapability:
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.
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.
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.
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.
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
resourcescapability:
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
listChangedcapability 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.
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.
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
promptscapability during initialization:
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
listChangedcapability 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.
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.
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.
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)
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
loggingcapability:
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
completionscapability:
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
nextCursoras 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
elicitationcapability during initialization:
client/elicitation#capabilities
Judged by elicitation.client-declares-capability.
ELIC-005
MUST · binds the client
Clients declaring the
elicitationcapability MUST support at least one mode (formorurl).
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
modefield 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 themodefield, 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", amessage, 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
urlparameter 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
elicitationIdestablished in the originalelicitation/createrequest.
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
elicitationIdproperty.
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/createrequest 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.
subclaim)
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
urlfield 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.
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.
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.
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.toolscapability 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.toolscapability.
client/sampling#tools-in-sampling
Judged by sampling.tools-declared.
SAMP-005
MUST · binds the client
Clients that support sampling MUST declare the
samplingcapability during initialization:
Judged by sampling.client-declares-capability.
SAMP-006
SHOULD · binds the server
Servers SHOULD avoid using these values (e.g. can just omit
includeContextsince it defaults to"none")
Judged by sampling.include-context-avoided.
SAMP-007
SHOULD NOT · binds the server
and SHOULD NOT use them unless the client declares
sampling.contextcapability.
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
ToolUseContentblocks MUST be followed by a user message that consists entirely ofToolResultContentblocks, with each tool use (e.g. withid: $id) matched by a corresponding tool result (withtoolUseId: $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", eachToolUseContentitem is responded to with aToolResultContentitem with a matchingtoolUseId, 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
rootscapability during initialization:
Judged by roots.client-declares-capability.
ROOT-002
MUST · binds the client
When roots change, clients that support
listChangedMUST 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 afile://URI in the current specification.
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)
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
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.
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
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).
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.
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:
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.
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_serversfield 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-Authenticateheaders 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
scopeparameter in theWWW-Authenticateheader 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-Authenticateheaders and respond appropriately toHTTP 401 Unauthorizedresponses 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
scopeparameter 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_idURL 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 theclient_idvalue 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_idmatches 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.
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
resourceparameter: 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) andhttps://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 Forbiddenstatus code (per RFC 6750 Section 3.1);WWW-Authenticateheader with theBearerscheme 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
scopeparameter.
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
resourceparameter 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
localhostor 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
S256code 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_supportedis 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_supportedin 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_supportedin 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
resourceparameter 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
CreateTaskResultto 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
CallToolResultorCreateTaskResultin 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
CreateTaskResultin 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
resultTypeto"task"when returning aCreateTaskResultso 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
resultTypeto"task"on result types other thanCreateTaskResult.
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 theerrorfield 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
CreateTaskResultuntil the task is durably created — that is, until atasks/getfor the returnedtaskIdwould 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
InputRequiredResultbefore creating a task) SHOULD resolve all MRTR exchanges synchronously before responding with aCreateTaskResult.
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
pollIntervalMsprovided 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/getrequest, the server MUST check the status of the task and respond accordingly: If the status isworking, the server MUST return a aTaskobject with statusworking.; If the status isinput_required, the server MUST return aTaskobject with statusinput_requiredand aninputRequestsfield defined in Multi Round-Trip Requests.; If the status iscompleted, the server MUST return aTaskobject with statuscompletedand aresultfield containing the final result of the task.; If the status iscancelled, the server MUST return aTaskobject with statuscancelled.; If the status isfailed, the server MUST return aTaskobject with statusfailedand 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
inputRequestsfield 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
resultTypefield MUST be set to"complete"on this object as it is the standard result shape for thetasks/getrequest.
specification/2026-07-28/tasks#response
Judged by tasks.results-complete.
TASK-018
SHOULD · binds the client
When a client observes a
tasks/getresponse (ornotifications/tasksnotification) withstatus: "input_required", the client SHOULD fulfill the outstanding requests ininputRequestsby sending one or moretasks/updaterequests with correspondinginputResponses.
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
inputRequestsas they would the equivalent standalone server-to-client request — for example, an elicitation request surfaced viainputRequestsis subject to the same trust model and user-facing behavior as a directelicitation/createrequest.
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
inputRequestskeys 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
inputRequestsMUST 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
taskIddoes 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
inputRequestskeys 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
inputResponsesresponses 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
resultTypefield MUST be set to"complete"onUpdateTaskResultas it is the standard result shape for thetasks/updaterequest.
specification/2026-07-28/tasks#response-1
Judged by tasks.results-complete.
TASK-029
MUST NOT · binds the client
The
notifications/cancellednotification 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
taskIddoes 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
resultTypefield MUST be set to"complete"onCancelTaskResultas it is the standard result shape for thetasks/cancelrequest.
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/tasksextension 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/progressandnotifications/messagenotifications MUST NOT be sent on thesubscriptions/listenstream 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, ortasks/cancelis sent over the Streamable HTTP transport, the client MUST set theMcp-Nameheader to the value ofparams.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 fortasks/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/updateandtasks/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 onsubscriptions/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, andtasks/cancelrequests.
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/getresponse SHOULD include astatusMessagefield 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
errorfield 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
failedstatus MUST NOT be used to represent non-JSON-RPC errors, such as a tool result that completed withisError: 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
completedstatus with the error details in theresultfield.
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
failedstatus and theerrorfield 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: truefor tool results), the task usescompletedstatus and theresultfield 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/listandskills/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: trueMUST implementresources/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/readagainst a server that has not declareddirectoryRead: 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
resourcescapability.
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.mdfile at its root.
specification/stable/skills.mdx
Judged by skills.entry-uri-is-skill-md.
SKILL-008
MUST · binds the server
SKILL.mdMUST begin with YAML frontmatter containing at minimum thenameanddescriptionfields 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’snameas declared in itsSKILL.mdfrontmatter.
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-nameper 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
mimeTypeSHOULD betext/markdown.
specification/stable/skills.mdx
Judged by skills.skill-md-mime-type.
SKILL-019
SHOULD · binds the server
nameSHOULD be set from thenamefield of theSKILL.mdYAML frontmatter.
specification/stable/skills.mdx
Judged by skills.skill-md-resource-name.
SKILL-020
SHOULD · binds the server
descriptionSHOULD be set from thedescriptionfield of theSKILL.mdYAML frontmatter.
specification/stable/skills.mdx
Judged by skills.skill-md-resource-description.
SKILL-021
SHOULD · binds the client and server
When
_metakeys are used for skill resources, implementations SHOULD use theio.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.mdread 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
urialone.
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
frontmatterobject MUST be identical in content to the frontmatter of theSKILL.mdit describes.
specification/stable/skills.mdx
Judged by skills.frontmatter-identical.
SKILL-027
MUST · binds the server
The final
<skill-path>segment of the entry’suriMUST equalfrontmatter.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
resourcesis REQUIRED on every skill entry and takes one of two forms: an array ofSkillResourceobjects enumerating the skill’s files (SKILL.mdand every supporting file), or the string"dynamic".
specification/stable/skills.mdx
Judged by skills.entry-resources-form.
SKILL-035
MUST · binds the server
When
resourcesis an array, it MUST be complete. It lists every file of the skill, each exactly once, including an entry whoseuriequals the skill’s top-leveluri.
specification/stable/skills.mdx
Judged by skills.manifest-lists-skill-md.
SKILL-036
MUST · binds the server
Each
uriMUST be the skill’sSKILL.mdor 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 thedigestcovers).
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
resourcesto 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
resourcesat 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/listmethod, which returns entries for the skills it serves.
specification/stable/skills.mdx
Judged by skills.list-implemented.
SKILL-045
MUST · binds the server
The
resultTypefield MUST be set to"complete"onListSkillsResult, as it is the standard result shape for theskills/listrequest.
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
ListSkillsResultextendsCacheableResult, sottlMsandcacheScopeare REQUIRED, as they are ontools/listandresources/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/getmethod, which returns the entry for a single skill named by the URI of itsSKILL.md.
specification/stable/skills.mdx
Judged by skills.get-implemented.
SKILL-049
MUST · binds the server
The
resultTypefield MUST be set to"complete"onGetSkillResult, as it is the standard result shape for theskills/getrequest.
specification/stable/skills.mdx
Judged by skills.get-result-complete.
SKILL-050
MUST · binds the client
params.uriMUST be the URI of a skill’sSKILL.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 coderesources/readuses 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/listresult.
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
GetSkillResultextendsCacheableResult, sottlMsandcacheScopeare REQUIRED, as they are onresources/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/readof aSKILL.mdthat 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’sdigestandsize.
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.mdfor which the host holds an entry (digest-verified when the entry’sresourcesis an array, unverifiable when it is"dynamic"), hosts MUST parse its YAML frontmatter and compare it field-by-field against the entry’sfrontmatter.
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
resultTypefield MUST be set to"complete"onReadResourceDirectoryResult, as it is the standard result shape for theresources/directory/readrequest.
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
ReadResourceDirectoryResultextendsCacheableResult, sottlMsandcacheScopeare REQUIRED, as they are onresources/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
directoryReadMUST 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/getwith aurithat does not identify a skill the server serves:-32602(Invalid params).;resources/directory/readwith aurithat does not exist or is not a directory resource:-32602(Invalid params).;resources/readof 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/readagainst 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-toolsfield 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
resourcesset (everyurianddigest) 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/listorskills/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.