{"id":"f7947c21-2bd0-4fea-bca0-af6d729be042","slug":"mcp-wire-lab","kind":"mcp","name":"mcp-wire-lab","summary":"MCP learning lab: a wire tap and lessons for the Model Context Protocol","owner":"mtahamasood","repoUrl":"https://github.com/mtahamasood/mcp-wire-lab","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-20T16:53:34.000Z","addedAt":"2026-10-21T22:07:11.875Z","logoUrl":"https://avatars.githubusercontent.com/u/8146293?u=01c31548f6bb103d5324b5a2732b898853043a40&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["memory-knowledge"],"tags":[],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-22T19:22:03.215Z","sample":false,"url":"https://conduits.dev/tools/mcp-wire-lab","language":"Python","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# mcp-wire-lab\n\nA hands-on lab for learning the Model Context Protocol by watching it. A small tap sits between\nan MCP client and a real, pinned MCP server, records every byte that crosses the connection,\nand a viewer replays the recording with the spec section and the server source behind each\nframe. You learn by predicting what the wire will carry, recording it, and reading what it did.\n\nIt is for anyone who wants to understand MCP below the SDK: how a connection opens, what each\nmessage means, and where the spec and a real implementation agree or differ. Nothing here is a\nclient or a server you would ship.\n\n## Start here\n\n1. Set up once (below): clone, then `scripts/bootstrap.sh`.\n2. Open [lesson 01](docs/lessons/01-the-wire-and-the-handshake.md) and do the lessons in order.\n   Each one has the same shape: *Goal*, *Predict* (write your answers down first), *Run*,\n   *Read the trace*, *Read the source*, *Experiments*, *Check yourself*. Every command in a\n   lesson is meant to be pasted at the repository root.\n3. If you only want to poke at it, jump to *Record and read a trace* below.\n\nThe answers to a lesson's *Predict* and *Check yourself* questions sit at the end of the same\nfile, inside a collapsed `<details>` block on GitHub. In a terminal or an editor they are plain\ntext, so stop reading at the heading *Answers* until you have written your own.\n\nA lesson's commands print what the lesson says they print; `scripts/gate.sh` checks that.\nAfter setup nothing needs a network, and the lessons write only inside this folder (setup itself uses npm's cache in your home folder).\n\n## What you need\n\n- Linux or WSL (tested); macOS untested. You need `bash`, `git` and `python3` (3.12 or newer)\n- Node `v24.22.0`, exactly (`scripts/bootstrap.sh` checks); `npm`\n- [`uv`](https://docs.astral.sh/uv/) for the lint and type gates (only needed to run `scripts/gate.sh`)\n\n## Set up\n\n```console\ngit clone --recurse-submodules https://github.com/mtahamasood/mcp-wire-lab\ncd mcp-wire-lab\nscripts/bootstrap.sh\n```\n\nThe bootstrap checks both submodules are at their pinned commits (the reference server and the\nspecification) 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\nvulnerabilities while it installs; that is expected for a pinned reference server that only ever\nruns locally under the tap, and nothing here is exposed. Bootstrap may also warn that\n`core.hooksPath` is set, so git might not run the repo's pre-push hook; that matters only if you\npush, and it is safe to ignore otherwise.\n\nIf bootstrap stops, it says why in one line. The common causes are in *Troubleshooting* below.\n\nCitations in the docs look like `servers:path:line \"fragment\"` or `spec:path:line \"fragment\"`.\n`servers:` means under `vendor/servers/` and `spec:` means under `vendor/spec/`; the quoted\nfragment is the text you should find on that line.\n\n## Record and read a trace\n\n```console\nscripts/record.sh tests/fixtures/handshake-2025.ndjson\npython3 -m tap.show <the path it printed>\n```\n\n`record.sh` prints the path of the recording, ending in `trace.jsonl` (and a ready `read it:` line\nto copy); put that path where `<the path it printed>` stands.\n\nTo record the raw client instead of a fixture, use `scripts/record.sh --client tour` (or\n`--client manual` to type messages yourself), and validate a recording against the spec's schema\nwith `python3 -m tap.check <the path> --revision 2025-11-25`.\n\n`python3 -m tap.show` also takes `--full` (every frame's decoded JSON, pretty-printed), `--mermaid` (a sequence diagram) and\n`--explain <walkthrough.md>` (a note after every frame). A walkthrough that explains several\nexperiments has one section per experiment: add `#<section>`, as in\n`--explain docs/walkthrough/04-edge-lab.md#unknown-method`. A wrong section name lists the right ones.\n\nReading a trace line: `->` is the client's frame, `<-` the server's, `!!` the server's stderr and\n`..` an event of the tap itself (spawn, exit, an injected fault). The number on the left is the\nframe's `seq`; walkthrough notes and citations refer to frames by it.\n\nTo compare two recordings (the lessons' \"change it, predict the difference\" experiments), strip the\ntimings and diff the decoded frames; no output means they are identical:\n\n```console\nstrip() { python3 -m tap.show \"$1\" --full | sed -E 's/^ *[0-9]+ +[0-9.]+ ms +//; s/, [0-9.]+ ms\\)/)/; s#runs/[^/ ]+#runs/X#g'; }\ndiff <(strip runs/<first>/trace.jsonl) <(strip runs/<second>/trace.jsonl)\n```\n\nRecordings go to `runs/`, which git ignores. This repository never holds run output: your traces\nstay on your machine. Nothing runs at commit time: the pre-push hook refuses a push that would\npublish one, and `scripts/gate.sh` fails if one is tracked.\n\n## The tools, in one place\n\nEvery command prints a usage text with `--help` (or `-h`). With a wrong or missing argument it\nprints the same text on stderr and exits non-zero.\n\n| Command | What it does |\n| --- | --- |\n| `scripts/bootstrap.sh` | Checks Node and the two pinned submodules, builds the reference server, installs the SDK client's packages. Run once. |\n| `scripts/record.sh <fixture> [name]` | Plays a scripted conversation from `tests/fixtures/` through the tap against the real server; prints the trace path. |\n| `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). |\n| `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). |\n| `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`. |\n| `python3 -m client.http_tour --url <url> [--no-stream]` | The raw client's HTTP tour against a running Streamable HTTP server. |\n| `python3 -m tap.show <trace> [--full\\|--mermaid\\|--explain <file>]` | Reads a trace: one line per frame, with replies paired to their requests and latencies. |\n| `python3 -m tap.check <trace> --revision 2025-11-25` | Validates every frame against the spec's JSON Schema. |\n| `python3 -m tap.diff <trace-a> <trace-b> [--explain <file>]` | Compares two traces message by message (ids normalised) and lists what differs (lesson 05). |\n| `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). |\n| `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. |\n| `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. |\n| `scripts/gate.sh` | Every automated check; needs `uv`. Contributors run it before a commit. |\n\n## What is where\n\n- `docs/lessons/`, `docs/walkthrough/`: the lessons and the frame-by-frame notes. `docs/atlas.md` is a generated table of every protocol method.\n- `tap/`: the recorder (`python3 -m tap`), the viewer (`tap.show`), the schema checker (`tap.check`) and the trace diff (`tap.diff`).\n- `client/`: the raw client, in small files. The lessons say which to read.\n- `clients/sdk/`: the SDK client (TypeScript, run as source by Node), lesson 05.\n- `tests/`: unit and end-to-end tests, plus `tests/fixtures/` (the scripted conversations).\n- `vendor/`: the pinned reference server and the specification, as git submodules.\n- `runs/`: your recordings (created on first use; git ignores it).\n- `specs/`, `audits/`: why the project is built the way it is, and its review records. You do not need them to do the lessons.\n\n## Troubleshooting\n\n- `need node v24.22.0`: install it with `nvm install 24.22.0 && nvm use 24.22.0`, then run bootstrap again.\n- `... is not checked out` or `... is at <sha>, expected <sha>`: run `git submodule update --init --recursive`.\n- `record: .../index.js missing (run scripts/bootstrap.sh)`: you have not run bootstrap, or it failed.\n- `record: no such fixture`: the path is relative to the repo root; fixtures are in `tests/fixtures/`.\n- A lesson's output differs from its `expect` block: you are probably on a different commit than the\n  submodule pins, or edited a tracked file; `git status` shows it. Undo your edit with `git checkout <file>`.\n- `scripts/gate.sh` says `uvx: command not found`: install [`uv`](https://docs.astral.sh/uv/). Only the gate needs it.\n- To start over, delete `runs/`; it holds nothing else.\n\n## Status\n\nLessons 01 to 07 are built and checked. Your feedback on the lessons is\nwelcome as a GitHub issue.\n\n## Lessons\n\nDo them in order.\n\n1. [The wire and the handshake](docs/lessons/01-the-wire-and-the-handshake.md): what crosses\n   stdio, the 2025 opening exchange, and three ways to break it.\n   Walkthrough: [the handshake, frame by frame](docs/walkthrough/01-handshake-2025.md).\n2. [A client with no library](docs/lessons/02-raw-client.md): a raw Python client speaks MCP over\n   stdio, you check its recording against the spec's JSON Schema, then type messages by hand.\n   Walkthrough: [the raw client's tour, frame by frame](docs/walkthrough/02-raw-client-tour.md).\n3. [When the server speaks first](docs/lessons/03-server-initiated.md): the raw client answers the\n   server's requests (roots, sampling, elicitation), reacts to its notifications, and you read\n   which client capability unlocks what.\n   Walkthrough: [the server tour, frame by frame](docs/walkthrough/03-server-initiated.md).\n4. [The edge lab](docs/lessons/04-edge-lab.md): malformed and out-of-order messages, what the\n   server answers against what the spec says, and losing a message at the tap.\n   Walkthrough: [the edge experiments](docs/walkthrough/04-edge-lab.md).\n\n5. [What an SDK hides](docs/lessons/05-sdk-client.md): the same tour through the official\n   TypeScript SDK, a message-by-message diff of the two traces, and one request followed through\n   the SDK's source.\n   Walkthrough: [the SDK client](docs/walkthrough/05-sdk-client.md).\n6. [The same client, a new era](docs/lessons/06-era-2026.md): the raw client speaks the\n   2026-07-28 revision (`--era 2026-07-28`): discovery, the `_meta` envelope, input requests in\n   results, `subscriptions/listen` and tasks; you check it and diff it against the 2025 tour.\n   Walkthrough: [the 2026 era, frame by frame](docs/walkthrough/06-era-2026.md).\n7. [The same messages over HTTP](docs/lessons/07-streamable-http.md): the raw client's HTTP tour\n   recorded through the tap's HTTP mode, read as exchanges: POSTs, event streams, the session id,\n   the GET stream, `202`s and a cancellation by closing the connection.\n   Walkthrough: [Streamable HTTP, record by record](docs/walkthrough/07-streamable-http.md).\n\nMore lessons arrive with later phases; [`specs/roadmap.md`](specs/roadmap.md) lists what each\nphase adds.\n\n## How it is checked\n\n`scripts/gate.sh` (about two minutes; it is not hung) runs every automated check: the provenance of each recorded decision (G1), lint\nand type checks (G2, G3), the unit and end-to-end tests (G4, G5), a check that every\n`servers:path:line \"fragment\"` citation in the docs resolves in the pinned source (G8), a check that\nevery runnable block in a lesson runs and prints what the lesson says it prints (G9), a check\nthat 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\n[`specs/phases/01-wire-tap/validation.md`](specs/phases/01-wire-tap/validation.md).\n\nThe reasoning behind the project lives in [`specs/`](specs/).\n\n## Licence\n\nThis repository's own content is under the MIT licence: see [`LICENSE`](LICENSE). The\nsubmodules under `vendor/` are upstream projects and keep their own licences.\n"}