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>
This commit is contained in:
106
src/server.py
Normal file
106
src/server.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user