mcp-cassette 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. mcp_cassette-0.3.3/.github/workflows/ci.yml +49 -0
  2. mcp_cassette-0.3.3/.github/workflows/publish.yml +60 -0
  3. mcp_cassette-0.3.3/.gitignore +27 -0
  4. mcp_cassette-0.3.3/CHANGELOG.md +349 -0
  5. mcp_cassette-0.3.3/LICENSE +201 -0
  6. mcp_cassette-0.3.3/PKG-INFO +202 -0
  7. mcp_cassette-0.3.3/README.md +170 -0
  8. mcp_cassette-0.3.3/docs/guide/01-getting-started.md +149 -0
  9. mcp_cassette-0.3.3/docs/guide/10-troubleshooting.md +50 -0
  10. mcp_cassette-0.3.3/docs/guide/how-to/02-record-and-replay.md +177 -0
  11. mcp_cassette-0.3.3/docs/guide/how-to/03-remote-http.md +99 -0
  12. mcp_cassette-0.3.3/docs/guide/how-to/04-use-as-a-library.md +116 -0
  13. mcp_cassette-0.3.3/docs/guide/how-to/05-inject-faults.md +99 -0
  14. mcp_cassette-0.3.3/docs/guide/how-to/06-replay-timing.md +87 -0
  15. mcp_cassette-0.3.3/docs/guide/how-to/07-inspect-and-diff.md +108 -0
  16. mcp_cassette-0.3.3/docs/guide/how-to/08-redact-secrets.md +83 -0
  17. mcp_cassette-0.3.3/docs/guide/how-to/09-lint-pattern-packs.md +120 -0
  18. mcp_cassette-0.3.3/docs/guide/index.md +62 -0
  19. mcp_cassette-0.3.3/docs/guide/operations/11-install.md +77 -0
  20. mcp_cassette-0.3.3/docs/guide/operations/12-configure.md +133 -0
  21. mcp_cassette-0.3.3/docs/guide/operations/13-ci.md +122 -0
  22. mcp_cassette-0.3.3/docs/guide/operations/14-cli-reference.md +153 -0
  23. mcp_cassette-0.3.3/docs/guide/operations/15-runbook-replay-misses.md +163 -0
  24. mcp_cassette-0.3.3/examples/README.md +233 -0
  25. mcp_cassette-0.3.3/examples/cassettes/deterministic.mcp.json +112 -0
  26. mcp_cassette-0.3.3/examples/cassettes/echo_and_add.mcp.json +155 -0
  27. mcp_cassette-0.3.3/examples/cassettes/fault.mcp.json +112 -0
  28. mcp_cassette-0.3.3/examples/cassettes/http_echo_and_add.mcp.json +171 -0
  29. mcp_cassette-0.3.3/examples/cassettes/injected.mcp.json +162 -0
  30. mcp_cassette-0.3.3/examples/cassettes/sampling.mcp.json +175 -0
  31. mcp_cassette-0.3.3/examples/cassettes/tools.mcp.json +162 -0
  32. mcp_cassette-0.3.3/examples/echo_http_server.py +64 -0
  33. mcp_cassette-0.3.3/examples/echo_server.py +179 -0
  34. mcp_cassette-0.3.3/examples/library_mode.py +68 -0
  35. mcp_cassette-0.3.3/examples/lint-pack.toml +26 -0
  36. mcp_cassette-0.3.3/examples/mcp_client.py +121 -0
  37. mcp_cassette-0.3.3/examples/mcp_http_client.py +46 -0
  38. mcp_cassette-0.3.3/examples/test_echo.py +125 -0
  39. mcp_cassette-0.3.3/examples/test_echo_http.py +90 -0
  40. mcp_cassette-0.3.3/pyproject.toml +112 -0
  41. mcp_cassette-0.3.3/scripts/check_version.py +57 -0
  42. mcp_cassette-0.3.3/src/mcp_cassette/__init__.py +64 -0
  43. mcp_cassette-0.3.3/src/mcp_cassette/__main__.py +10 -0
  44. mcp_cassette-0.3.3/src/mcp_cassette/_signals.py +55 -0
  45. mcp_cassette-0.3.3/src/mcp_cassette/_stdio.py +34 -0
  46. mcp_cassette-0.3.3/src/mcp_cassette/cassette.py +424 -0
  47. mcp_cassette-0.3.3/src/mcp_cassette/cli.py +790 -0
  48. mcp_cassette-0.3.3/src/mcp_cassette/diffing.py +248 -0
  49. mcp_cassette-0.3.3/src/mcp_cassette/lint/__init__.py +31 -0
  50. mcp_cassette-0.3.3/src/mcp_cassette/lint/engine.py +211 -0
  51. mcp_cassette-0.3.3/src/mcp_cassette/lint/packs.py +340 -0
  52. mcp_cassette-0.3.3/src/mcp_cassette/lint/patterns.py +37 -0
  53. mcp_cassette-0.3.3/src/mcp_cassette/lint/rules.py +221 -0
  54. mcp_cassette-0.3.3/src/mcp_cassette/matching.py +191 -0
  55. mcp_cassette-0.3.3/src/mcp_cassette/py.typed +0 -0
  56. mcp_cassette-0.3.3/src/mcp_cassette/pytest_plugin.py +150 -0
  57. mcp_cassette-0.3.3/src/mcp_cassette/record/__init__.py +7 -0
  58. mcp_cassette-0.3.3/src/mcp_cassette/record/checkpoint.py +87 -0
  59. mcp_cassette-0.3.3/src/mcp_cassette/record/proxy.py +239 -0
  60. mcp_cassette-0.3.3/src/mcp_cassette/record/pump.py +68 -0
  61. mcp_cassette-0.3.3/src/mcp_cassette/record/recorder.py +203 -0
  62. mcp_cassette-0.3.3/src/mcp_cassette/replay/__init__.py +7 -0
  63. mcp_cassette-0.3.3/src/mcp_cassette/replay/faults.py +100 -0
  64. mcp_cassette-0.3.3/src/mcp_cassette/replay/new_episodes.py +190 -0
  65. mcp_cassette-0.3.3/src/mcp_cassette/replay/pacing.py +75 -0
  66. mcp_cassette-0.3.3/src/mcp_cassette/replay/server.py +435 -0
  67. mcp_cassette-0.3.3/src/mcp_cassette/replay/server_requests.py +210 -0
  68. mcp_cassette-0.3.3/src/mcp_cassette/report.py +37 -0
  69. mcp_cassette-0.3.3/src/mcp_cassette/session.py +482 -0
  70. mcp_cassette-0.3.3/src/mcp_cassette/transports/__init__.py +1 -0
  71. mcp_cassette-0.3.3/src/mcp_cassette/transports/http/__init__.py +21 -0
  72. mcp_cassette-0.3.3/src/mcp_cassette/transports/http/proxy.py +403 -0
  73. mcp_cassette-0.3.3/src/mcp_cassette/transports/http/server.py +704 -0
  74. mcp_cassette-0.3.3/src/mcp_cassette/transports/http/wire.py +316 -0
  75. mcp_cassette-0.3.3/tests/conftest.py +5 -0
  76. mcp_cassette-0.3.3/tests/integration/test_cli_diff.py +116 -0
  77. mcp_cassette-0.3.3/tests/integration/test_cli_lint_packs.py +73 -0
  78. mcp_cassette-0.3.3/tests/integration/test_faults.py +184 -0
  79. mcp_cassette-0.3.3/tests/integration/test_http_pacing.py +163 -0
  80. mcp_cassette-0.3.3/tests/integration/test_http_record.py +284 -0
  81. mcp_cassette-0.3.3/tests/integration/test_http_replay.py +503 -0
  82. mcp_cassette-0.3.3/tests/integration/test_library_http.py +62 -0
  83. mcp_cassette-0.3.3/tests/integration/test_library_stdio.py +71 -0
  84. mcp_cassette-0.3.3/tests/integration/test_lint_recorded.py +39 -0
  85. mcp_cassette-0.3.3/tests/integration/test_passthrough.py +52 -0
  86. mcp_cassette-0.3.3/tests/integration/test_record.py +267 -0
  87. mcp_cassette-0.3.3/tests/integration/test_replay.py +158 -0
  88. mcp_cassette-0.3.3/tests/integration/test_replay_pacing.py +189 -0
  89. mcp_cassette-0.3.3/tests/integration/test_sampling_replay.py +493 -0
  90. mcp_cassette-0.3.3/tests/reference_http_server/server.py +114 -0
  91. mcp_cassette-0.3.3/tests/reference_server/server.py +109 -0
  92. mcp_cassette-0.3.3/tests/scripted_client.py +262 -0
  93. mcp_cassette-0.3.3/tests/scripted_http_client.py +222 -0
  94. mcp_cassette-0.3.3/tests/system/test_fixture.py +238 -0
  95. mcp_cassette-0.3.3/tests/system/test_http_fixture.py +147 -0
  96. mcp_cassette-0.3.3/tests/unit/test_cassette_schema.py +272 -0
  97. mcp_cassette-0.3.3/tests/unit/test_checkpoint.py +198 -0
  98. mcp_cassette-0.3.3/tests/unit/test_cli_surface.py +277 -0
  99. mcp_cassette-0.3.3/tests/unit/test_diffing.py +243 -0
  100. mcp_cassette-0.3.3/tests/unit/test_fault_injector.py +53 -0
  101. mcp_cassette-0.3.3/tests/unit/test_http_proxy_edges.py +364 -0
  102. mcp_cassette-0.3.3/tests/unit/test_http_proxy_shutdown.py +113 -0
  103. mcp_cassette-0.3.3/tests/unit/test_http_replay_edges.py +478 -0
  104. mcp_cassette-0.3.3/tests/unit/test_http_wire.py +293 -0
  105. mcp_cassette-0.3.3/tests/unit/test_inspect_views.py +191 -0
  106. mcp_cassette-0.3.3/tests/unit/test_library_api.py +93 -0
  107. mcp_cassette-0.3.3/tests/unit/test_lint.py +375 -0
  108. mcp_cassette-0.3.3/tests/unit/test_lint_packs.py +229 -0
  109. mcp_cassette-0.3.3/tests/unit/test_lint_project_config.py +186 -0
  110. mcp_cassette-0.3.3/tests/unit/test_lint_regression.py +117 -0
  111. mcp_cassette-0.3.3/tests/unit/test_matching.py +207 -0
  112. mcp_cassette-0.3.3/tests/unit/test_pacing.py +96 -0
  113. mcp_cassette-0.3.3/tests/unit/test_proxy_shutdown.py +226 -0
  114. mcp_cassette-0.3.3/tests/unit/test_pump.py +64 -0
  115. mcp_cassette-0.3.3/tests/unit/test_recorder.py +95 -0
  116. mcp_cassette-0.3.3/tests/unit/test_replay_server.py +372 -0
  117. mcp_cassette-0.3.3/tests/unit/test_server_requests.py +190 -0
  118. mcp_cassette-0.3.3/tests/unit/test_session.py +188 -0
  119. mcp_cassette-0.3.3/tests/unit/test_signals.py +103 -0
  120. mcp_cassette-0.3.3/uv.lock +1156 -0
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # No pipeline may silently record against a live server (project invariant).
9
+ env:
10
+ MCP_CASSETTE_MODE: none
11
+
12
+ jobs:
13
+ version-check:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ # No sync needed: the check reads files with stdlib only (tomllib + re).
19
+ - run: uv run --no-project python scripts/check_version.py
20
+
21
+ lint:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: astral-sh/setup-uv@v5
26
+ - run: uv sync
27
+ - run: uv run ruff check .
28
+ - run: uv run mypy src
29
+
30
+ test:
31
+ strategy:
32
+ fail-fast: false
33
+ matrix:
34
+ os: [ubuntu-latest, macos-latest, windows-latest]
35
+ python-version: ["3.12", "3.13"]
36
+ runs-on: ${{ matrix.os }}
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - uses: astral-sh/setup-uv@v5
40
+ with:
41
+ python-version: ${{ matrix.python-version }}
42
+ - run: uv sync
43
+ # Subprocess-patched coverage (see [tool.coverage.run] in pyproject.toml):
44
+ # record/replay engines run as subprocesses, so plain pytest-under-coverage
45
+ # would miss most of the codebase. combine merges the per-process data files;
46
+ # report enforces the fail_under gate.
47
+ - run: uv run coverage run -m pytest -q
48
+ - run: uv run coverage combine
49
+ - run: uv run coverage report
@@ -0,0 +1,60 @@
1
+ name: Publish
2
+
3
+ # Trusted Publishing (OIDC): the pypi/testpypi environments are registered as
4
+ # pending publishers on (test.)pypi.org — no API tokens stored in the repo.
5
+ on:
6
+ release:
7
+ types: [published]
8
+ workflow_dispatch: # manual trigger for the TestPyPI dry run
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv build
17
+ # A release tag that disagrees with project.version would publish the wrong
18
+ # version under the right name, and PyPI never lets that filename be reused.
19
+ - name: Verify the release tag matches the built version
20
+ if: github.event_name == 'release'
21
+ run: |
22
+ test -f "dist/mcp_cassette-${GITHUB_REF_NAME#v}.tar.gz" || {
23
+ echo "tag ${GITHUB_REF_NAME} does not match the built distribution:"
24
+ ls dist
25
+ exit 1
26
+ }
27
+ - uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish-testpypi:
33
+ if: github.event_name == 'workflow_dispatch'
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment: testpypi
37
+ permissions:
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/download-artifact@v4
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
45
+ with:
46
+ repository-url: https://test.pypi.org/legacy/
47
+
48
+ publish-pypi:
49
+ if: github.event_name == 'release'
50
+ needs: build
51
+ runs-on: ubuntu-latest
52
+ environment: pypi
53
+ permissions:
54
+ id-token: write
55
+ steps:
56
+ - uses: actions/download-artifact@v4
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,27 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Environments / tooling
10
+ .venv/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .pytest_cache/
14
+
15
+ # uv
16
+ # (uv.lock is committed; nothing to ignore here)
17
+
18
+ # OS
19
+ .DS_Store
20
+
21
+ # coverage data
22
+ .coverage
23
+ .coverage.*
24
+
25
+ # Recorded by examples/library_mode.py on its first run. The cassettes under
26
+ # examples/cassettes/ are committed on purpose; this one is generated on purpose.
27
+ examples/library_mode.mcp.json
@@ -0,0 +1,349 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.3] - 2026-07-25
11
+
12
+ First PyPI release, gated on a full pre-release audit. Packaging and
13
+ discoverability, plus the behavior fixes the audit turned up.
14
+
15
+ ### Added
16
+
17
+ - Ship the PEP 561 `py.typed` marker in the wheel so consumers' type checkers
18
+ see the library's type hints.
19
+ - `.github/workflows/publish.yml`: build once, publish to TestPyPI via manual
20
+ `workflow_dispatch` and to PyPI on GitHub release, both through Trusted
21
+ Publishing (OIDC) — no long-lived tokens.
22
+ - `PatternSet` is exported from the top-level `mcp_cassette` namespace. It was
23
+ public in `mcp_cassette.lint` and documented, but absent from the package's
24
+ `__all__` — fixed before a release freezes the surface.
25
+
26
+ ### Fixed
27
+
28
+ - The `mcp_cassette` fixture crashed with `AttributeError: 'Function' object has
29
+ no attribute 'fspath'` under `-p no:legacypath`, and would have broken outright
30
+ when pytest drops the deprecated `legacypath` plugin. The default cassette path
31
+ now derives the module name from `node.path`.
32
+ - A relative `mcp_cassette_dir` ini value resolved against the current working
33
+ directory, so running pytest from a subdirectory looked for cassettes in a
34
+ different place than the documented `<rootpath>/...` default. It now resolves
35
+ against pytest's `rootpath`; an absolute value still wins as written.
36
+ - A recording that captured zero messages still wrote an empty cassette file
37
+ (both transports). The runbook already said no cassette is written, and the
38
+ empty file was actively harmful: in `once` mode its existence sent every later
39
+ run down the replay branch, so a mis-wired first run could never re-record
40
+ itself. Nothing is written now; the session report — and therefore the
41
+ fixture's `recording captured zero messages` failure — is unchanged.
42
+ - Replay answered a request that matched a recorded one with *no recorded
43
+ response* (a recording cut short mid-call, or a hand-promoted `.partial`) with
44
+ the same JSON-RPC miss error it sends for an outright miss, but recorded no
45
+ miss: the process exited `0` and the fixture passed the test. Both transports
46
+ now count it as a miss, so it exits `3` and fails the test like any other. The
47
+ same hole in the `initialize` handshake, which bypasses the matcher entirely,
48
+ is closed too.
49
+ - `record` (and `serve --new-episodes`) hung forever when the wrapped server exited
50
+ while the agent still held the proxy's stdin open — a server crash mid-session
51
+ turned into a hung test rather than a failed one. The task-group cancel that was
52
+ supposed to end the session could not: the client stdin read is parked in an
53
+ anyio worker thread that is both un-cancellable and non-daemon, so the
54
+ interpreter would join it at exit. Server death now converges on the same hard
55
+ exit the interrupt path already used — cassette finalized first, and the process
56
+ leaves with the wrapped server's own exit code.
57
+ - `with_faults()` on a session built without an explicit `report_path` wrote its
58
+ generated overlay to `<cassette>.faults.json` — the exact filename the docs tell
59
+ you to hand-write for `--faults` — overwriting a committed overlay and then
60
+ leaving the generated file behind next to the cassette. The overlay now goes to a
61
+ private temporary directory removed by `close()`. The pytest fixture and
62
+ `use_cassette` were unaffected (both already pass a temp `report_path`).
63
+ - The replay-pacing integration tests asserted wall-clock floors within ~300 ms of
64
+ the expected signal, while each measurement spans two subprocess startups. Two
65
+ different tests flaked on consecutive local runs; the floors now sit at roughly
66
+ half the signal, which is still unambiguous (instant replay contributes ~0).
67
+ - Raise the `anyio` floor to `>=4.2`. The replay and HTTP servers construct
68
+ `anyio.Lock()` / `anyio.Event()` outside a running event loop, and the
69
+ adapters that make that legal only exist from 4.2 — on 4.0/4.1 a consumer got
70
+ `AsyncLibraryNotFoundError` the moment a replay server was built.
71
+ - `Cassette.load` raised `AttributeError` (a traceback, exit `1`) on a cassette
72
+ whose JSON top level is not an object, and `TypeError` on a non-integer
73
+ `format_version`. Both are read before validation, so pydantic never saw
74
+ them; both are now `ValueError` and exit `2` like every other malformed
75
+ cassette.
76
+ - `record --port` / `--max-idle` with a stdio `-- CMD` silently ignored both
77
+ flags. They belong to `--url` recording and are now a usage error (exit `2`),
78
+ matching how `--pace-scale` without `--pace` is handled.
79
+ - The `mcp_cassette` fixture finalized unconditionally, so a test that failed
80
+ *and* hit a replay miss reported both a FAILED and a teardown ERROR, burying
81
+ the real failure. Teardown now only closes the session when the test body
82
+ already failed — the behavior `use_cassette` always had.
83
+ - `CassetteSession._peek_transport` caught only `(FileNotFoundError,
84
+ ValueError)`; a directory or permission `OSError`, or an unsupported
85
+ `format_version`, escaped raw out of `server_command()`.
86
+ - `with_faults()` sessions are now finalized. The pytest fixture finalizes the
87
+ session it hands the test, but a fault test runs the *derivative* returned by
88
+ `with_faults()`; that derivative was never checked, so replay misses in fault
89
+ tests were silently unreported and an HTTP server started on it outlived the
90
+ test. `with_faults()` now registers the copy on its parent, and the parent's
91
+ `close()`/`finalize()` cover it.
92
+ - `serve`, `inspect`, and `diff` printed a raw traceback and exited `1` on a
93
+ malformed or unreadable cassette; `serve --faults` and `inspect --faults` did
94
+ the same on a missing or malformed overlay. All load sites now share one error
95
+ set and report the documented usage error (exit `2`). `inspect` also loads the
96
+ overlay before printing, so a bad overlay no longer fails halfway through a
97
+ report.
98
+ - Fill in the Apache-2.0 copyright holder, which was still the license
99
+ template's `[yyyy] [name of copyright owner]` placeholder.
100
+ - Restore the CHANGELOG compare links dropped for 0.3.3: `[Unreleased]` pointed
101
+ at `v0.3.2` and the `[0.3.3]` link was missing.
102
+ - Gitignore the cassette `examples/library_mode.py` records on its first run, so
103
+ following the examples README no longer dirties the working tree.
104
+
105
+ ### Changed
106
+
107
+ - `PatternSet.for_surface` is gone, inlined into its one caller
108
+ (`PatternSet.match`). It handed out an internal dataclass, which keeping it
109
+ public would have frozen into the API `PatternSet` now exports.
110
+ - README links are absolute GitHub URLs. The README is the PyPI long
111
+ description, where relative links resolve against `pypi.org` and 404.
112
+ - `[project.urls]` adds Documentation, Changelog, and Issues for the PyPI
113
+ sidebar.
114
+ - `publish.yml` fails the build when a release tag disagrees with the packaged
115
+ version, rather than publishing a filename PyPI will never let us reuse.
116
+ - License metadata now uses the PEP 639 SPDX form (`license = "Apache-2.0"`,
117
+ `license-files = ["LICENSE"]`) instead of the deprecated table form;
118
+ hatchling pinned `>=1.27` accordingly.
119
+ - The sdist no longer includes internal agent-workspace docs and repo tooling
120
+ (`.agents_workspace/`, `.claude/`, `CLAUDE.md`, `.pre-commit-config.yaml`).
121
+ - Unify the public one-liner (PyPI description, README opener) into a single
122
+ canonical description: "Record/replay testing for MCP (Model Context
123
+ Protocol) agents: capture real sessions as cassettes, replay them as
124
+ deterministic mock servers — vcrpy for MCP." — replacing three drifted
125
+ variants.
126
+ - Expand `pyproject.toml` keywords (`model-context-protocol`, `agent`) and
127
+ classifiers (`Operating System :: OS Independent`,
128
+ `Topic :: Software Development :: Testing :: Mocking`, `Typing :: Typed`)
129
+ for PyPI search and filtering.
130
+
131
+ ## [0.3.2] - 2026-07-22
132
+
133
+ Documentation-only release. No code, flag, or behavior changes.
134
+
135
+ ### Changed
136
+
137
+ - State explicitly that the unit of recording is the entire session — every
138
+ message from server launch to session end — never an individual tool call,
139
+ and that the record modes decide record-vs-replay once per test run (`all`
140
+ overwrites the whole cassette file, not single entries). Added to the guide's
141
+ record-mode chapter (§2.3), the operator configuration reference (§12.1), the
142
+ getting-started first-run walkthrough (§1.4), and the README's canonical
143
+ record-mode table (§2.1).
144
+
145
+ ## [0.3.1] - 2026-07-22
146
+
147
+ Documentation-only release: the guide and README are restructured and numbered.
148
+ No code, flag, or behavior changes.
149
+
150
+ ### Changed
151
+
152
+ - Number the guide as 15 chapters in reading order — test authors (1–10), then
153
+ operators (11–15) — with the chapter number in each filename
154
+ (`01-getting-started.md` … `operations/15-runbook-replay-misses.md`) and
155
+ numbered `X.Y`/`X.Y.Z` section headings throughout, so sections are citable
156
+ as e.g. §12.6.
157
+ - Rewrite `docs/guide/index.md` as a two-part numbered table of contents that
158
+ states the numbering convention.
159
+ - Number the README sections 1–9 and end each with a uniform "Full chapter:"
160
+ pointer into the guide; add a Redaction section so the capture-time
161
+ scrubbing defaults are visible from the front page.
162
+ - Present repeated content uniformly across README and guide: one canonical
163
+ record-mode table and precedence phrasing, word-identical ordering-discipline
164
+ tables, and one lint-disclaimer wording.
165
+
166
+ ## [0.3.0] - 2026-07-21
167
+
168
+ Four additions, all opt-in: a library front door, replay pacing, richer
169
+ inspect/diff, and per-project lint packs. Every existing command, flag, and
170
+ export behaves exactly as in 0.2.x when the new flags are absent, and the
171
+ cassette `format_version` stays 2.
172
+
173
+ ### Added
174
+
175
+ - **Embedded library mode.** `use_cassette(...)` is a context manager giving
176
+ plain Python code — an agent harness, a notebook, a benchmark runner, a
177
+ non-pytest test framework — the same session the pytest fixture gets: same
178
+ modes, same fault matrix, same failure semantics. New exports:
179
+ `use_cassette`, `resolve_mode`, `CassetteSession.close()`, `CassetteError`,
180
+ `Mode`, and `lint_cassette`. The session report goes to a temporary directory
181
+ removed on exit, so no untracked JSON lands next to committed cassettes; a
182
+ raising `with` body propagates untouched rather than being buried under a
183
+ replay-miss error. `examples/library_mode.py` is runnable.
184
+ - **Replay pacing.** `--pace recorded` replays the recorded `t_offset_ms` gaps
185
+ on both transports, including SSE inter-event spacing; `--pace-scale` and
186
+ `--pace-cap-ms` (default 5000, `0` uncapped) bound it. Also available as
187
+ `PaceConfig`, the `pace=`/`pace_scale=`/`pace_cap_ms=` marker arguments, and
188
+ `use_cassette(pace=...)`. Off by default — with pacing off the response path
189
+ still performs no sleep and reads no clock. Pacing precedes faults, so a
190
+ `delay` fault is additive and a `timeout` spends no sleep.
191
+ - **`inspect` views.** `--timeline` (one line per message with direction, kind,
192
+ method, id, and payload bytes; `exch`/`chan` for http), `--tools`,
193
+ `--grep PATTERN`, and `--format json` with byte-stable output.
194
+ - **`diff OLD NEW`.** Structural comparison of two cassettes — metadata, method
195
+ counts, tool surfaces, exchange sequence — with `--tools-only` and
196
+ `--format json`. Exit `0` identical, `5` differ, `2` load error. Ids,
197
+ `t_offset_ms`, and `seq` are never compared. Also `diff_cassettes()` and
198
+ `CassetteDiff` as library exports.
199
+ - **Lint pattern packs.** `--pattern-pack PATH` loads declarative TOML rules
200
+ with their own ids and severities; `[tool.mcp_cassette.lint]` in
201
+ `pyproject.toml` makes a project's packs, selection, and `fail_on` threshold
202
+ the default for every invocation; `--fail-on warning` and `--no-config`.
203
+ Packs extend the bundled rules and never replace them, and bundled findings
204
+ stay byte-identical. New exports: `PatternRule`, `ProjectLintConfig`.
205
+ `examples/lint-pack.toml` is a starter pack. There is deliberately no Python
206
+ rule-plugin API — `lint` should not execute third-party code on a
207
+ supply-chain-security surface.
208
+ - Guide pages: use as a library, replay timing, inspect and diff, lint pattern
209
+ packs. CLI reference, CI recipe, troubleshooting, and redaction pages updated.
210
+
211
+ ### Changed
212
+
213
+ - `--select` now wins over `--ignore` when a rule id appears in both, and the
214
+ run prints a note naming the id (previously the id was silently dropped).
215
+ - Mode validation is shared: the pytest fixture delegates to
216
+ `session.resolve_mode`, so the error message now names its source
217
+ (`env MCP_CASSETTE_MODE`, `marker mode=`, `ini mcp_cassette_mode`, or
218
+ `mode= argument`).
219
+
220
+ ## [0.2.2] - 2026-07-20
221
+
222
+ Documentation only; no code changes.
223
+
224
+ ### Added
225
+
226
+ - `docs/guide/` — a task-oriented user and operator guide, split by audience.
227
+ For test authors: getting started, and how-to pages for stdio record/replay,
228
+ remote Streamable HTTP, fault injection, and redaction. For operators:
229
+ install, configuration (every mode, ini option, marker, and matching
230
+ setting), CI pipeline, CLI reference with exit codes, and an incident
231
+ runbook for replay misses and failed recordings. Plus a symptom-to-fix
232
+ troubleshooting table.
233
+ - README now links to the guide.
234
+
235
+ ## [0.2.1] - 2026-07-19
236
+
237
+ Documentation only; no code changes.
238
+
239
+ ### Added
240
+
241
+ - `.agents_workspace/ARCHITECTURE.md`: living architecture doc — the standard
242
+ Mermaid diagram set (system context, components, record/replay sequences, data
243
+ model) plus a Key Decisions log.
244
+
245
+ ### Fixed
246
+
247
+ - Two CHANGELOG references left stale by the v0.x version relabel: the 0.1.0
248
+ release note (Beta, not "stable") and the `[Unreleased]` compare link.
249
+
250
+ ## [0.2.0] - 2026-07-19
251
+
252
+ Remote servers, server-initiated requests, and supply-chain linting. Cassettes now
253
+ record and replay Streamable HTTP sessions as well as stdio, sampling and elicitation
254
+ round-trip on both transports, and recorded third-party text can be linted in CI.
255
+
256
+ ### Added
257
+
258
+ - Streamable HTTP transport (`mcp-cassette[http]` extra): `mcp-cassette record --url`
259
+ stands up a local recording reverse proxy in front of a remote MCP endpoint, and
260
+ `mcp-cassette serve` infers the transport from the cassette and replays it as a local
261
+ mock HTTP server — offline, with no contact with the real server. SSE is passthrough
262
+ (never buffered), and `Mcp-Session-Id` is captured as evidence while replay issues its
263
+ own fresh id.
264
+ - `mcp_cassette.server_url(real_url)` — the HTTP twin of `server_command`, returning a
265
+ local URL to plug into the agent's MCP config. The fixture still never monkeypatches
266
+ the agent.
267
+ - Server-initiated request replay (sampling, elicitation) on both transports: anchored
268
+ emission with the recorded `msg_id`, accept-anything response handling (the agent's
269
+ answer is never matched against the recording), and release-on-response gating. v1
270
+ refused such cassettes at load; they now replay.
271
+ - `mcp-cassette lint` — heuristic rules over recorded tool descriptions and results
272
+ (third-party content that reaches a model), with `--baseline` drift detection and
273
+ `--format json`. Exposed programmatically as `LintFinding` and `LintReport`.
274
+ - Periodic crash-safety checkpoints during recording (`--checkpoint-interval SECONDS`,
275
+ default 5, `0` disables). A recording is written to a `<cassette>.partial` sidecar as
276
+ it runs, so a hard kill loses only what arrived since the last checkpoint instead of
277
+ the whole session. The sidecar is never written to the cassette path itself, because
278
+ `once` mode resolves record-vs-replay by that file's existence.
279
+ - Cassette format version 2, widening version 1 with optional HTTP metadata
280
+ (`transport`, `server_url`, `session_id`, per-message `exchange` and `channel`).
281
+
282
+ ### Changed
283
+
284
+ - Recording is no longer purely in-memory-until-shutdown; see checkpoints above.
285
+ - `Authorization` and every other HTTP header is forwarded upstream untouched but never
286
+ written to a cassette — stronger than redaction, since no field could hold it.
287
+
288
+ ### Removed
289
+
290
+ - **Breaking:** `UnsupportedCassetteFeature` is gone from the public API. It existed
291
+ only to refuse cassettes containing server-initiated requests at load; those cassettes
292
+ now replay, so nothing raises it. Remove any `except UnsupportedCassetteFeature`
293
+ handler — v1 cassettes themselves load unchanged.
294
+
295
+ ### Fixed
296
+
297
+ - HTTP proxy: cancel the run scope on a fatal first-contact error or a `disconnect`
298
+ fault, instead of hanging until the client gave up.
299
+ - Lint: use an ASCII minus in the R002 finding message, which crashed
300
+ `lint --baseline` on cp1252 Windows consoles.
301
+
302
+ ## [0.1.0] - 2026-07-18
303
+
304
+ First public release. `mcp-cassette` is "vcrpy for MCP": record real MCP stdio
305
+ sessions between an agent and an MCP server into cassettes, then replay them as
306
+ deterministic mock servers so agent test suites stop hitting live servers.
307
+
308
+ ### Added
309
+
310
+ - Recording proxy (`mcp-cassette record`) that wraps a real MCP server over
311
+ newline-delimited JSON-RPC stdio, taps both directions plus stderr, timestamps
312
+ against a monotonic clock, and saves an atomic cassette on any shutdown path.
313
+ - Replay server (`mcp-cassette serve`) that answers client requests from a
314
+ recorded cassette with no network, subprocess, or wall-clock reads, re-stamping
315
+ the JSON-RPC `id` onto each recorded response.
316
+ - Structural request matching with three ordering disciplines (`per_method`
317
+ default, `strict`, `none`) via `MatchConfig`; the JSON-RPC `id` is never matched.
318
+ - pytest fixture `mcp_cassette` and `@pytest.mark.mcp_cassette` marker with record
319
+ modes `once` (default), `none`, `all`, and `new_episodes`; mode precedence
320
+ `MCP_CASSETTE_MODE` env > marker > `mcp_cassette_mode` ini > `once`. The fixture
321
+ hands back a server command list rather than monkeypatching the agent.
322
+ - Fault injection (`Fault`, `FaultOverlay`, `FaultTarget`) with `delay`, `timeout`,
323
+ `error`, `malformed`, and `disconnect` faults; faults live in a separate overlay
324
+ (in-memory or `<cassette>.faults.json`) and never mutate the recorded cassette.
325
+ - Redaction at capture time on a deep copy, with default rules (`*token*`,
326
+ `*secret*`, `authorization`, …) always on unless disabled; bytes in flight are
327
+ never altered.
328
+ - `mcp-cassette inspect` for per-method counts, timing, and fault dry-runs.
329
+ - Cross-process miss signalling: the replay server exits `3` on any unmatched
330
+ request and the fixture surfaces misses (and empty recordings) as test failures.
331
+ - Graceful, cassette-finalizing shutdown on Linux, macOS, and Windows.
332
+ - Pydantic v2 cassette schema with `FORMAT_VERSION` forward-compat gating.
333
+
334
+ ### Notes
335
+
336
+ - Runtime dependencies are only `anyio` and `pydantic`; the `mcp` SDK is never a
337
+ runtime dependency.
338
+ - Server-initiated requests (sampling/elicitation) are recorded generically but
339
+ not replayable in this release; such cassettes are refused at load.
340
+
341
+ [Unreleased]: https://github.com/cheneeheng/mcp-cassette/compare/v0.3.3...HEAD
342
+ [0.3.3]: https://github.com/cheneeheng/mcp-cassette/compare/v0.3.2...v0.3.3
343
+ [0.3.2]: https://github.com/cheneeheng/mcp-cassette/compare/v0.3.1...v0.3.2
344
+ [0.3.1]: https://github.com/cheneeheng/mcp-cassette/compare/v0.3.0...v0.3.1
345
+ [0.3.0]: https://github.com/cheneeheng/mcp-cassette/compare/v0.2.2...v0.3.0
346
+ [0.2.2]: https://github.com/cheneeheng/mcp-cassette/compare/v0.2.1...v0.2.2
347
+ [0.2.1]: https://github.com/cheneeheng/mcp-cassette/compare/v0.2.0...v0.2.1
348
+ [0.2.0]: https://github.com/cheneeheng/mcp-cassette/compare/v0.1.0...v0.2.0
349
+ [0.1.0]: https://github.com/cheneeheng/mcp-cassette/releases/tag/v0.1.0