mcp server
mcp-wire-lab
MCP learning lab: a wire tap and lessons for the Model Context Protocol
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 22, 2026mcp-wire-lab
A hands-on lab for learning the Model Context Protocol by watching it. A small tap sits between an MCP client and a real, pinned MCP server, records every byte that crosses the connection, and a viewer replays the recording with the spec section and the server source behind each frame. You learn by predicting what the wire will carry, recording it, and reading what it did.
It is for anyone who wants to understand MCP below the SDK: how a connection opens, what each message means, and where the spec and a real implementation agree or differ. Nothing here is a client or a server you would ship.
Start here
- Set up once (below): clone, then
scripts/bootstrap.sh. - Open lesson 01 and do the lessons in order. Each one has the same shape: Goal, Predict (write your answers down first), Run, Read the trace, Read the source, Experiments, Check yourself. Every command in a lesson is meant to be pasted at the repository root.
- If you only want to poke at it, jump to Record and read a trace below.
The answers to a lesson's Predict and Check yourself questions sit at the end of the same
file, inside a collapsed <details> block on GitHub. In a terminal or an editor they are plain
text, so stop reading at the heading Answers until you have written your own.
A lesson's commands print what the lesson says they print; scripts/gate.sh checks that.
After setup nothing needs a network, and the lessons write only inside this folder (setup itself uses npm's cache in your home folder).
What you need
- Linux or WSL (tested); macOS untested. You need
bash,gitandpython3(3.12 or newer) - Node
v24.22.0, exactly (scripts/bootstrap.shchecks);npm uvfor the lint and type gates (only needed to runscripts/gate.sh)
Set up
git clone --recurse-submodules https://github.com/mtahamasood/mcp-wire-lab
cd mcp-wire-lab
scripts/bootstrap.sh
The bootstrap checks both submodules are at their pinned commits (the reference server and the
specification) and builds the server, then installs the SDK client's npm packages (clients/sdk/, lesson 05). The pins are why line numbers cited in the lessons stay true. npm may report high-severity
vulnerabilities while it installs; that is expected for a pinned reference server that only ever
runs locally under the tap, and nothing here is exposed. Bootstrap may also warn that
core.hooksPath is set, so git might not run the repo's pre-push hook; that matters only if you
push, and it is safe to ignore otherwise.
If bootstrap stops, it says why in one line. The common causes are in Troubleshooting below.
Citations in the docs look like servers:path:line "fragment" or spec:path:line "fragment".
servers: means under vendor/servers/ and spec: means under vendor/spec/; the quoted
fragment is the text you should find on that line.
Record and read a trace
scripts/record.sh tests/fixtures/handshake-2025.ndjson
python3 -m tap.show <the path it printed>
record.sh prints the path of the recording, ending in trace.jsonl (and a ready read it: line
to copy); put that path where <the path it printed> stands.
To record the raw client instead of a fixture, use scripts/record.sh --client tour (or
--client manual to type messages yourself), and validate a recording against the spec's schema
with python3 -m tap.check <the path> --revision 2025-11-25.
python3 -m tap.show also takes --full (every frame's decoded JSON, pretty-printed), --mermaid (a sequence diagram) and
--explain <walkthrough.md> (a note after every frame). A walkthrough that explains several
experiments has one section per experiment: add #<section>, as in
--explain docs/walkthrough/04-edge-lab.md#unknown-method. A wrong section name lists the right ones.
Reading a trace line: -> is the client's frame, <- the server's, !! the server's stderr and
.. an event of the tap itself (spawn, exit, an injected fault). The number on the left is the
frame's seq; walkthrough notes and citations refer to frames by it.
To compare two recordings (the lessons' "change it, predict the difference" experiments), strip the timings and diff the decoded frames; no output means they are identical:
strip() { python3 -m tap.show "$1" --full | sed -E 's/^ *[0-9]+ +[0-9.]+ ms +//; s/, [0-9.]+ ms\)/)/; s#runs/[^/ ]+#runs/X#g'; }
diff <(strip runs/<first>/trace.jsonl) <(strip runs/<second>/trace.jsonl)
Recordings go to runs/, which git ignores. This repository never holds run output: your traces
stay on your machine. Nothing runs at commit time: the pre-push hook refuses a push that would
publish one, and scripts/gate.sh fails if one is tracked.
The tools, in one place
Every command prints a usage text with --help (or -h). With a wrong or missing argument it
prints the same text on stderr and exits non-zero.
| Command | What it does |
|---|---|
scripts/bootstrap.sh | Checks Node and the two pinned submodules, builds the reference server, installs the SDK client's packages. Run once. |
scripts/record.sh <fixture> [name] | Plays a scripted conversation from tests/fixtures/ through the tap against the real server; prints the trace path. |
scripts/record.sh --client <tour|manual|server-tour|sdk-tour|sdk-server-tour|tour-2026|tasks-2026|manual-2026|sdk-tour-2026> [name] | The same, with a client as the conversation: the raw Python client, or (sdk-…) the TypeScript SDK client. manual reads your typed lines on stdin; in a terminal, end it with :quit or Ctrl-D. tour-2026, tasks-2026 and manual-2026 run the raw client in the 2026-07-28 era, and sdk-tour-2026 the SDK client (lesson 06). |
scripts/record.sh --http --client <http-tour|tour|server-tour|manual|tour-2026|manual-2026|sdk-tour|sdk-server-tour|sdk-tour-2026> [name] | The same over Streamable HTTP (lesson 07): the tap runs the server on a private Unix socket and proxies a free 127.0.0.1 port to it. http-tour is the HTTP tour; the other modes are the ones above over HTTP (tasks-2026 stays on stdio). |
python3 -m tap --trace <file> --http <port> -- <server command> | The tap in HTTP mode: a recording reverse proxy for Streamable HTTP servers that take their address from PORT. |
python3 -m client.http_tour --url <url> [--no-stream] | The raw client's HTTP tour against a running Streamable HTTP server. |
python3 -m tap.show <trace> [--full|--mermaid|--explain <file>] | Reads a trace: one line per frame, with replies paired to their requests and latencies. |
python3 -m tap.check <trace> --revision 2025-11-25 | Validates every frame against the spec's JSON Schema. |
python3 -m tap.diff <trace-a> <trace-b> [--explain <file>] | Compares two traces message by message (ids normalised) and lists what differs (lesson 05). |
python3 -m tap --trace <file> [--delay|--drop ...] -- <server command> | The tap itself, for when you want to put it in front of your own command. --drop and --delay inject faults (lesson 04). |
python3 -m client <tour|manual|server-tour|matrix> -- <server command> | The raw client without the tap. --caps chooses the capabilities it declares. --url <url> instead of -- <server command> (tour, manual, server-tour) runs it over Streamable HTTP. |
node clients/sdk/src/main.ts <tour|server-tour> -- <server command> | The SDK client without the tap. --caps chooses the capabilities it declares; --era 2026-07-28 (tour only) runs the new era. --url <url> instead of -- <server command> uses the SDK's HTTP transport. |
scripts/gate.sh | Every automated check; needs uv. Contributors run it before a commit. |
What is where
docs/lessons/,docs/walkthrough/: the lessons and the frame-by-frame notes.docs/atlas.mdis a generated table of every protocol method.tap/: the recorder (python3 -m tap), the viewer (tap.show), the schema checker (tap.check) and the trace diff (tap.diff).client/: the raw client, in small files. The lessons say which to read.clients/sdk/: the SDK client (TypeScript, run as source by Node), lesson 05.tests/: unit and end-to-end tests, plustests/fixtures/(the scripted conversations).vendor/: the pinned reference server and the specification, as git submodules.runs/: your recordings (created on first use; git ignores it).specs/,audits/: why the project is built the way it is, and its review records. You do not need them to do the lessons.
Troubleshooting
need node v24.22.0: install it withnvm install 24.22.0 && nvm use 24.22.0, then run bootstrap again.... is not checked outor... is at <sha>, expected <sha>: rungit submodule update --init --recursive.record: .../index.js missing (run scripts/bootstrap.sh): you have not run bootstrap, or it failed.record: no such fixture: the path is relative to the repo root; fixtures are intests/fixtures/.- A lesson's output differs from its
expectblock: you are probably on a different commit than the submodule pins, or edited a tracked file;git statusshows it. Undo your edit withgit checkout <file>. scripts/gate.shsaysuvx: command not found: installuv. Only the gate needs it.- To start over, delete
runs/; it holds nothing else.
Status
Lessons 01 to 07 are built and checked. Your feedback on the lessons is welcome as a GitHub issue.
Lessons
Do them in order.
-
The wire and the handshake: what crosses stdio, the 2025 opening exchange, and three ways to break it. Walkthrough: the handshake, frame by frame.
-
A client with no library: a raw Python client speaks MCP over stdio, you check its recording against the spec's JSON Schema, then type messages by hand. Walkthrough: the raw client's tour, frame by frame.
-
When the server speaks first: the raw client answers the server's requests (roots, sampling, elicitation), reacts to its notifications, and you read which client capability unlocks what. Walkthrough: the server tour, frame by frame.
-
The edge lab: malformed and out-of-order messages, what the server answers against what the spec says, and losing a message at the tap. Walkthrough: the edge experiments.
-
What an SDK hides: the same tour through the official TypeScript SDK, a message-by-message diff of the two traces, and one request followed through the SDK's source. Walkthrough: the SDK client.
-
The same client, a new era: the raw client speaks the 2026-07-28 revision (
--era 2026-07-28): discovery, the_metaenvelope, input requests in results,subscriptions/listenand tasks; you check it and diff it against the 2025 tour. Walkthrough: the 2026 era, frame by frame. -
The same messages over HTTP: the raw client's HTTP tour recorded through the tap's HTTP mode, read as exchanges: POSTs, event streams, the session id, the GET stream,
202s and a cancellation by closing the connection. Walkthrough: Streamable HTTP, record by record.
More lessons arrive with later phases; specs/roadmap.md lists what each
phase adds.
How it is checked
scripts/gate.sh (about two minutes; it is not hung) runs every automated check: the provenance of each recorded decision (G1), lint
and type checks (G2, G3), the unit and end-to-end tests (G4, G5), a check that every
servers:path:line "fragment" citation in the docs resolves in the pinned source (G8), a check that
every runnable block in a lesson runs and prints what the lesson says it prints (G9), a check
that no run output is tracked (G11), and a check that every test a validation row names exists (G12). Some things are not checked by any gate: see the Result in
specs/phases/01-wire-tap/validation.md.
The reasoning behind the project lives in specs/.
Licence
This repository's own content is under the MIT licence: see LICENSE. The
submodules under vendor/ are upstream projects and keep their own licences.