Files
7adc935632 refactor(mcp): use discover_and_filter in main.py; add per-service manifests
- main.py: drops the hard-coded McpService class and MCP_SERVICES list.
  Lifespan now calls discover_and_filter() which does import + env
  filtering. Unset MCP_SERVICES reproduces today's behavior exactly.
- spark_executor/service.py: new file exporting SERVICE = McpService(
  name='spark_executor', app=spark_executor.app, mount_path='/spark-executor-mcp').
- files_mcp/service.py: new file exporting SERVICE = McpService(
  name='files', app=files_mcp.app, mount_path='/files-mcp').

The McpService dataclass itself moved to common/mcp_service.py in the
prior commit. 327 existing tests pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-30 11:13:23 +00:00

78 lines
2.5 KiB
Python

# coding=utf-8
"""
@Time :2026/6/30
@Author :tao.chen
"""
from contextlib import asynccontextmanager
from fastapi import FastAPI
from common.factory import init_mcp_server
from common.logging import logger
from common.mcp_service import discover_and_filter
# --- MCP service registry ---
#
# Each service package declares itself via a `service.py` that exports a
# `SERVICE: McpService` instance. The list of known service import paths
# lives in `common.mcp_service.BUILTIN_SERVICES`. The lifespan below calls
# `discover_and_filter()` to import each, validate them, apply the
# MCP_SERVICES env whitelist, and mount the survivors.
#
# To add a new MCP service:
# 1. Build a package with a FastAPI app.
# 2. Add `<package>/service.py` exporting `SERVICE: McpService`.
# 3. Append `"<package>.service"` to `BUILTIN_SERVICES` in
# `common/mcp_service.py`.
def _log_mcp_tools(mcp_server, service) -> None:
"""Log every tool the MCP server exposes, with its description.
fastapi-mcp appends an auto-generated "### Responses: ..." block to
every tool description; we strip that here so the startup log stays
scannable. Each tool is logged on one line: ` N. tool_name - desc`.
"""
tools = mcp_server.tools
# Single INFO line per service summarizing how many tools it exposes.
# Per-tool name + description is DEBUG-level so production logs (INFO)
# stay scannable but operators can flip to DEBUG to see the full list.
logger.info(f"MCP service {service.name!r}: {len(tools)} tool(s) registered")
for i, tool in enumerate(tools, 1):
desc = (tool.description or "").strip()
if "### Responses" in desc:
desc = desc.split("### Responses", 1)[0].rstrip()
desc = " ".join(desc.split())
logger.debug(f" {service.name}.{tool.name} - {desc}")
@asynccontextmanager
async def lifespan(app: "FastAPI"):
logger.info(f"Startup {app.title}")
services = discover_and_filter()
for svc in services:
logger.info(
f"Mounting MCP service {svc.name!r} at {svc.mount_path} "
f"(source: {svc.app.title} v{svc.app.version})"
)
mcp = init_mcp_server(svc.app)
mcp.mount_http(app, mount_path=svc.mount_path)
_log_mcp_tools(mcp, svc)
yield
logger.info(f"Shutdown {app.title}")
app = FastAPI(title="Main App", lifespan=lifespan)
@app.get("/health")
def health_check():
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)