Skip to content

Pydantic model parameter not flattened in MCP tool schema — breaks SDK clients that send flat argument dicts #3637

Description

@alan-atkinson

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:

  1. 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
  2. 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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions