Skip to main content

mcp-cassette

Record/replay testing for MCP (Model Context Protocol) agents: capture real sessions as cassettes, replay them as deterministic mock servers — vcrpy for MCP.

A cassette records the entire session between your agent and a real MCP server — local stdio or remote Streamable HTTP — as a structured, diffable, committable file. Replayed, it stands in for the live server, so your agent test suite stops hitting live servers and stops being flaky, slow, and expensive.

mcp-cassette operates at the transport level (newline-delimited JSON-RPC over stdio; h11 + hand-rolled SSE framing over Streamable HTTP), treats messages semi-opaquely, and does not depend on the official mcp SDK at runtime — so it works with any MCP client (Claude Code included) unmodified. Sessions containing server-initiated requests (sampling, elicitation) record and replay too.

Full documentation: docs/guide/ — 15 numbered chapters in reading order: getting started and how-to guides for test authors (1–10), then installation, configuration, CI, CLI reference, and the runbook for operators (11–15). The sections below summarize; each ends with a pointer to its full chapter.

1. Install

uv add mcp-cassette              # or: pip install mcp-cassette
uv add "mcp-cassette[http]"      # remote (Streamable HTTP) record/replay

Python ≥ 3.12. Linux, macOS, and Windows supported. The core install depends only on anyio and pydantic; the [http] extra adds httpx and h11.

Full chapter: 11. Installation.

2. The pytest fixture (the main surface)

def test_agent_summarizes_repo(mcp_cassette):
    cmd = mcp_cassette.server_command(["python", "tools/github_server.py"])
    result = run_my_agent(mcp_servers={"github": cmd})
    assert "summary" in result

First run records through the recording proxy; every run after replays offline, deterministic and fast. The fixture never monkeypatches your agent — it hands you a command list to plug into the agent's MCP server configuration.

For a remote server, server_url is the drop-in twin (needs the [http] extra):

def test_agent_reads_remote_tracker(mcp_cassette):
    url = mcp_cassette.server_url("https://mcp.example.com/mcp")
    result = run_my_agent(mcp_servers={"tracker": {"url": url}})
    assert "triaged" in result

First run stands up a local recording proxy in front of the real URL; every run after replays from the cassette on a local mock Streamable HTTP server. Same record modes, same fault matrix. Authorization (and every other header) is forwarded upstream but never written to the cassette.

Full chapters: 2. Record and replay a stdio server, 3. Record and replay a remote HTTP server.

2.1 Record modes

The mode decides, once per test run, whether that run records or replays; the recording unit is always the entire session — every message from server launch to session end — never an individual tool call.

Precedence, highest first: MCP_CASSETTE_MODE (env) → marker mode= → mcp_cassette_mode (ini) → default once.

Mode Cassette absent Cassette present
once (default) record replay
none fail — recording is forbidden replay
all record re-record
new_episodes record replay; misses fall through to the real server and are appended

CI should set MCP_CASSETTE_MODE=none so no pipeline silently hits a live server.

Full chapters: 12. Configuration, 13. CI pipeline.

3. Use it as a library

Not a pytest suite? use_cassette is the same machinery behind a context manager — same modes, same fault matrix, same failure semantics:

from mcp_cassette import use_cassette

with use_cassette("cassettes/search.mcp.json", mode="once") as session:
    cmd = session.server_command(["python", "-m", "my_server"])
    run_my_agent(mcp_servers={"search": {"command": cmd[0], "args": cmd[1:]}})
# clean exit -> finalize(): background server stopped, report checked,
#               CassetteError raised on an empty recording or any replay miss

Precedence, highest first: MCP_CASSETTE_MODE (env) → mode= argument → default once — so the CI invariant holds through this door too. The session report goes to a temp directory that is removed on exit — no untracked JSON next to cassettes you commit. examples/library_mode.py is runnable.

Full chapter: 4. Use it as a library.

4. Fault injection

One recorded cassette drives a whole resilience matrix:

import mcp_cassette as mcc

@pytest.mark.parametrize("fault", [
    mcc.Fault.timeout("tools/call", nth=1),
    mcc.Fault.error("tools/call", code=-32000, message="rate limited"),
    mcc.Fault.disconnect("tools/call"),
])
def test_agent_survives_tool_trouble(mcp_cassette, fault):
    session = mcp_cassette.with_faults(fault)
    cmd = session.server_command(["python", "tools/github_server.py"])
    result = run_my_agent(mcp_servers={"github": cmd})
    assert result.completed_with_degraded_tools

Fault types: delay, timeout, error, malformed, disconnect. Faults live in a FaultOverlay; the recorded cassette is never mutated.

Full chapter: 5. Inject faults.

5. Replay timing

