Skip to content

fix(supply-chain): tell Dependabot why mcp is held below 2 — it proposed lifting it twice (INV-SUPPLY-2) - #63

Merged
neosun100 merged 1 commit into
mainfrom
fix/breaking-bounds-to-dependabot
Aug 6, 2026
Merged

fix(supply-chain): tell Dependabot why mcp is held below 2 — it proposed lifting it twice (INV-SUPPLY-2)#63
neosun100 merged 1 commit into
mainfrom
fix/breaking-bounds-to-dependabot

Conversation

@neosun100

Copy link
Copy Markdown
Contributor

The gap

mcp>=1.0,<2 was stated in three places Dependabot cannot read — a long pyproject.toml comment, INV-MCP-5 in docs/INVARIANTS.md, and tests/test_mcp_version_bound.py. So it proposed lifting the bound twice in one week:

PR change effect
#59 pyproject.toml: mcp>=1.0,<2mcp>=1.0,<3 widens the range
#60 specialists/*: mcp==1.28.1mcp==2.0.0 pins the deployed containers to it

PR #60 came from the container coverage added last round (INV-SUPPLY-1) — the very first PR that entry produced would have shipped a breaking version into the Runtime images.

Re-verified, not trusted from the record

A bound whose evidence has expired is worse than no bound, so both were measured against the real 2.0.0 release:

from mcp.server import Server                                 -> OK      <-- proves nothing
Server("probe").list_tools / .call_tool                       -> GONE
create_server()  -> AttributeError: 'Server' object has no attribute 'list_tools'

from mcp import ClientSession                                 -> OK
from mcp.client.streamable_http import streamablehttp_client   -> ImportError

The first block confirms INV-MCP-5's own recorded lesson: an import check is not a compatibility check.

The second block is what could have been missed. The root bound is about the server surface, which the specialists never touch — they use the client surface, so "the same bound applies" was an assumption that needed its own measurement. It held, for an independent reason: all four specialists/*/agent_a2a.py import streamablehttp_client, which 2.0 also removed. Two separate breakages behind one version number.

Fix

Declare the bound in .github/dependabot.yml — the one file Dependabot reads — with its evidence and its lift procedure. This generalises what iac-cdk's TypeScript bound already did right (INV-IAC): "a fix applied to one call site is not an invariant", applied to a supply-chain declaration.

The non-obvious detail the guard encodes

ignore is per-update-block, not global. The root pip entry's ignore does not cover the container pip entry — which is exactly why PR #60 existed — so a guard checking merely "mcp is ignored somewhere" would have passed while the dangerous PR was open. Every block of the relevant ecosystem is checked.

Also asserted:

  • Each ignore carries a version range. A bare ignore: mcp silently blocks 1.x security patches too, turning a compatibility bound into an unmaintained dependency.
  • The pyproject bound and the ignore describe the same boundary. Widening to <3 while the ignore stays at >=2.0.0 is the worst combination: the repo permits a version Dependabot has stopped warning about — the guard that would complain is the one now disabled.
  • The file records how to re-verify, so the bound gets lifted deliberately rather than becoming permanent by default.

Verification

PRs #59 and #60 should be closed once this merges; the ignore entries stop them recurring.

…sed lifting it twice (INV-SUPPLY-2)

`mcp>=1.0,<2` was stated in three places Dependabot cannot read: a long
pyproject.toml comment, INV-MCP-5 in docs/INVARIANTS.md, and
test_mcp_version_bound.py. So it proposed lifting the bound twice in one week:

  PR #59  pyproject.toml   mcp>=1.0,<2 -> mcp>=1.0,<3   (widens the range)
  PR #60  specialists/*    mcp==1.28.1 -> mcp==2.0.0    (pins DEPLOYED containers)

PR #60 came from the container coverage added last round (INV-SUPPLY-1) — the
first PR that entry produced would have shipped a breaking version into the
Runtime images.

Both re-verified against the real mcp 2.0.0 release rather than trusted from the
record, since a bound whose evidence expired is worse than no bound:

  from mcp.server import Server                            -> OK  (proves nothing)
  Server("probe").list_tools / .call_tool                  -> GONE
  create_server()  -> AttributeError: 'Server' object has no attribute 'list_tools'

  from mcp import ClientSession                            -> OK
  from mcp.client.streamable_http import streamablehttp_client -> ImportError

The second pair is what could have been missed. The root bound is about the
SERVER surface, which the specialists never touch — they use the CLIENT surface,
so "the same bound applies" needed its own measurement. It held for an
INDEPENDENT reason: all four specialists/*/agent_a2a.py import
streamablehttp_client, which 2.0 also removed. Two separate breakages behind one
version number.

Fix: declare the bound in .github/dependabot.yml with its evidence and its lift
procedure, generalising what iac-cdk's TypeScript bound already did right
(INV-IAC) — "a fix applied to one call site is not an invariant" applied to a
supply-chain declaration.

Guard (test_breaking_bounds_are_told_to_dependabot.py) encodes the non-obvious
detail: `ignore` is PER-UPDATE-BLOCK, not global, so the root pip entry's ignore
does not cover the container pip entry — exactly why PR #60 existed — and a
guard checking "mcp is ignored somewhere" would have passed while it was open.
Every block of the relevant ecosystem is checked. Also asserted: each ignore
carries a VERSION RANGE (a bare `ignore: mcp` would block 1.x security patches
too, turning a compatibility bound into an unmaintained dependency); the
pyproject bound and the ignore describe the SAME boundary (widening to <3 while
the ignore stays >=2.0.0 lets the repo install a version Dependabot no longer
warns about); and the file records HOW to re-verify, so the bound is lifted
deliberately rather than becoming permanent by default.

Mutation-tested 7/7, including the exact PR #59 and PR #60 conditions.

Counts synced repo-wide (4039->4073 tests, 171->175 files, +6->+8 skipped): the
file count drifted 4 past test_docs_drift's +-3 tolerance, which is the guard
doing its job. Suite: 4074 passed / 8 skipped. ruff clean.
@neosun100
neosun100 merged commit 18cc7ec into main Aug 6, 2026
15 checks passed
@neosun100
neosun100 deleted the fix/breaking-bounds-to-dependabot branch August 6, 2026 13:40
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