mcp-cassette

Record a real MCP session once — replay it forever.

VCR-style record/replay, contract snapshots, and safety checks for Model Context Protocol servers. Test your MCP integrations in CI: deterministic, offline, no live APIs, no tokens, no flakes.

v0.1.0 is on npm. npm install -g mcp-cassette — or run any command below without installing, via npx mcp-cassette …. Published with build provenance from the tagged release workflow.

┌────────┐   record    ┌──────────────┐   real    ┌────────────┐
│ client │ ──────────► │ mcp-cassette │ ────────► │ MCP server │
└────────┘             │    (proxy)   │           └────────────┘
                       └──────┬───────┘
                              ▼
                    session.cassette.jsonl
                              │
┌────────┐   replay           ▼
│   CI   │ ◄────────── deterministic mock — the real server never runs
└────────┘

Why it exists

Works at the transport level — any server, any SDK, any language, any spec revision.

The five commands

check

Health-check any live server: lifecycle handshake, tool/resource/prompt listing, JSON Schema validation, and the safety lint. Exits non-zero on errors.

mcp-cassette check --stdio "npx -y @modelcontextprotocol/server-everything stdio"
server: mcp-servers/everything@2.0.0  protocol: 2025-06-18
surface: 13 tools, 7 resources, 4 prompts
[OK] no findings
result: PASS (0 error(s), 0 warning(s))

record

A transparent stdio proxy. Put it between your client and the server; it captures every JSON-RPC frame in both directions into an open JSONL cassette. Bytes are forwarded verbatim, so recording is invisible to both sides — and secrets are redacted before they hit the file.

# wherever your client config points at the server command, wrap it:
mcp-cassette record -o session.cassette.jsonl -- npx -y @modelcontextprotocol/server-github

replay

Serves the cassette as a stdio MCP server — the cassette is the server now. Requests match on method plus arguments, repeated identical calls replay in recorded order, and anything unrecorded gets a clear JSON-RPC error.

mcp-cassette check --stdio "mcp-cassette replay session.cassette.jsonl"
# → identical results, no network, no tokens, deterministic

snapshot

Locks the contract: tools, schemas, and annotations in a canonical snapshot. --check classifies drift as breaking, minor, or info, and exits non-zero on breaking.

mcp-cassette snapshot --stdio "npx -y my-server"            # writes mcp-contract.snapshot.json
mcp-cassette snapshot --check --stdio "npx -y my-server"    # CI: fails on breaking changes
[BREAKING] add: parameter "precision" is now required
[BREAKING] slugify: tool removed
[MINOR]    add: parameter "mode" added
result: FAIL (2 breaking, 1 minor, 0 info)

redact

Cleans a cassette you already have, or audits one without touching it. --scan writes nothing and exits non-zero if it finds anything, so it drops into CI as a tripwire on committed fixtures. New recordings are redacted by default — this is for the ones that came before.

mcp-cassette redact session.cassette.jsonl -o session.redacted.jsonl   # clean a recording
mcp-cassette redact session.cassette.jsonl --scan                      # audit only, exit 1 on a hit
[keyctx] c2s tools/call params.arguments.token: ghp_**************** (39 chars)
[github] s2c tools/call result.content[0].text: ghp_**************** (39 chars)
result: FOUND (2 secret(s) detected)

CI recipe

GitHub Actions, though nothing here is Actions-specific:

- name: MCP contract & safety checks
  run: |
    npx mcp-cassette check    --stdio "node dist/my-server.js"
    npx mcp-cassette snapshot --check --stdio "node dist/my-server.js"

- name: Agent integration tests (offline, via cassette)
  run: npx vitest run   # your tests point the MCP client at:
                        # mcp-cassette replay fixtures/session.cassette.jsonl

Cassettes are plain files

Append-only JSONL: a header line, then one line per captured frame with direction and a millisecond offset. Non-JSON-RPC output from chatty servers is preserved too, so a cassette stays a faithful transcript even of a misbehaving server. The format is open, versioned, and documented so other tools can consume it.

Secrets redaction

A cassette is only useful if you can commit it, and you can only commit it if it has no credentials in it. record redacts by default — every string in every captured frame, plus the server command in the header where tokens often arrive as CLI flags, is scanned on the way to disk. The bytes forwarded to your client and to the real server are untouched, so the live session behaves exactly as if the proxy weren't there.

Each hit becomes a deterministic placeholder, and that determinism is what keeps replay working: when your test sends the live token, replay redacts the incoming request the same way before matching, so it collapses to the same placeholder that was recorded and hits the same response.

[REDACTED:<rule>:<hash8>]        e.g. [REDACTED:github:3f9a1c07]

Rules cover bearer tokens, JWTs, URL credentials in connection strings, GitHub / OpenAI / Anthropic / Slack / AWS / Google key shapes, and any JSON value under a key that looks like a credential. There is also a redact command to clean or audit a cassette you already recorded — --scan writes nothing and exits non-zero if it finds anything.

Redaction is pattern-based, so treat it as a strong default rather than a guarantee: a credential in a shape no rule knows can still reach the file. Review a cassette before you commit it.

Links