Spaced repetition for AI agents — CLI and MCP server for Anki flashcards
Spacedrep demonstrates solid tool design with 22 well-named, verb-prefixed tools covering flashcard CRUD, review workflows, and statistics. All tools have descriptions (avg ~180 chars, within baseline 194). Input schemas are present with type declarations and parameter descriptions. However, output schemas are not explicitly documented in the source, responses are inferred from return type hints. Error handling uses structured JSON via _handle_errors wrapper, which is good, but recovery guidance is minimal. Some parameter descriptions could be more prescriptive about constraints (e.g., rating must be 1-4, not just 'Rating: 1=again...'). Tool composition is strong: add_card, add_reversed_card, add_cloze_note, add_cards_bulk are well-separated by intent. Pagination is implemented (limit/offset in list_cards). No security issues detected, no secrets in params, path validation present. Overall: above-average community server with room for output schema documentation and richer error recovery hints.
Add a new basic flashcard (one Q→A direction). Re-adding the same question to the same deck updates the existing card instead of creating a duplicate. For bidirectional vocabulary, use add_reversed_card. For fill-in-the-blank over a sentence, use add_cloze_note. For batch creation, use add_cards_bulk. Dedup matches the question value used at note *creation* — if you later edit the Question via update_card and then re-add with the new text, you will get a separate note, not an update. Raises cross_model_collision if a reversed note with the same (question, deck) already exists; delete that card first to convert it to basic. Tags are space-separated. Use :: for hierarchy (e.g. 'foundations::rag'). Returns the card ID, deck name, and was_update flag.
Add multiple flashcards in one transaction. Each object has: question, answer, deck (optional), tags (optional), type (optional: 'basic', 'cloze', or 'reversed'). When type='cloze', question contains cloze text and answer is ignored. When type='reversed', two cards are created from one shared note (Q→A and A→Q). A cross_model_collision error (basic ↔ reversed with the same (question, deck)) aborts the entire batch — no partial inserts. Intra-batch collisions are caught too. The error includes `batch_index` (0-based) of the failing item, so the caller can pinpoint which entry collided without re-running.
Add a cloze deletion note that expands into multiple flashcards. Use {{c1::answer}} syntax. Each cloze number creates one card. Example: '{{c1::Ottawa}} is capital of {{c2::Canada}}' creates 2 cards. Dedup keys on the exact cloze text. To edit an existing cloze note, use update_cloze_note — re-adding with different text creates a new note and leaves old cards as orphans.
Output schemas not explicitly documented. Return types are inferred from function signatures (e.g., dict[str, Any]) but no structured schema is visible in tool registration. LLMs cannot plan downstream calls without knowing response field names and types.
Error recovery guidance is minimal. _handle_errors wrapper returns structured JSON with error_code, message, suggestion, but suggestions are generic ('Check the database and try again'). Should include specific recovery steps (e.g., 'Try search_cards() to find the card ID' or 'Call list_decks() to see available deck names').
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | D | 59 | 2026-07-28+ | v2 |
Add a reversed card pair. Creates 2 cards from 1 shared note: one Q→A and one A→Q. Use for vocabulary or any concept where you want to be quizzed in both directions. Re-adding with the same (question, deck) updates the existing note in place and preserves review history on both cards. Dedup matches the question value used at note *creation* — if you later edit the Question via update_card and then re-add with the new text, you will get a separate note, not an update. To edit just one side, use update_card on the specific card_id — it is template-aware and edits the correct card's front/back. Raises cross_model_collision if a basic note with the same (question, deck) already exists; delete that card first to convert it to reversed. Returns note_id, card_ids (both directions), card_count (=2), deck.
Delete a card and all its review history. Supports dry-run to preview what would be deleted without making changes.
Get full card detail by ID, including question, answer, scheduling state (due, interval, factor), FSRS parameters (difficulty, stability, retrievability), and recent review history with user feedback.
Get count of due cards by state (new, learning, review, relearning).
Show current FSRS scheduler status including parameters and optimization history.
Get the next due card for review, optionally filtered by deck, tags, state, search text, or due date. Returns card ID, question, answer, deck, tags, scheduling state, FSRS parameters, and recent review history with user feedback. Returns null if no cards are due.
Get overall database statistics including total cards, decks, reviews, and retention metrics.
Get statistics for a specific review session, including review count, ratings distribution, and time spent.
Initialize the database. Creates tables if they don't exist.
List cards with optional filters by deck, tags, state, search text, due dates, creation dates, review dates, FSRS parameters (difficulty, stability, retrievability), and suspension/burial status. Supports pagination.
List all decks with card counts and due counts.
List all unique tags in the database.
Open an .apkg deck file as the working database. Replaces the current database (with safety check unless --force is used). Returns card count and deck metadata.
Optimize FSRS parameters from review history. Requires 512+ reviews for meaningful optimization. Supports dry-run to compute params without persisting, and reschedule option to reschedule all cards with new params.
Preview what each rating (1-4) would produce for a card without submitting. Shows predicted new state, due date, interval, and FSRS parameters for each rating.
Save the entire collection as an Anki .apkg file. Returns card count and export path.
Submit a review for a card with a rating (1=again, 2=hard, 3=good, 4=easy), optional user answer text, optional feedback note, and optional session ID. Updates card scheduling via FSRS, records review in history, and returns updated card state.
Update a card's question and/or answer. Template-aware: for reversed cards, correctly updates the front/back of the specific card_id. Preserves review history and scheduling.
Update an existing cloze deletion note. Provide any card_id from the note to identify it, then the new cloze text. All cards from the note are updated in place, preserving review history.
Parameter constraints not fully prescriptive. 'rating' parameter in submit_review says 'Rating: 1=again, 2=hard, 3=good, 4=easy' but does not explicitly state 'must be integer 1-4' or use an enum. LLMs may pass invalid values (0, 5, 'good') without schema validation.
Destructive operations (delete_card, open_deck, optimize_parameters with reschedule=true) lack explicit confirmation/dry-run patterns in descriptions. While delete_card and optimize_parameters offer dry_run flags, the descriptions do not emphasize that these are irreversible without dry-run.
Discovery tools (list_tags, list_decks, get_due_count) have minimal descriptions. 'List all unique tags in the database' and 'List all decks with card counts and due counts' lack context on when to call them or what structure they return. Should explain: 'Call this first to see available tags before filtering list_cards().'