Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/integrity-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,38 @@ jobs:
- name: Run property-fuzz suite (deterministic, seed=0)
run: pytest tests/fuzz -p no:randomly --hypothesis-seed=0

# --------------------------------------------------------------------------
# Job 1a: mcp 1.x compatibility leg (#105)
#
# The digest-parity claim across the mcp SDK major rests on BOTH lines being
# exercised: capture must produce byte-identical locks whether the client SDK
# is 1.x (camelCase model fields, decorator server API) or 2.x (snake_case
# fields, on_* callbacks, nextCursor via params=). This leg re-resolves to the
# newest 1.x and runs only the capture/parity tests so CI time stays flat.
# --------------------------------------------------------------------------
test-mcp-1x:
name: Capture parity on mcp 1.x
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python 3.11
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
cache: pip

- name: Install package + dev deps pinned to the mcp 1.x line
run: pip install -e ".[dev,sigstore]" "mcp>=1.27.2,<2"

- name: Show resolved mcp (must be 1.x)
run: pip show mcp | grep -i '^version'

- name: Capture / parity / pagination tests under mcp 1.x
run: pytest -q tests/test_capture_model_dump.py tests/test_capture_pagination.py tests/test_e2e_pin_check.py tests/test_capture_http.py

# --------------------------------------------------------------------------
# Job 1b: lint gate (ruff)
#
Expand Down
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,40 @@ Streamable HTTP; the v0.3 `guard` proxy adds deterministic runtime *result* insp

### Changed

- **`mcp` SDK 2.x is now supported — the `<2` cap from #92 is lifted to `<3` (supersedes
#96, DSE-1261).** Lock files resolve to `mcp==2.1.1` (`requirements-dev.lock`,
`action/requirements.lock`). Two 2.x changes would have silently altered every
committed lock and are absorbed in `capture.py` rather than re-pinned: the SDK renamed
model fields to snake_case (`input_schema`, `mime_type`, `protocol_version`) while the
protocol keys stayed camelCase, so a plain `model_dump()` returned no `inputSchema` at
all; and `PromptArgument` grew a `title` field whose `None` default the server never
sent, which leaked into `arguments_hash`. `_model_dump` now returns the wire view
(`by_alias=True, exclude_none=True`) and the protocol version is read through it —
identical output on 1.x and 2.x, proven by `check` against the committed
`clean.warden.lock` and `clean_listchange.warden.lock` under 2.1.1 (`overall_digest`
byte-identical; the mutated fixture still drifts) and pinned by
`tests/test_capture_model_dump.py`. The three SDK-backed fixture servers are wired
through a new `tests/fixtures/_sdk_compat.py` (decorator API on 1.x, `on_*` callbacks on
2.x) with their declared surfaces unchanged. Prompt arguments keep the exact byte
shape every released warden hashed: the protocol field set `name` / `description` /
`required` is always emitted (`null` when the server omitted the optional), and only
keys outside that set — 2.x's `title`, `_meta` — are shed when null; a non-null extra is
kept on either SDK line. No `SCHEMA_VERSION` bump.
- **`nextCursor` pagination in capture.** `tools/list`, `resources/list` and `prompts/list`
are now drained across every page (a server that splits its surface used to be pinned
from page one only). A failure on any page after the first is a `CaptureError` (exit
2) — a partial surface is never pinned — and a cursor chain longer than 256 pages is
refused. The first-page swallow for capability-less servers is unchanged and is now
labelled as the deliberate fail-open it is; tightening it is DSE-1538, and hashing the
`Tool` fields capture still projects away (`annotations`, `outputSchema`) is DSE-1539.
- **Trust-anchor change for `--url` capture (from the SDK, stated plainly).** mcp 2.x
moves HTTP to `httpx2`, which verifies TLS against the **operating-system trust store**
via `truststore` instead of the bundled `certifi` CA set. A `pin --url` / `check --url`
against a server whose certificate chains to a corporate or private CA installed in the
OS store now succeeds where 1.x failed, and conversely an OS store with a removed or
distrusted public root now fails where 1.x succeeded. This is a change in *what
warden trusts*, not in what it captures; it is inherited from the SDK and cannot be
toggled from the CLI.
- **`auth audit` / `doctor`: template placeholders are `WRD-AUTH-PLACEHOLDER-SECRET`
(low), not committed credentials.** Running the audit over a 463-config public
corpus showed 74 % of `WRD-AUTH-TOKEN-IN-CONFIG` hits were fill-me-ins
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ uv pip install --python .venv/bin/python -e ".[dev]"
Runtime dependencies: `mcp` (official MCP Python SDK), `rfc8785`, `pydantic`,
`typer`, `rich`, `pyyaml`, `anyio`.

