You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Examples demonstrate ToolAnnotations only on a tool where they change nothing #3737
calc is pure arithmetic. The annotation is correct, and it is also the case
where it cannot affect anything a client decides.
Meanwhile examples/mcpserver/text_me.py posts to api.surgemsg.com and sends
a real SMS to a configured number. It declares nothing, so a client sees annotations: None and has no signal that approving this call has an effect
in the world and costs money. examples/mcpserver/screenshot.py and the
other side-effecting examples are the same.
So the examples show how to annotate a tool that cannot do anything, and
nothing shows the case where the annotation is load bearing.
The part that argues against the obvious fix
I started out assuming "add annotations to text_me.py" and then checked the
values against the spec defaults:
Field
Correct for textme
Spec default
read_only_hint
False
False
destructive_hint
False, it is additive
True
idempotent_hint
False, each call sends another message
False
open_world_hint
True
True
Only destructive_hint differs, and declaring it False would relax the
default rather than tighten it. For a tool that sends real messages, leaving
it undeclared arguably leaves a client more cautious, not less. So the
mechanical fix is not obviously an improvement, which is why I am reporting
the gap rather than proposing a patch.
If there is a direction you would prefer, I am happy to write it:
Annotate a side-effecting example where the values are genuinely
informative rather than default.
Leave the examples alone and say something in the docs about what the
hints are and are not for.
Treat this as not worth the churn.
How I found it
I wrote a static scanner that compares an MCP tool's declared annotations
against what its handler does, and ran it over 289 tools across this SDK,
FastMCP and the reference servers. 25 of the 289 declare any annotations at
all. textme surfaced as a handler posting to an external API while
declaring nothing.
No PR, since CONTRIBUTING asks for the issue first.
AI assistance disclosure: I used Claude Code while investigating this and
writing this issue. I read the scanner output and the example source myself
and can speak to the reasoning.
Confirmed on main (2af9e30): the only ToolAnnotations in examples/ are the two calc registrations in examples/stories/tools/. examples/mcpserver/text_me.py registers @mcp.tool(name="textme", ...) with no annotations, even though it sends a real SMS with httpx2.Client().post(...). MCPServer.tool() already takes annotations=, so annotating it needs no API change.
Approach I'd take: annotate text_me.py with ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=False, open_world_hint=True), plus a one-line comment on why (it has an external, non-repeatable side effect), so the examples show both ends: a pure tool (calc) and one with a real-world effect. screenshot.py could get read_only_hint=True. Would that be the direction you want?
Thanks for confirming it. screenshot.py is a good catch, and I would pair open_world_hint=False with the read_only_hint=True you suggest: it reads
the local display through pyautogui.screenshot() and touches nothing
external, so both fields carry information and read_only_hint=True is
non-default there.
On text_me.py I am less sure, and it is why I did not open a PR. Your four
values against the spec defaults:
Field
Your value
Spec default
read_only_hint
False
False
destructive_hint
False
True
idempotent_hint
False
False
open_world_hint
True
True
Three match the defaults, so they tell a spec-compliant client nothing it does
not already assume. The fourth moves destructive_hint from true to false, which means annotating textme this way leaves a client less
cautious about a tool that sends a real SMS than leaving it undeclared.
I do not think your values are wrong. An SMS is additive rather than
destructive, so destructive_hint=False is the honest reading. The problem is
that the vocabulary has no field for "irreversible effect outside the system",
and destructiveHint is about destroying existing state, not about
reversibility. So a faithful annotation on textme ends up communicating less
caution than silence does.
Which suggests the split is:
Annotate the examples where a value is non-default and informative. screenshot.py qualifies, calc already does.
For textme, the one-line comment you propose may be worth more than the
annotations themselves, since the comment can say the thing the hints
cannot.
No strong feelings on who writes it. And I am happy to be wrong on the destructive_hint reading if a maintainer sees it differently, since that is
the part the whole thing turns on.
Good point: on textme the annotations would signal less caution than leaving them off, so agreed: a comment there, and read_only_hint=True, open_world_hint=False on screenshot.py. Happy for whoever a maintainer assigns to write it.
Activity
vasikmansuri commentedon Oct 22, 2026
Confirmed on
main(2af9e30): the onlyToolAnnotationsinexamples/are the twocalcregistrations inexamples/stories/tools/.examples/mcpserver/text_me.pyregisters@mcp.tool(name="textme", ...)with no annotations, even though it sends a real SMS withhttpx2.Client().post(...).MCPServer.tool()already takesannotations=, so annotating it needs no API change.Approach I'd take: annotate
text_me.pywithToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=False, open_world_hint=True), plus a one-line comment on why (it has an external, non-repeatable side effect), so the examples show both ends: a pure tool (calc) and one with a real-world effect.screenshot.pycould getread_only_hint=True. Would that be the direction you want?gl1tchdust commentedon Oct 22, 2026
Thanks for confirming it.
screenshot.pyis a good catch, and I would pairopen_world_hint=Falsewith theread_only_hint=Trueyou suggest: it readsthe local display through
pyautogui.screenshot()and touches nothingexternal, so both fields carry information and
read_only_hint=Trueisnon-default there.
On
text_me.pyI am less sure, and it is why I did not open a PR. Your fourvalues against the spec defaults:
read_only_hintFalseFalsedestructive_hintFalseTrueidempotent_hintFalseFalseopen_world_hintTrueTrueThree match the defaults, so they tell a spec-compliant client nothing it does
not already assume. The fourth moves
destructive_hintfromtruetofalse, which means annotatingtextmethis way leaves a client lesscautious about a tool that sends a real SMS than leaving it undeclared.
I do not think your values are wrong. An SMS is additive rather than
destructive, so
destructive_hint=Falseis the honest reading. The problem isthat the vocabulary has no field for "irreversible effect outside the system",
and
destructiveHintis about destroying existing state, not aboutreversibility. So a faithful annotation on
textmeends up communicating lesscaution than silence does.
Which suggests the split is:
screenshot.pyqualifies,calcalready does.textme, the one-line comment you propose may be worth more than theannotations themselves, since the comment can say the thing the hints
cannot.
No strong feelings on who writes it. And I am happy to be wrong on the
destructive_hintreading if a maintainer sees it differently, since that isthe part the whole thing turns on.
vasikmansuri commentedon Oct 22, 2026
Good point: on
textmethe annotations would signal less caution than leaving them off, so agreed: a comment there, andread_only_hint=True, open_world_hint=Falseonscreenshot.py. Happy for whoever a maintainer assigns to write it.