Metadata-Version: 2.5
Name: mcpdump
Version: 0.1.0
Summary: The missing terminal toolkit for MCP servers — inspect, call, watch and test any Model Context Protocol server.
Project-URL: Homepage, https://github.com/xsw77492-code/mcpdump
Project-URL: Repository, https://github.com/xsw77492-code/mcpdump
Project-URL: Issues, https://github.com/xsw77492-code/mcpdump/issues
Project-URL: Changelog, https://github.com/xsw77492-code/mcpdump/blob/main/CHANGELOG.md
License: MIT
License-File: LICENSE
Keywords: ai-agents,cli,debugging,json-rpc,mcp,model-context-protocol,testing,tui
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Debuggers
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# mcpdump

**The missing terminal toolkit for MCP servers.** Inspect, call, watch and test any Model Context Protocol server — without leaving your shell.

> **Why the name?** `tcpdump` is what you reach for when you need to see what is really on the
> wire. `mcpdump` is that idea one layer up: it prints the raw JSON-RPC frames, records a session
> to a file, and replays that file later on a machine that has neither the server nor the client.

<p align="center">
  <a href="https://github.com/xsw77492-code/mcpdump/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/xsw77492-code/mcpdump/ci.yml?branch=main&label=CI"></a>
  <img alt="Python" src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue">
  <img alt="Runtime dependencies" src="https://img.shields.io/badge/runtime%20dependencies-2-brightgreen">
  <a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-blue"></a>
</p>

![mcpdump in action: listing a server, then reading the raw frames of a tool call](docs/demo.svg)

<p align="center">
  <a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a>
</p>

---

## Why

MCP is now the de-facto standard for connecting AI agents to tools. GitHub and the public registries already list **10,000+ MCP servers** — and the tooling around them has not kept up.

If you have ever built an MCP server, you know these three moments:

1. **"Did my server even start?"** — Your client shows nothing. The server may have crashed on line 3, or it may be printing a banner to stdout and corrupting the JSON stream. You have no idea which.
2. **"What does this server actually expose?"** — You need the full tool list with parameter schemas, not a truncated dropdown in a chat UI.
3. **"The agent called my tool wrong — what did it actually send?"** — Without the raw JSON-RPC frames you are guessing.

The official MCP Inspector is a browser GUI: heavy to launch, impossible to script, and useless in CI. `mcpdump` takes the opposite position — **terminal-native, scriptable, CI-friendly.**

## What makes it different

- **A real terminal UI, not a browser tab.** `mcpdump tui` puts the server identity, the tool list, the live wire traffic and a latency timeline on one screen — `tab` to switch panes, `⏎` to call a tool. No alt-screen takeover: the last frame stays in your scrollback as evidence.
- **Zero protocol dependencies.** The JSON-RPC codec and both transports are implemented here, not imported from an SDK. A debugging tool must own the wire format — that is the whole point.
- **Wire-level by default.** `mcpdump call` prints the exact request and response frames, unmodified, with timings.
- **Transparent by construction.** `mcpdump watch` sits between any client and any server, forwarding every frame byte for byte. It does not interpret, buffer, reorder or re-encode anything — so the client cannot tell it is there.
- **Reproducible by construction.** `mcpdump record` writes a session to disk; `mcpdump replay` walks it back **without starting a process or touching the network**, and `mcpdump diff` reports what actually changed between two recordings — tools, parameter schemas, capabilities, latency — while ignoring the timestamps that differ on every run. Replaying the same file twice gives byte-identical output, so a recording attached to an issue shows the maintainer exactly what you saw.
- **No command typing.** `mcpdump discover` reads the MCP configs of the clients you already use — Claude Desktop, Cursor, VS Code, Claude Code, Windsurf, Cline, Continue, Zed, OpenClaw — and hands you the list. It never prints your API keys, only the variable names.
- **Machine-readable everywhere.** Every command supports `--json` with a stable schema. Exit codes are meaningful (`0` ok / `1` usage / `2` server error / `3` timeout / `4` environment).
- **Conformance checking built in.** `mcpdump check` runs 10 spec checks and hands you the evidence, not a verdict — the offending frame is printed alongside the clause it breaks. Exit codes make it a CI gate, `--badge` makes it a README trophy, `--markdown` makes it a pull request comment — and a GitHub Action does exactly that.
- **Instant install.** Two dependencies (`typer`, `rich`). `uvx mcpdump` starts in well under a second.

## Quick start

