# AGENTS.md Guidance for AI agents and human contributors working in this repository. ## What this repo is `mcp-server-appwrite` is a [Model Context Protocol](https://modelcontextprotocol.io) server for Appwrite. It exposes Appwrite's API to MCP clients as a small set of operator-style tools, supporting two deployments from one codebase: - **Cloud (hosted HTTP):** a Starlette ASGI app that acts as an OAuth 2.1 Resource Server. It validates the client's bearer token and forwards it to the Appwrite REST API. Served primarily at `mcp.appwrite.io/`, with `/mcp` also available as a conventional alias. - **Self-hosted (`stdio`):** runs locally and authenticates with a project API key (`APPWRITE_PROJECT_ID`, `APPWRITE_API_KEY`, `APPWRITE_ENDPOINT`, and optionally `APPWRITE_ENVIRONMENT_ID` for a project environment). Python ≥ 3.12, packaged with `hatchling`, managed with `uv`. ## Architecture Source lives in `src/mcp_server_appwrite/`: | File | Responsibility | | --- | --- | | `__main__.py` / `server.py` | Entry point, CLI args, transport selection (`--transport stdio\|http`), service registration, low-level MCP server. | | `http_app.py` | Hosted Streamable-HTTP transport: `/` plus the `/mcp` alias, the public `/docs/mcp`, RFC 9728 protected-resource metadata, `/healthz`. | | `auth.py` | OAuth 2.1 resource-server layer — bearer-token validation against the project's Appwrite authorization server. | | `flags.py` | Central registry of tester/testing flags (CLI arg + env var per flag). Enabling and testing them is documented in [docs/flags.md](docs/flags.md). | | `service.py` | `Service` base class: introspects an Appwrite SDK service and turns its methods into MCP tool definitions. | | `tool_manager.py` | Registry of all services and their generated tools. | | `operator.py` | The compact "operator" surface — `appwrite_search_tools`, `appwrite_call_tool` / `appwrite_call_write_tool`, tool annotations, result/resource storage, write confirmation. | | `instructions.py` | `serverInfo.description` and the server instructions sent on initialize, rendered from `assets/instructions.json` (a description plus ordered, transport/docs-conditional Markdown sections). Edit the JSON to change the wording; keep the `pyproject.toml` description in sync (a test enforces it). | | `context.py` | `appwrite_get_context` — workspace summary (project, services, account/org for OAuth, a focused project's environments). | | `headers.py` | Per-call request headers: `X-Appwrite-Environment` (a project environment, next to the project's ID) and `X-Appwrite-Reason` (why a write is made, validated and percent-encoded as Appwrite expects). | | `redaction.py` | Redacts the response fields Appwrite's spec marks `format: password` (carried on Console SDK model fields) from tool results, leaving a fingerprint, unless a call sets `reveal_secrets`. | | `docs_search.py` | In-process semantic docs search (`appwrite_search_docs`) over a prebuilt index. | | `docs_server.py` | The public docs MCP server at `/docs/mcp`: `appwrite_search_docs` only, no sign-in. | | `docs_source.py` | Fetches the published docs (`/docs/llms.txt` + per-page `.md` exports on appwrite.io) and chunks them for the index build. | | `docs_index.py` | Writes the index artifact deterministically and atomically: page and chunk hashes, reuse of cached chunk vectors, build lock, and the added/changed/removed page report. | | `events/` | MCP Events (behind `flags.EVENTS`, HTTP only): `catalog.py` (event catalog), `protocol.py` (capability, `events/list` and the subscribe methods), `subscriptions.py` + `webhooks.py` (`events/subscribe` / `events/unsubscribe`: one managed Appwrite webhook per subscription), `envelope.py` (subscription state sealed with the `mcp-event-subscriptions` JWE profile), `egress.py` + `delivery.py` (SSRF-safe signed delivery), `projection.py` + `ingress.py` (Appwrite webhook ingress at `/appwrite/webhooks/{id}`), `streams.py` + `bus.py` (push delivery: `events/stream`, behind `flags.EVENTS_PUSH`, with events routed between replicas over Redis pub/sub). See [docs/events.md](docs/events.md). | | `apps/` | MCP Apps (behind `flags.APPS`): `protocol.py` (the `io.modelcontextprotocol/ui` extension and per-client gating), `views.py` (`appwrite_show` and the view-only `appwrite_view_write`), `resource.py` (`ui://appwrite/view`, served from `assets/view.html`: plain JavaScript, no build step). See [docs/apps.md](docs/apps.md). | | `telemetry.py` | OpenTelemetry metrics layer (OTLP/HTTP). No-op unless an OTLP endpoint is configured and the transport is `http`. | | `data/` | Committed docs index artifact (`docs_index.npz`: vectors, chunk map, chunk hashes, embedded `meta.json`), shipped in the wheel/image. | `scripts/build_docs_index.py` rebuilds the docs index (requires `OPENAI_API_KEY`). ### Telemetry (metrics) `telemetry.py` emits OpenTelemetry metrics (prefixed `mcp.`) over OTLP/HTTP. It is a no-op unless the transport is `http` and `OTEL_EXPORTER_OTLP_ENDPOINT` is set, so `stdio` and unconfigured servers stay silent. In the cluster that endpoint is the Alloy collector, which adds the `deployment.*` labels and forwards upstream — the app needs no credentials. The MCP dashboard lives in `appwrite-labs/monitoring` at `dashboards/Cloud/mcp.json`, synced to monitoring.appwrite.systems by Grafana Git Sync. ### Tool surface (key design point) The server boots in a compact workflow: the client sees up to 5 tools (`appwrite_get_context`, `appwrite_search_tools`, `appwrite_call_tool` for reads, `appwrite_call_write_tool` for everything else, and optionally `appwrite_search_docs`), while the full OAuth Appwrite catalog (41 services) stays internal and is searched at runtime. API-key stdio uses a filtered 27-service catalog. Mutating hidden tools run only through `appwrite_call_write_tool` and require `confirm_write=true` and a one-line `reason` (sent as `X-Appwrite-Reason`, recorded on the audit log), so each exposed tool's annotations match every call it accepts. Large outputs are stored as MCP resources and returned as a preview + resource URI. With MCP Apps on (`flags.APPS`), hosts that render them also see `appwrite_show`, whose result the host renders as an interactive view (`ui://appwrite/view`), and the view can call `appwrite_view_write`, which the model never sees. See [docs/apps.md](docs/apps.md). ## Local development ```bash # Install uv, then sync deps uv sync # runtime deps uv sync --group dev # + black, ruff (lint/format) uv sync --group e2e # + e2e-test deps uv sync --extra integration # + integration-test deps # Run hosted HTTP transport MCP_PUBLIC_URL=http://localhost:8000 APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 \ uv run mcp-server-appwrite --transport http # Run self-hosted stdio transport APPWRITE_ENDPOINT=http://localhost:9501/v1 \ APPWRITE_PROJECT_ID= APPWRITE_API_KEY= \ uv run mcp-server-appwrite # Or via Docker (hosted HTTP/OAuth) docker compose up --build # compose.yaml; endpoint at http://localhost:8000/ ``` ## Pre-PR checklist Run these locally before opening a PR. They mirror the `CI` workflow (`.github/workflows/ci.yml`), which runs on every pull request and on pushes to `main`. **All five jobs must pass.** 1. **Lint** (`lint` job) ```bash uv sync --group dev uv run --group dev ruff check src tests ``` Ruff config: `target-version = py312`, rules `E`, `F`, `W`, `I` (import sorting), with `E501` (line length) delegated to black. 2. **Format** (`lint` job) ```bash uv run --group dev black --check src tests ``` Run `uv run --group dev black src tests` (without `--check`) to auto-fix. 3. **Type check** (`lint` job) ```bash uv run --group dev pyright ``` Pyright config lives in `pyproject.toml` (`[tool.pyright]`): basic mode over `src/`, Python 3.12, resolving against the project `.venv`. 4. **Unit tests** (`unit` job) ```bash uv sync uv run python -m unittest discover -s tests/unit -v ``` Fast, no external services or credentials required. 5. **E2E tests** (`e2e` job) ```bash uv sync --group e2e uv run --group e2e python -m unittest discover -s tests/e2e -v ``` Boots the real hosted HTTP app on a random localhost port and drives it over real HTTP the way an MCP client does. No credentials: only the OAuth token verifier is stubbed. Runs on every PR, forks included. The push delivery flows start a real `dragonfly` (CI) or `redis-server`, which must be on `PATH` (`brew install redis` or `apt-get install redis-server`). Prefer adding assertions to an existing flow over adding unit tests; add a unit test only for what no e2e flow can reach, and say why in its module docstring. 6. **Docker build** (`docker` job) ```bash docker build -t appwrite-mcp:ci . ``` The hosted HTTP image must build cleanly. 7. **Integration tests** (`integration` job) — *CI runs these only for pushes and for PRs from branches on the same repo (not forks).* They create and delete **real** Appwrite resources, so they need live credentials and are skipped when absent: ```bash uv sync --extra integration APPWRITE_PROJECT_ID= APPWRITE_API_KEY= APPWRITE_ENDPOINT= \ uv run --extra integration python -m unittest discover -s tests/integration -v ``` ### CI environment versions CI pins Python `3.12` and `uv` `0.11.22`. Match these locally if you hit version-specific differences. ### If you change the docs index Rebuilding `src/mcp_server_appwrite/data/` requires `OPENAI_API_KEY`. Re-run the build script and commit the refreshed artifact: ```bash OPENAI_API_KEY=sk-... uv run python scripts/build_docs_index.py ``` ## Other workflows - `publish.yml` — package publishing. - `staging.yml` / `production.yml` — deployment (publishes images to Docker Hub). These are not gated on PRs the way `ci.yml` is, but be mindful when touching the `Dockerfile`, `pyproject.toml` version, or deployment config. ### Release metadata Release versions come from Git tags. Publish GitHub releases with tags in the form `vMAJOR.MINOR.PATCH` (for example, `v0.8.8`). Python package builds derive their version from the tag via Hatch VCS, and `server.json` is generated from `server.template.json` during the publish workflow before sending metadata to the MCP Registry. Do not hand-edit a version in `pyproject.toml`, `constants.py`, or `server.json`. For local registry metadata checks, render the generated file explicitly: ```bash uv run python scripts/render_server_json.py 0.8.8 ``` ## Conventions - Keep the exposed tool surface small — new Appwrite capabilities should flow through the operator/catalog mechanism, not become top-level tools. - New SDK services are registered automatically; you generally don't hand-write tool definitions. - Match existing style: black formatting, ruff-clean imports, type hints, module docstrings explaining intent (see `auth.py`, `http_app.py`, `docs_search.py`). - Cover hosted-server behavior with end-to-end flows under `tests/e2e/` (shared harness in `tests/e2e/support.py`). Add unit tests under `tests/unit/` only for what no e2e flow can reach. Add integration coverage under `tests/integration/` when touching real Appwrite API behavior.