anki-mcp
by zylomeara
README.md
# Anki MCP
**Multi-tenant, self-hosted [Anki](https://apps.ankiweb.net/) MCP server for
AI-assisted flashcard generation.**
Connect Claude (claude.ai, Claude Code, or any [MCP](https://modelcontextprotocol.io)
client) to your own Anki collection. Claude adds cards for you — from any device —
and they sync straight to AnkiMobile / AnkiDroid on your phone. It also surfaces
**weak-card analytics** so Claude can target the cards you keep getting wrong.
Each user (authenticated via Keycloak) gets their own isolated Anki collection and
sync slot. One deployment serves many people.
> **Why self-host?** Your collection, your sync server, your data. Cards generated
> by an AI flow through a stack you control — no third-party flashcard SaaS.
---
## What it is
- **An MCP server** (Python / FastMCP, streamable-HTTP) exposing Anki operations
as tools Claude can call.
- **Backed by the real `anki` library** — it operates on genuine Anki collections,
not a reimplementation, so everything round-trips through normal Anki sync.
- **Fronted by a stock self-hosted Anki sync server**, so your phone's AnkiMobile /
AnkiDroid syncs against the same collection Claude writes to.
- **Multi-tenant**: per-user (Keycloak `sub`) tenants, each = an Anki collection +
a sync slot, with secrets encrypted at rest in Postgres.
- **A self-service portal** (Next.js) where users provision their own tenant and
get one-time sync credentials + phone setup instructions.
The core loop: **add a card from claude.ai → it syncs to your phone.**
---
## Architecture / topology
```
Internet
│
│ HTTPS :443 (TLS 1.2 + 1.3)
▼
┌───────────────────┐
│ Caddy │ ← the ONLY public surface
│ (system service) │
└─────────┬─────────┘
┌────────────┬───────┴───────┬─────────────┐
│ /mcp │ /portal* │ /sync* │
│ /.well- │ │ /msync* │
│ known/* │ │ │
▼ ▼ ▼ │
┌──────────────┐ ┌───────────┐ ┌──────────────────┐ │
│ anki-mcp │ │anki-portal│ │ anki-sync-server │ │
│ FastMCP + │ │ Next.js │ │ (stock, AGPL) │ │
│ Keycloak JWT│ │ portal │ │ SYNC_USERn pool │◄┘ AnkiMobile /
│ per-tenant │ └─────┬─────┘ └────────┬─────────┘ AnkiDroid sync
│ AnkiStore │ │ │ against the same
└──────┬───────┘ │ /accounts API │ collection
│ ⇅ sync └───────────────►│
│ (uploads/downloads collections)
│ ▲
│ resolves tenant by sub │
▼ │
┌──────────────┐ │
│ anki-postgres│ tenant_accounts │
│ sub-scoped │ (slot password │
│ AES-256-GCM │ encrypted, AAD=sub) │
└──────────────┘ │
│
All internal ports bound to 127.0.0.1 ─┘ (see scripts/check-exposure.sh)
```
Identity flows `JWT → sub → DB scope → AES-256-GCM (AAD=sub)`. Auth is
single-realm; **data** is multi-tenant. See [docs/SECURITY.md](docs/SECURITY.md).
---
## How it was built — phases
| Phase | Delivered |
|------:|-----------|
| **0/1 — Foundation** | Repo skeleton + a headless `anki`-lib data layer (`AnkiStore`) that adds/searches/analyzes cards in a real collection and syncs it to a stock self-hosted Anki sync server — the vertical slice that de-risked the architecture. |
| **2 — MCP server** | Exposed the data layer as an authenticated MCP server: FastMCP streamable-HTTP, Keycloak JWT auth, tools over a single hardcoded tenant, served behind Caddy. |
| **3 — Multi-tenant** | Per-user (Keycloak `sub`) tenants — each an Anki collection + a sync slot — stored in Postgres with slot secrets encrypted (AES-256-GCM, AAD=sub), provisioned via an authenticated `/accounts` API. Sync-slot pool with consumed-slots (no reuse). |
| **4 — Portal** | A self-service Next.js portal: a Keycloak-authenticated user lists / creates (→ sync creds shown ONCE + AnkiMobile setup) / deletes / sets-default their tenants. A thin proxy to the `/accounts` API. |
| **5 — Hardening + OSS** | LRU-bounded store registry; container hardening (cap_drop, read_only+tmpfs, mem/pids limits, no-new-priv); edge locked to Caddy-only + TLS 1.2 + exposure-check script; full-stack core-loop integration test; AGPL LICENSE, this README, deploy runbook, handoff checklist. |
---
## MCP tools
All tools take a `tenant` argument (the tenant alias; `null` resolves the user's
default tenant). They operate on the caller's own collection only.
| Tool | Signature (abridged) | What it does |
|------|----------------------|--------------|
| `add_notes` | `(tenant, deck, notes: list[dict])` | Add a batch of notes to a deck. Reconciles (sync down) then publishes (sync up). |
| `find_notes` | `(tenant, query)` | Search notes by an Anki query string (e.g. `deck:English prop:lapses>=2`). Returns note IDs. |
| `weak_cards` | `(tenant, min_lapses=2, max_ease=2.1)` | List struggling cards (`lapses >= min_lapses` OR `ease < max_ease`) with ease / lapses / interval / reps. |
| `find_duplicates` | `(tenant, field_name="Front")` | Find notes sharing the same value in a field. |
| `sync` | `(tenant)` | Bidirectionally sync the collection with the sync server (pull phone reviews / push changes). |
---
## Quickstart (local)
Prerequisites: Docker + Docker Compose, and a reachable Keycloak realm named
`mcp` (the default points at `auth.ai.gerline.ru` — change it for your own realm).
```bash
# 1. Configure
cp .env.example .env
# 2. Generate secrets into .env
# ENCRYPTION_KEY (AES-256, 32 bytes / 64 hex):
openssl rand -hex 32 # -> ENCRYPTION_KEY=
# NEXTAUTH_SECRET (portal session signing):
openssl rand -base64 32 # -> NEXTAUTH_SECRET=
# Set POSTGRES_PASSWORD, the SYNC_USERn slot passwords, MCP_HOST, and the
# Keycloak URLs in .env. (See .env.example comments.)
# 3. Bring the stack up
docker compose up -d
# 4. Verify nothing but Caddy faces the internet
bash scripts/check-exposure.sh # must PASS
# 5. Provision your first tenant via the portal (behind Caddy at /portal),
# capture the one-time sync credentials, and configure AnkiMobile.
```
For a real internet-facing deployment (VPS, Caddy as a system service, Keycloak
clients, TLS, the slot pool, the `ENFORCE_AUDIENCE=true` cutover) follow the
**[deploy runbook](docs/DEPLOY.md)**.
---
## Documentation
- **[docs/DEPLOY.md](docs/DEPLOY.md)** — production deployment runbook.
- **[docs/SECURITY.md](docs/SECURITY.md)** — the security model and residual risks.
- **[docs/HANDOFF.md](docs/HANDOFF.md)** — remaining manual/ops actions + the real
end-to-end acceptance test.
---
## License
**AGPL-3.0.** This project links the `anki` library and is fronted by the Anki
sync server, both of which are AGPL-licensed, and it is served over a network — so
the AGPL applies. The full text is in [LICENSE](LICENSE). If you run a modified
version as a network service, you must offer your users its source.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing