# MCP Status Panel — Implementation Plan Spec: `docs/superpowers/specs/2026-08-03-mcp-status-panel-design.md` Backend first (tests alongside), then frontend, then verify. Each task is independently reviewable. ## Task 1 — `app/mcp/stats.py` - [x] `MCPToolStats` dataclass: `calls: int = 0`, `errors: int = 0`, `total_duration_ms: float = 0.0`, `last_called_at: datetime | None = None`; `avg_duration_ms` property returning `None` when `calls == 0`. - [x] `MCPStatsTracker` with `threading.Lock`, `record(tool, duration_ms, success)`, `snapshot()` returning a shallow copy. - [x] `record()` wraps its body in `try/except Exception` → `logger.warning`, never raises. - [x] `get_mcp_stats_tracker()` with `@lru_cache`. - [x] Module docstring + at least one `#` comment (AGENTS.md). ## Task 2 — `backend/tests/mcp/test_mcp_stats.py` - [x] 8 threads × 100 `record()` calls → `calls == 800` exactly. - [x] `avg_duration_ms` is `None` at zero calls, correct mean afterwards. - [x] `success=False` increments `errors` and `calls`. - [x] `record()` with a non-numeric duration logs and does not raise. ## Task 3 — instrument `app/mcp/server.py` - [x] Wrap `search_regulations` body: `time.perf_counter()` start, `try/except` records `success=False` and re-raises, `finally` not needed once both branches record. - [x] `async def get_mcp_status(public_url: str) -> dict` returning `{endpoint_url, auth_required, allowed_hosts, tools: [...]}` where each tool is `{name, description, calls, errors, avg_duration_ms, last_called_at}`. - [x] `allowed_hosts` parsed from `settings.mcp_allowed_hosts` with the same split/strip logic `_build_transport_security()` already uses. - [x] `last_called_at` serialized as ISO-8601 string or `None`. ## Task 4 — `mcp_public_url` setting - [x] `app/config/settings.py`: `mcp_public_url: str = ""` in the existing `# ── MCP ──` block. - [x] `.env.example`: documented under the existing MCP section, in Chinese, with the `http://6.86.80.9:8000/mcp/` example and a note that it is only needed when a proxy rewrites `Host`. ## Task 5 — `GET /status/mcp` - [x] Add route to `backend/app/api/routes/status.py`, taking `request: Request`. - [x] `public_url = settings.mcp_public_url or f"{str(request.base_url).rstrip('/')}/mcp/"`. - [x] Delegate to `get_mcp_status()`; no MCP internals in the route. ## Task 6 — `backend/tests/mcp/test_mcp_status.py` - [x] Tool advertised with zeroed stats before any call. - [x] Stats reflected after `record()`. - [x] `auth_required` follows a patched `settings.auth_enabled`. - [x] `public_url` passes through unmodified. ## Task 7 — frontend types + client - [x] `frontend/src/api/index.ts`: `MCPToolEntry`, `MCPStatusResponse`. - [x] `frontend/src/api/status.ts`: `getMCPStatus()` + re-export. ## Task 8 — MCP Server card - [x] Add `getMCPStatus()` to the existing `Promise.allSettled` batch in `StatusPage.tsx`, with its own `mcpLoading` state. - [x] Card below "AI Models": endpoint row + one row per tool. - [x] Copy-config button: builds the `mcpServers` JSON, embeds the `localStorage` token when `auth_required`, writes via `navigator.clipboard.writeText`, and reflects success/failure in its label for ~2s. - [x] `handleExport()` includes `mcp`. - [x] Reuse `card` / `card-header` / `service-row` / `StatusIcon`. No new CSS. ## Task 9 — i18n - [x] `locales/zh.ts` and `locales/en.ts`: `cardMcp`, `mcpEndpoint`, `mcpAuthRequired`, `mcpAuthDisabled`, `mcpAllowedHosts`, `mcpCopyConfig`, `mcpCopied`, `mcpCopyFailed`, `mcpCalls`, `mcpErrors`, `mcpAvgDuration`, `mcpNoTools`, `mcpUnavailable`. - [x] Both files must stay structurally identical (`en.ts` is typed against `zh.ts`). ## Task 10 — verify - [x] `python -m pytest backend/tests -q` — all pass. - [x] `npm --prefix frontend run lint`. - [x] `npm --prefix frontend run build`. - [x] Live check: start uvicorn, `GET /api/v1/status/mcp`, confirm the tool is listed and counters move after a real MCP `tools/call`.