appwrite / appwrite/mcp

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

Open
#86 2 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.