Files
AIRegulation-DocAnalysis/docs/superpowers/plans/2026-08-03-mcp-status-panel.md
T
wangweiandCopilot 31bbf80aeb feat: surface MCP server status in System Status page
Add per-tool in-memory call counters to the MCP module and a
GET /api/v1/status/mcp endpoint that joins them with the live tool
registry and endpoint config, then render it as a new card on the
System Status page with a one-click client-config copy button.

- app/mcp/stats.py: lock-guarded MCPStatsTracker (the mcp SDK runs sync
  tool bodies via anyio.to_thread.run_sync, so this is genuinely
  multi-threaded, unlike the async REST routes)
- app/mcp/server.py: instrument search_regulations, add get_mcp_status()
- app/config/settings.py: optional MCP_PUBLIC_URL override, required
  because the Vite proxy and reverse proxies rewrite the Host header
- StatusPage.tsx: MCP Server card, joins the existing parallel fetch

Counters are process-local by design; token usage is already persisted
by ModelUsageTracker since MCP calls route through ask().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-03 11:37:22 +08:00

90 lines
4.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.