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, nonow(). 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
- alexa
- amazon-bedrock
- amazon-web-services
- boto3
- claude
- fastmcp
- model-context-protocol
- pytest
- python
- sm-2
- spaced-repetition
- sqlite
- starlette
- streamable-http
- uvicorn
- web-speech-api
Log in or sign up for Devpost to join the conversation.