feat: add MCP server module exposing search_regulations tool

- New backend/app/mcp/ module: MCPServer instance with a single
  search_regulations tool backed by the existing AgentConversationService.
- MCPAuthMiddleware reuses existing JWT auth (no new auth mechanism).
- Mounted at /mcp/ in api/main.py via Streamable HTTP transport; wired the
  MCP session manager into the existing lifespan() via AsyncExitStack
  (app.mount() does not propagate nested ASGI lifespans automatically).
- Fixed a doubled /mcp/mcp path by setting streamable_http_path to "/"
  (MCPServer.streamable_http_app() defaults to registering its own /mcp route).
- Verified end-to-end with the real mcp Python client: list_tools() returns
  search_regulations, auth correctly 401s without or with an invalid token.
- 7 new tests, 76 total (up from 69), all passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
wangwei
2026-07-29 13:00:52 +08:00
co-authored by Copilot
parent e78c8a989f
commit bd3dc38d1d
9 changed files with 302 additions and 20 deletions
@@ -1,6 +1,6 @@
# MCP Regulation Search Server — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** Expose the existing compliance knowledge base as an MCP tool (`search_regulations`) via a standalone `backend/app/mcp/` module, mounted into the existing FastAPI backend over Streamable HTTP, reusing existing JWT auth and the existing `AgentConversationService`.
@@ -11,6 +11,7 @@
## Global Constraints
- Design source of truth: `docs/superpowers/specs/2026-07-29-mcp-search-regulations-design.md`.
- **Correction discovered during implementation:** the `mcp` package's latest release is `2.0.0`, which renamed `FastMCP` to `MCPServer` (`mcp.server.MCPServer`) and its client helper `streamablehttp_client` to `streamable_http_client`. `mcp>=2.0.0` is pinned in `requirements.txt` (not `>=1.9.0` as originally estimated below) since the shipped code uses the `MCPServer` name. Also discovered: `MCPServer.streamable_http_app()` defaults to registering its route at `/mcp`, requiring `streamable_http_path="/"` to avoid a doubled `/mcp/mcp` when mounted at `/mcp`; the effective client URL is `/mcp/` (trailing slash, due to Starlette's `Mount` redirect behavior).
- All comments and docstrings in `backend/**/*.py` must be in English; every function/method needs a docstring; every file (including `__init__.py`) needs a module docstring + at least one meaningful `#` comment (`AGENTS.md`).
- No new business orchestration — `search_regulations` is a thin protocol adapter over the existing `AgentConversationService`, same tier as `app/api/routes/agent.py`.
- Python interpreter for this repo checkout: `C:\software\Python312\python.exe` (no `.venv` present in this checkout; this is the interpreter with all project dependencies already installed and is what the previous session's work was verified against).
@@ -32,7 +33,7 @@
- Consumes: `app.shared.bootstrap.get_agent_conversation_service()` (existing, returns `AgentConversationService`).
- Produces: `app.mcp.server.search_regulations(query: str, top_k: int = 5) -> dict` — a plain function (before the `@mcp.tool()` decorator is applied, it remains directly callable/testable; the decorator only adds MCP schema metadata, it does not change the function's Python call signature or return value).
- [ ] **Step 1: Write the failing test**
- [x] **Step 1: Write the failing test**
Create `backend/tests/mcp/__init__.py`:
@@ -129,7 +130,7 @@ Run it — confirm it fails on import (`app.mcp.server` does not exist yet):
C:\software\Python312\python.exe -m pytest backend/tests/mcp/test_search_regulations_tool.py -v
```
- [ ] **Step 2: Implement the tool**
- [x] **Step 2: Implement the tool**
Create `backend/app/mcp/__init__.py`:
@@ -183,7 +184,7 @@ def search_regulations(query: str, top_k: int = 5) -> dict:
}
```
- [ ] **Step 3: Run the test — confirm it passes**
- [x] **Step 3: Run the test — confirm it passes**
```powershell
C:\software\Python312\python.exe -m pytest backend/tests/mcp/test_search_regulations_tool.py -v
@@ -203,7 +204,7 @@ Expected: 3 passed. Note: this step imports `mcp.server.fastmcp`, which is not y
- Consumes: `app.config.settings.settings.auth_enabled` (existing), `app.shared.bootstrap.get_jwt_handler()` (existing, returns `JWTHandler`).
- Produces: `app.mcp.server.MCPAuthMiddleware` (ASGI middleware class), `app.mcp.server.build_mcp_asgi_app() -> ASGIApp` (returns `mcp.streamable_http_app()` with the middleware already attached). Task 3's `main.py` change consumes `build_mcp_asgi_app()` directly — it does not need to attach the middleware itself.
- [ ] **Step 1: Write the failing test**
- [x] **Step 1: Write the failing test**
Create `backend/tests/mcp/test_mcp_auth_middleware.py`:
@@ -285,7 +286,7 @@ Run it — confirm it fails (`MCPAuthMiddleware` does not exist yet):
C:\software\Python312\python.exe -m pytest backend/tests/mcp/test_mcp_auth_middleware.py -v
```
- [ ] **Step 2: Implement the middleware and ASGI app builder**
- [x] **Step 2: Implement the middleware and ASGI app builder**
Append to `backend/app/mcp/server.py`:
@@ -338,7 +339,7 @@ def build_mcp_asgi_app() -> ASGIApp:
return asgi_app
```
- [ ] **Step 3: Run the test — confirm it passes**
- [x] **Step 3: Run the test — confirm it passes**
```powershell
C:\software\Python312\python.exe -m pytest backend/tests/mcp/test_mcp_auth_middleware.py -v
@@ -358,7 +359,7 @@ Expected: 4 passed.
- Consumes: `app.mcp.server.build_mcp_asgi_app()` (from Task 2).
- Produces: a running `/mcp` Streamable HTTP endpoint on the existing FastAPI app/port — no new port, process, or deployment step.
- [ ] **Step 1: Add the dependency**
- [x] **Step 1: Add the dependency**
In `backend/requirements.txt`, add to the "Web framework" section (or a new small section — either is fine, keep it near `fastapi`/`uvicorn` since it is another transport-layer concern):
@@ -372,7 +373,7 @@ Install it (already done ad hoc in Task 1 to unblock those tests — this step j
C:\software\Python312\python.exe -m pip install -r backend/requirements.txt
```
- [ ] **Step 2: Mount the MCP app and fix the lifespan gap**
- [x] **Step 2: Mount the MCP app and fix the lifespan gap**
In `backend/app/api/main.py`, add the import and build the ASGI app at module scope (before `lifespan()` is defined, since `lifespan()` needs to reference it):
@@ -424,7 +425,7 @@ app.include_router(api_router, prefix="/api/v1")
app.mount("/mcp", mcp_app)
```
- [ ] **Step 3: Run the full backend test suite**
- [x] **Step 3: Run the full backend test suite**
```powershell
C:\software\Python312\python.exe -m pytest backend/tests -q
@@ -432,7 +433,7 @@ C:\software\Python312\python.exe -m pytest backend/tests -q
Expected: `76 passed` (69 existing + 3 + 4 new from Tasks 12). No regressions.
- [ ] **Step 4: Manual end-to-end verification (not automated)**
- [x] **Step 4: Manual end-to-end verification (not automated)**
Start the backend the normal way (`dev.bat start api --foreground` or the documented `uvicorn` command) and, from a separate shell, run:
@@ -25,6 +25,8 @@
## Architecture
> **Implementation note (post-design correction):** the `mcp` PyPI package released version `2.0.0` shortly before implementation and renamed the `FastMCP` class referenced below to `MCPServer` (import path `mcp.server.MCPServer` instead of `mcp.server.fastmcp.FastMCP`). The `.tool()` / `.streamable_http_app()` API surface used throughout this doc is otherwise unchanged. `backend/app/mcp/server.py` uses the actual shipped `MCPServer` name — treat every `FastMCP` mention below as that rename. Two other corrections discovered during implementation: (1) `MCPServer.streamable_http_app()` registers its own internal route at a fixed `/mcp` path, so mounting it at `/mcp` in `api/main.py` would double the path to `/mcp/mcp` — fixed by calling `mcp.streamable_http_app(streamable_http_path="/")`; (2) the effective external URL for clients is `/mcp/` (**with** a trailing slash) — Starlette's `Mount` 307-redirects the bare `/mcp` to `/mcp/`, which most HTTP clients follow automatically, but it is more robust to configure clients with the trailing slash directly.
### Module layout
```
@@ -146,7 +148,7 @@ class MCPAuthMiddleware:
{
"mcpServers": {
"ai-regulations": {
"url": "http://6.86.80.9:8000/mcp",
"url": "http://6.86.80.9:8000/mcp/",
"headers": { "Authorization": "Bearer <jwt>" }
}
}