Skip to content

deps: lift the mcp<2 cap — fixtures ported to the 2.x SDK (supersedes #96, DSE-1261) - #105

Merged
ernestprovo23 merged 6 commits into
mainfrom
ernest/mcp-sdk-2x-fixture-port
Sep 5, 2026
Merged

deps: lift the mcp<2 cap — fixtures ported to the 2.x SDK (supersedes #96, DSE-1261)#105
ernestprovo23 merged 6 commits into
mainfrom
ernest/mcp-sdk-2x-fixture-port

Conversation

@ernestprovo23

@ernestprovo23 ernestprovo23 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Supersedes #96. Lifts mcp>=1.27.2,<2mcp>=1.27.2,<3; locks resolve to mcp==2.1.1.

What changed

  • tests/fixtures/_sdk_compat.py (new) — wires a fixture's declared surface against the 1.x decorator API or the 2.x on_* callback API (capability-detected, not version-sniffed). clean_server.py, mutated_server.py, poison_server.py now route through it with their declared surfaces unchanged.
  • src/mcp_warden/capture.py — two 2.x changes would have silently altered every committed lock; both are absorbed instead of re-pinned:
    1. 2.x renamed model fields to snake_case (input_schema, mime_type, protocol_version) while the protocol keys stayed camelCase → a plain model_dump() returned no inputSchema at all (9 drift rows against the committed lock).
    2. PromptArgument gained title=None; the server never sent it, but it leaked into arguments_hash (prompt-modified drift).
      _model_dump now returns the wire view (by_alias=True, exclude_none=True); the protocol version is read by attribute on either name.
  • tests/test_capture_model_dump.py (new) — pins the wire view on whichever SDK line is installed.
  • requirements-dev.lock regenerated with the CI-documented uv pip compile command (+--upgrade-package mcp); the CI sync re-compile reproduces it byte-for-byte. action/requirements.lock regenerated the same way so the composite Action's cross-OS tests exercise 2.x.
  • pyproject.toml comment and CHANGELOG [Unreleased] updated.

Digest-parity evidence (builder, mcp 2.1.1)

Proof Result
check python tests/fixtures/clean_server.py --lock tests/fixtures/clean.warden.lock OK no drift, exit 0
fresh pin overall_digest vs committed sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219identical
check mutated fixture vs clean lock exit 1 (still drifts, as designed)
negative control — plain model_dump() DRIFT DETECTED (9 item(s))
negative control — by_alias without exclude_none DRIFT DETECTED (2 item(s)), prompt-modified
clean_listchange.warden.lock (hand-crafted guard fixture, zero digests) same 3 placeholder rows under 1.29.1 and 2.1.1 — SDK-neutral

Verification

  • Full suite under mcp 2.1.1 with COVERAGE_PROCESS_START: 1115 passed, 2 skipped, coverage 88.26% (floor 80) — after the security-review fixes below.
  • ruff check . clean.

Security review remediation (CSO RED → fixes → GREEN; head de2b76a)

Classification: security-specific. Beyond the capture-digest changes, mcp 2.x moves HTTP to httpx2, which verifies TLS against the OS trust store via truststore instead of certifi — a trust-anchor change for --url capture, documented in CHANGELOG + README.

Finding Fix Test
F1 exclude_none=True dropped description/required nulls from prompt arguments — arguments_hash diverged from every released warden for any prompt with a bare argument _normalize_prompt_argument: the protocol field set name/description/required is always emitted (null when absent, byte-identical to the 1.x model_dump() shape); only keys outside that set (2.x title, _meta) are shed when null; non-null extras kept. No SCHEMA_VERSION bump. CHANGELOG sentence corrected to name exactly which keys are shed. test_bare_prompt_argument_hashes_byte_identically_to_every_released_wardencanon([norm]) == b'[{"description":null,"name":"text","required":null}]' and hash_arguments equality vs the literal; title absent. test_non_null_extra_prompt_argument_fields_are_kept.
F3 no nextCursor pagination — a server that splits tools/list was pinned from page one _list_all drains every page via the SDK's params=PaginatedRequestParams(cursor=…) (same call shape on 1.x and 2.x; nextCursor/next_cursor both honoured). Hard cap MAX_LIST_PAGES = 256CaptureError (exit 2). A failure on any page after the first is CaptureError — a partial surface is never pinned. First-page swallow for capability-less servers unchanged, now labelled as the deliberate fail-open with the follow-up ticket referenced in the docstring. tests/test_capture_pagination.py (7): two-page == one-page digest, cursor passed through params on every follow-up page, snake_case cursor attr, prompts/resources paginate, never-terminating chain refused, later-page failure fails closed, first-page failure stays fail-open.
TLS trust anchor (truststore) Verified: mcp 2.1.1 → httpx2 2.12.0 → truststore 0.10.4, referenced in httpx2/_config.py. Stated plainly in CHANGELOG [Unreleased] and the README runtime-dependencies note. n/a (documentation)

| CSO re-verify fold-ins | New CI job test-mcp-1x (Capture parity on mcp 1.x): installs mcp>=1.27.2,<2 and runs the capture/parity/pagination/e2e tests, so both SDK lines back the digest-parity claim (permissions: contents: read inherited, SHA-pinned actions, no ${{ }} in run blocks). Fail-open docstring + CHANGELOG now cite DSE-1538 (capability-aware error handling) and DSE-1539 (annotations/outputSchema hashing). | CI job on this PR |

Parity for the null-field prompt argument: _normalize_prompt_argument(PromptArgument(name="text")) canonicalises to exactly [{"description":null,"name":"text","required":null}] — the pre-PR bytes — and check python tests/fixtures/clean_server.py --lock tests/fixtures/clean.warden.lock still exits 0 (OK no drift) under mcp 2.1.1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lu4ZLyWWL7TXMGYpNFU5Dc

…de SDK-neutral (supersedes #96, DSE-1261)

The 2.x SDK removed the @server.list_tools() decorators the fixture servers
used (why #92 capped mcp<2). tests/fixtures/_sdk_compat.py wires the same
declared surfaces against the 1.x decorator API or the 2.x on_* callbacks.

Two 2.x changes would have silently altered every committed lock and are
absorbed in capture.py rather than re-pinned:
  * model fields renamed to snake_case (input_schema, mime_type,
    protocol_version) while wire keys stayed camelCase — a plain model_dump()
    under 2.x returned no inputSchema at all;
  * PromptArgument gained title=None, which leaked into arguments_hash.
_model_dump now returns the wire view (by_alias=True, exclude_none=True) and
the protocol version is read by attribute on either name. Proven: `check`
against clean.warden.lock exits 0 and a fresh pin reproduces overall_digest
cb20a16c… under mcp 2.1.1; the mutated fixture still drifts; the negative
controls (plain dump → 9 drift rows; alias-only → prompt-modified) fail.

Locks resolve to mcp==2.1.1 (requirements-dev.lock via the CI-documented uv
command, byte-identical on the CI sync re-compile; action/requirements.lock).
…r pages — CSO review of #105

F1: exclude_none had dropped the description/required nulls of a bare prompt
argument, changing arguments_hash vs every released warden. The protocol field
set {name, description, required} is now always emitted (null when absent) and
only keys outside it (2.x title, _meta) are shed when null. No SCHEMA_VERSION
bump; pinned by a canon() byte-equality test.

F3: tools/resources/prompts list calls now follow nextCursor through the SDK's
params= cursor (same call shape on 1.x and 2.x). A failure on any later page is
a CaptureError — a partial surface is never pinned — and a chain longer than 256
pages is refused. The first-page swallow for capability-less servers is
unchanged and labelled as the deliberate fail-open it is.

Docs: mcp 2.x -> httpx2 -> truststore verifies --url TLS against the OS trust
store instead of certifi; stated plainly in CHANGELOG and README.
…en note (CSO re-verify of #105)

The cross-major digest-parity claim needs both SDK lines exercised: a
test-mcp-1x job re-resolves to the newest 1.x and runs the capture, parity,
pagination and e2e tests only (CI time stays flat). The first-page fail-open
docstring and CHANGELOG now cite DSE-1538 (capability-aware error handling)
and DSE-1539 (annotations/outputSchema hashing) instead of "a follow-up".
@ernestprovo23
ernestprovo23 merged commit 8dc59e4 into main Sep 5, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant