Recall
Allows users to subscribe to an ICS feed that mirrors due review dates onto Google Calendar.
Allows agents to log LeetCode problems, track solve attempts, and manage spaced-repetition reviews with grades and confidence.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Recalllog the problem we just solved"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.

The idea
A flashcard app asks "did you remember this?" and takes your word for it. That question is useless for algorithms, because the thing you need to recall isn't a fact — it's the approach: which pattern applies, why it works, and what it costs.
So Recall does two things differently.
The grade is derived, not self-reported. You finish a tutoring session and paste (or auto-log) a summary of what happened: hints used, how fast the approach came, confidence before and after, what went wrong. Recall turns those signals into an FSRS grade itself. You never rate your own memory, because people are bad at it — and the calibration chart exists to prove exactly how bad.
flowchart LR
sig["<b>What happened in the session</b><br/>hints used · recall speed<br/>confidence before / after<br/>mistakes made"]
tutor{"did the tutor<br/>suggest a grade?"}
use["<b>use it</b><br/>auditable, a human judged it"]
derive["<b>deriveGrade()</b><br/>infer from the signals"]
grade(["<b>again</b> · <b>hard</b> · <b>good</b> · <b>easy</b>"])
fsrs["<b>applyReview()</b><br/>updates the FSRS-6 card:<br/>stability · difficulty · reps"]
due(["<b>next due date</b>"])
out["Today queue · calendar feed<br/>14-day forecast · catch-up plan"]
sig --> tutor
tutor -- "yes" --> use
tutor -- "no" --> derive
use --> grade
derive --> grade
grade --> fsrs --> due --> out
classDef sigcls fill:#3b4a63,stroke:#232c40,color:#ffffff
classDef gradecls fill:#f0b429,stroke:#a8761c,color:#161616
classDef fsrscls fill:#2f6f4f,stroke:#1d4733,color:#ffffff
class sig sigcls
class grade,due gradecls
class fsrs fsrsclsA review is a recall, not a re-read. The answer ships hidden. You say the pattern, the invariant and the complexity out loud first, then reveal. The recommender is built around the same rule: when it suggests your next problem it deliberately withholds the pattern, because recognising it is the skill being trained.
Related MCP server: recall
The loop
Solve a problem with your Claude tutor. The session ends with a Problem Log.
Log it — paste it, or let Claude call the MCP tool directly. ~15 seconds.
Each morning Today shows what's due. Two minutes per card: name the pattern, state the invariant, give the complexity, reveal, grade honestly.
Due dates also mirror onto your calendar, so the queue finds you even when you don't open the app.
Architecture
Three ways in, one way to write, one scheduler. Bold arrows mutate; dotted arrows only read.
flowchart LR
chat["Claude<br/>tutoring session"]
browser["Browser"]
calapp["Calendar app"]
subgraph edge["Next.js 16 — every request passes src/proxy.ts"]
direction TB
mcp["<b>/api/mcp</b><br/>7 tools · bearer token"]
pages["<b>Today · Review</b><br/><b>Library · Insights</b><br/>server components"]
rest["<b>/api/problems</b><br/><b>/api/reviews</b>"]
ics["<b>/api/calendar/….ics</b><br/>token in URL"]
end
subgraph core["src/lib — the only two functions that write"]
direction TB
save["<b>saveParsedSummary()</b><br/>new solve or re-solve"]
logr["<b>logReview()</b><br/>a graded review"]
sched["<b>applyReview()</b><br/>ts-fsrs · FSRS-6"]
end
db[("<b>Drizzle ORM</b><br/>Turso libSQL deployed<br/>better-sqlite3 local")]
chat --> mcp
browser --> pages
calapp --> ics
pages --> rest
mcp ==> save
mcp ==> logr
rest ==> save
rest ==> logr
save ==> sched
logr ==> sched
sched ==> db
pages -.-> db
ics -.-> db
mcp -.-> db
classDef client fill:#f0b429,stroke:#a8761c,color:#161616
classDef writer fill:#2f6f4f,stroke:#1d4733,color:#ffffff
classDef store fill:#3b4a63,stroke:#232c40,color:#ffffff
class chat,browser,calapp client
class save,logr,sched writer
class db storeBuilt with Next.js 16 App Router · React 19 · TypeScript (strict) · Tailwind v4 + shadcn/ui on Base UI · Drizzle ORM · ts-fsrs (FSRS-6, long-term mode, retention 0.9, max interval 365d, first interval floored at 2 days).
The shape worth noticing: exactly two functions in the whole codebase write
to the database — saveParsedSummary() for a solve, logReview() for a
graded review — and both reach the scheduler through the same applyReview().
The MCP tool and the web form are two doors into one funnel, so logging from a
Claude session and logging from the browser cannot drift into producing
different rows.
Where things live
src/
proxy.ts passphrase gate; MCP + .ics carry their own tokens
app/
page.tsx Today — due queue, streak, forecast
review/ the answer-hidden review player
log/ library/ insights/
api/
mcp/ MCP server, 7 tools (+ /[token] for claude.ai)
calendar/[token]/ dynamic .ics feed
problems/ reviews/ parse/ settings/ backlog/
lib/
data.ts every query + the two write paths
fsrs.ts grade rubric and FSRS scheduling
recommend.ts what to solve next (pattern-blind on purpose)
parser.ts Problem Log → structured summary
patterns.ts canonical pattern vocabulary + aliases
records.ts streaks, freezes, personal records
coach.ts weekly report data
db/
schema.ts 9 tables, Drizzle
index.ts runtime driver choice
drizzle/ migration files (0000_baseline.sql onward)
scripts/ 4 test suites, backup, seeding, plan builderDecisions worth reading
The grade is evidence, not a vote |
|
Answer-hiding as a type |
|
Two doors, one funnel |
|
One schema, two drivers |
|
Day math is the product |
|
A backup that proves itself |
|
Insights

The calibration chart is the one that earns the design. Grey is perfect calibration; amber is your actual recall rate at each confidence level. Amber sitting below grey at 4/5 and 5/5 means you are overconfident — which is exactly the failure mode that makes people skip the reviews they most need, and the reason Recall never asks you to grade your own memory.
Alongside it: an activity heatmap, confidence over time, per-pattern mastery weighted toward recent grades, and the spread of review outcomes. Sparse until a few weeks of reviews have accumulated.
Run it
npm install
npm run db:migrate # create/update the schema from drizzle/
npm run seed:lists # one-time: seed the problem list and make it active
npm run dev # http://localhost:3000Works immediately on local SQLite with no accounts and no cloud setup.
Skip seed:lists and there is no active list, so the recommender has nothing
to draw from and "what should I solve next" stays empty.
These follow your
.env, not the filename above.drizzle.config.tsloads dotenv, so ifTURSO_DATABASE_URLis set (see Deploy) thendb:migrate,db:pushandimport:sheettarget the deployed database, notdata/recall.db. Unset theTURSO_*vars for a purely local run.
npm run import:sheet is a one-time historical import from the original Google
Sheet; it is not part of a normal setup.
Tests
npm test # parser, identity ladder, FSRS scheduler, timezoneNo test runner — each suite is a plain tsx script that exits non-zero on
failure. The DB-backed suites seed a scratch SQLite file in os.tmpdir() and
never touch a real database. All four run in CI
(.github/workflows/ci.yml) alongside lint, typecheck, drizzle-kit check
and a production build.
Each suite covers something that fails silently rather than loudly — the only kind of bug that survives in a single-user app:
Suite | Guards against |
| a misread log writing the wrong FSRS grade |
| the lcSlug → slug → number → bare-slug ladder drifting apart |
| the scheduler quietly returning the wrong next date |
| day-boundary math shifting when |
One thing test:fsrs documents rather than enforces: maximum_interval: 365
is a soft cap. LongTermScheduler.next_interval clamps each grade and then
enforces again < hard < good < easy, so a fully saturated card settles at
365/366/367/368 days rather than 365. Harmless, but surprising if unexplained.
The Problem Log template
Anything close to this parses perfectly (free-form text also works, best-effort;
set ANTHROPIC_API_KEY to parse messy summaries with Claude):
## Problem Log — 49. Group Anagrams
URL / Difficulty / Patterns: https://leetcode.com/problems/group-anagrams/ · Medium · arrays-hashing
Solved: with 2 hints Recall speed: slow
Confidence: before 2/5 → after 3/5 Time: approach 12 min · code 20 min
Fundamentals missing: definition of anagram
Issues: wrong pattern; code was messy
Brute force: compare every pair — O(n²k) / O(1)
Optimal: bucket by sorted-string key — O(nk log k) / O(nk)
Key insight: all anagrams share one canonical form
Tips: char-count tuple avoids the sort
Revise: yes — pattern recognition still weak
Suggested grade: hard — needed hints for the invariantGrades: again (couldn't do it) · hard (hints/slow/low confidence) ·
good (solo with friction) · easy (instant + optimal). The tutor's
suggested grade wins; otherwise Recall derives it from the signals.
MCP — let the tutoring chat log problems itself
Recall is also an MCP server at /api/mcp. Tools: get_next_action (call it
first — it returns the day's plan), add_problem, get_due_reviews,
log_review, get_stats, get_weekly_report_data, save_coach_report. With
the dev server running, connect Claude Code:
claude mcp add --transport http recall http://localhost:3000/api/mcp --header "Authorization: Bearer dev"(Replace dev if you set MCP_TOKEN.) Then at the end of a tutoring session,
say "log it" — Claude calls add_problem directly, zero copy-paste. For
claude.ai custom connectors the endpoint must be publicly reachable, i.e.
after deployment.
get_next_action omits the recommended problem's pattern from its reply on
purpose: tool results are visible to the user in most MCP clients, and the
pattern is the answer.
The tutor prompt in docs/tutor-prompt.md is written to use these tools when available and fall back to the paste template otherwise.
Library and calendar

Every problem keeps its approaches, key insight and full review journal. The detail page reuses the review player's reveal, so browsing your own notes doesn't spoil a problem still sitting in the queue.
For the calendar, subscribe once in Google Calendar: Other calendars → From
URL → http://<host>:3000/api/calendar/dev.ics (set CALENDAR_TOKEN to
change the secret). Overdue reviews appear on today. Google only refreshes
subscribed feeds every 12–24h, which is fine for multi-day intervals; Apple
Calendar refreshes faster. While the app only runs on localhost the feed can't
be reached by Google's servers — use the in-app Today queue as primary (it is
anyway), or deploy first.
Backups
npm run backup # verified snapshot
npm run backup:verify backups/<file>.db # re-verify an existing oneA snapshot is only a backup if it opens and makes sense, so the script reopens
what it wrote and checks integrity, foreign keys, exact row counts, and the
domain invariants that decide whether the data is actually restorable. Anything
that fails is renamed *.FAILED.db rather than kept — a backup that looks like
protection but isn't is worse than none. See
deploy/backup/README.md for scheduling and the
restore drill.
Environment (.env)
Variable | Default | Purpose |
|
| SQLite location |
|
| Secret in the ICS feed URL |
|
| Bearer token for the MCP endpoint |
| — | Enables AI parsing of free-form summaries |
| Haiku 4.5 | Model for AI parsing |
|
| Row owner (multi-user later) |
|
| Timezone all day math uses (streaks, "due today") |
| Vercel URL, else localhost | Base URL the MCP tools put in their replies |
The
devfallback forCALENDAR_TOKENandMCP_TOKENis local-only. It is revoked the momentAPP_PASSWORDis set orNODE_ENV=production, because those two routes are exempt from the login gate and a shared default would leave them open. On a gated instance, set both explicitly or the ICS feed and the MCP endpoint return "Not found" / 401. Seesrc/lib/secrets.ts.Set
APP_TIMEZONE, notTZ, to change the app's day boundary —TZis overwritten at startup (src/db/index.ts) because the deploy platform presets it to UTC.
Deploy (Vercel + Turso)
The app runs on local SQLite with zero setup; setting TURSO_DATABASE_URL
switches it to Turso (hosted libSQL — same schema, same
queries). Deployed instances should also set APP_PASSWORD, which activates
the login gate on every page and API (the MCP endpoint and ICS feed keep their
own tokens).
One-time, interactive (browser OAuth — only steps a human can do):
turso auth signupnpx vercel loginThen, from recall/:
sqlite3 data/recall.db "PRAGMA wal_checkpoint(TRUNCATE);"thenturso db create recall --from-file data/recall.db— creates the cloud DB with all existing data.turso db show recall --urlandturso db tokens create recall→ the twoTURSO_*values.npx vercel link, add env vars (TURSO_DATABASE_URL,TURSO_AUTH_TOKEN,APP_PASSWORD,CALENDAR_TOKEN,MCP_TOKEN, optionallyANTHROPIC_API_KEY), thennpx vercel --prod.
After deploy:
Calendar: Google Calendar → Other calendars → From URL →
https://<app>.vercel.app/api/calendar/<CALENDAR_TOKEN>.ics(now reachable by Google's servers, so it actually syncs).MCP from claude.ai: Settings → Connectors → Add custom connector →
https://<app>.vercel.app/api/mcpwith headerAuthorization: Bearer <MCP_TOKEN>.Note: local SQLite and Turso are now separate databases. Treat the deployed app as the source of truth; for cloud-backed local dev, put the
TURSO_*values in.env.
Data and schema notes
Every user-owned table carries
user_id, so multi-user is a migration rather than a rewrite. The three that don't are not user-scoped:list_items(owned by its list),problem_concepts(a join table), andproblems_catalog(a shared cache of public LeetCode metadata).A re-solve of an existing problem logs a review against the existing record — never a duplicate row. Identity resolves lcSlug → slug → number → exact bare slug, in one place (
findProbleminsrc/lib/data.ts).Schema changes go through migration files: edit
src/db/schema.ts, thennpm run db:generateto emit reviewable SQL intodrizzle/, thennpm run db:migrateto apply it.drizzle/0000_baseline.sqlis a baseline — it describes the schema as it already existed when migrations were adopted, and is recorded as applied, so it never re-runs against a live database.npm run db:pushstill exists but is for throwaway/scratch databases only. Against a database with real rows, push resolves a column rename or a newNOT NULLcolumn without a default by recreating the table, with no SQL to review and nothing to roll back to. Usedb:generate+db:migrate.Roadmap: concept-level scheduling (the
concepts/problem_conceptstables are declared and waiting for it); sibling-problem substitution on mature cards.
License
MIT © Shree Bohara
This server cannot be deployed
Maintenance
Related MCP Connectors
An MCP server that gives your AI access to the source code and docs of all public github repos
MCP server for RiverScript, an AI transcription platform - fetches transcripts shared via a link.
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
Related MCP Servers
- FlicenseAqualityDmaintenanceAn MCP server that provides tools to interact with the LeetCode API, enabling problem fetching, code template generation, and solution execution/submission.71-
- AlicenseAqualityCmaintenanceA zero-dependency MCP server for cross-session memory recall in Claude Code. Provides lexical search, listing, and retrieval of past session memories to avoid re-explaining context.39 npmMIT
- FlicenseNot gradedqualityDmaintenanceA tutorial and working MCP server that connects to your LeetCode profile, enabling AI assistants to check solved problems and recommend new ones by topic and difficulty.1-
- AlicenseNot gradedqualityBmaintenanceA production-ready MCP server for GitHub and competitive programming (Codeforces) that enables AI assistants to fetch user profiles, repository stats, contest history, and personalized problem recommendations.MIT