No API key, no Node.js, no configuration. An example MCP server ships **inside** `mcpdump`,
so the whole toolkit can be tried in two lines:

```bash
pip install "mcpdump @ git+https://github.com/xsw77492-code/mcpdump"
mcpdump demo
```

`mcpdump demo` runs three steps against that built-in server and prints their real output —
the same output you get by typing the commands yourself:

```text
1. What does this server expose?                                        mcpdump ls
───────────────────────────────────────────────────────────────────────────────
┌──────────────── echo-server v0.1.0  (mcpdump sample server) ────────────────┐
│    transport  stdio · python -m mcpdump.demo                                │
│     protocol  2025-06-18                                                    │
│ capabilities  tools · resources · prompts                                   │
│ instructions  A sample MCP server for testing mcpdump, exposing the echo /  │
│               add / boom tools.                                             │
└─────────────────────────────────────────────────────────────────────────────┘
Tools (3)
┌──────┬────────────────────────┬─────────────────────────────────────────────┐
│ name │ arguments              │ description                                 │
├──────┼────────────────────────┼─────────────────────────────────────────────┤
│ echo │ (text: string)         │ Returns the input text unchanged. Use it to │
│      │                        │ verify the link is alive.                   │
│ add  │ (a: number, b: number) │ Adds two numbers.                           │
│ boom │ —                      │ Always returns isError=true; used to test   │
│      │                        │ mcpdump's error rendering.                  │
└──────┴────────────────────────┴─────────────────────────────────────────────┘
…

2. What does a tool call actually send and receive?     mcpdump call <SERVER> echo
───────────────────────────────────────────────────────────────────────────────
→ initialize
  {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2…
← initialize                                                           894.7 ms
  {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18", "cap…

→ tools/list
  {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
← tools/list                                                             0.2 ms
  {"jsonrpc": "2.0", "id": 2, "result": {"tools": [{"name": "echo", "title": "…

→ tools/call
  {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","argum…
← tools/call                                                             0.1 ms
  {"jsonrpc": "2.0", "id": 3, "result": {"content": [{"type": "text", "text": …

3 round trips · 895.1 ms total · slowest ← initialize 894.7 ms

┌── Tool result ──┐
│ hello from mcpdump │
└─────────────────┘

3. Does it follow the MCP spec?                                      mcpdump check
───────────────────────────────────────────────────────────────────────────────
Conformance report
10 checks · 10 passed · 0 failed · 0 skipped
…
```

Abridged — the `…` marks stand for the Resources and Prompts tables, the ten passing
checks, and the closing `Next` block. The full run is 98 lines in 6.5 seconds.

The rest of this document points at that same built-in server, so its examples are
copy-pasteable. One caveat: `python -m mcpdump.demo` needs `python` on your `PATH` to be the
interpreter that has `mcpdump` installed — which is **not** true for `uvx`, `pipx`, or an
activated virtualenv. If it fails with `No module named 'mcpdump'`, run `mcpdump demo --cmd`
to print the exact launch command for your installation.

Against a real server:

```bash
mcpdump discover   # find MCP servers already configured on this machine
mcpdump ls "npx -y @modelcontextprotocol/server-filesystem /tmp"
mcpdump call "npx -y @modelcontextprotocol/server-filesystem /tmp" read_file \
  --args '{"path":"/tmp/notes.md"}'
```

### Install

```bash
uvx --from "git+https://github.com/xsw77492-code/mcpdump" mcpdump demo   # run without installing
pipx install "git+https://github.com/xsw77492-code/mcpdump"             # install as a CLI
pip install "mcpdump @ git+https://github.com/xsw77492-code/mcpdump"    # into the current env
```

Requires Python 3.10+.

`mcpdump` is not on PyPI yet, so the three commands above install from the repository.
Once it is published, plain `pip install mcpdump` will work and these become redundant.

## Commands

### `mcpdump demo`

The fastest way in. Runs `ls`, `call` and `check` against the server that ships **inside**
`mcpdump` — no arguments, no config, no network — and prints each step's real output, which is
the same output you get by typing those three commands yourself.

```bash
mcpdump demo
mcpdump demo --cmd     # print the launch command for the built-in server, then exit
```

