headroom/tests/test_ccr_mcp_http.py
Rod Boev 4ea96a417c
feat(mcp): add streamable HTTP MCP transport (#1773)
## Description

`headroom mcp serve` only exposed stdio, which blocked MCP clients that
require a Streamable HTTP endpoint. This PR adds an explicit HTTP
transport mode around the existing Headroom MCP server while keeping
stdio as the default and keeping tool registration single-sourced.

Closes #1346.

## Type of Change

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

## Changes Made

- Add `headroom mcp serve --transport http` with host, port, and path
options.
- Serve `headroom_compress`, `headroom_retrieve`, and `headroom_stats`
through the same MCP server instance used by stdio.
- Keep `headroom mcp serve` defaulting to stdio for current Claude Code
and local MCP host configs.
- Update MCP docs for stdio and HTTP setup without implying the proxy
automatically owns `/mcp`.
- Keep the scope clean, rebased, and covered by focused tests.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_http.py
tests/test_cli/test_mcp.py -q`)
- [x] Linting passes (`uv run ruff check headroom/cli/mcp.py
headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py
tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py`)
- [x] Type checking passes (`uv run mypy headroom
--ignore-missing-imports`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py -q
20 passed in 0.53s

uv run ruff check headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py
All checks passed!

uv run ruff format headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py --check
5 files already formatted

uv run mypy headroom --ignore-missing-imports
Success: no issues found in 407 source files
```

## Real Behavior Proof

- Environment: Local Python environment with Headroom dev dependencies
and MCP extra installed.
- Exact command / steps: Start `headroom mcp serve --transport http
--host 127.0.0.1 --port <test-port> --path /mcp`, then perform an MCP
SDK Streamable HTTP initialize/list-tools exchange.
- Observed result: The HTTP transport initializes and lists the existing
Headroom MCP tools; `headroom mcp serve` without `--transport` still
selects stdio, and mixed-case `--transport HTTP` routes to the HTTP
transport.
- Not tested: live validation against external MCP hosts

## Review Readiness

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

## Additional Notes

`CHANGELOG.md` is not edited because this repository generates changelog
entries from conventional commits. Full-suite validation is left to CI.
2026-07-14 13:25:45 -04:00

51 lines
1.7 KiB
Python

"""Contract tests for the Headroom Streamable HTTP MCP transport."""
from __future__ import annotations
import httpx
import pytest
pytest.importorskip("mcp")
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamable_http_client
from headroom.ccr.mcp_http import (
create_streamable_http_app,
create_streamable_http_session_manager,
)
from headroom.ccr.mcp_server import create_ccr_mcp_server
pytestmark = pytest.mark.anyio
@pytest.fixture
def anyio_backend() -> str:
return "asyncio"
async def test_streamable_http_initialize_and_list_tools() -> None:
server = create_ccr_mcp_server()
session_manager = create_streamable_http_session_manager(server)
app = create_streamable_http_app(session_manager, path="/mcp")
transport = httpx.ASGITransport(app=app)
async with session_manager.run():
async with httpx.AsyncClient(
transport=transport, base_url="http://testserver"
) as http_client:
async with streamable_http_client(
"http://testserver/mcp",
http_client=http_client,
terminate_on_close=False,
) as (read_stream, write_stream, get_session_id):
async with ClientSession(read_stream, write_stream) as session:
initialize_result = await session.initialize()
list_tools_result = await session.list_tools()
tool_names = [tool.name for tool in list_tools_result.tools]
assert initialize_result.protocolVersion
assert get_session_id() is not None
assert "headroom_compress" in tool_names
assert "headroom_retrieve" in tool_names
assert "headroom_stats" in tool_names