Inspiration

Spaced repetition works. Flashcard apps mostly don't get used, because reviewing means sitting down with a screen — competing for the exact time of day you have least of.

But the thing that actually started this was noticing a flaw hiding in plain sight. SM-2, the spaced repetition algorithm every flashcard app is built on, has required a 0–5 quality score for each review since 1987. Every app collects that score the same way: it shows you the correct answer, then asks you to tap Again / Hard / Good / Easy. That is a self-assessment made at the exact moment nobody can judge themselves honestly — you have just seen the answer, so of course it looks familiar.

The algorithm has had a slot for an honest signal for thirty years. No interface could fill it, because filling it needs something that can hear a spoken answer and judge whether it meant the right thing. Voice and language models arrived together, and now it can be filled.

What it does

Recall is a self-hosted MCP server. You answer flashcards out loud — cooking, driving, walking — and a semantic grader scores what you said for meaning rather than wording, before you have seen anything.

Five tools over Streamable HTTP: add_card, next_due_card, submit_answer, get_grade, get_streak_summary.

The clearest evidence is in the calibration suite. An answer transcribed as "the capital of peru is leemah" scored 5 — a string comparison marks that wrong; the grader read the meaning. "It's definitely Rio de Janeiro" scored 0. "Honestly I have no idea" also scored 0, but the explanation says why it differs. Eleven of eleven calibration cases landed inside their expected bands on the first live run.

How I built it

  • scheduler.py — SM-2 as pure functions with an injected clock. No I/O, no now(). That is what lets a test walk a card through ten reviews and a year of scheduling in microseconds.
  • store.py — SQLite. Two tables kept deliberately apart: card state, and an append-only log of every attempt. The log is what makes streaks reconstructible and lets grader calibration be re-checked against real answers months later.
  • grader.py — Amazon Bedrock via the Converse API, temperature=0. It never raises: every failure path returns an unscored result.
  • server.py — FastMCP. No business logic, just translation and speech shaping.
  • sim/index.html — a browser harness with a hand-written MCP client, served from the server's own origin so it drives the identical endpoint Alexa+ would.

Built test-first throughout. 80 tests, plus an opt-in suite that calls the live model.

Challenges I ran into

The latency budget contradicts the platform. The Alexa+ MCP Toolkit requires a round-trip tool latency under 500 ms. I measured every candidate on a sixteen-token prompt: Nova Lite 557 ms, Haiku 4.5 956 ms, Sonnet 4.6 1054 ms, Sonnet 4.5 1510 ms. Every model is over budget before the prompt is even real — including Amazon's own smallest.

I could have downgraded the grader to fit. That would optimise the wrong thing: it degrades the one component the entire project rests on, to satisfy a budget a two-call split satisfies anyway. So submit_answer records the answer and returns in 18 ms, and get_grade collects the verdict. It also turns out to be better conversation — "let me think", then an answer, is how people talk; four seconds of silence is a fault.

A model that exists but cannot be called. anthropic.claude-sonnet-5 is returned by ListFoundationModels and refuses to invoke, with an error naming no cause and offering no self-service remedy. Three hypotheses fitted that message equally well. Establishing that it was one specific model — not the key, the region, the use-case form, or Anthropic access in general — took a five-model probe. That probe is also where the latency numbers above came from, so the wasted afternoon produced the best evidence in the project.

Silence is a terrible failure mode. Eleven live calibration calls came back "grading unavailable" and I lost time to it, because the grader swallows errors by design — a failed grade must never break a review. The cause was a missing credential. It now logs why.

What I learned

The 0–5 slot is the whole idea, and it was already there. I did not invent a scoring scale; I found a thirty-year-old input that has always been filled dishonestly and filled it from outside. The best ideas are often a missing interface rather than a missing algorithm.

Design rules earn their keep at the boundaries. The rule I am most glad I wrote down early: a speech-recognition failure is not a memory failure. An empty or unintelligible transcript is "no answer given", never a wrong answer, and the card's schedule is left untouched. There is a test asserting the card is byte-identical afterwards. Without that rule, a noisy kitchen would quietly corrupt months of scheduling data — and every test would still pass.

Read CONTRIBUTING.md before writing the patch. For the open-source contribution I nearly opened a pull request that would have been auto-closed, because FastMCP requires enhancements to start as an issue with a design proposal. Reading their process first changed the entire plan.

What's next

Document ingestion was deliberately cut to protect the time that went into grading quality — it is the obvious next step. Beyond that: cards with screens (Alexa+ devices have them), and using the review log to detect which cards are badly written rather than badly remembered, since a card everyone fails is usually a bad question.

Built With

Share this project:

Updates

Submission history