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>
This commit is contained in:
@@ -322,17 +322,20 @@ backend/app/
|
||||
- 以 Model Context Protocol 对外暴露平台已有能力
|
||||
- MCP tool 注册与入参 schema 绑定
|
||||
- MCP 专用鉴权(复用现有 JWT)与 Streamable HTTP 子 ASGI 应用装配
|
||||
- MCP 传输自身的可观测性:进程内 per-tool 调用计数(`stats.py`),以及供 System Status 页面读取的状态汇总 `get_mcp_status()`
|
||||
|
||||
非职责:
|
||||
|
||||
- 不实现任何新的检索、问答或业务编排逻辑
|
||||
- 不直接访问 Milvus、MinIO、LLM SDK
|
||||
- 不统计 token 消耗 —— MCP 调用经由 `AgentConversationService.ask()`,已由 `shared/model_usage_tracker.py` 记账
|
||||
|
||||
说明:
|
||||
|
||||
- `mcp` 与 `api` 是并列的两个 transport 适配层:`api` 面向 HTTP REST 客户端,`mcp` 面向 MCP 客户端(Claude Desktop、IDE 等)。二者共用同一套 application service。
|
||||
- 它独立成顶层模块而不是放进 `api/routes/`,因为 MCP 使用装饰器式 tool 注册和自带的子 ASGI 应用,与 `APIRouter` 是不同的传输机制。
|
||||
- 当前实现见 `backend/app/mcp/server.py`,只暴露 `search_regulations` 一个 tool,内部直接调用 `get_agent_conversation_service()`。
|
||||
- `api/routes/status.py` 的 `GET /status/mcp` 是薄适配层:它只负责解析对外可达的 URL(`settings.mcp_public_url` 或从请求推导),其余全部交给 `mcp.server.get_mcp_status()`,路由不得直接读取 MCP 内部结构。
|
||||
|
||||
## 5. Module Responsibilities
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user