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.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    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