`--cmd` exists because the built-in server is started through the interpreter that has
`mcpdump` installed, which is not always the `python` on your `PATH` — the caveat under
[Quick start](#quick-start) explains when that matters.

### `mcpdump ls <server>`

Handshake with the server and print its identity, protocol version, declared capabilities, tools, resources and prompt templates.

```bash
mcpdump ls "npx -y @modelcontextprotocol/server-filesystem /tmp"
mcpdump ls "npx -y @modelcontextprotocol/server-filesystem /tmp" --json   # for scripts
mcpdump ls "npx -y @modelcontextprotocol/server-filesystem /tmp" -v       # expand schemas
mcpdump ls https://mcp.example.com/mcp                                    # remote, over HTTP
```

`mcpdump` only calls `resources/list` and `prompts/list` when the server actually declares those capabilities — sending them anyway is a protocol violation, and a lot of clients get this wrong.

**stdio or Streamable HTTP — same command, same output.** An `http(s)://` argument switches the
transport; everything else is identical. Both `application/json` and `text/event-stream`
responses are handled, `Mcp-Session-Id` is remembered and sent back on every subsequent
request, and the session is closed with a `DELETE` on exit. It is built on the standard
library, so the runtime dependencies are still just `typer` and `rich`.

### `mcpdump call <server> <tool>`

Call a tool and print the complete round trip.

```bash
mcpdump call "python -m mcpdump.demo" echo --args '{"text":"hi"}'
mcpdump call "python -m mcpdump.demo" add  --args '{"a":3,"b":4}' --no-wire
mcpdump call "python -m mcpdump.demo" echo --args '{"text":"hi"}' --full
mcpdump call "python -m mcpdump.demo" echo --args '{"text":"hi"}' --json
```

Sample output — every frame, every timing:

```text
→ initialize
  {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2…
← initialize                                                           724.7 ms
  {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18", "cap…

→ tools/list
  {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
← tools/list                                                             0.2 ms
  {"jsonrpc": "2.0", "id": 2, "result": {"tools": [{"name": "echo", "title": "…

→ tools/call
  {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","argum…
← tools/call                                                             0.2 ms
  {"jsonrpc": "2.0", "id": 3, "result": {"content": [{"type": "text", "text": …

3 round trips · 725.1 ms total · slowest ← initialize 724.7 ms

┌─ Tool result ─┐
│ hi            │
└───────────────┘
```

Frames are truncated to your terminal width. Pass `--full` to see them in their
entirety — nothing is reformatted or re-serialised either way, so what you read is
what actually crossed the pipe.

If you typo a tool name, `mcpdump` suggests the closest matches instead of dumping an opaque error.

### `mcpdump tui <server>`

Everything at once, on one screen — server identity, tools, live traffic, and how long each call took. A real capture (`--script "enter,h,i,enter,enter,o,k,enter,q"`), with only the interpreter path shortened:

```
┌────────────────────────────────── Server ───────────────────────────────────┐
│ echo-server v0.1.0 · stdio · ~/…/binaries/…/python.exe -m mcpdump.demo         │
├─ Tools · 3 ───────────┬─ Wire · 8 ──────────────────────────────────────────┤
│▸ echo       Returns t…│ ⋯ 1 older                                           │
│  add        Adds two …│  ← initialize                                 1.22 s│
│  boom       Always re…│  {"jsonrpc": "2.0", "id": 1, "result": {"protocolVe…│
│                       │  → tools/list                                       │
│                       │  {"jsonrpc":"2.0","id":2,"method":"tools/list","par…│
│                       │  ← tools/list                                 0.2 ms│
│                       │  {"jsonrpc": "2.0", "id": 2, "result": {"tools": [{…│
│                       │  → tools/call                                       │
│                       │  {"jsonrpc":"2.0","id":3,"method":"tools/call","par…│
│                       │  ← tools/call                                 0.2 ms│
│                       │  {"jsonrpc": "2.0", "id": 3, "result": {"content": …│
│                       │  → tools/call                                       │
│                       │  {"jsonrpc":"2.0","id":4,"method":"tools/call","par…│
│                       │▸ ← tools/call                                 0.2 ms│
│                       │  {"jsonrpc": "2.0", "id": 4, "result": {"content": …│
├───────────────────────┴─────────────────────────────────────────────────────┤
 Timeline · 2 calls · p50 0.3 ms · max 0.3 ms                                  
 echo            ████████████████████████████████████████████▋      0.3 ms     
 echo            ██████████████████████████████████████████████     0.3 ms     
                                                                               
└─────────────────────────────────────────────────────────────────────────────┘
 BROWSE  tab panes · ↑↓ move · ⏎ call · / filter · q quit                      
```

The tool descriptions are the strings the example server itself returns — `mcpdump` never rewrites what a server sends.

| Key | |
|---|---|
| `tab` | move focus between tools, wire and timeline |
| `↑` `↓` | move within the focused pane — in the wire and timeline, `↑` scrolls *back* in time |
| `⏎` | call the selected tool; on a frame, expand it to the full payload |
| `/` | filter tools as you type — name, title or description |
| `q` | quit |

Worth knowing:

- **No full-screen repaint.** `Live(screen=False)` rewrites only the lines that changed, and redraws are driven by *state changes* rather than a timer — nothing is written while you sit still.
- **The last frame stays in your scrollback.** No alt-screen takeover. Scroll up after quitting and the screen is still there, which is exactly what a debugging tool should leave behind.
- **Latency outliers are flagged for you.** Every call is drawn as a half-cell water bar scaled against the session **median**; a call is marked `⚠` only when it is both `≥1.5×` the median *and* `≥+50 ms` slower. Median, not mean — one 30-second timeout would drag a mean up tenfold and make every later call look fast.
- **Failures are not drawn as short bars.** A bar has a length, and "failed" is not a length. Those rows get `✗` and the reason instead.
- **Arguments come from the schema.** `⏎` on a tool that needs parameters opens a one-line JSON skeleton generated from its `inputSchema` — required parameters only, cursor already inside the first string. Edit, press `⏎` again. There is no form widget to fight with.
- **Recordable without hand speed.** `mcpdump tui --script "tab,enter,q"` drives the UI from a key sequence, so a demo recording and a CI smoke test run the same reproducible path.

Two things it deliberately refuses to do:

- **It will not start without a real terminal.** Piped output and `TERM=dumb` are both rejected, with a command you can copy instead (`mcpdump ls <SERVER>`). On a dumb terminal Rich reports the size as a fixed 80×25, so anything drawn there would be *wrong*, not merely ugly.
- **It will not let the server's stderr shred the screen.** `Live` repaints stdout in place, so the stderr passthrough is switched off for the duration. Use `mcpdump watch` or `mcpdump call` when you want those logs.

### `mcpdump check <server>`

Run 10 protocol conformance checks against any MCP server. Every failure comes with
the spec clause it breaks **and the raw frame that proves it** — not a summary you
have to take on faith.

```bash
mcpdump check "python -m mcpdump.demo"
mcpdump check "npx -y @modelcontextprotocol/server-filesystem /tmp" --json   # for CI
mcpdump check "python -m mcpdump.demo" --badge                       # paste-ready badge
mcpdump check "python -m mcpdump.demo" --markdown                    # for a PR comment
```

Passing checks take one line each; only failures expand:

```text
Conformance report
10 checks · 9 passed · 1 failed · 0 skipped

✓ handshake-protocol-version  initialize returns protocolVersion

✓ handshake-initialized-order  no server request before initialized

…

✗ error-invalid-params-code  missing arguments return -32602
  spec: MCP tools: missing required arguments map to -32602 Invalid params
    needs-arg ran successfully without its required arguments; the caller thinks its input took effect.
      evidence: {"jsonrpc": "2.0", "id": 7, "result": {"content": [{"type": "t…

✗ 1 of 10 checks failed.
```

Exit code is `0` when everything passes and `2` otherwise, so it drops straight into CI:

```yaml
- run: mcpdump check "$MCP_SERVER" --json > conformance.json
```

`--markdown` prints the same report as Markdown, for a pull request comment or a CI
summary. Nothing is wrapped or colourised, so it pipes straight into a file:

```bash
mcpdump check "python -m mcpdump.demo" --markdown >> "$GITHUB_STEP_SUMMARY"
```

`--json`, `--badge` and `--markdown` are mutually exclusive — pick one per run.

#### GitHub Action

`--markdown` is what the Action is built on: the formatting lives in `mcpdump`, where it
is covered by tests, instead of in YAML. The integration is a single `uses:` — the step
above it is your own build, not ours:

```yaml
# .github/workflows/conformance.yml
name: conformance
on: [push, pull_request]

jobs:
  mcpdump:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write     # to comment; the job summary needs no permission
    steps:
      - uses: actions/checkout@v7
      - run: npm ci            # whatever your server needs before it can start
      - uses: xsw77492-code/mcpdump@v1
        with:
          server: node build/index.js
```

What you get:

- **The report always lands in the job summary.** No token, no permissions, and it works on
  pull requests from forks — where it is also visible under the Checks tab.
- **The comment is updated in place, not appended.** The Action finds its previous comment
  by an HTML marker and edits it, so ten pushes leave one comment rather than ten.
- **The job fails with mcpdump's own exit code**, after the report has been published —
  never before, or you would lose the reason.

Inputs: `server` (required), `version`, `python-version`, `install`, `comment`, `token`.
Outputs: `conformant`, `exit-code`, `report`.

Two caveats worth knowing up front:

- **A pull request from a fork cannot be commented on** with the default workflow token. The
  Action detects that case, prints a warning, and leaves the report in the job summary.
- **`@v1` and the package version are independent.** Pinning the Action tag does not pin the
  package; set `version: 0.1.0` if you want both frozen.

`--badge` prints a single line of Markdown. The colour tracks the score — `10/10` is
bright green, below 60% is red:

```markdown
[![mcpdump conformance 10/10](https://img.shields.io/badge/mcpdump%20conformance-10%2F10-brightgreen)](https://github.com/xsw77492-code/mcpdump?utm_source=badge&utm_medium=readme&utm_campaign=conformance)
```

**Adding a check is adding a file.** Drop a module in `src/mcpdump/services/checks/`,
export `CHECKS = (YourCheck(),)`, and the registry picks it up — no core code changes.
The numeric filename prefix is the execution order, not decoration: `stdout-purity`
must run last because it reads everything the session accumulated, and running it
early would turn a real violation into a false pass.

### `mcpdump watch <server>`

Sit between a client and a server. `mcpdump watch` launches the real server, forwards
every frame in both directions **byte for byte**, and prints each one as it goes by.

Point any MCP client at it — Claude Desktop, Cursor, or `mcpdump` itself:

```bash
mcpdump call "mcpdump watch \"python -m mcpdump.demo\"" echo --args '{"text":"hello"}'
```

```text
mcpdump watch
  proxying ~/…/binaries/…/python.exe -m mcpdump.demo
  stdout carries the protocol; this view goes to stderr.

→ initialize
  {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVers…
← initialize                                                           841.3 ms
  {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18", "cap…
→ notifications/initialized
  {"jsonrpc": "2.0", "method": "notifications/initialized"}
→ tools/list
  {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
→ tools/call
  {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "echo…
← tools/list                                                             1.1 ms
  {"jsonrpc": "2.0", "id": 2, "result": {"tools": [{"name": "echo", "title": "…
← tools/call                                                             1.6 ms
  {"jsonrpc": "2.0", "id": 3, "result": {"content": [{"type": "text", "text": …

4 sent · 3 received · 1.66 s
```

The interpreter path in `proxying` is shortened here; `watch` prints it in full.

The first frame is slow because the child process is still booting — that is the
honest number, not an artifact of the proxy.

**stdout is the protocol channel.** Everything above goes to stderr; stdout carries
only frames, untouched. That is what makes the proxy transparent — a client cannot
tell it is there. It also means `mcpdump watch` is itself an MCP server, so it drops
into any client config without changing how that config works.

Get a ready-to-paste config entry:

```bash
mcpdump watch --print-config "npx -y @modelcontextprotocol/server-filesystem /tmp"
```

```json
{
  "mcpServers": {
    "server-filesystem": {
      "command": "mcpdump",
      "args": ["watch", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    }
  }
}
```

Record a whole session, one frame per line:

```bash
mcpdump watch --record session.jsonl "python -m mcpdump.demo"
```

```json
{"format": "mcpdump-session", "version": 1, "at": "2026-09-26T16:36:55+08:00", "argv": ["~/…/binaries/…/python.exe", "-m", "mcpdump.demo"]}
{"seq": 1, "atMs": 28.336, "direction": "to_server", "method": "initialize", "elapsedMs": null, "line": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", …"}
{"seq": 2, "atMs": 753.377, "direction": "to_client", "method": "initialize", "elapsedMs": 725.04, "line": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"result\": {\"protocolVersion\": \"2025-06-18\", …"}
```

Only the `line` fields are shortened here. `elapsedMs` is `null` on requests — a
request has no duration of its own; the number lives on the response that answered it.

The first line is a header carrying the format name, a version and the server's
startup command — so you can still tell which server a recording came from months
later. A replay tool that does not recognise the version **refuses** the file
instead of guessing: a wrong guess looks like success, and you are using the
recording as evidence.

`atMs` is relative to the start of the proxy, not wall-clock — so recordings compare
across machines. Every line is flushed immediately: `watch` usually ends with Ctrl-C,
and the last few frames are exactly the ones you want to look at afterwards.

Exit code is the **real server's** exit code. The proxy does not put its own status
on top of the server's.

### `mcpdump record`

`mcpdump record` is `watch` with the view taken away. Point a client at it and the whole
session lands on disk; progress goes to stderr, and stdout carries nothing but the
protocol traffic.

```bash
mcpdump record --out session.jsonl "python -m mcpdump.demo"

# or hand your client a ready-made config
mcpdump record --print-config --out session.jsonl "npx -y @acme/weather-mcp"
```

It is its own command rather than `watch --record --quiet` because the two have
different subjects: `watch`'s subject is the live view and the file is a side effect;
`record`'s subject *is* the file. Keeping them separate gives you an output a script
can consume cleanly.

### `mcpdump replay`

Replay a recording. **No process is started, no network is touched.** The output is a
function of the file alone: elapsed times are *reproduced*, not re-measured; frames
are not reordered; bad lines are reported by line number rather than skipped.

```bash
mcpdump replay session.jsonl            # summary + steps
mcpdump replay session.jsonl --step     # each step with its raw frame
mcpdump replay session.jsonl --wire     # same layout as `ls` / `call`
mcpdump replay session.jsonl --json     # for scripts
```

```text
source  session.jsonl
steps  4
round trips  3
payload  1.5 KB
notifications  1

     1  → ←  initialize                                                725.0 ms
     2  ⇢    notifications/initialized
     3  → ←  tools/list                                                  0.1 ms
     4  → ←  tools/call                                                  0.2 ms
```

Two kinds of frames get their own treatment, because collapsing them loses the
diagnosis:

- **Notifications** (`⇢`) have no `id` and, per the spec, expect no response.
  Counting them as "requests that never got an answer" would make every healthy
  recording look broken.
- **Orphan responses** (`⇢` with a response) are answers to requests that are not in
  the recording. They are not dropped — often they *are* the problem.

Replaying the same file twice gives byte-identical output. That is the whole point:
you attach a recording to an issue, and what the other person sees on their machine
is what you saw on yours.

### `mcpdump diff <a> <b>`

Did my change alter the behaviour? `diff` compares **contracts**, not text.

```bash
mcpdump diff before.jsonl after.jsonl
mcpdump diff before.jsonl after.jsonl --json   # exit 2 when anything changed
```

A text diff is useless here: recordings are full of `seq`, `atMs` and `elapsedMs`,
so every line differs and the real change is buried. `diff` looks at:

| Change | Reported |
|---|---|
| Tool added / removed | yes |
| Parameter added / removed / renamed / retyped, required changed, nested and array schemas, enums | yes |
| Capability declarations, protocol version | yes |
| Latency regression | only past **both** thresholds: 1.5× *and* +50 ms |

Three things are deliberately **not** reported, because a report full of noise stops
being read: tool `description` rewrites, the order of `required`, and latency
jitter below the thresholds. Medians are used rather than means — one 30-second
timeout can drag a mean up tenfold, and that single outlier is exactly what would
fake a regression.

Exit code is `2` when anything changed, reusing the "non-conformant" code from
`check`: for CI, *behaviour changed* and *out of spec* are the same thing — this
commit needs a human look.

### `mcpdump mock <recording>`

Serve a recording back as a working MCP server. **No original server required** — the
recording *is* the server.

```bash
mcpdump mock session.jsonl                                   # answer from a recording
mcpdump mock --from "python -m mcpdump.demo" -o s.jsonl   # record, then serve
```

The client asking questions is the one in charge here, which is the opposite of `replay`
(where the recording drives). Two consequences follow from that:

- **Answers are matched by method name, not by `id`.** JSON-RPC ids are chosen by
  whoever sends the request, and the client you are serving now is not the one that was
  recorded — so the `id` in each answer is rewritten to match the incoming request.
- **The timeline is dropped entirely.** The `atMs` delays in a recording belonged to
  *that* server on *that* machine; replaying them against a different client would be
  arbitrary. A request is answered as soon as it arrives.

Ask the same method a second time and the next recorded answer is used, wrapping around.
A method with no recorded answer gets `-32601` — **never a fake success**, because a
client that gets an empty result has no way to tell it apart from a real one and will
keep going on a false premise. A recording with no `initialize` answer is refused
outright: serving it would leave the client waiting for a handshake that can never
complete, reporting "server not responding" for what is really "wrong recording".

`--from` connects once to a real server, asks it everything, and writes that to disk —
**record once, serve many times**. That makes a broken-server report into a few-KB file
anyone can reproduce. `--max-requests N` exits after serving N requests, which is handy
in a smoke test.

### `mcpdump discover`

You already have MCP servers configured — in Claude Desktop, Cursor, VS Code, Claude Code,
Windsurf, Cline, Continue, Zed or OpenClaw. `mcpdump discover` reads those config files and
hands you the list, so you never retype a launch command. Running `mcpdump` with no arguments
does the same thing.

```text
$ mcpdump discover
2 client config(s) found · 4 server(s)

Servers
 1  filesystem    Cursor · mcpServers                                           · ready
 2  memory        Claude Desktop · mcpServers                                   · ready
 3  github        Cursor · mcpServers                                         ✗ blocked
     ⚠ environment variable not set: GITHUB_TOKEN
 4  notion        Cursor · mcpServers                                         ✗ blocked
     ⚠ mcpdump cannot drive http transports yet — only stdio

Availability is from static checks only — no server was started. Add --probe to really connect.


Next
▸ mcpdump discover --use filesystem
▸ mcpdump discover --probe
```

Pick one and you are connected — no command to retype:

```bash
mcpdump discover --use filesystem      # straight into `ls`
mcpdump discover --probe               # really connect to each one and verify
mcpdump discover --json                # machine-readable
```

```json
{
  "servers": [
    {
      "name": "github",
      "client": "cursor",
      "origin": "Cursor · mcpServers",
      "transport": "stdio",
      "envKeys": ["GITHUB_PERSONAL_ACCESS_TOKEN"],
      "availability": "blocked",
      "issues": [
        {"problem": "env-missing", "detail": "GITHUB_TOKEN"}
      ]
    }
  ]
}
```

**Your secrets stay put.** Config files are full of `"env": {"GITHUB_TOKEN": "ghp_..."}`.
`discover` reports the **variable names** and whether they are set — never the values.
That holds for `--json` too.

**Availability is honest about what it knows.** `ready` means no static problem was found;
`verified` means `--probe` actually completed a handshake. Nothing is reported as working
on the strength of a config file alone.

**Nothing is started unless you ask.** The default scan does zero process launches and zero
network calls — a discovery command that takes 30 seconds every run has no reason to exist.
`--probe` is the opt-in, and it skips servers that already failed static checks (proceeding
would only bury the real reason behind a timeout).

**Where it looks** comes from the clients' own docs where possible, and community reports
otherwise — in which case the output says so. OpenClaw's path is documented four different
ways publicly, so all four are listed as candidates; a path that does not exist is simply
skipped, while guessing one might never find anything.

### Language

The interface is English by default. Set `MCPDUMP_LANG=zh` (or run under a Chinese
locale) to get Chinese output — tables, panel labels, error messages and `--help`
all switch together.

```bash
MCPDUMP_LANG=zh mcpdump ls "npx -y @modelcontextprotocol/server-filesystem /tmp"
```

Translations live in a plain Python dict (`src/mcpdump/i18n.py`), one key per message
with both languages side by side. No build step, no `.po` files — changing a
string is changing a line of code.

### Global flags

| Flag | Applies to | Effect |
|---|---|---|
| `--json` | `ls`, `call`, `check`, `discover`, `replay`, `diff` | Stable JSON on stdout, no ANSI, no wrapping |
| `--badge` | `check` | Print a paste-ready Markdown badge instead of the report |
| `--markdown` | `check` | Print the report as Markdown, for a PR comment or a CI summary |
| `--record <path>` | `watch` | Append every frame to a JSONL file as it flows |
| `--out <path>` | `record` | Where the recording goes |
| `--step` | `replay` | Show each step with its raw frame |
| `--limit <N>` | `replay` | Show at most N steps; `0` means all |
| `--wire` | `replay` | Render using the same layout as `ls` / `call` |
| `--print-config` | `watch` | Print a ready-to-paste MCP client config, then exit |
| `--use <name>` | `discover` | Connect to a discovered server immediately |
| `--client <name>` | `discover` | Restrict the scan to one client (repeatable) |
| `--probe` | `discover` | Really connect to each server instead of checking statically |
| `--trace` | `ls`, `call`, `check` | Mirror every frame sent and received to stderr |
| `--quiet-server` | all | Suppress the child process's stderr passthrough |
| `--timeout` | `ls`, `call`, `check` | Seconds to wait for a response (default 30) |
| `--protocol-version` | `ls`, `call`, `check` | Protocol version to offer during `initialize` |
| `--full` | `call`, `watch` | Print frames in full instead of truncating to the terminal width |
| `--cmd` | `demo` | Print the launch command for the built-in server, then exit |

Set `NO_COLOR=1` to disable colour everywhere — including the server's own stderr
passthrough, which goes through the same theme.

## Comparison

| | Official Inspector | IDE plugins | **mcpdump** |
|---|:--:|:--:|:--:|
| Terminal-native | ✗ | ✗ | **✓** |
| Works over SSH | ✗ | ✗ | **✓** |
| Scriptable / CI-friendly | ✗ | ✗ | **✓** |
| Raw JSON-RPC frames by default | partial | ✗ | **✓** |
| Transparent proxy — watch a *real* client | ✗ | ✗ | **✓** |
| Replay a recorded session offline | ✗ | ✗ | **✓** |
| Semantic diff of two sessions | ✗ | ✗ | **✓** |
| Finds servers you already configured | ✗ | ✗ | **✓** |
| Interactive TUI — tools, wire and timeline on one screen | ✗ | ✗ | **✓** |
| Machine-readable output | ✗ | ✗ | **✓** |
| Zero protocol dependencies | — | — | **✓** |

## Roadmap

| Milestone | Status | Scope |
|---|---|---|
| **Foundation** | ✅ done | `ls`, `call`, wire-level output, `--json`, zero-dep stdio transport |
| **Conformance** | ✅ done | English/Chinese interface (`MCPDUMP_LANG`), `check` conformance suite + compliance badge, `watch` transparent proxy + `--record` |
| **Discovery** | ✅ done | `discover` — reads the MCP configs of 11 clients, no command typing |
| **Black box** | ✅ done | `record`, `replay` (deterministic, `--step`, `--wire`), `diff` (structural: tool/parameter/capability changes, gated latency regressions) |
| **Toolkit** | ✅ done | Streamable HTTP transport · `mock` (record once, replay anywhere) · `tui` — four panes and a latency timeline · `demo` — the whole toolkit in two lines |
| **Ecosystem** | in progress | GitHub Action ✅ · config file, docs site, third-party compatibility reports |

Everything above ships in the first release, and so does the GitHub Action. The rest of the
Ecosystem milestone does not — see [`CHANGELOG.md`](CHANGELOG.md).

The end goal is simple to state: **mcpdump is the black box for MCP.** Not another inspector that shows you the present — a recorder that lets you examine the past.

## Design decisions

Five rules that any change must respect:

1. **Own the wire format.** No MCP SDK in the dependency chain. If the tool cannot show you the exact bytes, it cannot debug them.
2. **Terminal first.** Every capability must work in a plain shell before any GUI is considered.
3. **Scriptable by default.** If a command cannot be consumed by CI, it is not finished.
4. **Rules are tests, not prose.** Layering, colour sourcing, message-table ownership and the exit-code contract are all enforced by `tests/test_architecture.py`. A rule written only in a document gets violated — the two worst bugs in this project's history both came from rules that nothing checked.
5. **The type annotations are checked, not decorative.** `mypy --strict` covers `src/` and `tests/` with no exemptions. It earned its place on the first run: a mutable default hidden inside a `typer.Option([], ...)` call, a `Protocol` whose methods were declared without a return path, a fixture type that claimed a parameter was required when it had a default, and a helper whose annotation was narrower than its own implementation.

## Development

```bash
git clone https://github.com/xsw77492-code/mcpdump
cd mcpdump
python -m venv .venv && . .venv/bin/activate    # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest -q
ruff check src tests
mypy
```

`mypy` and `ruff` take no arguments — which paths are checked comes from `pyproject.toml`, so the local run and CI can never drift apart.

The test suite starts real MCP server subprocesses — no mocks, no network, no credentials required.

## Contributing

Issues and PRs are welcome. The most useful contributions right now:

- **Bug reports with the `--trace` output attached.** That single flag turns a vague report into a reproducible one.
- **Compatibility reports** against real MCP servers (which ones work, which ones don't, and what the frames look like).
- **New conformance checks.** Drop a file in `src/mcpdump/services/checks/` and export `CHECKS` — the registry finds it, no core code changes. The numeric filename prefix is the execution order: `stdout-purity` must run last, because it reads everything the session accumulated.

## License

MIT
