AnkiMCP Server
by ankimcp
README.md
# AnkiMCP Server (Addon)
<div align="center">
<img src="./docs/images/ankimcp.png" alt="Anki + MCP Integration" width="600" />
<p><strong>Seamlessly integrate <a href="https://apps.ankiweb.net">Anki</a> with AI assistants through the <a href="https://modelcontextprotocol.io">Model Context Protocol</a></strong></p>
</div>
An Anki addon that exposes your collection to AI assistants via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/).
## What is this?
AnkiMCP Server runs a local MCP server inside Anki, allowing AI assistants like Claude to interact with your flashcard collection. This enables AI-powered study sessions, card creation, and collection management.
Part of the [ankimcp.ai](https://ankimcp.ai) project.
## Note on First Run
On first run, this addon downloads `pydantic_core` (~2MB) from PyPI. This is required because pydantic_core contains platform-specific binaries (Windows/macOS/Linux) that cannot be bundled in a single addon file.
A second native dependency, `rpds` (from `rpds-py`), is handled the same way — but it is almost never downloaded: Anki already ships `rpds` as a transitive dependency of its own `jsonschema`, so the addon just imports it. The download only kicks in on the rare install where that import fails. Both downloads are cached under Anki's own data folder (outside the addon's own folder, so it survives addon updates cleanly on every OS), falling back to the addon's `_cache/` directory when that location isn't available — so either way they happen once, not on every launch. The old in-folder fallback cache is only ever removed once the outside-the-folder cache has proven itself usable (a successful load from it) and is never touched while a native module is still loaded from it — a failed first attempt (e.g. offline) leaves the old cache untouched rather than destroyed.
## Features
- **Local HTTP server** - Runs on `http://127.0.0.1:3141/` by default
- **Remote tunnel** - Access your collection from anywhere via a public HTTPS URL
- **MCP protocol** - Compatible with any MCP client (Claude Desktop, etc.)
- **Auto-start** - HTTP server starts automatically when Anki opens
- **Tunnel-friendly** - Works with Cloudflare Tunnel, ngrok, or the built-in tunnel (exposing the HTTP server this way also requires extending the [allowed hosts/origins](#allowed-hosts-and-origins-dns-rebinding-protection))
- **DNS-rebinding protection** - The HTTP server validates `Host`/`Origin` headers against a loopback allowlist by default; [extend it](#allowed-hosts-and-origins-dns-rebinding-protection) for tunnel/reverse-proxy exposure
- **Optional API key** - Require an `Authorization: Bearer` token on the HTTP transport via [`http_api_key`](#api-key-optional-http-auth) (AnkiConnect-style; empty = disabled)
- **Toolbar indicator** - A `● AnkiMCP` item in the top toolbar shows tunnel connection state at a glance (opt out via `show_toolbar_indicator`)
- **Diagnostic logging** - Opt-in [`log_to_file`](#diagnostic-file-logging) writes a rotating, secret-redacted log to `user_files/ankimcp.log`, with **Open log folder** / **Copy diagnostics** buttons in settings
- **Field management** - Add, rename, and reposition note-type fields via the `model_fields` tool (with an opt-in [destructive](#destructive-tools-opt-in) remove)
- **Bulk card stats** - The read-only `cards_stats` tool returns compact per-card scheduling metrics (type/queue/interval/tags/`dueToday`) for a whole deck including subdecks, FSRS-independent — a lean bulk read for analytics
- **Cross-platform** - Works on macOS, Windows, and Linux (x64 and ARM)
## Installation
### From AnkiWeb (recommended)
1. Open Anki and go to *Tools → Add-ons → Get Add-ons...*
2. Enter code: `124672614`
3. Restart Anki
### From GitHub Releases
1. Download `anki_mcp_server.ankiaddon` from [Releases](https://github.com/ankimcp/anki-mcp-server-addon/releases)
2. Double-click to install, or use *Tools → Add-ons → Install from file...*
3. Restart Anki
### NixOS
#### With flakes (recommended)
Add the flake input and use the pre-built package:
```nix
# flake.nix
{
inputs.anki-mcp.url = "github:ankimcp/anki-mcp-server-addon";
outputs = { nixpkgs, anki-mcp, ... }: {
# Option A: Standalone — Anki with the addon pre-installed
environment.systemPackages = [
anki-mcp.packages.${system}.default
];
# Option B: Composable with other addons via overlay
nixpkgs.overlays = [ anki-mcp.overlays.default ];
environment.systemPackages = [
(pkgs.anki.withAddons [ pkgs.ankiAddons.anki-mcp-server ])
];
};
}
```
#### Without flakes
```nix
# configuration.nix
{ pkgs, ... }:
let
python3 = pkgs.python3;
ankiMcpPythonDeps = python3.withPackages (ps: with ps; [
mcp pydantic pydantic-settings starlette uvicorn anyio httpx websockets
]);
anki-mcp-server = pkgs.anki-utils.buildAnkiAddon (finalAttrs: {
pname = "anki-mcp-server";
version = "0.20.0";
src = pkgs.fetchFromGitHub {
owner = "ankimcp";
repo = "anki-mcp-server-addon";
rev = "v${finalAttrs.version}";
hash = ""; # nix will tell you the correct hash on first build
};
sourceRoot = "${finalAttrs.src.name}/anki_mcp_server";
});
ankiWithMcp = pkgs.anki.withAddons [ anki-mcp-server ];
ankiWrapped = pkgs.symlinkJoin {
name = "anki-with-mcp";
paths = [ ankiWithMcp ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram $out/bin/anki \
--prefix PYTHONPATH ':' "${ankiMcpPythonDeps}/${python3.sitePackages}"
'';
};
in
{
environment.systemPackages = [ ankiWrapped ];
}
```
## Usage
The server starts automatically when you open Anki. Check status via *Tools → AnkiMCP Server Settings...*
### Connect with Claude Desktop
Requires [Node.js](https://nodejs.org/) installed. Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"anki": {
"command": "npx",
"args": ["mcp-remote", "http://127.0.0.1:3141"]
}
}
}
```
> **Note:** Claude Desktop doesn't natively support HTTP servers in its JSON config — `mcp-remote` bridges the connection via stdio.
### Connect with Claude Code
```bash
claude mcp add anki --transport http http://127.0.0.1:3141/
```
### Opencode
```bash
opencode mcp add anki --url http://127.0.0.1:3141/
```
### Tunnel (Remote Access)
The built-in tunnel gives your Anki collection a public HTTPS URL, so AI assistants can reach it from anywhere — no port forwarding or reverse proxy needed. The collection is relayed through a WebSocket tunnel server (`wss://tunnel.ankimcp.ai` by default). Requires an [ankimcp.ai](https://ankimcp.ai) account to log in.
**How to connect:**
1. Open *Tools -> AnkiMCP Server Settings...*
2. Click **Connect Tunnel**
3. If not logged in, a login dialog appears — it shows a one-time code; click **Open Browser** and enter that code at the verification URL (OAuth 2.0 device flow)
4. Once connected, a public tunnel URL is displayed (e.g., `https://tunnel.ankimcp.ai/e3439277-9d1e-47a1-b961-d193a4590da0`)
5. Use this URL in your AI client instead of `http://127.0.0.1:3141`
**Using with Claude Desktop:**
Replace the localhost URL with your tunnel URL in the Claude Desktop config:
```json
{
"mcpServers": {
"anki": {
"command": "npx",
"args": ["mcp-remote", "https://tunnel.ankimcp.ai/<your-tunnel-id>"]
}
}
}
```
**Using with Claude Code:**
```bash
claude mcp add anki --transport http https://tunnel.ankimcp.ai/<your-tunnel-id>
```
**Disconnect vs. Logout:**
- **Disconnect** closes the tunnel connection. Credentials stay on disk — next Connect reconnects without re-login.
- **Logout** deletes credentials. Next Connect triggers the login dialog again.
**Tunnel config fields** (for advanced users / self-hosters):
- `tunnel_server_url` — WebSocket URL of the tunnel relay server (default: `wss://tunnel.ankimcp.ai`)
- `tunnel_client_id` — OAuth client identifier (default: `ankimcp-cli`)
Credentials are stored in the addon's own `user_files/credentials.json` (preserved across addon updates). They are not shared with the [AnkiMCP CLI](https://github.com/ankimcp/anki-mcp-cli) — the CLI keeps its own credentials under `~/.ankimcp/`, so you log in to the addon and the CLI independently. The on-disk format is identical between the two.
## Configuration
Edit via Anki's *Tools → Add-ons → AnkiMCP Server → Config*:
```json
{
"http_enabled": true,
"http_port": 3141,
"http_host": "127.0.0.1",
"http_path": "",
"http_allowed_hosts": [],
"http_allowed_origins": [],
"http_api_key": "",
"cors_origins": [],
"cors_expose_headers": ["mcp-protocol-version"],
"disabled_tools": [],
"enabled_destructive_tools": [],
"max_notes_per_batch": 100,
"tunnel_server_url": "wss://tunnel.ankimcp.ai",
"tunnel_client_id": "ankimcp-cli",
"media_import_dir": "",
"media_allowed_types": [],
"media_allowed_hosts": [],
"show_settings_menu_item": true,
"show_toolbar_indicator": true,
"show_sync_tooltip": true,
"log_to_file": false
}
```
### HTTP Server Toggle
The `http_enabled` setting controls whether the local HTTP server runs. When set to `false`, the HTTP server won't start — only the tunnel transport is available. Default is `true`.
```json
{
"http_enabled": false
}
```
This is useful if you only use the tunnel and don't want a local HTTP server listening.
### Tools Menu Item
The *AnkiMCP Server Settings…* entry in Anki's *Tools* menu is shown by default. Set `show_settings_menu_item` to `false` to hide it (takes effect after an Anki restart).
```json
{
"show_settings_menu_item": false
}
```
Note: if you hide the menu item **and** the toolbar indicator, there's no in-app way left to open the settings dialog — you can still edit the config via *Tools → Add-ons → AnkiMCP Server → Config*.
### Toolbar Status Indicator
A persistent `● AnkiMCP` item in Anki's top toolbar shows tunnel connection state (grey = off, amber = connecting, green = connected); clicking it opens the settings dialog. It's shown by default. Set `show_toolbar_indicator` to `false` to hide it (takes effect after an Anki restart).
```json
{
"show_toolbar_indicator": false
}
```
### Sync Tooltip
When an AI client triggers a sync, the addon shows a brief, non-modal tooltip in Anki's UI as the sync starts and finishes (e.g. `AnkiMCP: syncing…`, `AnkiMCP: sync complete`). This is the only visual cue for these otherwise-silent background syncs. It's shown by default. Set `show_sync_tooltip` to `false` to suppress it.
```json
{
"show_sync_tooltip": false
}
```
### Diagnostic File Logging
Set `log_to_file` to `true` to write a rotating log to `user_files/ankimcp.log` (~1 MB per file, 3 backups). It's **off by default**. When enabled, the addon records a startup diagnostics snapshot (addon/Anki/Qt/Python versions plus the live provenance of shared libraries like `pydantic`, `mcp`, `protobuf`, etc.), which is the key data for diagnosing cross-add-on conflicts. Secrets — the `http_api_key`, OAuth tokens, and any `Bearer` token — are **redacted** before anything is written to disk.
```json
{
"log_to_file": true
}
```
The settings dialog (*Tools → AnkiMCP Server Settings…*) has an **Open log folder** button and a **Copy diagnostics** button (the same snapshot, formatted for pasting into a forum post). Takes effect after an Anki restart.
### Disabling Tools
Hide specific tools or actions from AI clients to reduce token usage:
```json
{
"disabled_tools": [
"sync",
"card_management:bury",
"card_management:unbury"
]
}
```
- `"tool_name"` — disables the entire tool
- `"tool_name:action"` — disables a specific action within a multi-action tool
Disabled tools are removed from the MCP schema entirely — AI clients never see them. Typos in tool/action names will produce console warnings.
### Destructive Tools (Opt-In)
Tools or actions classified as destructive (high-risk operations) are **hidden from AI clients by default**. To expose them, add them to the `enabled_destructive_tools` allow-list:
```json
{
"enabled_destructive_tools": [
"some_destructive_tool",
"some_tool:destructive_action"
]
}
```
- `"tool_name"` — opts in an entire destructive tool
- `"tool_name:action"` — opts in a destructive action within a multi-action tool (a whole-tool entry does not implicitly opt in its destructive actions)
- `disabled_tools` still applies on top — an opted-in tool can still be disabled
- Entries that don't match anything, or match a non-destructive tool/action, produce console warnings
This is server-side enforcement: until opted in, destructive tools are absent from the MCP schema, so even a misbehaving client cannot call them. Currently shipped destructive entries: `change_note_type` (whole tool — rewrites every selected note's field layout) and `model_fields:remove` (action — permanently deletes a field and its content on every note of the type).
### Custom Path
Set `http_path` to serve the MCP endpoint under a custom path. Useful when exposing Anki via a tunnel (Cloudflare, ngrok) to avoid a fully open endpoint:
```json
{
"http_path": "my-secret-path"
}
```
The server will be accessible at `http://localhost:3141/my-secret-path/` instead of the root. Leave empty for default behavior.
> **Note:** A custom path alone is not enough to expose the HTTP server through a tunnel or reverse proxy. You must also populate `http_allowed_hosts`/`http_allowed_origins`, or requests are rejected (`421` for a non-loopback `Host`, `403` for a non-loopback `Origin`) — see [Allowed Hosts and Origins (DNS-Rebinding Protection)](#allowed-hosts-and-origins-dns-rebinding-protection).
## Security
The local HTTP server accepts requests only from loopback `Host`/`Origin` values by default, so **ordinary local use needs no setup** — localhost clients work out of the box. The sections below cover the available hardening layers (DNS-rebinding allowlist, optional API key, CORS, and media-import validation) for when you expose the server beyond your machine.
**Upgrading from ≤ 0.20.0:** if you reach the server through a tunnel, reverse proxy, or by binding to `0.0.0.0`, requests now arrive with a non-loopback `Host` and are rejected with `421` until you allowlist that host — see [Allowed Hosts and Origins (DNS-Rebinding Protection)](#allowed-hosts-and-origins-dns-rebinding-protection). (Browser clients additionally need their `Origin` allowlisted, otherwise `403`.) Plain localhost setups are unaffected.
### Allowed Hosts and Origins (DNS-Rebinding Protection)
The HTTP server enables DNS-rebinding protection with a built-in loopback allowlist (copied verbatim from the MCP SDK's own default):
- Hosts: `127.0.0.1:*`, `localhost:*`, `[::1]:*`
- Origins: `http://127.0.0.1:*`, `http://localhost:*`, `http://[::1]:*`
Ordinary localhost clients therefore work out of the box. Two details of these defaults are worth knowing:
- The `:*` patterns match a **host with a port** (`localhost:3141` ✓). A port-less `Host` header — which browsers and clients send only when the server runs on port 80 — does not match, so a non-default `"http_port": 80` needs `"localhost"`/`"127.0.0.1"` added to `http_allowed_hosts` explicitly.
- The default origins are **`http://`-only**. An `https://localhost` origin is not covered and must be added to `http_allowed_origins`. (Requests with no `Origin` header at all — the normal case for non-browser MCP clients — always pass.)
If you expose the HTTP server through a tunnel or reverse proxy (e.g. ngrok, Cloudflare), requests arrive with a non-loopback `Host` (rejected with `421`) — and, for browser clients, a non-loopback `Origin` (rejected with `403`) — unless you extend the allowlist:
```json
{
"http_allowed_hosts": ["myapp.ngrok.io", "myapp.ngrok.io:443"],
"http_allowed_origins": ["https://myapp.example"]
}
```
- `http_allowed_hosts` — `Host`-header values **without** a scheme (e.g. `"myapp.ngrok.io"` or `"myapp.ngrok.io:443"`)
- `http_allowed_origins` — full origins **with** a scheme (e.g. `"https://myapp.example"`)
Both lists are appended to the built-in loopback defaults (the defaults are not replaced). Changing these requires an Anki restart, consistent with the other `http_*` settings.
> DNS-rebinding vulnerability reported by [avishaigo-commits](https://github.com/avishaigo-commits).
### API Key (Optional HTTP Auth)
`http_api_key` adds an optional shared-secret auth layer on top of the HTTP transport (AnkiConnect-style). It is **empty by default**, which disables the layer and leaves the default behavior unchanged. When set to a non-empty value, **every** HTTP request must send an `Authorization: Bearer <key>` header matching that value, or it is rejected with `403`:
```json
{
"http_api_key": "a-long-random-secret-key"
}
```
Configure your client to send the header. With Claude Code:
```bash
claude mcp add anki --transport http http://127.0.0.1:3141/ --header "Authorization: Bearer a-long-random-secret-key"
```
With `mcp-remote` (e.g. Claude Desktop):
```json
{
"mcpServers": {
"anki": {
"command": "npx",
"args": [
"mcp-remote",
"http://127.0.0.1:3141",
"--header",
"Authorization: Bearer a-long-random-secret-key"
]
}
}
}
```
Notes:
- **HTTP-only.** The key applies only to the local HTTP transport. The tunnel does its own OAuth login and is unaffected by `http_api_key`.
- **Complementary, not a replacement.** It sits alongside DNS-rebinding protection (Allowed Hosts and Origins) — it does not replace it. Both layers apply independently.
- **Pairs naturally with `http_path` + `http_allowed_hosts`** when exposing the HTTP server through a tunnel or reverse proxy: a secret path obscures the endpoint, the allowlist permits the proxy host, and the API key authenticates each request.
- Use a long, random key (at least 16 characters). Leading/trailing whitespace is stripped from the presented token, so a configured key with surrounding whitespace will never match.
- Changing this requires an Anki restart, consistent with the other `http_*` settings.
### CORS Configuration
To allow browser-based MCP clients (like web-hosted MCP Inspector), add allowed origins:
```json
{
"cors_origins": ["https://inspector.example.com", "http://localhost:5173"]
}
```
Use `["*"]` to allow all origins (not recommended for production).
> **Note:** A browser origin allowed via `cors_origins` must **also** be added to `http_allowed_origins`. CORS and the DNS-rebinding allowlist are separate layers: even with CORS configured, a non-loopback `Origin` is rejected with `403` by DNS-rebinding protection unless it is in `http_allowed_origins`.
The `cors_expose_headers` setting controls which response headers browsers can read. The default (`mcp-protocol-version`) lets browser-based MCP clients negotiate the protocol version. Since v0.16.0 the server runs in stateless mode, so `mcp-session-id` is no longer emitted and no longer needs to be exposed.
### Media Security
> Thanks to **[Hideaki Takahashi](https://github.com/Koukyosyumei)** (Columbia University) for responsibly disclosing the media path traversal vulnerability.
The `store_media_file` tool validates all inputs to prevent path traversal and SSRF attacks:
- **File paths** are restricted to media files only (images, audio, video) via MIME type checking
- **URLs** must use `http://` or `https://` and cannot target private/internal networks
- **Filenames** are sanitized to remove path traversal sequences
Optional hardening via config:
```json
{
"media_import_dir": "/Users/me/anki-media",
"media_allowed_types": ["application/pdf"],
"media_allowed_hosts": ["192.168.1.50", "my-nas.local"]
}
```
- `media_import_dir` — restrict file path imports to this directory tree (empty = no restriction)
- `media_allowed_types` — allow additional MIME types beyond image/audio/video
- `media_allowed_hosts` — allow specific hosts to bypass private network blocking
## Available Tools
Every tool parameter carries a short `description` in the MCP input schema (since 0.29.0), so clients that show or feed the schema to the model get argument shapes without trial and error. Nested action parameters of multi-action tools are described on their Pydantic models and appear under `$defs`.
**Upgrading from ≤ 0.27.x:** `get_due_cards` no longer returns `back` unless you pass `include_answer=true`, and `front` is now the rendered question HTML rather than the raw field. Clients that read `back` from this tool need the flag.
### Essential Tools
| Tool | Description |
|------|-------------|
| `sync` | Synchronize collection with AnkiWeb (asynchronous job: `sync()` starts a sync, `sync(job_id)` polls its status, `sync(job_id, resolve=...)` resolves a full-sync conflict) |
| `list_decks` | List all decks in the collection |
| `create_deck` | Create a new deck |
| `find_notes` | Search for notes using Anki's search syntax. `include_first_field=true` adds a `noteLabels` array (`{noteId, firstField, truncated, fullLength}`) so you can search and label notes in one call |
| `notes_info` | Get detailed information about notes. `excerpt_chars` caps every field value at that many characters, marking each with `truncated`/`fullLength` — a cheap way to survey many notes |
| `add_note` | Add a new note to a deck |
| `add_notes` | Batch-add up to `max_notes_per_batch` notes (default 100) sharing the same deck and model. Uses Anki's native batch API for atomic undo. Supports partial success — individual failures don't affect others |
| `card_management` | Manage cards with 9 actions: `reposition` (set learning order), `change_deck` (move between decks), `bury`/`unbury` (hide until tomorrow), `suspend`/`unsuspend` (indefinitely exclude from review), `set_flag` (color flags 0-7), `set_due_date` (reschedule with days DSL), `forget_cards` (reset to new) |
| `tag_management` | Manage tags with 6 actions: `add_tags`/`remove_tags` (bulk add/remove on notes), `replace_tags` (swap one tag for another), `get_tags` (list all, or scoped to a deck via the optional `deck` param — distinct tags on notes with a card in that deck, subdecks included), `clear_unused_tags` (remove orphans), `batch_tags` (apply multiple add/remove operations in one call, partial success) |
| `filtered_deck` | Filtered deck lifecycle with 5 actions: `create_or_update` (create or modify a filtered deck from 1-2 search terms — Anki's hard limit), `rebuild` (repopulate), `empty` (return cards to home decks), `delete` (return cards, then remove the deck), `get_info` (read-only inspection of up to 50 deck IDs per call — returns search terms, limit, order, reschedule flag and card count; non-filtered decks come back with `is_filtered=false`, unknown IDs are skipped and counted in `not_found`) |
| `update_note_fields` | Update fields of existing notes. Two modes: full replace, or patch via `old_str`/`new_str` (find-and-replace within a field; must match exactly once or nothing is written) |
| `update_notes` | Batch-update fields of multiple notes in one atomic undo step (single backend call). Validates every entry first; supports partial success up to `max_notes_per_batch` |
| `delete_notes` | Delete notes from the collection |
| `get_due_cards` | Get next due card for review (supports `skip_images`/`skip_audio` for voice mode). Returns the rendered question only; the answer is omitted unless `include_answer=true` |
| `cards_stats` | Bulk per-card scheduling stats for a deck (incl. subdecks): type/queue/interval/tags/dueToday, paginated. FSRS-independent, compact payload for analytics |
| `present_card` | Get card content for review |
| `rate_card` | Rate a card after review (Again/Hard/Good/Easy) |
| `model_names` | List available note types |
| `model_field_names` | Get field names and descriptions for a note type |
| `model_styling` | Get CSS styling for a note type. `include_latex=true` also returns the LaTeX preamble (`latex_pre`, `latex_post`, `latex_svg`) |
| `update_model_styling` | Update CSS styling for a note type — full replace, or patch via `old_str`/`new_str` (must match exactly once). Can also write the LaTeX preamble (`latex_pre`/`latex_post`/`latex_svg`) |
| `model_templates` | Read the Front/Back HTML templates for each card type in a note type |
| `update_model_templates` | Update Front/Back template HTML — full replace, or patch via `old_str`/`new_str` (must match exactly once). Rejects unrecognized keys (case-sensitive) and unknown template names up front, applying all edits atomically — a failed call leaves the model unchanged |
| `model_fields` | Manage fields on an existing note type: `add` (optionally at a 0-based index), `rename` (preserves content; card templates are **not** auto-updated), `reposition` (reorder). A `remove` action also exists but is [destructive](#destructive-tools-opt-in) — hidden unless opted in via `enabled_destructive_tools`. `add`, `remove` and `reposition` change field ordinals and force a one-way full sync; a pure `rename` does not. Every result carries `will_force_full_sync`, the collection's actual (sticky, collection-wide) state after the write |
| `create_model` | Create a new note type |
| `change_note_type` | Move existing notes to a different note type, remapping fields by name (`{old field: new field or null}`). [Destructive](#destructive-tools-opt-in) — hidden unless opted in via `enabled_destructive_tools`. Two-step flow: `dry_run=true` returns the resolved plan (mapping, dropped fields, cards removed), then the same call with `dry_run=false` **and** `confirm=true` applies it. All notes must share one source note type |
| `store_media_file` | Store a media file (image/audio) via base64, file path, or URL. File paths are validated against a media-type allowlist; URLs are checked for SSRF |
| `get_media_files_names` | List media files matching a pattern |
| `delete_media_file` | Move a media file to Anki's trash (recoverable via Check Media) |
### FSRS Tools
| Tool | Description |
|------|-------------|
| `get_fsrs_params` | Get FSRS scheduler parameters for deck presets |
| `set_fsrs_params` | Update FSRS parameters (weights, desired retention, max interval) |
| `get_card_memory_state` | Get FSRS memory state (stability, difficulty, retrievability) for cards |
| `optimize_fsrs_params` | Run FSRS parameter optimization using Anki's built-in optimizer |
### GUI Tools
These tools interact with Anki's user interface:
| Tool | Description |
|------|-------------|
| `gui_browse` | Open the card browser with a search query |
| `gui_add_cards` | Open the Add Cards dialog |
| `gui_edit_note` | Open the note editor for a specific note |
| `gui_current_card` | Get info about the currently displayed card |
| `gui_show_question` | Show the question side of current card |
| `gui_show_answer` | Show the answer side of current card |
| `gui_select_card` | Select a specific card in the reviewer |
| `gui_deck_browser` | Navigate to deck browser |
| `gui_undo` | Undo the last operation |
### Resources
| Resource | URI | Description |
|----------|-----|-------------|
| `system_info` | `anki://system-info` | Anki version, profile, and scheduler info |
| `query_syntax` | `anki://query-syntax` | Anki search query syntax reference |
| `schema` | `anki://schema` | Data model documentation (entities, fields, relationships) |
| `stats_today` | `anki://stats/today` | Today's study statistics |
| `stats_forecast` | `anki://stats/forecast` | 30-day review forecast |
| `stats_collection` | `anki://stats/collection` | Overall collection statistics |
| `fsrs_config` | `anki://fsrs/config` | FSRS configuration summary and parameters |
### Prompts
| Prompt | Description |
|--------|-------------|
| `review_session` | Guided review session workflow. Args: `deck_name` (default `Default`), `card_limit` (default 20), `review_style` — `interactive`, `quick`, or `voice` |
| `twenty_rules` | Dr. Piotr Woźniak's *Twenty Rules of Formulating Knowledge* (SuperMemo), as card-authoring guidance for the assistant. No arguments |
## Requirements
- **Anki 25.07 or later** (ships Python 3.13)
- Anki 25.02 and earlier ship Python 3.9, which is **not supported** — the MCP SDK requires Python 3.10+ ([#8](https://github.com/ankimcp/anki-mcp-server-addon/issues/8))
## Architecture
The addon runs an MCP server in a background thread with two independent transports: local HTTP (FastMCP + uvicorn) and remote tunnel (WebSocket relay with in-memory transport). Both share the same FastMCP server instance. All Anki operations are bridged to the main Qt thread via a queue system, following the same proven pattern as AnkiConnect.
For details, see [Anki Add-on Development Documentation](https://addon-docs.ankiweb.net/).
## Development
### Running E2E Tests
E2E tests run against a real Anki instance in Docker using [headless-anki](https://github.com/ankimcp/headless-anki).
```bash
# Install test dependencies
python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
# Build the addon
./package.sh
# Start Anki container
cd .docker && docker compose up -d && cd ..
# Run tests (waits for server automatically)
pytest tests/e2e/ -v
# Stop container
cd .docker && docker compose down
```
Or use the Makefile shortcuts:
```bash
make e2e # Build, start container, run tests, stop
make e2e-up # Just start container
make e2e-test # Just run tests
make e2e-down # Just stop container
```
### CI
E2E tests run automatically on push to any branch and on PRs to `main`. See `.github/workflows/e2e.yml`.
## License
AGPL-3.0-or-later
## Links
- [ankimcp.ai](https://ankimcp.ai) - Project homepage
- [MCP Protocol](https://modelcontextprotocol.io/) - Model Context Protocol specification
- [Anki Add-on Docs](https://addon-docs.ankiweb.net/) - Official Anki addon development documentation
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessResponsive