mcp-cassette
Record a real MCP session once, replay it forever. It sits between an agent and a server as a transport level proxy, writes every frame to a cassette, and replays that cassette as a deterministic mock. Contract snapshots fail the PR that breaks your tools.
$ mcp-cassette record -- ./my-serverrecording frames -> session.jsonl$ mcp-cassette replay session.jsonlserving deterministic mock on stdio$ mcp-cassette snapshot --checkbreaking: tools.search args changed8
safety lint rules
mcp-cassette safety lint, v0.1.1
Apache-2.0any server, any SDK, any languageopen JSONL cassette format
Problem
Testing an agent against a real MCP server is slow, costs money, and gives a different answer every run. Testing against a hand written mock is fast and tells you nothing, because the mock drifts from the server the moment someone ships a change.
Both options fail the same way: nothing proves the tools still work before an agent or a user touches them.
Constraints
It has to work with any server, any SDK, and any language, which rules out anything that needs to be imported into the server process.
Recordings will contain secrets, so redaction cannot be an optional flag someone remembers to set.
Replay has to be deterministic. A test suite that passes only sometimes is worse than no test suite.
What I did
The recorder is a stdio proxy at the transport level. It captures every JSON-RPC frame in both directions into an append only JSONL cassette, which means it does not care what the server is written in.
Replay serves that cassette back as a deterministic mock server. Matching is by method and arguments, and volatile `_meta` is ignored so unrelated churn does not break a match.
`snapshot --check` classifies contract drift as breaking, minor, or info, and fails CI on breaking. That turns a schema change into a failed pull request instead of a runtime surprise.
A safety lint covers eight rules for tool poisoning shapes: instruction overrides, concealment, exfiltration URLs, and invisible Unicode among them.
Decisions
| Decision | Why | What I rejected | What it cost |
|---|---|---|---|
| Record at the transport level, as a stdio proxy | It does not care what the server is written in. | Anything that has to be imported into the server process. | |
| Make redaction placeholders deterministic | The secret never lands on disk and the cassette still matches. | Random placeholders, which redact correctly and then break replay. |
Details that mattered
Secrets are redacted by default, and the placeholders are deterministic: `[REDACTED:rule:hash8]`. That detail matters more than it looks. A random placeholder would redact correctly and then break replay, because the recorded frame would no longer match the one under test.
Deterministic redaction keeps both properties at once. The secret never lands on disk, and the cassette still matches.