Skip to content

Consider MCP ToolAnnotations on the 4 public tools (proposal + draft PR ready) #86

Description

@louzt

Background

The MCP 2025-06-18 spec defines optional ToolAnnotations on every
public tool (title, readOnlyHint, destructiveHint,
idempotentHint, openWorldHint). Leaving a hint unset signals
"unknown" to MCP clients, which forces them to be conservative —
typically prompting the user before executing the tool.

mcp-server-appwrite exposes 4 public tools via
Operator.get_public_tools() (registered through
server.handle_list_tools):

Tool Behavior
appwrite_get_context Reads account/org/project metadata
appwrite_search_tools Searches the in-memory tool catalog (local)
appwrite_call_tool Dispatches to Appwrite SDK (may mutate, gated by confirm_write=true)
appwrite_search_docs Searches the committed docs index (local, requires OPENAI_API_KEY)

Today none of them declare annotations, so MCP clients treat all four
as unknown and prompt the user even for read-only context queries.

Proposal

Add explicit ToolAnnotations to each of the 4 public tools:

  • appwrite_get_context: readOnly=True, destructive=False, idempotent=True, openWorld=True (calls Appwrite Cloud APIs for live data)
  • appwrite_search_tools: readOnly=True, destructive=False, idempotent=True, openWorld=False (local in-memory catalog)
  • appwrite_call_tool: readOnly=False, destructive=False (the destructive gate lives inside the tool via confirm_write=true, not at the MCP hint layer), idempotent=False, openWorld=True (dispatches to live Appwrite APIs)
  • appwrite_search_docs: readOnly=True, destructive=False, idempotent=True, openWorld=False (local docs index)

Trade-offs considered

  • Marking appwrite_call_tool as destructiveHint=True was rejected:
    it would force clients to require destructive-action confirmation
    even for read calls like users.list. The destructive gate is
    already enforced inside the tool via confirm_write=true, which is
    the right place for it (per-call context matters, not blanket MCP
    hints).
  • Marking appwrite_search_docs as openWorldHint=True was rejected:
    the search itself runs locally over the committed index. The
    OpenAI API call only happens when an embedder is configured, and
    that is an implementation detail of the search, not a side effect
    the caller needs to authorize.

Draft PR ready

I have a draft PR on my fork
(louzt/appwrite-mcp#2) with:

  • Inline-commented annotations on all 4 tools (matching the
    pattern established in louzt/serpapi-mcp-fork so
    reviewers can audit each hint at a glance).
  • 13 unittest-style tests in tests/unit/test_annotations.py
    covering shape (every hint is bool), semantic invariants
    (readOnly ⇒ not-destructive, readOnly ⇒ idempotent), and
    per-tool specifics.
  • All 111 unit tests pass locally on the 4-job CI matrix
    (ruff / black / pyright / unittest discover).

Before opening an upstream PR I wanted to check:

  1. Is this direction welcome? Or are there reasons to keep the
    surface annotation-free?
  2. Is the appwrite_call_tool annotation choice (explicit
    destructiveHint=False) consistent with how the Appwrite team
    thinks about the per-call confirm_write=true gate?
  3. Should the 25 hidden SDK services (registered dynamically by
    service.py) get annotations too, even though they never appear
    in list_tools()? My current draft does NOT touch them, on the
    grounds that annotations on hidden tools are unreachable by
    clients.

Validation snapshot

$ uv run --group dev ruff check src tests
All checks passed!

$ uv run --group dev black --check src tests
34 files would be left unchanged.

$ uv run --group dev pyright
0 errors, 0 warnings, 0 informations

$ uv run python -m unittest discover -s tests/unit
Ran 111 tests in 5.000s
OK

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions