# coding=utf-8 """ @Time :2026/6/24 @Author :tao.chen """ from contextlib import asynccontextmanager from dataclasses import dataclass from fastapi import FastAPI from common.factory import init_mcp_server from common.logging import logger from spark_executor import app as spark_executor_app # --- 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"), @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 # 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}") 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}") 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)