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.
- mcp_cassette-0.3.3/.github/workflows/ci.yml +49 -0
- mcp_cassette-0.3.3/.github/workflows/publish.yml +60 -0
- mcp_cassette-0.3.3/.gitignore +27 -0
- mcp_cassette-0.3.3/CHANGELOG.md +349 -0
- mcp_cassette-0.3.3/LICENSE +201 -0
- mcp_cassette-0.3.3/PKG-INFO +202 -0
- mcp_cassette-0.3.3/README.md +170 -0
- mcp_cassette-0.3.3/docs/guide/01-getting-started.md +149 -0
- mcp_cassette-0.3.3/docs/guide/10-troubleshooting.md +50 -0
- mcp_cassette-0.3.3/docs/guide/how-to/02-record-and-replay.md +177 -0
- mcp_cassette-0.3.3/docs/guide/how-to/03-remote-http.md +99 -0
- mcp_cassette-0.3.3/docs/guide/how-to/04-use-as-a-library.md +116 -0
- mcp_cassette-0.3.3/docs/guide/how-to/05-inject-faults.md +99 -0
- mcp_cassette-0.3.3/docs/guide/how-to/06-replay-timing.md +87 -0
- mcp_cassette-0.3.3/docs/guide/how-to/07-inspect-and-diff.md +108 -0
- mcp_cassette-0.3.3/docs/guide/how-to/08-redact-secrets.md +83 -0
- mcp_cassette-0.3.3/docs/guide/how-to/09-lint-pattern-packs.md +120 -0
- mcp_cassette-0.3.3/docs/guide/index.md +62 -0
- mcp_cassette-0.3.3/docs/guide/operations/11-install.md +77 -0
- mcp_cassette-0.3.3/docs/guide/operations/12-configure.md +133 -0
- mcp_cassette-0.3.3/docs/guide/operations/13-ci.md +122 -0
- mcp_cassette-0.3.3/docs/guide/operations/14-cli-reference.md +153 -0
- mcp_cassette-0.3.3/docs/guide/operations/15-runbook-replay-misses.md +163 -0
- mcp_cassette-0.3.3/examples/README.md +233 -0
- mcp_cassette-0.3.3/examples/cassettes/deterministic.mcp.json +112 -0
- mcp_cassette-0.3.3/examples/cassettes/echo_and_add.mcp.json +155 -0
- mcp_cassette-0.3.3/examples/cassettes/fault.mcp.json +112 -0
- mcp_cassette-0.3.3/examples/cassettes/http_echo_and_add.mcp.json +171 -0
- mcp_cassette-0.3.3/examples/cassettes/injected.mcp.json +162 -0
- mcp_cassette-0.3.3/examples/cassettes/sampling.mcp.json +175 -0
- mcp_cassette-0.3.3/examples/cassettes/tools.mcp.json +162 -0
- mcp_cassette-0.3.3/examples/echo_http_server.py +64 -0
- mcp_cassette-0.3.3/examples/echo_server.py +179 -0
- mcp_cassette-0.3.3/examples/library_mode.py +68 -0
- mcp_cassette-0.3.3/examples/lint-pack.toml +26 -0
- mcp_cassette-0.3.3/examples/mcp_client.py +121 -0
- mcp_cassette-0.3.3/examples/mcp_http_client.py +46 -0
- mcp_cassette-0.3.3/examples/test_echo.py +125 -0
- mcp_cassette-0.3.3/examples/test_echo_http.py +90 -0
- mcp_cassette-0.3.3/pyproject.toml +112 -0
- mcp_cassette-0.3.3/scripts/check_version.py +57 -0
- mcp_cassette-0.3.3/src/mcp_cassette/__init__.py +64 -0
- mcp_cassette-0.3.3/src/mcp_cassette/__main__.py +10 -0
- mcp_cassette-0.3.3/src/mcp_cassette/_signals.py +55 -0
- mcp_cassette-0.3.3/src/mcp_cassette/_stdio.py +34 -0
- mcp_cassette-0.3.3/src/mcp_cassette/cassette.py +424 -0
- mcp_cassette-0.3.3/src/mcp_cassette/cli.py +790 -0
- mcp_cassette-0.3.3/src/mcp_cassette/diffing.py +248 -0
- mcp_cassette-0.3.3/src/mcp_cassette/lint/__init__.py +31 -0
- mcp_cassette-0.3.3/src/mcp_cassette/lint/engine.py +211 -0
- mcp_cassette-0.3.3/src/mcp_cassette/lint/packs.py +340 -0
- mcp_cassette-0.3.3/src/mcp_cassette/lint/patterns.py +37 -0
- mcp_cassette-0.3.3/src/mcp_cassette/lint/rules.py +221 -0
- mcp_cassette-0.3.3/src/mcp_cassette/matching.py +191 -0
- mcp_cassette-0.3.3/src/mcp_cassette/py.typed +0 -0
- mcp_cassette-0.3.3/src/mcp_cassette/pytest_plugin.py +150 -0
- mcp_cassette-0.3.3/src/mcp_cassette/record/__init__.py +7 -0
- mcp_cassette-0.3.3/src/mcp_cassette/record/checkpoint.py +87 -0
- mcp_cassette-0.3.3/src/mcp_cassette/record/proxy.py +239 -0
- mcp_cassette-0.3.3/src/mcp_cassette/record/pump.py +68 -0
- mcp_cassette-0.3.3/src/mcp_cassette/record/recorder.py +203 -0
- mcp_cassette-0.3.3/src/mcp_cassette/replay/__init__.py +7 -0
- mcp_cassette-0.3.3/src/mcp_cassette/replay/faults.py +100 -0
- mcp_cassette-0.3.3/src/mcp_cassette/replay/new_episodes.py +190 -0
- mcp_cassette-0.3.3/src/mcp_cassette/replay/pacing.py +75 -0
- mcp_cassette-0.3.3/src/mcp_cassette/replay/server.py +435 -0
- mcp_cassette-0.3.3/src/mcp_cassette/replay/server_requests.py +210 -0
- mcp_cassette-0.3.3/src/mcp_cassette/report.py +37 -0
- mcp_cassette-0.3.3/src/mcp_cassette/session.py +482 -0
- mcp_cassette-0.3.3/src/mcp_cassette/transports/__init__.py +1 -0
- mcp_cassette-0.3.3/src/mcp_cassette/transports/http/__init__.py +21 -0
- mcp_cassette-0.3.3/src/mcp_cassette/transports/http/proxy.py +403 -0
- mcp_cassette-0.3.3/src/mcp_cassette/transports/http/server.py +704 -0
- mcp_cassette-0.3.3/src/mcp_cassette/transports/http/wire.py +316 -0
- mcp_cassette-0.3.3/tests/conftest.py +5 -0
- mcp_cassette-0.3.3/tests/integration/test_cli_diff.py +116 -0
- mcp_cassette-0.3.3/tests/integration/test_cli_lint_packs.py +73 -0
- mcp_cassette-0.3.3/tests/integration/test_faults.py +184 -0
- mcp_cassette-0.3.3/tests/integration/test_http_pacing.py +163 -0
- mcp_cassette-0.3.3/tests/integration/test_http_record.py +284 -0
- mcp_cassette-0.3.3/tests/integration/test_http_replay.py +503 -0
- mcp_cassette-0.3.3/tests/integration/test_library_http.py +62 -0
- mcp_cassette-0.3.3/tests/integration/test_library_stdio.py +71 -0
- mcp_cassette-0.3.3/tests/integration/test_lint_recorded.py +39 -0
- mcp_cassette-0.3.3/tests/integration/test_passthrough.py +52 -0
- mcp_cassette-0.3.3/tests/integration/test_record.py +267 -0
- mcp_cassette-0.3.3/tests/integration/test_replay.py +158 -0
- mcp_cassette-0.3.3/tests/integration/test_replay_pacing.py +189 -0
- mcp_cassette-0.3.3/tests/integration/test_sampling_replay.py +493 -0
- mcp_cassette-0.3.3/tests/reference_http_server/server.py +114 -0
- mcp_cassette-0.3.3/tests/reference_server/server.py +109 -0
- mcp_cassette-0.3.3/tests/scripted_client.py +262 -0
- mcp_cassette-0.3.3/tests/scripted_http_client.py +222 -0
- mcp_cassette-0.3.3/tests/system/test_fixture.py +238 -0
- mcp_cassette-0.3.3/tests/system/test_http_fixture.py +147 -0
- mcp_cassette-0.3.3/tests/unit/test_cassette_schema.py +272 -0
- mcp_cassette-0.3.3/tests/unit/test_checkpoint.py +198 -0
- mcp_cassette-0.3.3/tests/unit/test_cli_surface.py +277 -0
- mcp_cassette-0.3.3/tests/unit/test_diffing.py +243 -0
- mcp_cassette-0.3.3/tests/unit/test_fault_injector.py +53 -0
- mcp_cassette-0.3.3/tests/unit/test_http_proxy_edges.py +364 -0
- mcp_cassette-0.3.3/tests/unit/test_http_proxy_shutdown.py +113 -0
- mcp_cassette-0.3.3/tests/unit/test_http_replay_edges.py +478 -0
- mcp_cassette-0.3.3/tests/unit/test_http_wire.py +293 -0
- mcp_cassette-0.3.3/tests/unit/test_inspect_views.py +191 -0
- mcp_cassette-0.3.3/tests/unit/test_library_api.py +93 -0
- mcp_cassette-0.3.3/tests/unit/test_lint.py +375 -0
- mcp_cassette-0.3.3/tests/unit/test_lint_packs.py +229 -0
- mcp_cassette-0.3.3/tests/unit/test_lint_project_config.py +186 -0
- mcp_cassette-0.3.3/tests/unit/test_lint_regression.py +117 -0
- mcp_cassette-0.3.3/tests/unit/test_matching.py +207 -0
- mcp_cassette-0.3.3/tests/unit/test_pacing.py +96 -0
- mcp_cassette-0.3.3/tests/unit/test_proxy_shutdown.py +226 -0
- mcp_cassette-0.3.3/tests/unit/test_pump.py +64 -0
- mcp_cassette-0.3.3/tests/unit/test_recorder.py +95 -0
- mcp_cassette-0.3.3/tests/unit/test_replay_server.py +372 -0
- mcp_cassette-0.3.3/tests/unit/test_server_requests.py +190 -0
- mcp_cassette-0.3.3/tests/unit/test_session.py +188 -0
- mcp_cassette-0.3.3/tests/unit/test_signals.py +103 -0
- 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
|