> **TLS trust anchor for `--url` capture.** With the `mcp` 2.x SDK, HTTP goes through
> `httpx2`, which verifies server certificates against the **OS trust store** (via
> `truststore`) rather than Python's bundled `certifi` set. Private/corporate CAs installed
> in the OS now work; a root removed from the OS store is now rejected. Inherited from the
> SDK; not configurable from the CLI. See CHANGELOG.

### TypeScript verifier — `@mcp-warden/lock` (zero dependencies)

The lock **format** is vendor-neutral ([`docs/SPEC.md`](docs/SPEC.md)), and the ecosystem's
Expand Down
89 changes: 45 additions & 44 deletions action/requirements.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile /tmp/requirements.in --generate-hashes --python-version 3.11 --universal --output-file /tmp/req-universal.lock
# uv pip compile --universal --generate-hashes --python-version 3.11 pyproject.toml -o action/requirements.lock
annotated-doc==0.0.4 \
--hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \
--hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4
Expand All @@ -12,8 +12,8 @@ anyio==4.13.0 \
--hash=sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 \
--hash=sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc
# via
# -r /tmp/requirements.in
# httpx
# mcp-warden-cli (pyproject.toml)
# httpx2
# mcp
# sse-starlette
# starlette
Expand All @@ -23,12 +23,6 @@ attrs==26.1.0 \
# via
# jsonschema
# referencing
certifi==2026.5.20 \
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
# via
# httpcore
# httpx
cffi==2.0.0 ; platform_python_implementation != 'PyPy' \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
Expand Down Expand Up @@ -119,11 +113,10 @@ click==8.4.1 ; sys_platform != 'emscripten' \
--hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \
--hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96
# via uvicorn
colorama==0.4.6 \
colorama==0.4.6 ; sys_platform == 'win32' \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via
# -r /tmp/requirements.in
# click
# typer
cryptography==48.0.0 \
Expand Down Expand Up @@ -177,30 +170,30 @@ cryptography==48.0.0 \
--hash=sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c \
--hash=sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b
# via pyjwt
h11==0.16.0 \
h11==0.16.0 ; sys_platform != 'emscripten' \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via
# httpcore
# httpcore2
# uvicorn
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via mcp
httpx-sse==0.4.3 \
--hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \
--hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d
httpcore2==2.12.0 ; sys_platform != 'emscripten' \
--hash=sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb \
--hash=sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648
# via httpx2
httpx2==2.12.0 \
--hash=sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf \
--hash=sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36
# via mcp
httpx2-jsfetch==1.0 ; python_full_version >= '3.12' and sys_platform == 'emscripten' \
--hash=sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60 \
--hash=sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32
# via httpx2
idna==3.18 \
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
# via
# anyio
# httpx
# httpx2
jsonschema==4.26.0 \
--hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \
--hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
Expand All @@ -213,14 +206,22 @@ markdown-it-py==4.2.0 \
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
# via rich
mcp==1.27.2 \
--hash=sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef \
--hash=sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5
# via -r /tmp/requirements.in
mcp==2.1.1 \
--hash=sha256:1c6c31c5d6471c58db76af3af8af67f46d11d01f0a59077d0a308cbdb3d3e915 \
--hash=sha256:50b7ba1ebbe117008ea7bdd288234043e69c20b403d6851d19661e6d431a75ef
# via mcp-warden-cli (pyproject.toml)
mcp-types==2.1.1 \
--hash=sha256:26f9f7f03f2a5730717a5b98e2ab7eb640ac352d05a00cdc725c311864778295 \
--hash=sha256:77dcbe48fba73cca71a673f2646a5f037a017b7a0a07ac89cec1113028890eda
# via mcp
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
# via markdown-it-py
opentelemetry-api==1.44.0 \
--hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \
--hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef
# via mcp
pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
Expand All @@ -229,9 +230,9 @@ pydantic==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# -r /tmp/requirements.in
# mcp-warden-cli (pyproject.toml)
# mcp
# pydantic-settings
# mcp-types
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
Expand Down Expand Up @@ -354,10 +355,6 @@ pydantic-core==2.46.4 \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via pydantic
pydantic-settings==2.14.1 \
--hash=sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de \
--hash=sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa
# via mcp
pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
Expand All @@ -366,10 +363,6 @@ pyjwt==2.13.0 \
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
# via mcp
python-dotenv==1.2.2 \
--hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \
--hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3
# via pydantic-settings
python-multipart==0.0.32 \
--hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \
--hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23
Expand Down Expand Up @@ -471,7 +464,7 @@ pyyaml==6.0.3 \
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via -r /tmp/requirements.in
# via mcp-warden-cli (pyproject.toml)
referencing==0.37.0 \
--hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \
--hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8
Expand All @@ -481,12 +474,12 @@ referencing==0.37.0 \
rfc8785==0.1.4 \
--hash=sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48 \
--hash=sha256:e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da
# via -r /tmp/requirements.in
# via mcp-warden-cli (pyproject.toml)
rich==15.0.0 \
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
# via
# -r /tmp/requirements.in
# mcp-warden-cli (pyproject.toml)
# typer
rpds-py==2026.5.1 \
--hash=sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead \
Expand Down Expand Up @@ -636,16 +629,25 @@ starlette==1.2.1 \
# via
# mcp
# sse-starlette
truststore==0.10.4 ; sys_platform != 'emscripten' \
--hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \
--hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981
# via
# httpcore2
# httpx2
typer==0.26.7 \
--hash=sha256:5c87cfbc5d34491c5346ebf49c23e18d56ccb863268d3a8d592b26087c2f5e58 \
--hash=sha256:e314a34c617e419c091b2830dda3ea1f257134ff593061a8f5b9717ab8dddb3a
# via -r /tmp/requirements.in
# via mcp-warden-cli (pyproject.toml)
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# anyio
# httpx2
# mcp
# mcp-types
# opentelemetry-api
# pydantic
# pydantic-core
# referencing
Expand All @@ -657,7 +659,6 @@ typing-inspection==0.4.2 \
# via
# mcp
# pydantic
# pydantic-settings
uvicorn==0.49.0 ; sys_platform != 'emscripten' \
--hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \
--hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3
Expand Down
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ authors = [{ name = "Ernest Provo", email = "ernest@thedataexperts.us" }]
keywords = ["mcp", "supply-chain", "security", "ci", "integrity"]

dependencies = [
# Capped below 2.0.0: the 2.x SDK removed the `@server.list_tools()` decorator
# that tests/fixtures/*_server.py use (DSE-1261). Production capture.py is
# already 2.x-compatible — it calls ClientSession.list_tools(), which 2.0.0
# keeps — so lifting this cap only needs the three fixture servers ported to
# the 2.x add_request_handler API.
"mcp>=1.27.2,<2",
# 1.x and 2.x are both supported. The 2.x SDK removed the ``@server.list_tools()``
# decorators the fixture servers used (DSE-1261, #92) — tests/fixtures/_sdk_compat.py
# now wires fixtures against either API — and renamed model fields to snake_case,
# which capture.py absorbs by dumping models by wire alias. Bump the upper bound
# only after re-running the digest-parity check in tests/test_e2e_pin_check.py.
"mcp>=1.27.2,<3",
"rfc8785>=0.1.4",
"pydantic>=2.0.0",
"typer>=0.26.7",
Expand Down
Loading
Loading