{"id":48671132,"url":"https://github.com/devhelmhq/mcp-recorder","last_synced_at":"2026-04-10T12:10:40.317Z","repository":{"id":340872065,"uuid":"1159425861","full_name":"devhelmhq/mcp-recorder","owner":"devhelmhq","description":"A lightweight proxy that records and replays MCP server interactions for deterministic testing and CI-friendly regression workflows.","archived":false,"fork":false,"pushed_at":"2026-03-06T10:55:05.000Z","size":2534,"stargazers_count":7,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-06T14:52:47.924Z","etag":null,"topics":["developer-tools","mcp","mcp-testing","testing-tools"],"latest_commit_sha":null,"homepage":"https://devhelm.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devhelmhq.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-16T18:02:08.000Z","updated_at":"2026-03-06T12:25:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/devhelmhq/mcp-recorder","commit_stats":null,"previous_names":["devhelmhq/mcp-recorder"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/devhelmhq/mcp-recorder","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devhelmhq%2Fmcp-recorder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devhelmhq%2Fmcp-recorder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devhelmhq%2Fmcp-recorder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devhelmhq%2Fmcp-recorder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devhelmhq","download_url":"https://codeload.github.com/devhelmhq/mcp-recorder/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devhelmhq%2Fmcp-recorder/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31641569,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-10T07:40:12.752Z","status":"ssl_error","status_checked_at":"2026-04-10T07:40:11.664Z","response_time":98,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["developer-tools","mcp","mcp-testing","testing-tools"],"created_at":"2026-04-10T12:10:40.217Z","updated_at":"2026-04-10T12:10:40.311Z","avatar_url":"https://github.com/devhelmhq.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"hero.gif\" alt=\"mcp-recorder demo\" width=\"720\" /\u003e\n\u003c/p\u003e\n\n# mcp-recorder — VCR.py for MCP servers\n\nRecord, replay, and verify Model Context Protocol interactions for deterministic testing.\n\n[![PyPI version](https://img.shields.io/pypi/v/mcp-recorder.svg)](https://pypi.org/project/mcp-recorder/)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://python.org)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![CI](https://github.com/devhelmhq/mcp-recorder/actions/workflows/check.yml/badge.svg)](https://github.com/devhelmhq/mcp-recorder/actions)\n\nMCP 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.\n\n## Record. Replay. Verify.\n\nTry 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:\n\n```bash\npip install mcp-recorder\n\n# 1. Record cassettes from a scenarios file (zero code)\nmcp-recorder record-scenarios scenarios.yml\n\n# 2. Inspect what was captured\nmcp-recorder inspect cassettes/demo_walkthrough.json\n\n# 3. Verify your server hasn't regressed — compare responses to the recording\nmcp-recorder verify --cassette cassettes/demo_walkthrough.json --target https://mcp.devhelm.io\n\n# 4. Replay as a mock server — test your client without the real server\n# (starts a local server on port 5555, point your MCP client at it)\nmcp-recorder replay --cassette cassettes/demo_walkthrough.json\n\n# Works with stdio servers too — no HTTP wrapper needed\nmcp-recorder verify --cassette cassettes/golden.json \\\n  --target-stdio \"node dist/index.js\"\n```\n\nOne cassette. Three modes. HTTP and stdio transports. Full coverage for both client and server testing.\n\n## Contents\n\n- [Install](#install)\n- [How It Works](#how-it-works)\n- [Scenarios](#scenarios)\n- [CLI Usage](#cli-usage)\n- [pytest Integration](#pytest-integration)\n- [Python API](#python-api)\n- [Configuration](#configuration)\n- [Cassette Format](#cassette-format)\n- [CLI Reference](#cli-reference)\n- [CI Integration](#ci-integration)\n- [Roadmap](#roadmap)\n- [Contributing](#contributing)\n\n## Install\n\n```bash\npip install mcp-recorder\n```\n\nOr with [uv](https://docs.astral.sh/uv/):\n\n```bash\nuv add mcp-recorder\n```\n\n## How It Works\n\nmcp-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:\n\n```\nRecord:   Client -\u003e mcp-recorder (proxy) -\u003e Real Server -\u003e cassette.json\n                                            (HTTP or stdio subprocess)\n\nReplay:   Client -\u003e mcp-recorder (mock)  -\u003e cassette.json     (test your client)\nVerify:   mcp-recorder (client mock) -\u003e Real Server            (test your server)\n```\n\n**Replay** serves recorded responses back to your client. No real server, no credentials, no network.\n\n**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.\n\n## Scenarios\n\nDefine what to test in a YAML file. No Python scripts, no boilerplate — works with MCP servers written in any language.\n\n```yaml\nschema_version: \"1.0\"\n\ntarget: http://localhost:3000\n\nredact:\n  server_url: true\n  env:\n    - API_KEY\n  patterns:\n    - \"sk-[a-zA-Z0-9]+\"\n\nscenarios:\n  tools_and_schemas:\n    description: \"Discover tools and call search\"\n    actions:\n      - list_tools\n      - call_tool:\n          name: search\n          arguments:\n            query: \"test\"\n\n  error_handling:\n    description: \"Invalid inputs return proper errors\"\n    actions:\n      - call_tool:\n          name: search\n          arguments: {}\n```\n\nFor stdio MCP servers, use a target object instead of a URL:\n\n```yaml\ntarget:\n  command: \"node\"\n  args: [\"dist/index.js\"]\n  env:\n    API_KEY: \"test-key\"\n  cwd: \"./server\"\n```\n\n| Target field | Required | Description |\n|---|---|---|\n| `command` | yes | Executable to spawn |\n| `args` | no | List of command-line arguments |\n| `env` | no | Extra environment variables (merged with current env) |\n| `cwd` | no | Working directory for the subprocess |\n\n### Environment Variables\n\nString 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.\n\n```yaml\nschema_version: \"1.0\"\n\ntarget:\n  command: \"node\"\n  args: [\"dist/index.js\"]\n  env:\n    API_KEY: \"${API_KEY}\"\n    REGION: \"${AWS_REGION:-us-east-1}\"\n\nredact:\n  env:\n    - API_KEY\n\nscenarios:\n  authenticated_search:\n    description: \"Search with a real API key\"\n    actions:\n      - list_tools\n      - call_tool:\n          name: search\n          arguments:\n            query: \"test\"\n```\n\nThis works naturally with CI systems. In GitHub Actions, expose repository secrets as environment variables and `scenarios.yml` picks them up:\n\n```yaml\n# .github/workflows/mcp-test.yml\njobs:\n  test:\n    runs-on: ubuntu-latest\n    env:\n      API_KEY: ${{ secrets.API_KEY }}\n    steps:\n      - uses: actions/checkout@v4\n      - run: pip install mcp-recorder\n      - run: mcp-recorder record-scenarios scenarios.yml -o cassettes/\n```\n\nInterpolation 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.\n\nRecord all scenarios at once, or pick one:\n\n```bash\nmcp-recorder record-scenarios scenarios.yml\nmcp-recorder record-scenarios scenarios.yml --scenario tools_and_schemas\n```\n\nEach scenario key becomes the cassette filename (`tools_and_schemas` -\u003e `tools_and_schemas.json`). Protocol handshake (`initialize` + `notifications/initialized`) is handled automatically.\n\nSupported actions:\n\n| Action | Description |\n|---|---|\n| `list_tools` | Call `tools/list` |\n| `call_tool` | Call `tools/call` with `name` and `arguments` |\n| `list_prompts` | Call `prompts/list` |\n| `get_prompt` | Call `prompts/get` with `name` and optional `arguments` |\n| `list_resources` | Call `resources/list` |\n| `read_resource` | Call `resources/read` with `uri` |\n\n## CLI Usage\n\n### Interactive Recording\n\nStart the proxy pointing at your MCP server:\n\n```bash\n# HTTP target\nmcp-recorder record \\\n  --target http://localhost:8000 \\\n  --port 5555 \\\n  --output golden.json\n\n# stdio target — spawns the server as a subprocess\nmcp-recorder record \\\n  --target-stdio \"node dist/index.js\" \\\n  --target-env API_KEY=test-key \\\n  --output golden.json\n```\n\nPoint your MCP client at `http://localhost:5555` and interact normally. Press `Ctrl+C` when done — the cassette is saved.\n\nWorks with remote servers too:\n\n```bash\nmcp-recorder record \\\n  --target https://mcp.example.com/v1/mcp \\\n  --redact-env API_KEY \\\n  --output golden.json\n```\n\nFor automated recording, see [Scenarios](#scenarios).\n\n### Verify\n\nAfter making changes to your server, verify nothing broke:\n\n```bash\n# HTTP target\nmcp-recorder verify --cassette golden.json --target http://localhost:8000\n\n# stdio target\nmcp-recorder verify --cassette golden.json \\\n  --target-stdio \"node dist/index.js\" \\\n  --target-env API_KEY=test-key\n```\n\n```\nVerifying golden.json against http://localhost:8000\n\n  1. initialize          [PASS]\n  2. tools/list          [PASS]\n  3. tools/call [search] [FAIL]\n       $.result.content[0].text: \"old output\" != \"new output\"\n  4. tools/call [analyze] [PASS]\n\nResult: 3/4 passed, 1 failed\n```\n\nExit code is non-zero on any diff — plug it straight into CI.\n\nFor fields that change every run, skip them by name or by exact path:\n\n```bash\nmcp-recorder verify --cassette golden.json --target http://localhost:8000 \\\n  --ignore-fields timestamp \\\n  --ignore-paths '$.result.content[0].text.metadata.requestId'\n```\n\nWhen 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.\n\nWhen a change is intentional, update the cassette:\n\n```bash\nmcp-recorder verify --cassette golden.json --target http://localhost:8000 --update\n```\n\n### Replay\n\nServe recorded responses without the real server:\n\n```bash\nmcp-recorder replay --cassette golden.json\n```\n\nA mock server starts on port `5555`. Point your client at it. No network, no credentials, same responses every time.\n\n### Inspect\n\n```bash\nmcp-recorder inspect golden.json\n```\n\n```\ngolden.json\n  Recorded: 2026-02-17 20:25:23\n  Server:   Test Calculator v2.14.5\n  Protocol: 2025-11-25\n  Target:   http://127.0.0.1:8000\n\n  Interactions (9):\n    1. initialize -\u003e 200 SSE (7ms)\n    2. notifications/initialized -\u003e 202 (1ms)\n    3. tools/list -\u003e 200 SSE (22ms)\n    4. tools/call [add] -\u003e 200 SSE (18ms)\n    ...\n\n  Summary: 6 requests, 1 notification, 2 lifecycle\n```\n\n## pytest Integration\n\nThe pytest plugin activates automatically on install. Mark tests with a cassette and use the `mcp_replay_url` fixture:\n\n```python\nimport pytest\nfrom fastmcp import Client\n\n@pytest.mark.mcp_cassette(\"cassettes/golden.json\")\nasync def test_tool_call(mcp_replay_url):\n    async with Client(mcp_replay_url) as client:\n        result = await client.call_tool(\"add\", {\"a\": 2, \"b\": 3})\n        assert result.content[0].text == \"5\"\n```\n\nFor server regression testing, use `mcp_verify_result`:\n\n```python\n@pytest.mark.mcp_cassette(\"cassettes/golden.json\")\ndef test_no_regression(mcp_verify_result):\n    assert mcp_verify_result.failed == 0, mcp_verify_result.results\n```\n\nTo ignore volatile fields, pass them via the marker:\n\n```python\n@pytest.mark.mcp_cassette(\n    \"cassettes/golden.json\",\n    ignore_fields=[\"timestamp\"],\n    ignore_paths=[\"$.result.metadata.requestId\"],\n)\ndef test_no_regression(mcp_verify_result):\n    assert mcp_verify_result.failed == 0\n```\n\n```bash\npytest                                        # replay from cassettes (default)\npytest --mcp-target http://localhost:8000      # verify against live HTTP server\npytest --mcp-target-stdio \"node dist/index.js\" # verify against stdio server\npytest --mcp-record-mode=auto                  # replay if cassette exists, skip if not\n```\n\nEach test gets an isolated server on a random port. No manual server management.\n\n## Python API\n\nFor programmatic recording:\n\n```python\nfrom mcp_recorder import RecordSession\n\nasync with RecordSession(\n    target=\"http://localhost:8000\",\n    output=\"golden.json\",\n) as client:\n    await client.list_tools()\n    await client.call_tool(\"add\", {\"a\": 2, \"b\": 3})\n```\n\n`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`).\n\n## Configuration\n\n### Matching Strategies\n\n| Strategy | Flag | Description |\n|---|---|---|\n| **Method + Params** | `method_params` | Match on JSON-RPC `method` and `params`, ignoring `_meta` (default) |\n| **Sequential** | `sequential` | Return next unmatched interaction in recorded order |\n| **Strict** | `strict` | Full structural equality of the request body including `_meta` |\n\n### Secret Redaction\n\nRedaction is explicit — no magic scanning, no hidden behavior. You control exactly what gets scrubbed.\n\n**`--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/\u003ckey\u003e/mcp`.\n\n```bash\nmcp-recorder record --target https://mcp.firecrawl.dev/$FIRECRAWL_KEY/mcp\n# metadata shows: https://mcp.firecrawl.dev/[REDACTED]\n\nmcp-recorder record --target http://localhost:8000 --no-redact-server-url\n# metadata shows full URL\n```\n\n**`--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.\n\n```bash\nmcp-recorder record \\\n  --target https://mcp.firecrawl.dev/$FIRECRAWL_KEY/mcp \\\n  --redact-env FIRECRAWL_KEY\n```\n\n**`--redact-patterns REGEX`** — for values not in environment variables. Same scope (metadata + responses only).\n\n```bash\nmcp-recorder record --target http://localhost:8000 \\\n  --redact-patterns \"sk-[a-zA-Z0-9]+\" \\\n  --redact-patterns \"session-[0-9a-f]{32}\"\n```\n\nIn 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.\n\n## Cassette Format\n\nCassettes store JSON-RPC messages at the protocol level:\n\n```json\n{\n  \"version\": \"1.0\",\n  \"metadata\": {\n    \"recorded_at\": \"2026-02-17T20:25:23Z\",\n    \"server_url\": \"http://127.0.0.1:8000\",\n    \"transport_type\": \"http\",\n    \"protocol_version\": \"2025-11-25\",\n    \"server_info\": { \"name\": \"Test Calculator\", \"version\": \"2.14.5\" }\n  },\n  \"interactions\": [\n    {\n      \"type\": \"jsonrpc_request\",\n      \"request\": {\n        \"jsonrpc\": \"2.0\", \"id\": 0, \"method\": \"initialize\",\n        \"params\": { \"protocolVersion\": \"2025-11-25\", \"capabilities\": {} }\n      },\n      \"response\": {\n        \"jsonrpc\": \"2.0\", \"id\": 0,\n        \"result\": {\n          \"protocolVersion\": \"2025-11-25\",\n          \"capabilities\": { \"tools\": { \"listChanged\": true } },\n          \"serverInfo\": { \"name\": \"Test Calculator\", \"version\": \"2.14.5\" }\n        }\n      },\n      \"response_is_sse\": true,\n      \"response_status\": 200,\n      \"latency_ms\": 7\n    }\n  ]\n}\n```\n\nThe `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.\n\n## CLI Reference\n\n### `mcp-recorder record`\n\n| Option | Default | Description |\n|---|---|---|\n| `--target` | — | URL of the real MCP server (HTTP). Mutually exclusive with `--target-stdio` |\n| `--target-stdio` | — | Command to spawn a stdio MCP server (e.g. `\"node dist/index.js\"`). Mutually exclusive with `--target` |\n| `--target-env` | — | Environment variable for stdio subprocess as `KEY=VALUE`. Repeatable |\n| `--port` | `5555` | Local proxy port |\n| `--output` | `recording.json` | Output cassette file path |\n| `--verbose` | — | Log full headers and bodies to stderr |\n| `--redact-server-url / --no-redact-server-url` | `true` | Strip URL path from metadata (keeps scheme + host) |\n| `--redact-env VAR` | — | Redact named env var value from metadata + responses. Repeatable |\n| `--redact-patterns REGEX` | — | Redact regex matches from metadata + responses. Repeatable |\n\n### `mcp-recorder record-scenarios`\n\n| Argument / Option | Default | Description |\n|---|---|---|\n| `SCENARIOS_FILE` | *(required)* | Path to YAML scenarios file |\n| `--output-dir` | `cassettes/` next to file | Output directory for cassettes |\n| `--scenario NAME` | all | Record only the named scenario(s). Repeatable |\n| `--verbose` | — | Log full request/response details to stderr |\n\n### `mcp-recorder replay`\n\n| Option | Default | Description |\n|---|---|---|\n| `--cassette` | *(required)* | Path to cassette file |\n| `--port` | `5555` | Local server port |\n| `--match` | `method_params` | Matching strategy (see [Matching Strategies](#matching-strategies)) |\n| `--verbose` | — | Log every matched request to stderr |\n\n### `mcp-recorder verify`\n\n| Option | Default | Description |\n|---|---|---|\n| `--cassette` | *(required)* | Path to golden cassette file |\n| `--target` | — | URL of the server to verify (HTTP). Mutually exclusive with `--target-stdio` |\n| `--target-stdio` | — | Command to spawn a stdio MCP server. Mutually exclusive with `--target` |\n| `--target-env` | — | Environment variable for stdio subprocess as `KEY=VALUE`. Repeatable |\n| `--ignore-fields KEY` | — | Key name to ignore at **any depth** (e.g. `timestamp`). Repeatable |\n| `--ignore-paths PATH` | — | Exact dot-path to ignore (e.g. `$.result.metadata.scrapeId`). Repeatable |\n| `--update` | — | Update the cassette with new responses (snapshot update) |\n| `--verbose` | — | Show full diff for each failing interaction |\n\n### `mcp-recorder inspect`\n\n| Argument | Description |\n|---|---|\n| `CASSETTE` | Path to cassette file to inspect |\n\n## CI Integration\n\n### GitHub Actions\n\nUsing scenarios and verify (recommended for any language):\n\n```yaml\nsteps:\n  - uses: actions/checkout@v4\n  - uses: actions/setup-python@v5\n    with:\n      python-version: \"3.12\"\n  - run: pip install mcp-recorder\n\n  # Start your MCP server\n  - run: npm start \u0026\n  - run: sleep 5\n\n  # Verify cassettes against the live server\n  - run: |\n      mcp-recorder verify \\\n        --cassette integration/cassettes/tools_and_schemas.json \\\n        --target http://localhost:3000\n```\n\nWith the pytest plugin (Python projects):\n\n```yaml\nsteps:\n  - uses: actions/checkout@v4\n  - uses: actions/setup-python@v5\n    with:\n      python-version: \"3.12\"\n  - run: pip install mcp-recorder\n  - run: pytest\n```\n\nCassettes committed to the repo are replayed automatically. No server needed in CI for replay mode.\n\n## Roadmap\n\n- [x] `stdio` transport — subprocess wrapping for local MCP servers\n- [ ] WebSocket transport\n- [ ] `mcp-recorder diff` — compare two cassettes for breaking changes\n- [ ] TypeScript/JS cassette support — same JSON format, Vitest/Jest plugin\n\n## Contributing\n\n```bash\ngit clone https://github.com/devhelmhq/mcp-recorder.git\ncd mcp-recorder\nuv sync --group dev\nuv run pytest\n```\n\n## License\n\nMIT — see [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevhelmhq%2Fmcp-recorder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevhelmhq%2Fmcp-recorder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevhelmhq%2Fmcp-recorder/lists"}