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>
This commit is contained in:
Claude
2026-06-30 11:13:23 +00:00
committed by tao.chen
co-authored by Claude Fable 5
parent 47e5a275ef
commit 7adc935632
3 changed files with 47 additions and 35 deletions
+15
View File
@@ -0,0 +1,15 @@
# coding=utf-8
"""
@Time :2026/6/30
@Author :tao.chen
Pluggable manifest for the files MCP service.
"""
from common.mcp_service import McpService
from files_mcp import app
SERVICE: McpService = McpService(
name="files",
app=app,
mount_path="/files-mcp",
)
+15 -35
View File
@@ -1,54 +1,33 @@
# coding=utf-8 # coding=utf-8
""" """
@Time :2026/6/24 @Time :2026/6/30
@Author :tao.chen @Author :tao.chen
""" """
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from dataclasses import dataclass
from fastapi import FastAPI from fastapi import FastAPI
from common.factory import init_mcp_server from common.factory import init_mcp_server
from common.logging import logger from common.logging import logger
from files_mcp import app as files_app from common.mcp_service import discover_and_filter
from spark_executor import app as spark_executor_app
# --- MCP service registry --- # --- MCP service registry ---
# #
# Each McpService entry becomes one mount under the root app. The lifespan # Each service package declares itself via a `service.py` that exports a
# iterates this list at startup, so adding a new MCP service is a one-line # `SERVICE: McpService` instance. The list of known service import paths
# append here — no edits to the lifespan function needed. # 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.
# #
# Example (future): # To add a new MCP service:
# McpService(name="metrics", app=metrics_app, mount_path="/metrics-mcp"), # 1. Build a package with a FastAPI app.
# McpService(name="catalog", app=catalog_app, mount_path="/catalog-mcp"), # 2. Add `<package>/service.py` exporting `SERVICE: McpService`.
# 3. Append `"<package>.service"` to `BUILTIN_SERVICES` in
# `common/mcp_service.py`.
@dataclass(frozen=True) def _log_mcp_tools(mcp_server, service) -> None:
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",
),
McpService(
name="files",
app=files_app,
mount_path="/files-mcp",
),
]
def _log_mcp_tools(mcp_server, service: McpService) -> None:
"""Log every tool the MCP server exposes, with its description. """Log every tool the MCP server exposes, with its description.
fastapi-mcp appends an auto-generated "### Responses: ..." block to fastapi-mcp appends an auto-generated "### Responses: ..." block to
@@ -71,7 +50,8 @@ def _log_mcp_tools(mcp_server, service: McpService) -> None:
@asynccontextmanager @asynccontextmanager
async def lifespan(app: "FastAPI"): async def lifespan(app: "FastAPI"):
logger.info(f"Startup {app.title}") logger.info(f"Startup {app.title}")
for svc in MCP_SERVICES: services = discover_and_filter()
for svc in services:
logger.info( logger.info(
f"Mounting MCP service {svc.name!r} at {svc.mount_path} " f"Mounting MCP service {svc.name!r} at {svc.mount_path} "
f"(source: {svc.app.title} v{svc.app.version})" f"(source: {svc.app.title} v{svc.app.version})"
+17
View File
@@ -0,0 +1,17 @@
# coding=utf-8
"""
@Time :2026/6/30
@Author :tao.chen
Pluggable manifest for the spark_executor MCP service. The name and
mount_path here are matched against the MCP_SERVICES env whitelist and
the BUILTIN_SERVICES list in common.mcp_service.
"""
from common.mcp_service import McpService
from spark_executor import app
SERVICE: McpService = McpService(
name="spark_executor",
app=app,
mount_path="/spark-executor-mcp",
)