# Mcp Recorder

> A lightweight proxy that records and replays MCP server interactions for deterministic testing and CI-friendly regression workflows.

- **Type:** MCP server
- **Install:** `agentstack add mcp-devhelmhq-mcp-recorder`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [devhelmhq](https://agentstack.voostack.com/s/devhelmhq)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [devhelmhq](https://github.com/devhelmhq)
- **Source:** https://github.com/devhelmhq/mcp-recorder
- **Website:** https://devhelm.io

## Install

```sh
agentstack add mcp-devhelmhq-mcp-recorder
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# mcp-recorder — VCR.py for MCP servers

Record, replay, and verify Model Context Protocol interactions for deterministic testing.

[](https://pypi.org/project/mcp-recorder/)
[](https://python.org)
[](LICENSE)
[](https://github.com/devhelmhq/mcp-recorder/actions)

MCP servers break silently. Tool schemas change, prompts drift, responses shift. Without wire-level regression tests, you find out from your users. mcp-recorder captures the full protocol exchange into a cassette file and lets you test from both sides.

## Record. Replay. Verify.

Try it right now — a [`scenarios.yml`](scenarios.yml) and a public demo server at `https://mcp.devhelm.io` are included so you can run this without any setup:

```bash
pip install mcp-recorder

# 1. Record cassettes from a scenarios file (zero code)
mcp-recorder record-scenarios scenarios.yml

# 2. Inspect what was captured
mcp-recorder inspect cassettes/demo_walkthrough.json

# 3. Verify your server hasn't regressed — compare responses to the recording
mcp-recorder verify --cassette cassettes/demo_walkthrough.json --target https://mcp.devhelm.io

# 4. Replay as a mock server — test your client without the real server
# (starts a local server on port 5555, point your MCP client at it)
mcp-recorder replay --cassette cassettes/demo_walkthrough.json

# Works with stdio servers too — no HTTP wrapper needed
mcp-recorder verify --cassette cassettes/golden.json \
  --target-stdio "node dist/index.js"
```

One cassette. Three modes. HTTP and stdio transports. Full coverage for both client and server testing.

## Contents

- [Install](#install)
- [How It Works](#how-it-works)
- [Scenarios](#scenarios)
- [CLI Usage](#cli-usage)
- [pytest Integration](#pytest-integration)
- [Python API](#python-api)
- [Configuration](#configuration)
- [Cassette Format](#cassette-format)
- [CLI Reference](#cli-reference)
- [CI Integration](#ci-integration)
- [Roadmap](#roadmap)
- [Contributing](#contributing)

## Install

```bash
pip install mcp-recorder
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv add mcp-recorder
```

## How It Works

mcp-recorder captures the full MCP exchange into a cassette file. It supports both HTTP (Streamable HTTP / SSE) and stdio (subprocess) transports — the transport is an implementation detail, the cassette format is the same. That single recording unlocks two testing directions:

```
Record:   Client -> mcp-recorder (proxy) -> Real Server -> cassette.json
                                            (HTTP or stdio subprocess)

Replay:   Client -> mcp-recorder (mock)  -> cassette.json     (test your client)
Verify:   mcp-recorder (client mock) -> Real Server            (test your server)
```

**Replay** serves recorded responses back to your client. No real server, no credentials, no network.

**Verify** sends recorded requests to your (updated) server and compares the actual responses to the golden recording. Catches regressions after changing tools, schemas, or prompts.

## Scenarios

Define what to test in a YAML file. No Python scripts, no boilerplate — works with MCP servers written in any language.

```yaml
schema_version: "1.0"

target: http://localhost:3000

redact:
  server_url: true
  env:
    - API_KEY
  patterns:
    - "sk-[a-zA-Z0-9]+"

scenarios:
  tools_and_schemas:
    description: "Discover tools and call search"
    actions:
      - list_tools
      - call_tool:
          name: search
          arguments:
            query: "test"

  error_handling:
    description: "Invalid inputs return proper errors"
    actions:
      - call_tool:
          name: search
          arguments: {}
```

For stdio MCP servers, use a target object instead of a URL:

```yaml
target:
  command: "node"
  args: ["dist/index.js"]
  env:
    API_KEY: "test-key"
  cwd: "./server"
```

| Target field | Required | Description |
|---|---|---|
| `command` | yes | Executable to spawn |
| `args` | no | List of command-line arguments |
| `env` | no | Extra environment variables (merged with current env) |
| `cwd` | no | Working directory for the subprocess |

### Environment Variables

String values in `scenarios.yml` support `${VAR}` interpolation — the variable is resolved from the current environment at load time. Use `${VAR:-default}` to provide a fallback when the variable is not set. If a referenced variable is missing and no default is provided, loading fails with a clear error.

```yaml
schema_version: "1.0"

target:
  command: "node"
  args: ["dist/index.js"]
  env:
    API_KEY: "${API_KEY}"
    REGION: "${AWS_REGION:-us-east-1}"

redact:
  env:
    - API_KEY

scenarios:
  authenticated_search:
    description: "Search with a real API key"
    actions:
      - list_tools
      - call_tool:
          name: search
          arguments:
            query: "test"
```

This works naturally with CI systems. In GitHub Actions, expose repository secrets as environment variables and `scenarios.yml` picks them up:

```yaml
# .github/workflows/mcp-test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      API_KEY: ${{ secrets.API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - run: pip install mcp-recorder
      - run: mcp-recorder record-scenarios scenarios.yml -o cassettes/
```

Interpolation applies to all string values: `target` URLs, `target.env` values, tool arguments, resource URIs, etc. Non-string values (numbers, booleans) are left unchanged. Dictionary keys are not expanded.

Record all scenarios at once, or pick one:

```bash
mcp-recorder record-scenarios scenarios.yml
mcp-recorder record-scenarios scenarios.yml --scenario tools_and_schemas
```

Each scenario key becomes the cassette filename (`tools_and_schemas` -> `tools_and_schemas.json`). Protocol handshake (`initialize` + `notifications/initialized`) is handled automatically.

Supported actions:

| Action | Description |
|---|---|
| `list_tools` | Call `tools/list` |
| `call_tool` | Call `tools/call` with `name` and `arguments` |
| `list_prompts` | Call `prompts/list` |
| `get_prompt` | Call `prompts/get` with `name` and optional `arguments` |
| `list_resources` | Call `resources/list` |
| `read_resource` | Call `resources/read` with `uri` |

## CLI Usage

### Interactive Recording

Start the proxy pointing at your MCP server:

```bash
# HTTP target
mcp-recorder record \
  --target http://localhost:8000 \
  --port 5555 \
  --output golden.json

# stdio target — spawns the server as a subprocess
mcp-recorder record \
  --target-stdio "node dist/index.js" \
  --target-env API_KEY=test-key \
  --output golden.json
```

Point your MCP client at `http://localhost:5555` and interact normally. Press `Ctrl+C` when done — the cassette is saved.

Works with remote servers too:

```bash
mcp-recorder record \
  --target https://mcp.example.com/v1/mcp \
  --redact-env API_KEY \
  --output golden.json
```

For automated recording, see [Scenarios](#scenarios).

### Verify

After making changes to your server, verify nothing broke:

```bash
# HTTP target
mcp-recorder verify --cassette golden.json --target http://localhost:8000

# stdio target
mcp-recorder verify --cassette golden.json \
  --target-stdio "node dist/index.js" \
  --target-env API_KEY=test-key
```

```
Verifying golden.json against http://localhost:8000

  1. initialize          [PASS]
  2. tools/list          [PASS]
  3. tools/call [search] [FAIL]
       $.result.content[0].text: "old output" != "new output"
  4. tools/call [analyze] [PASS]

Result: 3/4 passed, 1 failed
```

Exit code is non-zero on any diff — plug it straight into CI.

For fields that change every run, skip them by name or by exact path:

```bash
mcp-recorder verify --cassette golden.json --target http://localhost:8000 \
  --ignore-fields timestamp \
  --ignore-paths '$.result.content[0].text.metadata.requestId'
```

When both values are JSON-encoded strings (common in MCP `content[0].text`), mcp-recorder automatically parses and compares them structurally instead of as raw strings.

When a change is intentional, update the cassette:

```bash
mcp-recorder verify --cassette golden.json --target http://localhost:8000 --update
```

### Replay

Serve recorded responses without the real server:

```bash
mcp-recorder replay --cassette golden.json
```

A mock server starts on port `5555`. Point your client at it. No network, no credentials, same responses every time.

### Inspect

```bash
mcp-recorder inspect golden.json
```

```
golden.json
  Recorded: 2026-02-17 20:25:23
  Server:   Test Calculator v2.14.5
  Protocol: 2025-11-25
  Target:   http://127.0.0.1:8000

  Interactions (9):
    1. initialize -> 200 SSE (7ms)
    2. notifications/initialized -> 202 (1ms)
    3. tools/list -> 200 SSE (22ms)
    4. tools/call [add] -> 200 SSE (18ms)
    ...

  Summary: 6 requests, 1 notification, 2 lifecycle
```

## pytest Integration

The pytest plugin activates automatically on install. Mark tests with a cassette and use the `mcp_replay_url` fixture:

```python
import pytest
from fastmcp import Client

@pytest.mark.mcp_cassette("cassettes/golden.json")
async def test_tool_call(mcp_replay_url):
    async with Client(mcp_replay_url) as client:
        result = await client.call_tool("add", {"a": 2, "b": 3})
        assert result.content[0].text == "5"
```

For server regression testing, use `mcp_verify_result`:

```python
@pytest.mark.mcp_cassette("cassettes/golden.json")
def test_no_regression(mcp_verify_result):
    assert mcp_verify_result.failed == 0, mcp_verify_result.results
```

To ignore volatile fields, pass them via the marker:

```python
@pytest.mark.mcp_cassette(
    "cassettes/golden.json",
    ignore_fields=["timestamp"],
    ignore_paths=["$.result.metadata.requestId"],
)
def test_no_regression(mcp_verify_result):
    assert mcp_verify_result.failed == 0
```

```bash
pytest                                        # replay from cassettes (default)
pytest --mcp-target http://localhost:8000      # verify against live HTTP server
pytest --mcp-target-stdio "node dist/index.js" # verify against stdio server
pytest --mcp-record-mode=auto                  # replay if cassette exists, skip if not
```

Each test gets an isolated server on a random port. No manual server management.

## Python API

For programmatic recording:

```python
from mcp_recorder import RecordSession

async with RecordSession(
    target="http://localhost:8000",
    output="golden.json",
) as client:
    await client.list_tools()
    await client.call_tool("add", {"a": 2, "b": 3})
```

`RecordSession` starts a recording proxy, runs `initialize` automatically, and saves the cassette on exit. Supports all redaction options (`redact_server_url`, `redact_env`, `redact_patterns`).

## Configuration

### Matching Strategies

| Strategy | Flag | Description |
|---|---|---|
| **Method + Params** | `method_params` | Match on JSON-RPC `method` and `params`, ignoring `_meta` (default) |
| **Sequential** | `sequential` | Return next unmatched interaction in recorded order |
| **Strict** | `strict` | Full structural equality of the request body including `_meta` |

### Secret Redaction

Redaction is explicit — no magic scanning, no hidden behavior. You control exactly what gets scrubbed.

**`--redact-server-url`** (enabled by default) — strips the URL path from `metadata.server_url`, keeping only scheme + host. Handles API keys in URLs like `https://mcp.firecrawl.dev//mcp`.

```bash
mcp-recorder record --target https://mcp.firecrawl.dev/$FIRECRAWL_KEY/mcp
# metadata shows: https://mcp.firecrawl.dev/[REDACTED]

mcp-recorder record --target http://localhost:8000 --no-redact-server-url
# metadata shows full URL
```

**`--redact-env VAR_NAME`** — reads the env var's value and replaces it in metadata and response bodies. Request bodies are never modified to preserve replay and verify integrity.

```bash
mcp-recorder record \
  --target https://mcp.firecrawl.dev/$FIRECRAWL_KEY/mcp \
  --redact-env FIRECRAWL_KEY
```

**`--redact-patterns REGEX`** — for values not in environment variables. Same scope (metadata + responses only).

```bash
mcp-recorder record --target http://localhost:8000 \
  --redact-patterns "sk-[a-zA-Z0-9]+" \
  --redact-patterns "session-[0-9a-f]{32}"
```

In scenarios files, redaction is configured in the `redact` block and applies to all cassettes from that file. HTTP headers (Authorization, Cookie, etc.) are not stored in cassettes — the proxy only captures JSON-RPC message bodies.

## Cassette Format

Cassettes store JSON-RPC messages at the protocol level:

```json
{
  "version": "1.0",
  "metadata": {
    "recorded_at": "2026-02-17T20:25:23Z",
    "server_url": "http://127.0.0.1:8000",
    "transport_type": "http",
    "protocol_version": "2025-11-25",
    "server_info": { "name": "Test Calculator", "version": "2.14.5" }
  },
  "interactions": [
    {
      "type": "jsonrpc_request",
      "request": {
        "jsonrpc": "2.0", "id": 0, "method": "initialize",
        "params": { "protocolVersion": "2025-11-25", "capabilities": {} }
      },
      "response": {
        "jsonrpc": "2.0", "id": 0,
        "result": {
          "protocolVersion": "2025-11-25",
          "capabilities": { "tools": { "listChanged": true } },
          "serverInfo": { "name": "Test Calculator", "version": "2.14.5" }
        }
      },
      "response_is_sse": true,
      "response_status": 200,
      "latency_ms": 7
    }
  ]
}
```

The `transport_type` field (`"http"` or `"stdio"`) is informational. For stdio recordings, `response_is_sse` is `false` and `response_status` is `null` since there is no HTTP layer.

## CLI Reference

### `mcp-recorder record`

| Option | Default | Description |
|---|---|---|
| `--target` | — | URL of the real MCP server (HTTP). Mutually exclusive with `--target-stdio` |
| `--target-stdio` | — | Command to spawn a stdio MCP server (e.g. `"node dist/index.js"`). Mutually exclusive with `--target` |
| `--target-env` | — | Environment variable for stdio subprocess as `KEY=VALUE`. Repeatable |
| `--port` | `5555` | Local proxy port |
| `--output` | `recording.json` | Output cassette file path |
| `--verbose` | — | Log full headers and bodies to stderr |
| `--redact-server-url / --no-redact-server-url` | `true` | Strip URL path from metadata (keeps scheme + host) |
| `--redact-env VAR` | — | Redact named env var value from metadata + responses. Repeatable |
| `--redact-patterns REGEX` | — | Redact regex matches from metadata + responses. Repeatable |

### `mcp-recorder record-scenarios`

| Argument / Option | Default | Description |
|---|---|---|
| `SCENARIOS_FILE` | *(required)* | Path to YAML scenarios file |
| `--output-dir` | `cassettes/` next to file | Output directory for cassettes |
| `--scenario NAME` | all | Record only the named scenario(s). Repeatable |
| `--verbose` | — | Log full request/response details to stderr |

### `mcp-recorder replay`

| Option | Default | Description |
|---|---|---|
| `--cassette` | *(required)* | Path to cassette file |
| `--port` | `5555` | Local server port |
| `--match` | `method_params` | Matching strategy (see [Matching Strategies](#matching-strategies)) |
| `--verbose` | — | Log every matched request to stderr |

### `mcp-recorder verify`

| Option | Default | Description |
|---|---|---|
| `--cassette` | *(required)* | Path to golden cassette file |
| `--target` | — | URL of the server to verify (HTTP). Mutually exclusive with `--target-stdio` |
| `--target-stdio` | — | Command to spawn a stdio MCP server. Mutually exclusive with `--target` |
| `--target-env` | — | Environment variable for stdio subprocess as `KEY=VALUE`. Repeatable |
| `--ignore-fields KEY` | — | Key name to ignore at **any depth** (e.g. `timestamp`). Repeatable |
| `--ignore-paths PATH` | — | Exact dot-path to ignore (e.g. `$.result.metadata.scrapeId`). Repeatable |
| `--update` | — | Update the cassette with new responses (snapshot update) |
| `--verbose` | — | Show full diff for each failing interaction |

### `mcp-recorder inspect`

| Argument | Description |
|---|---|
| `CAS

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [devhelmhq](https://github.com/devhelmhq)
- **Source:** [devhelmhq/mcp-recorder](https://github.com/devhelmhq/mcp-recorder)
- **License:** MIT
- **Homepage:** https://devhelm.io

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-devhelmhq-mcp-recorder
- Seller: https://agentstack.voostack.com/s/devhelmhq
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
