diff --git a/main.py b/main.py index 323f4e6..b88fc3f 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ @Author :tao.chen """ from contextlib import asynccontextmanager +from dataclasses import dataclass from fastapi import FastAPI @@ -12,30 +13,63 @@ from common.logging import logger from spark_executor import app as spark_executor_app -@asynccontextmanager -async def lifespan(app: "FastAPI"): - logger.info(f"Startup {app.title}") - spark_executor_mcp = init_mcp_server(spark_executor_app) - spark_executor_mcp.mount_http(app, mount_path="/spark-executor-mcp") - logger.info( - f"MCP server mounted at /spark-executor-mcp " - f"(source: {spark_executor_app.title} v{spark_executor_app.version})" - ) +# --- MCP service registry --- +# +# Each McpService entry becomes one mount under the root app. The lifespan +# iterates this list at startup, so adding a new MCP service is a one-line +# append here — no edits to the lifespan function needed. +# +# Example (future): +# McpService(name="metrics", app=metrics_app, mount_path="/metrics-mcp"), +# McpService(name="catalog", app=catalog_app, mount_path="/catalog-mcp"), - # Enumerate every MCP tool + its description so operators can see at a - # glance what the server exposes (useful for debugging / change review). - tools = spark_executor_mcp.tools - logger.info(f"MCP tools registered ({len(tools)}):") + +@dataclass(frozen=True) +class McpService: + """One MCP-exposed FastAPI sub-app to be mounted under the root.""" + + name: str # short identifier used in startup logs (e.g. "spark_executor") + app: FastAPI # the sub-app whose routes will be exposed as MCP tools + mount_path: str # HTTP path under the root app (e.g. "/spark-executor-mcp") + + +MCP_SERVICES: list[McpService] = [ + McpService( + name="spark_executor", + app=spark_executor_app, + mount_path="/spark-executor-mcp", + ), +] + + +def _log_mcp_tools(mcp_server, service: McpService) -> 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 + logger.info(f"MCP tools registered ({service.name}, {len(tools)}):") for i, tool in enumerate(tools, 1): desc = (tool.description or "").strip() - # fastapi-mcp appends an auto-generated "### Responses: ..." block to - # every tool description; drop it so the startup log stays scannable. if "### Responses" in desc: desc = desc.split("### Responses", 1)[0].rstrip() - # Collapse internal newlines / extra spaces into one line. desc = " ".join(desc.split()) logger.info(f" {i:>2}. {tool.name} - {desc}") + +@asynccontextmanager +async def lifespan(app: "FastAPI"): + logger.info(f"Startup {app.title}") + for svc in MCP_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}") @@ -43,6 +77,11 @@ async def lifespan(app: "FastAPI"): app = FastAPI(title="Main App", lifespan=lifespan) +@app.get("/health") +def health_check(): + return {"status": "ok"} + + if __name__ == "__main__": import uvicorn