Files
anki-mcp/src/server.py
joungmin f0a3eaeec5 feat: initial implementation of anki-mcp MCP server
Implements a FastMCP server that wraps AnkiConnect (localhost:8765)
to expose Anki flashcard management as Claude MCP tools.

Tools provided:
- list_decks: list all deck names
- create_deck: create nested decks (:: separator)
- add_card: add Basic flashcard with optional tags
- get_due_cards: fetch today's due cards with optional deck filter
- update_card: edit front/back of existing note
- update_deck: rename deck (move-cards approach)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 07:42:31 +09:00

107 lines
2.8 KiB
Python

"""Anki MCP server — manage Anki flashcards via AnkiConnect."""
import os
import sys
# sys.path injection for `mcp dev` compatibility
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _root not in sys.path:
sys.path.insert(0, _root)
from dotenv import load_dotenv
load_dotenv(os.path.join(_root, ".env"))
from mcp.server.fastmcp import FastMCP
from src import anki
mcp = FastMCP(
name="anki",
instructions=(
"Anki flashcard manager via AnkiConnect. "
"Add cards to decks, review due cards, manage categories. "
"Requires Anki to be running with the AnkiConnect add-on (code: 2055492159)."
),
)
@mcp.tool()
def list_decks() -> list[str]:
"""List all Anki deck names."""
return anki.list_decks()
@mcp.tool()
def create_deck(deck_name: str) -> dict:
"""Create a new Anki deck (or return existing deck ID).
Args:
deck_name: Deck name. Use '::' separator for nested decks (e.g. 'Korean::Vocabulary').
"""
deck_id = anki.create_deck(deck_name)
return {"deck_name": deck_name, "deck_id": deck_id}
@mcp.tool()
def add_card(
deck_name: str,
front: str,
back: str,
tags: list[str] = [],
) -> dict:
"""Add a Basic flashcard to a deck.
Args:
deck_name: Target deck name (must already exist or be created first).
front: Front (question) side of the card.
back: Back (answer) side of the card.
tags: Optional list of tags to attach to the card.
"""
note_id = anki.add_note(deck_name, front, back, tags)
return {"note_id": note_id, "deck_name": deck_name, "front": front, "back": back}
@mcp.tool()
def get_due_cards(deck_name: str = "", limit: int = 20) -> list[dict]:
"""Get today's due cards for review.
Args:
deck_name: Filter by deck name. Leave empty to get due cards from all decks.
limit: Maximum number of cards to return (default 20).
"""
return anki.find_due_cards(deck_name, limit)
@mcp.tool()
def update_card(note_id: int, front: str = "", back: str = "") -> dict:
"""Edit an existing card's Front and/or Back content.
Args:
note_id: The note ID to update (obtained from add_card or get_due_cards).
front: New front text. Leave empty to keep existing.
back: New back text. Leave empty to keep existing.
"""
anki.update_note_fields(note_id, front, back)
return {"note_id": note_id, "updated": {"front": bool(front), "back": bool(back)}}
@mcp.tool()
def update_deck(deck_name: str, new_name: str) -> dict:
"""Rename an existing deck.
Args:
deck_name: Current deck name.
new_name: New deck name.
"""
anki.rename_deck(deck_name, new_name)
return {"old_name": deck_name, "new_name": new_name}
def main() -> None:
mcp.run()
if __name__ == "__main__":
main()