Context
Today src/adcp/server/mcp_tools.py maintains a list of 57 tool dicts + a runtime _generate_pydantic_schemas() pass that maps each tool name to its <ToolName>Request Pydantic model and calls .model_json_schema() at import. This works, but it reinvents what FastMCP already does when you register a tool function with Pydantic-typed parameters.
If FastMCP's native registration can express everything we need — descriptions, annotations (RO / MUT / DEST / IDEMP), nested Pydantic models, and the ADCPHandler delegation pattern — we could delete the generation layer entirely and the drift surface goes to zero.
What to investigate
- Can FastMCP's
@mcp.tool decorator accept a pydantic-typed params argument directly? What does the resulting schema look like vs. our current output?
- How do we preserve the hand-written tool descriptions and annotations (
readOnlyHint, destructiveHint, etc.) through the native path?
- How does this interact with
ADCPHandler being a subclass hook — tools need to route to handler.<tool_name>(params, context), which is what _register_tool in src/adcp/server/serve.py does today.
- Compare startup cost: current design runs
.model_json_schema() for all 57 tools at import (~100ms). Native registration may defer or batch this.
Acceptance
- A short writeup (
docs/design/mcp-native-registration.md or issue comment) that picks: stay with generation layer, or migrate. If migrate, follow-up PR deletes ADCP_TOOL_DEFINITIONS + _generate_pydantic_schemas() and uses FastMCP native registration.
Priority
4.1+. Architectural cleanup, not a user-visible bug.
Surfaced from: #205
Context
Today
src/adcp/server/mcp_tools.pymaintains a list of 57 tool dicts + a runtime_generate_pydantic_schemas()pass that maps each tool name to its<ToolName>RequestPydantic model and calls.model_json_schema()at import. This works, but it reinvents what FastMCP already does when you register a tool function with Pydantic-typed parameters.If FastMCP's native registration can express everything we need — descriptions, annotations (RO / MUT / DEST / IDEMP), nested Pydantic models, and the
ADCPHandlerdelegation pattern — we could delete the generation layer entirely and the drift surface goes to zero.What to investigate
@mcp.tooldecorator accept a pydantic-typedparamsargument directly? What does the resulting schema look like vs. our current output?readOnlyHint,destructiveHint, etc.) through the native path?ADCPHandlerbeing a subclass hook — tools need to route tohandler.<tool_name>(params, context), which is what_register_toolinsrc/adcp/server/serve.pydoes today..model_json_schema()for all 57 tools at import (~100ms). Native registration may defer or batch this.Acceptance
docs/design/mcp-native-registration.mdor issue comment) that picks: stay with generation layer, or migrate. If migrate, follow-up PR deletesADCP_TOOL_DEFINITIONS+_generate_pydantic_schemas()and uses FastMCP native registration.Priority
4.1+. Architectural cleanup, not a user-visible bug.
Surfaced from: #205