Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2936252
feat: add the MCP events catalog and error codes
ChiragAgg5k Oct 9, 2026
4e34f73
feat: advertise MCP events and serve events/list
ChiragAgg5k Oct 9, 2026
101d8de
docs: document MCP events and the events flag
ChiragAgg5k Oct 9, 2026
e96848f
test: cover events discovery end to end against the hosted server
ChiragAgg5k Oct 9, 2026
a2cb6e7
ci: run the e2e suite on every pull request
ChiragAgg5k Oct 9, 2026
a952407
feat: seal MCP event subscriptions into an envelope
ChiragAgg5k Oct 9, 2026
a995f8f
test: keep only envelope tests that e2e cannot reach
ChiragAgg5k Oct 9, 2026
d14a220
fix: accept only generated subscription ids
ChiragAgg5k Oct 11, 2026
7ede499
refactor: seal subscriptions with the mcp-event-subscriptions JWE pro…
ChiragAgg5k Oct 11, 2026
d4d5931
fix: pin the clock for the conformance vector and keep secrets out of…
ChiragAgg5k Oct 11, 2026
350cea4
test: check a subscription's repr hides its secret
ChiragAgg5k Oct 11, 2026
c9a405b
feat: sign and deliver MCP events safely
ChiragAgg5k Oct 9, 2026
4964cbb
test: keep only delivery and egress tests that e2e cannot reach
ChiragAgg5k Oct 9, 2026
321f72b
fix: confine test-only http to loopback and bound queued egress
ChiragAgg5k Oct 11, 2026
656ac20
refactor: deliver with one secret and keep it out of repr
ChiragAgg5k Oct 11, 2026
2ef797b
refactor: report delivery failures with errors.CallbackFailure
ChiragAgg5k Oct 9, 2026
e1b43b1
feat: receive Appwrite webhooks and forward MCP events
ChiragAgg5k Oct 9, 2026
5fa3ad6
docs: describe the events ingress and the always-2xx rule
ChiragAgg5k Oct 9, 2026
370d86c
test: cover the events ingress end to end
ChiragAgg5k Oct 9, 2026
e833662
fix: bound the delivery backlog and verify against every ring key
ChiragAgg5k Oct 11, 2026
b8ab977
fix: bound how much of a webhook body the ingress reads
ChiragAgg5k Oct 11, 2026
edd3eab
refactor: open JWE subscription envelopes in the ingress
ChiragAgg5k Oct 11, 2026
91b2fa9
fix: bound how many webhook bodies the ingress reads at once
ChiragAgg5k Oct 11, 2026
e59e0af
feat: serve events/subscribe and events/unsubscribe
ChiragAgg5k Oct 9, 2026
9339d89
refactor: hand the ingress callback its one delivery secret
ChiragAgg5k Oct 11, 2026
6bae431
test: cover subscribe and unsubscribe end to end
ChiragAgg5k Oct 9, 2026
728c75c
docs: describe subscribe, unsubscribe and the managed webhook
ChiragAgg5k Oct 9, 2026
38b048a
fix: forbid unsubscribe from missing projects and guard cleanup races
ChiragAgg5k Oct 11, 2026
49f4345
fix: check write scope before verifying and keep refreshed webhooks
ChiragAgg5k Oct 11, 2026
75b7f73
refactor: seal subscribe requests as JWE subscription records
ChiragAgg5k Oct 11, 2026
281373e
refactor: verify subscribe callbacks with their one secret
ChiragAgg5k Oct 11, 2026
386a7c1
fix: write the signing key before the envelope on refresh
ChiragAgg5k Oct 12, 2026
205840a
docs: retire a sealing key a full TTL after the rotation reaches ever…
ChiragAgg5k Oct 12, 2026
dcfd4c2
fix: acknowledge expired subscriptions before checking the signature
ChiragAgg5k Oct 12, 2026
345754f
chore: merge feat/events-ingress into feat/events-subscribe
ChiragAgg5k Oct 12, 2026
c0f5622
docs: note that expiry is checked before the signature
ChiragAgg5k Oct 12, 2026
ba8aed8
chore: merge feat/events-ingress into feat/events-subscribe
ChiragAgg5k Oct 12, 2026
70b0e83
docs: name the expired drop for orphans after a key is retired
ChiragAgg5k Oct 12, 2026
969c6ac
Merge #131 into feat/events
ChiragAgg5k Oct 12, 2026
5ecb40e
Merge #132 into feat/events
ChiragAgg5k Oct 12, 2026
c9ad4b3
Merge #133 into feat/events
ChiragAgg5k Oct 12, 2026
9aeabda
Merge #134 into feat/events
ChiragAgg5k Oct 12, 2026
4d0a54c
Merge #135 into feat/events
ChiragAgg5k Oct 12, 2026
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
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ PORT=8000
# SENTRY_DSN=https://public@sentry.example/1
# SENTRY_ENVIRONMENT=development

# ── Integration tests only (not used by the running server) ─────────────────
# Required when MCP_EVENTS is on (the server refuses to start without it). MCP
# Events sealing keys: comma-separated <id>:<base64 of 32 random bytes>, each a
# JWE key for the mcp-subscription+jwe envelope. The first one seals and every
# one opens. Rotate by adding a new key in front and dropping the old one after
# the longest subscription TTL (24h) has passed.
# Generate a key with `openssl rand -base64 32`.
# MCP_EVENTS_SEALING_KEYS=k1:<base64 key>

# Integration tests only (not used by the running server).
# The live integration suite authenticates to the Appwrite API with an API key
# and runs against APPWRITE_PROJECT_ID (above).
# APPWRITE_PROJECT_ID=your-project-id
Expand Down
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
25 changes: 20 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Source lives in `src/mcp_server_appwrite/`:
| `docs_search.py` | In-process semantic docs search (`appwrite_search_docs`) over a prebuilt index. |
| `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}`). See [docs/events.md](docs/events.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. |

Expand Down Expand Up @@ -63,6 +64,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 +84,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 +114,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 +184,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 development, not available yet).
- [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
13 changes: 13 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ docker compose up --build
> To enable hosted HTTP error monitoring locally, set `SENTRY_DSN`. You can also
> set `SENTRY_ENVIRONMENT`; Compose defaults it to `development`.

> MCP Events seals each subscription into its Appwrite webhook as a
> [`mcp-subscription+jwe`](https://github.com/ChiragAgg5k/mcp-event-subscriptions/blob/main/profiles/encrypted-envelope.md)
> envelope, with the keys in `MCP_EVENTS_SEALING_KEYS` (`<id>:<base64 of 32
> random bytes>`, comma separated, first one active). Generate a key with
> `openssl rand -base64 32`. To rotate, add the new key last and roll that out,
> then move it first, then remove the old one 25 hours after that reached every
> replica (see "Key rotation" in [events.md](events.md)).

**`uv` directly — HTTP:**

```bash
Expand All @@ -58,8 +66,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
Loading
Loading