Repository navigation
MCP: ToolAnnotations silently accepts misspelled hints #142
Description
ToolAnnotations accepts a misspelled hint without error and silently discards
it, producing a tool whose annotations are quietly missing.
Verified against mcp 2.1.1 / mcp-types 2.1.1:
from mcp.types import ToolAnnotations
ToolAnnotations(readOnlyHnt=True).model_dump(exclude_none=True, by_alias=True)
# {} <- no error, no warning, hint silently goneMechanism. Corrected 2026-08-28: the original report cited extra="allow" at
mcp/types/_types.py:1033, which does not match the version we use. There is no
explicit extra setting at all. The base class MCPModel
(mcp_types/_types.py:45) sets only:
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)so pydantic's default extra="ignore" applies and unknown keys are dropped
rather than stored. This is shared by all 189 protocol models in that module.
populate_by_name also means both readOnlyHint and read_only_hint are
accepted, which widens the surface a typo can land on.
The valid fields are title, read_only_hint, destructive_hint,
idempotent_hint, open_world_hint (mcp_types/_types.py:1365). A wrong name
produces a tool whose annotations are missing, with no warning at build time, no
test failure, and no runtime symptom. The visible consequence is client-side: a
read-only tool that is not marked read-only gets treated as mutating.
Our mitigation (in the shared-core refactor for #133): Operation carries
four explicitly typed Optional[bool] fields rather than an open dict, and the
adapter constructs ToolAnnotations by keyword, not **dict, so a wrong name is
a TypeError. A test asserts every hint maps to a declared field.
Upstream state, searched 2026-08-28. No existing issue covers this:
- FastMCP tool argument models silently ignore unknown/misspelled arguments (extra=ignore default) modelcontextprotocol/python-sdk#3067 is the same root
cause one layer up:ArgModelBasesilently ignores unknown tool-call
arguments. Open, labelledneeds decision. - fix: forbid extra arguments in FastMCP tool arg models (#3067) modelcontextprotocol/python-sdk#3068 fixes #3067 but
touches onlyfunc_metadata.py, soToolAnnotationsis unaffected by it. - Feature: enforce tool annotation hints (readOnlyHint/destructiveHint/idempotentHint) modelcontextprotocol/python-sdk#2937 asked the SDK to
enforce hints and was closed: annotations are hints, and the SDK cannot know
a tool's real behaviour. Our report has to be explicit that it is not asking
for enforcement, only that a non-field should not be silently discarded.
Remaining action: file the upstream report (draft in the comment below).
Keep our guard test regardless.
Activity
LukasGold commentedon Aug 28, 2026
Draft for the upstream report
For https://github.com/modelcontextprotocol/python-sdk. Not yet filed; hand-amend
before posting.
Framing note: it is deliberately anchored to
modelcontextprotocol/python-sdk#3067 (same root cause,
different model) and it explicitly disclaims the ask that got
modelcontextprotocol/python-sdk#2937 closed, so it does
not read as another request to enforce hints.
Title:
ToolAnnotations silently ignores misspelled hint names (extra=ignore default in MCPModel)Version:
mcp2.1.1,mcp-types2.1.1This is modelcontextprotocol/python-sdk#3067 one layer
down: the same
extra="ignore"default, but in the protocol types rather than inFastMCP's argument models. PR
modelcontextprotocol/python-sdk#3068 fixes it for
ArgModelBaseonly, so this path is unaffected by that change.The valid fields are
title,read_only_hint,destructive_hint,idempotent_hint,open_world_hint(mcp_types/_types.py:1365).MCPModel(
mcp_types/_types.py:45) sets only:so pydantic's default
extra="ignore"applies and unknown keys are discarded.This is shared by all 189 protocol models in that module.
populate_by_namealsomeans both
readOnlyHintandread_only_hintare accepted, which widens thesurface a typo can land on.
To be clear about scope: this is not a request to enforce or verify hints,
which was proposed and reasonably declined in
modelcontextprotocol/python-sdk#2937. Whether a tool
marked read-only really is read-only is the server author's problem. The narrower
complaint is that
readOnlyHntis not a field ofToolAnnotations, will never beone, and is nonetheless accepted in silence, so the author never finds out their
annotation was dropped.
Why the current default is defensible on one path and not the other: ignoring
unknown fields is correct when deserializing a peer's message, since that is
what makes protocol forward-compatibility work. It is not what you want when a
server author constructs an annotation in Python. Both share one config today,
so the lenient wire behaviour is applied to the authoring path as well.
Impact: a read-only tool that fails to be marked read-only is treated as
mutating by clients. No error at build time, no test failure, no runtime symptom;
the mistake surfaces only as a client-side behaviour difference.
Possible directions, least to most intrusive:
ToolAnnotations, given the hint set is fixed andspec-defined.
extra="forbid"viamodel_validate, or akeyword-only helper) for models a server builds rather than receives.
extra="forbid"to server-constructed models while leaving inboundparsing lenient, consistent with wherever #3067 lands.
Happy to open a PR once the direction on #3067 is settled.
Lower-friction alternative: #3067 is labelled
needs decision, so commentingthere to ask that the decision cover
MCPModeltoo would also work, instead of asecond issue.
LukasGold commentedon Aug 31, 2026
Mitigated in #133: Operation carries explicitly typed hint fields and the adapter builds ToolAnnotations by keyword, so a misspelled hint raises TypeError. A test asserts every hint maps to a declared field.
The upstream report to https://github.com/modelcontextprotocol/python-sdk is not filed yet. The draft stays in the comment above for whoever picks it up; nothing on our side depends on it.