Skip to content

Fix FastMCP tool annotation tags under FastMCP 4.x (MCP SDK v2) - #856

Open
jrothrock wants to merge 2 commits into
masterfrom
fix-fastmcp-3x-annotations
Open

jrothrock wants to merge 2 commits into
masterfrom
fix-fastmcp-3x-annotations

Conversation

@jrothrock

Copy link
Copy Markdown
Contributor

Fix FastMCP tool annotation tags under FastMCP 4.x (MCP SDK v2)

Problem

CI (and any app on the latest FastMCP) stopped capturing the tool behavioral
annotation tags — read_only / idempotent / external / destructive.
tests/integration/test_fastmcp.py::test_tool_with_metadata fails:

assert tags.get("read_only") is True
E   assert None is True

Root cause

tox.ini pins fastmcp>=2.9.0 with no upper bound, so CI now resolves
FastMCP 4.x, which bundles MCP SDK v2. v2 renamed the ToolAnnotations
hint fields from camelCase to snake_case (readOnlyHintread_only_hint,
etc.). The old camelCase attributes still exist (hasattr is True) but
raise a deprecation error when accessed:

Accessing `ToolAnnotations.readOnlyHint` is deprecated; MCP SDK v2 renamed
this field to `read_only_hint`. Update your code to read `.read_only_hint`.

The instrumentation read annotations.readOnlyHint, which raised, got swallowed
by the try/except in on_call_tool, and silently dropped every annotation
tag
(not just returning wrong values — none at all).

Fix

Add a version-agnostic _annotation_hint() helper that reads snake_case
first, then falls back to camelCase
, handling both dict- and object/pydantic-
model-style annotations, and guards every access so a renamed/removed field
can never raise out of the instrumentation. This keeps FastMCP 2.x/3.x
(camelCase) working and fixes 4.x (snake_case).

Verification (live, all three majors)

  • fastmcp 2.14.7 — full fastmcp suite 16/16 pass
  • fastmcp 3.4.716/16 pass
  • fastmcp 4.0.3 — reproduced the failure, then 16/16 pass after the fix
  • End-to-end: a real FastMCP 4.x app with ScoutMiddleware calls an annotated
    tool → no exception, and read_only/idempotent/external are captured
    correctly.

Unit tests extended to cover both camelCase (2.x/3.x) and snake_case (4.x)
object annotations, with spec'd mocks so this can't silently drift again.

Note: this is independent of the ca_cert work; it fixes the pre-existing CI
failure on master caused by FastMCP 4.x.

jrothrock and others added 2 commits September 15, 2026 18:21
FastMCP 4.x bundles MCP SDK v2, which renamed the ToolAnnotations hint
fields from camelCase (readOnlyHint, ...) to snake_case (read_only_hint,
...). The old camelCase attributes still exist but raise a deprecation
error on access, so the instrumentation was reading .readOnlyHint,
raising, getting swallowed by the try/except in on_call_tool, and
silently dropping all read_only/idempotent/external/destructive tags.

Add a version-agnostic _annotation_hint() helper that reads snake_case
first, falls back to camelCase, and guards every access so a renamed or
removed field can never raise out of the instrumentation. Works for
dict- and object/pydantic-model-style annotations across FastMCP
2.x/3.x (camelCase) and 4.x (snake_case).

Extend the unit tests to cover both camelCase and snake_case object
annotations. Verified: full fastmcp test suite passes under fastmcp
2.14.7, 3.4.7, and 4.0.3, plus an end-to-end 4.x app with ScoutMiddleware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
flake8 6.0.0 bundles an old pyflakes that crashes on Python 3.12+ with
"module ast has no attribute Str" (ast.Str was removed), breaking the
pre-commit CI check on all recent Python. Bump flake8 to 7.3.0
(pyflakes 3.4.0), which is 3.12/3.13/3.14 safe.

flake8 7.x surfaces two pre-existing violations the old pin missed:
  * rq.py: remove two unused `global installed` declarations (F824;
    the name is only read in those scopes, never assigned).
  * tests/.../test_commands.py: rewrite `type(a) == type(b)` as
    `isinstance(a, type(b))` (E721).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant