mcp-tape is the stdio proxy that records every
JSON-RPC frame between your agent and any MCP server.
mcp-replay is this page — it renders the trace,
and mcp-tape share turns it into a link anyone can
open. The receipt for the journey.
npx mcp-tape install --target=claude-code
mcp-tape share <trace>.jsonl
Tip: drag & drop .jsonl traces anywhere — one
file opens it, two files diff them, more files merge them by
timestamp.
The simplest reliable design we could draw. mcp-tape is a transparent proxy — it wraps any MCP server, observes every frame on the in/out pipes, writes them to a JSONL file. mcp-replay reads that file and shows you everything.
Whatever's making the agent calls. Sees the proxy as a normal MCP server — no client changes.
The stdio proxy. Sits between client and server, observes every frame on both pipes, redacts known secret shapes, writes the JSONL trace. Optionally serves a live websocket.
The real MCP server. Unmodified. Tape spawns it as a child process and pipes through it — no monkey-patching.
A single .jsonl trace rendered five ways. Click a
plate name to flip the leaf — same data, different question
answered.
| INSTRUMENT (TOOL) | CALLS | PROFILE | MEAN | p95 | ERR |
|---|---|---|---|---|---|
| ⊙read_text_file | 12 | 95% | 118 ms | 280 ms | 1 (8.3%) |
| ⊙list_directory | 8 | 64% | 84 ms | 180 ms | — |
| ⊙write_file | 6 | 48% | 102 ms | 220 ms | — |
| ⊙search_files | 3 | 24% | 412 ms | 740 ms | — |
| ⊙move_file | 1 | 8% | 74 ms | 74 ms | — |
read_text_file dominates this traverse (40% of calls) and accounts for the only error — an attempt at /etc/passwd the server correctly rejected. Investigate the prompt that produced it.
{
"path": "/home/me/proj/src"
}
[FILE] index.ts [FILE] util.ts [DIR] components
{
"path": "/home/me/proj/src/index.ts"
}
export function main() {
console.log("hello");
}
{
"path": "/etc/passwd"
}
Access denied: /etc/passwd is outside allowed directories
═ same · ≠ changed args/result · + added in B · - removed from B.
MCP debugging has good instruments — the official Inspector, mcpsnoop, mcpscope. Several record real traffic; one replays it in CI. What this pair adds is the shareable web replay link, plus trace diff and timestamp merge in the browser — while staying local-first by default.
| Instrument | Sees real client ↔ server traffic | Replay | Share | Reading |
|---|---|---|---|---|
| mcp-tape + mcp-replay | Yes — transparent stdio proxy in the pipe | Web replay: timeline · tools · calls · raw, plus two-trace diff and multi-trace merge | Hosted replay links — mcp-tape share → a URL anyone opens here. Redaction on by default, 30-day expiry, deletable |
Local-first (traces stay on disk; this viewer reads files in your browser) · MIT |
| MCP Inspector | No — connects to your server as its own client, so it shows Inspector-to-server traffic, not what your real client sent | Interactive testing UI; no trace files to replay | — | The official developer tool. Excellent for exercising a server by hand. |
| mcpsnoop | Yes — transparent proxy, "Wireshark for MCP" | Live terminal TUI; can re-send a captured call; exports JSON/HTML/text/OTLP | File exports; no hosted replay links | Terminal-first. Strong live debugging with stderr and hung-call views. |
| mcpscope | Yes — transparent proxy | Local dashboard with latency histograms; replays traces against your server in CI; schema diff | File exports; no hosted replay links | Observability & CI-regression angle · MIT |
The pipeline runs locally. mcp-tape install is
idempotent; uninstall reverses every change. Traces
never leave your machine unless you share one — and
shares are redacted before upload.
Also detects Claude Desktop, Antigravity, Gemini CLI (or omit --target for all). Backs up to <file>.mcp-tape.bak. --dry-run previews.
Every server writes a session trace to ./mcp-traces/ — one file per session per server, ISO-timestamped.
Click Open local trace… or drag the .jsonl onto this page. The file stays in your browser.
Re-redacts, uploads, prints mcpreplay.dev/?trace=<id>. Links expire after 30 days; --delete removes one early.
Anyone opens it right here — timeline, tools, calls, raw. No account, no install, nothing to build.
Add --serve to stream a running session straight into
this page instead of opening a finished file. The websocket binds
to 127.0.0.1 only — mcp-replay refuses any other host.
Run mcp-tape with --serve. It prints a localhost websocket URL on stderr. Open that URL here as ?live=… and frames arrive as they're written. New connections receive a snapshot, then live appends.
?since=<lastSeq>--no-file for serve-only (no on-disk trace)
> mcp-tape --serve -- npx -y \ @modelcontextprotocol/server-filesystem ~/projects mcp-tape: live mode on ws://127.0.0.1:7777/ open: mcpreplay.dev/?live=ws://127.0.0.1:7777
The trace format is open and stable. Any producer
can emit it. mcp-tape is the reference producer; the
spec is documented enough that any team can reach the format
without depending on a specific tool.
| Line type | Required? | Description |
|---|---|---|
| meta | first line | The bookplate. {v, type, startedAt, label, command, mcpTapVersion?} — format version, start timestamp, server label, and the argv used to launch the server. |
| message (in) | repeated | An inbound bearing. {t, dir:"in", raw: <JSON-RPC>} — client → server frame. Request or notification. |
| message (out) | repeated | An outbound reading. {t, dir:"out", raw: <JSON-RPC>} — server → client frame. Response or error. |
| end | last line | The endpiece. {t, type:"end", exitCode, durationMs} — written when the proxy closes. exitCode 0 = clean. |
"[REDACTED]" before writing.
Multi-server. One file per server; merge by timestamp at render time using ?trace=a;b;c.
Versioning. Additive changes don't bump v; renderers ignore unknown fields.
The field-book of every agent traverse. The receipt for the journey — readable, shareable, replayable.
?trace=<url>?trace=<a>;<b> · merge?diff=<a>;<b> · discrepancy?live=ws://127.0.0.1:<port>?since=<lastSeq> · resumemcp-tape is running with --serve (or MCP_TAPE_SERVE set on its environment). The stderr banner shows live mode on ws://127.0.0.1:<port>/ when it's listening.port N was unavailable — bound M instead.ws://127.0.0.1:<port> or ws://localhost:<port>. Remote hosts are refused intentionally.curl or another browser tab to isolate.Full guide: docs/live-mode.md.