Initial Checks
Release line
2.x (current stable)
Description
Description
When a FastMCP tool is defined with a Pydantic model as its parameter, FastMCP 4.x generates a JSON schema that wraps the model's fields under a "request" key rather than flattening them to the top level. This breaks MCP clients that send the fields flat — including the official adcp Python SDK, which serialises request objects with model.model_dump() and sends the result as a flat dict.
What happens:
Tool defined as:
async def get_adcp_capabilities(
request: GetAdcpCapabilitiesRequest = GetAdcpCapabilitiesRequest(),
ctx: Context | None = None,
) -> ToolResult:
...
FastMCP generates this inputSchema:
{
"type": "object",
"properties": {
"request": {
"type": "object",
"additionalProperties": true,
"properties": {
"adcp_version": { ... },
"protocols": { ... }
}
}
},
"additionalProperties": false
}
Client sends:
{ "adcp_version": "3.2", "protocols": null }
FastMCP validates this against the schema and raises:
1 validation error for call[get_adcp_capabilities]
adcp_version
Unexpected keyword argument [type=unexpected_keyword_argument, ...]
What should happen:
Either:
- FastMCP should flatten the fields of a Pydantic model parameter into the tool's top-level inputSchema (matching how bare field: type = default kwargs work today), or
- FastMCP should accept both the flat form {"adcp_version": "3.2"} and the nested form {"request": {"adcp_version": "3.2"}} when the parameter is a Pydantic model.
The spec (GetAdcpCapabilitiesRequest) already defines the full wire shape including extra: 'allow' for forward compatibility. Using the model type as the parameter should be the idiomatic approach — it avoids re-declaring every field individually.
Workaround: Redeclare every field from the Pydantic model as individual kwargs on the tool function (defeats the purpose of having the model type).
Example Code
from pydantic import BaseModel
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("example")
class MyRequest(BaseModel):
model_config = {"extra": "allow"}
adcp_version: str | None = None
protocols: list[str] | None = None
@mcp.tool()
async def my_tool(request: MyRequest = MyRequest()) -> str:
return f"got: {request.adcp_version}"
# Client sends: {"adcp_version": "3.2"}
# FastMCP raises: Unexpected keyword argument 'adcp_version'
# Client must send: {"request": {"adcp_version": "3.2"}}
# — but the SDK and spec generate flat dicts from model_dump()
Python & MCP Python SDK
Python: 3.12
FastMCP: 4.0.3
mcp Python SDK: 2.x (latest)
Initial Checks
Release line
2.x (current stable)
Description
Description
When a FastMCP tool is defined with a Pydantic model as its parameter, FastMCP 4.x generates a JSON schema that wraps the model's fields under a "request" key rather than flattening them to the top level. This breaks MCP clients that send the fields flat — including the official adcp Python SDK, which serialises request objects with model.model_dump() and sends the result as a flat dict.
What happens:
Tool defined as:
FastMCP generates this inputSchema:
Client sends:
{ "adcp_version": "3.2", "protocols": null }FastMCP validates this against the schema and raises:
1 validation error for call[get_adcp_capabilities]
adcp_version
Unexpected keyword argument [type=unexpected_keyword_argument, ...]
What should happen:
Either:
The spec (GetAdcpCapabilitiesRequest) already defines the full wire shape including extra: 'allow' for forward compatibility. Using the model type as the parameter should be the idiomatic approach — it avoids re-declaring every field individually.
Workaround: Redeclare every field from the Pydantic model as individual kwargs on the tool function (defeats the purpose of having the model type).
Example Code
Python & MCP Python SDK