Skip to content

Tool parameters model_dump and field_model_dump collide in the generated input schema #3710

Open

Description

@inanna-malick

Initial Checks

  • Reproduced with the newest 2.x release, 2.3.0.
  • Searched the issue tracker for an existing report.

Release line

2.x (current stable)

Description

Registering a function with two ordinary parameters, model_dump and field_model_dump, succeeds. However, Client.list_tools() advertises only field_model_dump; the required Python parameter model_dump disappears from the generated input schema. I expected both parameters to remain available to clients and schema-valid inputs to bind to their original Python parameter names.

The SDK protects reserved Pydantic names by internally prefixing model_dump as field_model_dump. A real parameter already named field_model_dump collides with that generated name. The test stops at the missing schema property, before its follow-up invocation assertion.

This is adjacent to #3099 and closed PR #3416, but no user-defined Field(alias=...) is involved: the collision is between an internal generated name and a literal second parameter. Merely mapping an existing model field back to its Python name does not recover a field already overwritten during model construction.

Test: standalone failing reproduction, branch repro/tool-reserved-name-collision (diff).

From that branch, after installing its development dependencies:

uv run --frozen pytest tests/server/mcpserver/tools/test_base.py::test_tool_reserved_parameter_does_not_collide_with_prefixed_field_name -q

Observed pytest failure:

AssertionError: assert {'field_model_dump', 'model_dump'} <= dict_keys(['field_model_dump'])

Example Code

import anyio

from mcp import Client
from mcp.server.mcpserver import MCPServer


async def main() -> None:
    server = MCPServer()

    @server.tool()
    def inspect(model_dump: int, field_model_dump: int) -> str:
        return f"seen:{model_dump}:{field_model_dump}"

    async with Client(server) as client:
        (tool,) = (await client.list_tools()).tools
        assert {"model_dump", "field_model_dump"} <= tool.input_schema["properties"].keys()


anyio.run(main)

Python & MCP Python SDK

Python 3.14.7 / Pydantic 2.12.5. Fails on latest stable mcp/mcp-types 2.3.0 (2118f14) and current main 754254c. Each targeted run: 1 intentional failure, 2 passing existing controls. The same test runs against each version’s source.

AI-assisted investigation and test reproduction, with a human in the loop.

Activity

  1. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Oct 14, 2026
  2. Naeemeh146 commented on Oct 17, 2026

    @Naeemeh146

    I'd like to take this one — I've reproduced it locally and traced the root cause.

    Diagnosis: In func_metadata (src/mcp/server/mcpserver/utilities/func_metadata.py), a parameter shadowing a BaseModel attribute gets renamed to field_<name> with its original name kept as the alias. But the rename isn't collision-checked: with params model_dump and field_model_dump, the first becomes field field_model_dump (alias model_dump), and the second also lands on field name field_model_dump — the dict assignment silently overwrites the first, so model_dump disappears from the generated schema entirely. I confirmed the schema only advertises field_model_dump.

    Approach: Make the rename collision-proof — after the field_ prefixing, loop the prefix until the field name is unique within the model's fields, and set the alias to the original parameter name so the schema keys and the kwargs from model_dump_one_level() keep using the natural names. This preserves the existing contract (verified against test_basemodel_reserved_names: schema and call kwargs still use original names) and is order-independent — whichever parameter is renamed, both stay visible.

    One question: is there any consumer that reads arg_model.model_fields keys directly (rather than via alias/schema), where the extra field_ prefix on a non-shadowing parameter could surprise it? I checked model_dump_one_level and the schema path, but wanted to confirm there's no other field-name reader before I write it up.

    If that approach sounds right, I'd appreciate being assigned.

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

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions