headroom/tests/test_mcp_dependency_contract.py
JD Davis 6077e5a149
fix(mcp): restore SDK v1 compatibility cap (#2978)
## Description

PR #2963 widened the MCP dependency to 2.x while Headroom's live MCP
server still uses the v1 low-level `Server.list_tools()` and
`Server.call_tool()` decorators. Fresh installs therefore crash before
serving tools. Restore the v1 cap until the explicit SDK 2.x port in
#2658 lands, and pin that compatibility contract with a regression test.

Closes #2977

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Restore `mcp>=1.28.1,<2.0.0` in the `proxy` and `mcp` extras.
- Regenerate `uv.lock`, resolving MCP 1.28.1 and removing the
incompatible 2.x transitive set.
- Add a dependency-contract test covering both shipping extras.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ uv run pytest -q tests/test_mcp_dependency_contract.py tests/test_ccr_mcp_server.py tests/test_cli/test_mcp.py
41 passed in 0.56s

$ uv run ruff check tests/test_mcp_dependency_contract.py
All checks passed!

$ uv run ruff format --check tests/test_mcp_dependency_contract.py
1 file already formatted
```

## Real Behavior Proof

- Environment: macOS arm64, Python 3.13.14, uv-managed project
environment.
- Exact command / steps: resolve the `mcp` extra, inspect the installed
SDK version and v1 decorators, then instantiate
`HeadroomMCPServer(check_proxy=False)`.
- Observed result: `1.28.1 True True`; server construction returns a v1
`Server` successfully.
- Not tested: full stdio exchange against every external MCP client;
existing MCP unit and CLI suites cover server setup and handlers.

## Runtime Rollout Safety

- Rollout-managed feature(s): None; dependency resolution guard.
- Minimum rollout channel: Stable/default.
- Stable/default behavior changed: Fresh installs stop resolving the
incompatible MCP SDK 2.x release.
- Kill switch / disable path: Revert the dependency cap after #2658
lands.
- Unsafe override required: No.
- Qualification impact: MCP extras and proxy installs remain on the
maintained MCP 1.x line.
- Rollback path: Revert this PR; not recommended until the v2 server
port is merged and tested.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable.

## Additional Notes

The documentation change is the inline dependency rationale next to the
cap. The long-term migration remains #2658; this PR deliberately does
not mix that breaking SDK port into the release-blocker rollback.
2026-08-13 12:35:03 -05:00

29 lines
1.1 KiB
Python

"""Keep the installed MCP SDK compatible with Headroom's live server API."""
from __future__ import annotations
from pathlib import Path
import tomllib
from packaging.requirements import Requirement
ROOT = Path(__file__).resolve().parents[1]
def _mcp_requirement(dependencies: list[str]) -> Requirement:
matches = [Requirement(value) for value in dependencies if Requirement(value).name == "mcp"]
assert len(matches) == 1, "expected exactly one mcp requirement"
return matches[0]
def test_all_shipping_mcp_extras_exclude_incompatible_sdk_v2() -> None:
"""SDK 2 removed Server.list_tools()/call_tool(); #2658 owns that port."""
project = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]
optional = project["optional-dependencies"]
for extra in ("proxy", "mcp"):
requirement = _mcp_requirement(optional[extra])
assert requirement.specifier.contains("1.28.1")
assert not requirement.specifier.contains("2.0.0"), (
f"the {extra!r} extra admits MCP SDK 2.x before the server port in #2658"
)