feat(mcp): port the server to MCP Python SDK v2 (FastMCP → MCPServer) (#2196)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 31s

- server.py: `mcp.server.mcpserver.MCPServer`; StrictArgsFastMCP becomes
  StrictArgsMCPServer, whose call_tool takes and forwards v2's `context`
  and raises ToolError (the SDK logs anything else as an unexpected crash;
  the message reaches the caller either way).
- stateless_http and transport_security moved from the constructor to
  `streamable_http_app(...)` in mount_mcp, with their reasons.
- Per-request user identity is unchanged: the contextvar set around the
  ASGI call reaches the handler on both v2 paths (legacy stateless spawns
  from the request task; the 2026-07-28 modern path opens its task group
  inside the request).
- pyproject: mcp[cli]>=2.2, no ceiling (installs are --locked). uv.lock
  regenerated with --upgrade-package mcp in the ci-python image: only mcp
  and its own dependencies moved (106 → 110 packages).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 07:21:27 -04:00
co-authored by Claude Opus 5.5
parent 4502f0a1ae
commit c26b7f248e
9 changed files with 153 additions and 95 deletions
+6 -6
View File
@@ -3,7 +3,7 @@
These exercise the ASGI dispatch + auth middleware by driving the app's ASGI
callable directly. We avoid Quart's test_client() because it expects Quart's
request pipeline to set `app._preserved_context` as a side effect, but our
ASGI middleware forwards /mcp requests to FastMCP without touching the Quart
ASGI middleware forwards /mcp requests to MCPServer without touching the Quart
pipeline (correct production behavior), which causes test_client to fail on
teardown.
@@ -85,12 +85,12 @@ async def test_mcp_endpoint_invalid_token_returns_401():
@pytest.mark.asyncio
async def test_mcp_endpoint_valid_token_passes_auth():
"""With a valid Bearer, the request must successfully reach FastMCP's
"""With a valid Bearer, the request must successfully reach MCPServer's
initialize handler. Asserting `!= 401` is too weak: it lets a 404 from
a path-mismatch (the original bug) through. FastMCP responds 200 to a
a path-mismatch (the original bug) through. MCPServer responds 200 to a
well-formed initialize handshake.
FastMCP's session manager normally starts via Quart's @before_serving
MCPServer's session manager normally starts via Quart's @before_serving
hook in production. This raw-ASGI test doesn't go through Quart's
serving lifecycle, so we manually enter the session manager."""
fake_key = MagicMock()
@@ -113,7 +113,7 @@ async def test_mcp_endpoint_valid_token_passes_auth():
status, _ = await _send_request(
app, "POST", "/mcp",
headers={
# FastMCP's transport_security module enforces a Host
# MCPServer's transport_security module enforces a Host
# header (DNS-rebinding protection); without it the
# request gets a 421 Misdirected Request.
"Host": "testserver",
@@ -123,7 +123,7 @@ async def test_mcp_endpoint_valid_token_passes_auth():
},
body=initialize_body,
)
assert status == 200, f"expected 200 from FastMCP initialize, got {status}"
assert status == 200, f"expected 200 from MCPServer initialize, got {status}"
# Note: there's no explicit "non-/mcp paths bypass the middleware" test here