Skip to main content
Glama

mcptape

ci PyPI Python Dependency License: MIT

mcptape is a tape recorder for the Model Context Protocol: wrap any MCP server, record a session once (REC ⏺), replay it offline forever (PLAY ▶), and diff exactly where the tool behavior drifted (⏩).

Your agent talks to MCP servers — filesystem, browser, search, your own tools. Those servers change, their tools change, their results change, and your agent breaks in ways you cannot reproduce without paying for a dozen live runs. mcptape records the whole session — every tools/call, every result, every notification — as local JSONL. From then on, replay serves the recorded results offline for free and byte-identically, and one command diffs two recordings to the first drifted call.

Zero dependencies: pure Python standard library. Nothing leaves your machine — a trace is a local file.

mcptape demo: record / replay / diff, rendered from real command output

(The screenshot is rendered from real command output by docs/make_demo_svg.py — not staged.)

中文文档:README.zh-CN.md

How it works

flowchart LR
    A["MCP client<br/>(your agent)"] -->|"stdio"| B["mcptape rec<br/>transparent proxy ⏺"]
    B -->|"stdio"| C["real MCP server"]
    B --> D[".mcptape/runs/<br/>trace.jsonl + meta.json"]
    A -->|"stdio"| E["mcptape play<br/>served from tape ▶"]
    D -.->|"byte-for-byte"| E
    F["second recording"] --> G["mcptape diff<br/>first drifted call"]
    E -.-> G
    G -->|"exit 0 / 1 / 2"| H["CI ✅ ❌"]

In rec mode mcptape is a transparent proxy: it spawns the real server, forwards every JSON-RPC line both ways unchanged, and files each message to the tape. Your agent cannot tell it is being recorded.

In play mode mcptape is the server: it answers initialize, tools/list and tools/call from the tape — no child process, no network, identical results.

Related MCP server: mcp-vcr

30 seconds

pip install mcptape

# 1. record a session: put mcptape between your client and any MCP server
python examples/demo_client.py -- mcptape rec --name demo-stable -- python examples/demo_server.py

# 2. replay it offline -- same results, no server process, $0.00
python examples/demo_client.py -- mcptape play demo-stable

# 3. the server changes; record again and diff
MCPTAPE_DEMO_DRIFT=1 python examples/demo_client.py -- mcptape rec --name demo-drift -- python examples/demo_server.py
mcptape diff demo-stable demo-drift

Real output (paste the commands and you get this verbatim):

=== replay ===
server: demo-tools
tools: echo, add
echo: tapedeck, but for MCP
add(2, 3): 5

=== diff demo-stable demo-drift ===
run A: demo-stable (11 events)
run B: demo-drift (11 events)
event 10 (s2c response):
  result changed
  A: {"result": {"content": [{"type": "text", "text": "5"}]}}
  B: {"result": {"content": [{"type": "text", "text": "6"}]}}
  context (events …i):
    c2s/request tools/call
    s2c/notification notifications/message

diff exit codes are designed for CI: 0 identical, 1 output drift, 2 structural drift.

In your agent config

rec is one config edit away from any stdio-based MCP client (Claude Desktop, Claude Code, Cursor, ...):

{
  "mcpServers": {
    "my-tools": {
      "command": "mcptape",
      "args": ["rec", "--name", "daily-run", "--", "python", "-m", "my_tools"]
    }
  }
}

Flip "rec" to "play", "daily-run" and the agent runs against the tape — full regression pass with zero live calls. The client cannot tell the difference: same tools, same results, same notifications.

Why matching is by content, not position

tapedeck aligns calls by position; MCP clients pipeline and run tool calls in parallel, so positional alignment would false-positive on every concurrent session. mcptape matches each incoming call against the earliest unconsumed recorded exchange with the same method and JSON-equal arguments. Identical repeated calls consume identical recorded entries in order; reordered or parallel calls match freely; a call whose arguments changed matches nothing — that is the drift, reported with the exact arguments.

initialize is matched leniently on parameters (client name/version/protocol bumps are not behavior drift) and its recorded capabilities answer is served verbatim.

Why not LangSmith / LangFuse / OTel platforms

Cloud observability

mcptape

Position

watch runs (dashboard)

reproduce runs (recorder)

Replay

re-calls the tools (state changed, money spent)

replays recorded results (free, deterministic)

Data

uploaded to a third party

local files only

Dependencies

SDK + reporting config

zero, two commands

Complementary, not competing: platforms show trends; mcptape does forensics and regression. The same role tapedeck plays for model calls, one layer down.

CLI

mcptape rec --name RUN -- <server command>   # proxy + record a live session
mcptape play RUN [--lenient]                 # serve a recording as an MCP server
mcptape diff A B                             # first drift + context; exit 0/1/2
mcptape ls                                   # list recorded runs
mcptape report RUN                           # per-message table

mcptape ls and report, rendered from real command output

Trace format

One directory per run, two plain files:

.mcptape/runs/20261031-011921-demo-stable/
    meta.json     # server command, protocol version, counts
    trace.jsonl   # one JSON-RPC message per line, with request echoes
{"v": 1, "i": 10, "dir": "s2c", "kind": "response", "id": 4,
 "payload": {"result": {"content": [{"type": "text", "text": "5"}]}},
 "for_method": "tools/call", "for_params": {"name": "add", "arguments": {"a": 2, "b": 3}},
 "wall_ms": 0.9}

Every response carries an echo of the request it answers, so each line is self-contained — replay and diff never need to join across lines.

Honest boundaries

  • stdio transport only (v0.1). HTTP/SSE-based MCP servers are not wrapped yet; streamable-HTTP support is on the roadmap.

  • Sessions with server-initiated requests cannot be replayed. If the server asked the client to sample or elicit, the tape would have to impersonate the client — play refuses such recordings up front (exit 2); diff still works on them.

  • Replay is deterministic only up to the tape. Client-side logic that reads the clock, uses randomness, or branches on anything outside the session may take a different path than during recording — that is true of every record/replay tool and is exactly what diff between two real recordings is for.

  • Server-side state is frozen at record time. Replaying a tools/call that wrote a file serves the recorded result without writing again; replay never touches the real world. If you need the side effects re-applied, that is a live run, not a replay.

  • No monkeypatching. rec is a proxy process; the client config opts in explicitly.

Roadmap

  • streamable-HTTP transport wrapping

  • --subset replay (serve only named tools from the tape, live for the rest)

  • trace signing (tamper-evident hash chain)

  • OTel GenAI span import/export

The tape suite

The same "record once, replay forever" idea, at three layers:

  • tapedeck — model & tool calls inside your agent (Python, PyPI)

  • mcptape — the MCP tool-protocol boundary (this repo)

  • mocktape — HTTP traffic behind your frontend (TypeScript, npm)

Design trade-offs live in the source docstrings; every image in this README is rendered from real command output.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    VCR for MCP servers: a zero-dependency stdio proxy that records and replays MCP JSON-RPC tool calls to local cassette files, enabling deterministic, offline testing of AI agent workflows without side-effects or rate limits.
    1
    MIT