mcp-replay renders the JSON-RPC traces recorded
by mcp-tape — the stdio proxy that sits between your
agent and any MCP server. Every tool call, every argument, every
byte that came back. The receipt for the journey.
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.
The whole pipeline runs locally. mcp-tape install is
idempotent; uninstall reverses every change.
Traces never leave your machine unless you mail one.
mcp-tape globallyRequires Node 20+. -g (not npx) so the wrapped binary path in your client config survives upgrades.
Auto-detects Claude Code, Desktop, Antigravity, Gemini configs. 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.
A collaborator drops the same file into mcpreplay.dev. No public URL, no upload, no auth — just the bytes.
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.