Record to test (mcptest record)
mcptest record is a recording proxy: a stdio MCP server that forwards every JSON-RPC envelope to your real server and writes everything that happened to a cassette as it goes. You wedge it between an MCP client you already use (Claude Code, Cursor, mcp-inspector) and the server you are building, work with the server normally for a while, and walk away with a server cassette full of real traffic. mcptest distill then turns that tape into a regression suite.
This is the cheapest way to bootstrap tests for an existing server: no YAML authoring, no guessing what arguments a real client sends. The client writes your test fixtures for you while you use it.
How it works
MCP client <-- stdio --> mcptest record <-- stdio or HTTP --> real server
|
v
cassette file (rewritten after each exchange)
The proxy speaks newline-delimited JSON-RPC on its own stdin/stdout and forwards envelopes verbatim: request ids are untouched, notifications flow in both directions, and there is no second initialize handshake injected in the middle. The client believes it is talking to the real server, because on the wire it is.
Each completed request/response pair is appended to the tape, and the cassette file is rewritten crash-safe (write to <output>.tmp, then rename) after every exchange. Kill the client, kill the proxy, pull the plug: every exchange that finished is already on disk.
Pointing a client at the proxy
Start from the real server invocation and wrap it:
mcptest record --target "node ./my-server.js --port 3000" --output cassettes/session.json
--target also accepts an http:// or https:// URL, in which case the proxy bridges the client's stdio session onto a streamable-http connection (the global --proxy, --http-proxy, and --https-proxy flags apply to that egress leg). The proxy's own client-facing transport is stdio by default; --front http --port <N> serves the streamable-HTTP front instead, for a client that dials HTTP rather than spawning a process.
Once you have a tape, mcptest serve --cassette replays it back to a client offline, the reverse of recording.
In a client config (.mcp.json, .cursor/mcp.json), replace the server entry with the proxy. mcptest record prints this snippet, with your paths filled in and --output rendered absolute, on startup:
{
"mcpServers": {
"recorded": {
"command": "mcptest",
"args": [
"record",
"--target", "node ./my-server.js --port 3000",
"--output", "/home/you/project/cassettes/session.json"
]
}
}
}
Everything human-facing (the banner, the snippet, the summary line) goes to stderr. Stdout carries only JSON-RPC frames, so the client never sees a stray banner where an envelope should be.
From tape to suite
When you have exercised the behaviors you care about, distill the tape:
mcptest distill cassettes/session.json --output suite.yaml
mcptest validate --config suite.yaml
mcptest run --config suite.yaml

The distilled suite replays against the cassette itself, offline and green, with zero edits. See Distilling a cassette into a suite for the derivation policy (assertions, review markers, replay wiring).
The proxy also stamps the live target into the cassette's meta block (source_command or source_url), so a later mcptest run --update-cassettes knows where to re-record from when the server legitimately changes.
Sessions append, they do not overwrite
Clients like Claude Code restart their MCP servers between sessions. If --output already names a parseable cassette, the proxy seeds the new session with its exchanges and appends; a second session adds to the tape instead of clobbering the first. Delete the file (or point --output somewhere fresh) when you want a clean take.
Secret redaction
Recorded traffic is scrubbed with the same redaction policy every reporter, log line, and cassette writer uses: secret-named keys (api_key, token, password, ...) and token-shaped values are replaced with [REDACTED:...] markers before any byte lands on disk. Responses are additionally normalized (timestamps, UUIDs, trace ids become placeholders) so the tape replays deterministically and diffs cleanly. Distill's review markers then flag personal-looking values (emails, bearer-like strings) that redaction deliberately leaves alone, so deciding what may be committed stays your call.
Local-only, by design
Per the open-core boundary (ADR-019), mcptest record is a single developer recording their own server on their own machine. The tape is a local file you review and commit yourself. Hosted collectors, shared cassette registries, and multi-user traffic capture are enterprise concerns and out of scope for the OSS engine.
Latency overhead
The proxy adds one process hop and a crash-safe file rewrite to every exchange. Measured procedure:
- Build the release binary (
cargo build --release -p mcptest). - Serve a one-tool echo catalog with
mcptest mock --tools-from echo.yamlas the backend. - Run a suite of 100 identical
tools/calltests (--parallel 1) two ways:command:pointing directly at the mock, andcommand:pointing atmcptest record --target "<mock command>". - Compare the per-test
duration_msmedians from the JSON report, and the median wall-clock of five whole-suite runs.
Results on a Linux dev container (release build, June 2026):
| Metric | Direct | Proxied |
|---|---|---|
Median per-test duration_ms | 0 ms | 0 ms |
| Median wall-clock, 100 calls x 5 runs | 0.335 s | 0.402 s |
Both modes are below the reporter's millisecond resolution per call. The wall-clock delta works out to roughly 0.7 ms per exchange, which includes the per-exchange cassette rewrite. Against any server doing real work, the overhead disappears into the noise; it is safe to leave the proxy in your client config for a whole working session.