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
└────────┘
snapshot --check fails the PR
that removes a tool, adds a required parameter, or changes a type — before your users' agents
break at 3am.check lints tool descriptions for the
known shapes of tool-poisoning attacks — instruction overrides, concealment directives,
exfiltration URLs, invisible Unicode — and validates every schema (draft-07 and 2020-12).
Heuristics, not proofs: treat findings as review triggers, and pair with a dedicated security
scanner for depth.Works at the transport level — any server, any SDK, any language, any spec revision.
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))
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
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
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)
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)
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
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.
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.
mcp-cassette@0.1.0