- 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>
78 lines
2.5 KiB
Python
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)
|