Skip to main content

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.

CI Python Runtime dependencies License

mcpdump in action: listing a server, then reading the raw frames of a tool call

English · 简体中文


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:

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:

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:

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

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.

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 explains when that matters.

mcpdump ls <server>

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

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.

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:

→ 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.

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:

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:

- 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:

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:

# .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:

[![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:

mcpdump call "mcpdump watch \"python -m mcpdump.demo\"" echo --args '{"text":"hello"}'
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:

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

Record a whole session, one frame per line:

mcpdump watch --record session.jsonl "python -m mcpdump.demo"
{"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.

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.

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
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.

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.

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.

$ 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:

mcpdump discover --use filesystem      # straight into `ls`
mcpdump discover --probe               # really connect to each one and verify
mcpdump discover --json                # machine-readable
{
  "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.

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.

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

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

Release files for mcpdump 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mcpdump 0.1.0
File Size Uploaded
mcpdump-0.1.0.tar.gz 306.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mcpdump 0.1.0
File Interpreter ABI Platform
mcpdump-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 483.4 kB

Release files / mcpdump-0.1.0.tar.gz

Download URL mcpdump-0.1.0.tar.gz
Size 306.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b13a4c90a4d92ec07b11446fc2395c3a66af60664671707dee2674a66cc8e031
BLAKE2b-256 checksum
How to use checksums
99473d83d3d084556fdb2c700c50c8cb500a82d90533b4c8004aaeaba914d604
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / mcpdump-0.1.0-py3-none-any.whl

Download URL mcpdump-0.1.0-py3-none-any.whl
Size 177.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
131e3fdb2261eacb3a908833f068aad8ab025402090e13d1ab82022033adfc1d
BLAKE2b-256 checksum
How to use checksums
c51fc3787e201fd477c4b0d13eb7e20020199afcc47768830151c8f0765e716e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page