Skip to content

Examples demonstrate ToolAnnotations only on a tool where they change nothing #3737

Open

Description

@gl1tchdust

What I noticed

Across examples/, 2 tool registrations out of 119 declare ToolAnnotations,
and both are the same calc tool:

  • examples/stories/tools/server.py
  • examples/stories/tools/server_lowlevel.py
annotations=ToolAnnotations(read_only_hint=True, idempotent_hint=True),

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:

  1. Annotate a side-effecting example where the values are genuinely
    informative rather than default.
  2. Leave the examples alone and say something in the docs about what the
    hints are and are not for.
  3. 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.

Activity

  1. vasikmansuri commented on Oct 22, 2026

    @vasikmansuri

    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?

  2. gl1tchdust commented on Oct 22, 2026

    @gl1tchdust
    Author

    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.

  3. vasikmansuri commented on Oct 22, 2026

    @vasikmansuri

    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.

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

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions