Files
mcp-server/main.py
T
Claude a1710ec4db refactor(main): count MCP tools at INFO, per-tool details at DEBUG
Production log noise: every container start was emitting 14 INFO lines
listing every tool name and full description, which is dev-time
debugging info that doesn't belong in INFO.

Now:
  - INFO: one line per service with the count
    'MCP service \042spark_executor\042: 14 tool(s) registered'
  - DEBUG: per-tool name + description, prefixed with the service name
    so multi-service deploys are grep-friendly
    '  spark_executor._prepare_submit_job_prepare_submit_job_post  -  ...'

Operators can flip SPARK_EXECUTOR_LOG_LEVEL=INFO for production (no
per-tool spam) or DEBUG for dev (full inventory).

146/146 still pass. Live verified: at INFO level only the count line
shows; at DEBUG level both the count and per-tool lines show.
2026-06-25 13:05:55 +08:00

92 lines
2.9 KiB
Python

# 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)