Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ jobs:
- name: Run unit tests
run: uv run python -m unittest discover -s tests/unit -v

e2e:
name: E2E
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
with:
version: "0.11.22"

- name: Install dependencies
run: uv sync --frozen --group e2e

- name: Run e2e tests
run: uv run --group e2e python -m unittest discover -s tests/e2e -v

docker:
name: Docker build
runs-on: ubuntu-latest
Expand Down
24 changes: 19 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ preview + resource URI.
# 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
Expand All @@ -82,7 +83,7 @@ docker compose up --build # compose.yaml; endpoint at http://localhost:8000/

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 four jobs must pass.**
`main`. **All five jobs must pass.**

1. **Lint** (`lint` job)
```bash
Expand Down Expand Up @@ -112,13 +113,24 @@ Run these locally before opening a PR. They mirror the `CI` workflow
```
Fast, no external services or credentials required.

5. **Docker build** (`docker` job)
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. 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.

6. **Integration tests** (`integration` job) — *CI runs these only for pushes and
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:
Expand Down Expand Up @@ -171,5 +183,7 @@ uv run python scripts/render_server_json.py 0.8.8
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`).
- Add unit tests under `tests/unit/` for any non-trivial logic; add integration
coverage under `tests/integration/` when touching real API behavior.
- 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.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ for per-client setup.
- [How Cloud authentication works](docs/authentication.md) — the OAuth 2.1 flow.
- [Documentation search](docs/documentation-search.md) — the in-process
`appwrite_search_docs` tool and how to rebuild its index.
- [Events](docs/events.md) — MCP Events: webhook subscriptions to changes in
your Appwrite project (in progress, behind a flag).
- [Self-hosted Appwrite](docs/self-hosted.md) — run the server locally with a
project API key.
- [Local development](docs/development.md) — running, testing, and debugging the
Expand Down
5 changes: 5 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ APPWRITE_API_KEY=<YOUR_API_KEY> \
| Suite | Command | Needs credentials |
| --- | --- | --- |
| Unit | `uv run python -m unittest discover -s tests/unit -v` | No |
| E2E | `uv run --group e2e python -m unittest discover -s tests/e2e -v` | No |
| Integration | `uv run --extra integration python -m unittest discover -s tests/integration -v` | Yes |

E2E tests boot the real hosted HTTP app on a random localhost port and drive it
over real HTTP like an MCP client. Only the OAuth token verifier is stubbed, so
they need no credentials and run on every PR.

Integration tests create and delete **real** Appwrite resources. They
authenticate via `APPWRITE_PROJECT_ID`, `APPWRITE_API_KEY`, `APPWRITE_ENDPOINT`
(shell or `.env`) and are skipped when no credentials are present.
Expand Down
94 changes: 94 additions & 0 deletions docs/events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Events

> **In progress.** Events are being built behind the `--events` flag
> ([#127](https://github.com/appwrite/mcp/issues/127)). Today the server
> advertises the capability and lists the catalog; subscribing and delivery are
> not available yet.

[MCP Events](https://developers.openai.com/plugins/build/mcp-events) let an MCP
client such as ChatGPT subscribe to things that happen in an Appwrite project
and receive a signed webhook when they occur, for example "when a function
execution fails in project X, read the logs and open a fix".

Events are served only by the hosted HTTP server (`mcp.appwrite.io`), and v1
delivers only by webhook. Each subscription will be backed by one Appwrite
webhook in the subscriber's project, so the MCP server keeps no subscription
store.

## Enabling

```bash
MCP_PUBLIC_URL=http://localhost:8000 \
uv run mcp-server-appwrite --transport http --events 1
```

`MCP_EVENTS=1` works too. See [flags.md](flags.md#--events--mcp-events-in-progress)
for quick checks with `curl`.

With the flag on, the server advertises the capability in both shapes that
clients read today:

| Where | Read by |
| --- | --- |
| `capabilities.events: {}` | ChatGPT |
| `capabilities.extensions["io.modelcontextprotocol/events"]: {"listChanged": false}` | SEP-3415 clients |

The `mcp` SDK does not model this capability yet
([python-sdk#3640](https://github.com/modelcontextprotocol/python-sdk/issues/3640)),
so a server middleware adds it to the `server/discover` and `initialize`
results.

## Catalog

`events/list` returns these events. Every event takes a required `project_id`.
ID arguments accept 1–36 letters, digits, `_` or `-`, so a `*` or `.` can never
widen a subscription beyond the named resource. Payloads carry IDs and metadata
only; the agent fetches details with `appwrite_call_tool`.

| Event | Arguments | Appwrite events | Delivered when | Payload |
| --- | --- | --- | --- | --- |
| `functions.execution.failed` | `function_id` | `functions.{function_id}.executions.*.update`, `functions.{function_id}.executions.*.create` | status is `failed` | `function_id`, `execution_id`, `status`, `trigger`, `response_status_code`, `created_at` |
| `functions.deployment.completed` | `function_id`, `status?` (`ready` \| `failed`) | `functions.{function_id}.deployments.*.update` | status is `ready` or `failed` (or the requested one) | `function_id`, `deployment_id`, `status`, `updated_at` |
| `sites.deployment.completed` | `site_id`, `status?` (`ready` \| `failed`) | `sites.{site_id}.deployments.*.update` | status is `ready` or `failed` (or the requested one) | `site_id`, `deployment_id`, `status`, `updated_at` |
| `tablesdb.row.created` | `database_id`, `table_id` | `tablesdb.{database_id}.tables.{table_id}.rows.*.create` | always | `database_id`, `table_id`, `row_id`, `created_at` |
| `storage.file.created` | `bucket_id` | `buckets.{bucket_id}.files.*.create` | always | `bucket_id`, `file_id`, `mime_type`, `size`, `created_at` |
| `users.user.created` | none | `users.*.create` | always | `user_id`, `created_at` |

Synchronous function executions fire only `.create`, after they finish, which
is why `functions.execution.failed` listens to both. Executions served through
function domains fire no Appwrite event and are not covered.

Timestamps are ISO 8601 strings and may be `null`. The catalog lives in
[`events/catalog.py`](../src/mcp_server_appwrite/events/catalog.py).

## Errors

Error codes follow the values ChatGPT uses today. SEP-3415 renumbers them, so
they are kept behind constants in
[`events/errors.py`](../src/mcp_server_appwrite/events/errors.py).

| Meaning | Code | `data` |
| --- | --- | --- |
| Bad arguments, URL or secret | `-32602` | |
| NotFound | `-32011` | `kind` |
| Forbidden | `-32012` | |
| ResourceExhausted | `-32013` | `limit`, `max` |
| Unsupported | `-32014` | |
| CallbackEndpointError | `-32015` | `reason`: `challenge_failed`, `timeout`, `connection_refused`, `tls_error`, `http_4xx`, `http_5xx` |

## Testing

Events are tested end to end in `tests/e2e/`: the real hosted app runs under
uvicorn on a random localhost port, and the tests talk to it over HTTP exactly
like ChatGPT does (`MCP-Protocol-Version: 2026-07-28`, `Mcp-Method`, `_meta`).
Only the OAuth token verifier is stubbed, because Cloud OAuth is not reachable
from CI. The suite needs no credentials and runs on every PR:

```bash
uv run --group e2e python -m unittest discover -s tests/e2e -v
```

A live end-to-end run against Cloud staging (real OAuth tokens, real Appwrite
webhooks) is planned once
[appwrite/appwrite#14293](https://github.com/appwrite/appwrite/issues/14293)
ships; until then the server's surroundings are played locally.
39 changes: 39 additions & 0 deletions docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Flags are for testing overrides only — permanent configuration belongs in
| CLI | Env | What it does |
| --- | --- | --- |
| `--console-url` | `MCP_CONSOLE_URL` | Send OAuth login/consent to an alternative Appwrite Console (HTTP transport only). |
| `--events` | `MCP_EVENTS` | Advertise MCP Events and serve `events/list` (HTTP transport only). See [events.md](events.md). |

### `--console-url` — test OAuth against a pre-release console

Expand Down Expand Up @@ -80,3 +81,41 @@ claude mcp add --transport http appwrite-test http://localhost:8000/
Then run `/mcp` in Claude Code and authenticate — the browser should open the
override console's sign-in page, and after consent the client completes the
token exchange against the real authorization server.

### `--events` — MCP Events (in progress)

Turns on [MCP Events](events.md): `server/discover` (and legacy `initialize`)
advertise the events capability, and the server answers `events/list` with the
event catalog. It is off by default while the feature is built
([#127](https://github.com/appwrite/mcp/issues/127)); `events/subscribe` and
delivery are not available yet. Any of `1`, `true`, `yes` or `on` enables it.

**Enable it:**

```bash
MCP_PUBLIC_URL=http://localhost:8000 \
uv run mcp-server-appwrite --transport http --events 1
```

**Quick checks** (any valid Appwrite OAuth access token works as `$TOKEN`):

```bash
mcp() {
curl -s http://localhost:8000/ \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H "Mcp-Method: $1" \
-d '{"jsonrpc":"2.0","id":1,"method":"'"$1"'","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
}

# Both shapes: capabilities.events and capabilities.extensions["io.modelcontextprotocol/events"]
mcp server/discover | jq .result.capabilities

# The catalog
mcp events/list | jq '.result.events[].name'
```

With the flag off, neither capability appears and `events/list` returns
`-32601` (method not found).
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ dev = [
"ruff==0.15.18",
"pyright==1.1.411",
]
# tests/e2e: the hosted server driven over real HTTP; no credentials needed.
e2e = [
"jsonschema>=4.26",
]

[project.scripts]
mcp-server-appwrite = "mcp_server_appwrite.__main__:main"
Expand Down
1 change: 1 addition & 0 deletions src/mcp_server_appwrite/events/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""MCP Events: webhook subscriptions backed by Appwrite project webhooks."""
Loading
Loading