Repository navigation
FastMCP tool argument models silently ignore unknown/misspelled arguments (extra=ignore default) #3067
Description
Description
mcp.server.fastmcp.utilities.func_metadata.ArgModelBase does not set extra in its
model_config, so it inherits Pydantic v2's default, extra="ignore":
class ArgModelBase(BaseModel):
"""A model representing the arguments to a function."""
...
model_config = ConfigDict(
arbitrary_types_allowed=True,
)Every per-tool *Arguments model that FastMCP generates via create_model(..., __base__=ArgModelBase, ...)
inherits this. As a result, when a client calls a tool with an argument name that doesn't
exist on the function (a typo, or a hallucinated parameter name from an LLM), Pydantic
silently drops it instead of raising a validation error. The tool then runs with that
parameter at its default value, and returns a "successful" but semantically wrong result —
with no signal anywhere that anything was off.
Reproduction
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("repro")
@mcp.tool()
def read_doc(topic: str = "") -> str:
return f"topic={topic!r}"
# Simulate what happens when a client calls with the wrong argument name:
tool = mcp._tool_manager.get_tool("read_doc")
result = tool.fn_metadata.arg_model.model_validate({"path": "something"})
print(result.model_dump_one_level())
# {'topic': ''} <- no error, no signal that "path" was bogusExpected behavior
An unknown argument in a tools/call request should fail validation immediately with a
clear error (e.g. Pydantic's own "Extra inputs are not permitted"), the same way a missing
required argument already does. This is especially important for MCP given the primary
caller is frequently an LLM: a wrong parameter name is a common, plausible failure mode, and
a loud, immediate error is far more useful (and self-correcting for the model) than a
silently-wrong "successful" response.
Suggested fix
Set extra="forbid" on ArgModelBase.model_config:
model_config = ConfigDict(
arbitrary_types_allowed=True,
extra="forbid",
)This should be safe: the fields validated by ArgModelBase subclasses are exactly the
arguments dict of a tools/call request (i.e. exactly what's declared in the tool's
inputSchema). Protocol-level fields (_meta, progressToken, etc.) live on the
surrounding params object, not inside arguments, and Context is injected separately
via arguments_to_pass_directly — neither passes through ArgModelBase. The side effect is
that model_json_schema() will now emit "additionalProperties": false on the published
inputSchema, which seems like a feature, not a regression (it lets clients that validate
against the schema catch this class of error before the round-trip).
Scope checked
- Confirmed present in
mcp1.27.2 and 1.28.1 (latest on PyPI as of 2026-07). - Confirmed still present on
main(the in-progress v2, wherefastmcpis being renamed to
mcpserver):src/mcp/server/mcpserver/utilities/func_metadata.pyhas the same
ArgModelBasewithoutextraset. - I didn't find an existing issue about this specific behavior (searched for "extra fields",
"forbid", "unknown argument", "additionalProperties", "silently ignored"). - For context: the TypeScript SDK has an equivalent gap for a different underlying reason
(Zod's defaultstripbehavior on unknown object keys) — see
Additional parameters are not passed through to tools typescript-sdk#147
Happy to open a PR with the one-line change plus a regression test if that's useful.
Activity
elbachir-salik commentedon Jul 6, 2026
Hi! I went ahead and opened a PR for this: #3068 . Added extra="forbid" to ArgModelBase.model_config and a regression test for unknown arguments. Happy to adjust if needed!
maxisbey commentedon Oct 6, 2026
Thanks for the clear write-up, and sorry this sat for so long. I'm going to close it as not planned for now.
Rejecting unknown arguments by default would change how existing 2.x servers behave and add
additionalProperties: falseto every publishedinputSchema, so it isn't something we can switch on within 2.x. The spec doesn't ask for it either, and the other SDKs are split: TypeScript and Rust ignore unknown arguments by default like we do, while Go rejects them.If you need this today, the raw arguments are on
ctx.request_context.params["arguments"]inside a tool, so you can compare them to the names you expect and raiseToolError. A middleware can do the same check for every tool at once.What would change our minds is hearing from people who've actually been bitten by this. "I want this because..." helps us prioritise far more than knowing the behaviour exists, and there's a bit more on that in CONTRIBUTING.md. So if this has caused a real problem for you, please comment here with what happened and we'll take another look :)
AI Disclaimer
Generated by Claude Code