- 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>
- main.py: import files_app, append McpService entry to MCP_SERVICES.
Lifespan already iterates the list, so no edit to the lifespan function.
- tests/conftest.py: autouse fixture rebinds files_mcp.core.path_guard._ROOT
to a per-test tmp_path so the sandbox is fresh for every test.
- files_mcp/__init__.py: drop the try/except around the server import now
that server.py exists, restoring fail-fast on a missing/corrupt server
module in production. The _init_root() try/except stays, since pytest
collection can import the package before FILES_MCP_ROOT is set.
Verified via uv run pytest: 327 tests pass, no regressions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
The lifespan was hardcoded to mount just one MCP service
(spark_executor_app at /spark-executor-mcp). To add a second MCP
service, you'd have to edit the lifespan function body — error-prone
and not obvious where to add a new entry.
Refactor:
- New McpService dataclass (name, app, mount_path)
- MCP_SERVICES list at module top — adding a new service is one append
- Lifespan iterates MCP_SERVICES; the per-service mount + tool-list
logic is now a helper (_log_mcp_tools)
- Tool-list log now prefixes the service name:
'MCP tools registered (spark_executor, 14):'
instead of just '(14):', so multi-service startups are unambiguous
116/116 still pass. Live verified: gunicorn 2-worker startup logs
each service with its tool list and the source app version.
Future services just need a McpService entry — no lifespan edit. The
MCP_SERVICES list has a docstring example showing the one-liner.
main.py lifespan now enumerates every MCP tool with its description at
startup so operators can see at a glance what the server exposes.
The auto-generated '### Responses:' suffix that fastapi-mcp appends to
each tool description is truncated before logging to keep the startup
log scannable.
Docstrings added to all 12 route handlers in server.py so the MCP tool
descriptions are meaningful (without them, fastapi-mcp would emit empty
descriptions). Each route now carries a summary + description used by
the auto-generated OpenAPI schema.