Replay is instant by default. When your agent's timeout, progress-stream, or retry logic depends on how long the server took, replay the recorded gaps instead:

mcp-cassette serve demo.json --pace recorded                     # recorded latency
mcp-cassette serve demo.json --pace recorded --pace-scale 0.2    # 5x faster

Also @pytest.mark.mcp_cassette(pace="recorded", pace_scale=0.2) and use_cassette(..., pace=PaceConfig(mode="recorded")). Per-gap cap defaults to 5000 ms so one pathological recorded pause cannot look like a hung job; --pace-cap-ms 0 opts into uncapped. A delay fault stacks on top of recorded latency.

Full chapter: 6. Replay timing.

6. The CLI

mcp-cassette record --cassette demo.json -- python tools/server.py   # wrap a real server
mcp-cassette record --cassette demo.json --url https://mcp.example.com/mcp   # proxy a remote one
mcp-cassette serve demo.json                                         # drop-in replay server (transport inferred)
mcp-cassette serve demo.json --faults demo.faults.json               # replay with faults
mcp-cassette inspect demo.json                                       # per-method counts + timing
mcp-cassette inspect demo.json --timeline --grep 'tools/call'        # message timeline, payload-grepped
mcp-cassette inspect demo.json --format json > summary.json          # deterministic, diffable
mcp-cassette inspect demo.json --faults demo.faults.json             # dry-run: which requests a fault hits
mcp-cassette diff old.json new.json --tools-only                     # exit 5 when the server surface moved

A recording is checkpointed to a <cassette>.partial sidecar every 5 seconds (--checkpoint-interval SECONDS, 0 disables), so a hard kill loses only what arrived since the last checkpoint. The sidecar is a valid cassette — see §12.6 Checkpointing for recovery and why it is never written to the cassette path itself.

Full chapter: 14. CLI reference.

7. Redaction

Cassettes are verbatim transcripts, and you commit them — so redaction runs at capture time, on a deep copy, with defaults always on: values under keys matching *token*, *secret*, *password*, *apikey*, *api_key*, or authorization are replaced with REDACTED before the cassette is written. Add your own rules with --redact (key-glob or JSON pointer). Read every new cassette before its first commit anyway.

Full chapter: 8. Redact secrets.

8. Linting your cassettes

Recorded tool descriptions and results are third-party content; lint them in CI before they reach a model:

mcp-cassette lint demo-http.json
mcp-cassette lint new.json --baseline tests/cassettes/old.json --format json

Rules: R001 instruction injection in a tool description (error), R002 description/schema drift vs a baseline — the "rug pull" (error), R003 duplicate tool names (warning), R004 instruction-shaped tool results (warning). Exit 0 = no error-severity findings, 4 = at least one. Each finding carries a JSON-pointer locator into the cassette.

Bring your own rules with a declarative TOML pattern pack — no Python plugin API, deliberately, because lint should never execute third-party code on a supply-chain-security surface:

mcp-cassette lint demo.json --pattern-pack examples/lint-pack.toml
mcp-cassette lint demo.json --fail-on warning

[tool.mcp_cassette.lint] in pyproject.toml makes your packs, selection, and failure threshold the default for every invocation, so the CI command stays generic. Packs extend the bundled rules; they never replace them.

Heuristic pattern rules, not a guarantee — a clean lint is the absence of known smells, nothing more.

Full chapter: 9. Lint with your own pattern packs.

9. License

See LICENSE.

Metadata

Release files for mcp-cassette 0.3.3

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

Source distribution (sdist)

Source distribution for mcp-cassette 0.3.3
File Size Uploaded
mcp_cassette-0.3.3.tar.gz 245.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mcp-cassette 0.3.3
File Interpreter ABI Platform
mcp_cassette-0.3.3-py3-none-any.whl Python 3 none any Details

Total release size: 334.4 kB

Release files / mcp_cassette-0.3.3.tar.gz

Download URL mcp_cassette-0.3.3.tar.gz
Size 245.3 kB
Tags Source
SHA-256 checksum
How to use checksums
6e0f70dfc33bfde515303ab2a1650e29a67e43934e81a5f5d47fdf00237ce33c
BLAKE2b-256 checksum
How to use checksums
2f3785c6e188663ab673430520268e8f55caeb517602faa24f82ac40e000581d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 25, 2026.

Transparency log

Release files / mcp_cassette-0.3.3-py3-none-any.whl

Download URL mcp_cassette-0.3.3-py3-none-any.whl
Size 89.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
de086096bcdbb0ff4dd6d0f2b972614e261cffca1947f431013ca48d4ae7f8be
BLAKE2b-256 checksum
How to use checksums
d4af6925140a6f7d5f5497f05de270fc5ff851e703d6df3430a3a8cfefa63624
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 25, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.0

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

This release

0.3.3 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