Skip to main content
Glama
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.