mcp-cassette

The cassette is itself an MCP server, so any client in any language connects to it exactly as it connects to the live one: no library to import, no product code to change, no transport to wrap.

Record one session against a real Model Context Protocol server, then run your agent tests against the recording: no credentials, no rate limits, no network. The same binary gates your tool contract against breaking changes and lints tool descriptions for poisoning.

mcp-cassette 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

For vitest specifically there is an adapter, mcp-cassette/vitest: one useCassette() call wraps a describe block with a replay server, and a fingerprint miss fails the test that caused it instead of arriving as a JSON-RPC error the assertion never looks at. See Testing with vitest.

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