Consider MCP ToolAnnotations on the 4 public tools (proposal + draft PR ready)
- Dominant language
- Python
- Stars
- 72
- Forks
- 18
- Avg merge
- 5h 6m
- Merged PRs (30d)
- 16
Description
## 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][pr-fork]) with:
- Inline-commented annotations on all 4 tools (matching the
pattern established in [louzt/serpapi-mcp-fork][serpapi-pr] 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.
[pr-fork]: https://github.com/louzt/appwrite-mcp/pull/2
[serpapi-pr]: https://github.com/louzt/serpapi-mcp-fork
## 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
```
Contributor guide
Research direction
Start with Operator.get_public_tools() and server.handle_list_tools to trace how the four public tools are registered. Review the proposed annotations and tests/unit/test_annotations.py, then run the listed ruff, black, pyright, and unittest commands; done means all four public tools expose the agreed hints and the tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100