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:
@@ -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 1–2). 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>" }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user