A persistent personal memory system for Claude, exposed as an MCP server. It accumulates, organizes, and surfaces a single user's knowledge over time. All data is plain Markdown in a git repo: no database, no embeddings, no RAG. Git history is the audit log and the undo button.
The idea: while you talk to Claude, it captures notes into a fast "short-term" layer. A nightly dream then distils those captures into curated long-term pages, the same way memory consolidates during sleep. You can read and edit everything through a private web console.
It is a standalone Python service: a Docker image, an HTTP MCP endpoint, and a
named volume holding the wiki. The code is path-agnostic via the WIKI_ROOT
environment variable, so it runs the same locally or on any host behind a
TLS-terminating reverse proxy.
A deliberately small surface (a small surface is easier for the model to use correctly):
prime()- call FIRST: loads the grounding context (theself/pages, then the long-term and short-term indexes) in one call.read(path)- read any file: a page, an index, or a short-term entry. Tolerant of paths and suggests alternatives on a miss.search(query_text, max_results?)- full-text search, returningpath:line: textmatches.remember(content, summary?, tags?, due?, type?)- capture into short-term memory;due/typefile dated items (todo, reminder, event) intotemporal/.GET /health- liveness probe for the Docker healthcheck (public, not a tool).
Writing to long-term memory is intentionally not an MCP tool. It happens
through the web console or the nightly dream, never from a live conversation.
This keeps structural edits deliberate. All tools honour the owner allow-list
and a path guard, so long_term/private/ is never read or searched.
- short-term (
short_term/): the open, fast-to-write layer. Everyrememberappends an entry here. - long-term (
long_term/): curated pages, including aself/section (identity, grounding) and aprivate/section never exposed over MCP. - temporal (
temporal/): dated, transient items (todo / reminder / event) that live until their due date, then are archived. A durable fact with no expiry belongs on a long-term page instead. - index: each layer has a one-line-per-page index, regenerated by code so it cannot drift from the contents.
The dream reads short-term memory plus a policy file (DREAM.md, editable in the
console) and decides how to fold captures into long-term pages. It runs as a
three-stage pipeline so no single call ever needs the whole memory (cost
scales with the number of captures, not the wiki size):
- triage - cluster and route each capture;
- decide - per unit, reading only the touched pages, choose the action;
- write - produce each page's final content and its index line.
It has two modes, set from the console (/ui/dream):
- dry-run - stops after stage 2 and writes a report to
dream_reports/, changing nothing. - execute - runs stage 3 and applies everything in one revertible git commit (writes pages, files temporal items, drops consumed short-term entries, expires past-due items). It never deletes long-term content.
A built-in scheduler thread can run the dream once a night (off /
dry-run / execute, stored in dream_schedule.json). "Once a day" is enforced
by the presence of the day's report, so restarts never double-run and a missed
night is caught up. The model is WIKI_DREAM_MODEL (defaults to Opus,
overridable per stage via _TRIAGE / _DECIDE / _WRITE) and needs
ANTHROPIC_API_KEY. The three stage prompts are editable files
(prompts/{triage,decide,write}.md) viewable at /ui/prompts; the JSON schema
is injected by code, so editing the guidance cannot break the contract.
A private, browser-facing console served by the same process:
/ui- lists every markdown file under the wiki (the structure)./ui/page/{path}- view a page rendered from markdown./ui/edit?path=...- edit a page (or create one when path is empty).POST /ui/save,POST /ui/delete- write or soft-delete, committed with amanual:prefix (soft-delete keeps history)./ui/dream,/ui/prompts- run/schedule the dream and edit its prompts.
Markdown is rendered with raw HTML disabled, so stored content cannot inject markup. Forms carry a signed CSRF token.
The server uses FastMCP's GitHubProvider (an OAuth proxy running the OAuth 2.1
- PKCE flow Claude.ai expects). On top of "any valid GitHub login", an allow-list
middleware restricts access to a single account (
WIKI_ALLOWED_GITHUB_LOGIN), so the wiki stays private to its owner. The browser console uses the same OAuth app.
| Variable | Required | Purpose |
|---|---|---|
GH_OAUTH_CLIENT_ID / GH_OAUTH_CLIENT_SECRET |
yes (prod) | GitHub OAuth; the server refuses to start without them unless auth is disabled |
WIKI_PUBLIC_URL |
prod | the public base URL of the server, e.g. https://YOUR_DOMAIN |
WIKI_ALLOWED_GITHUB_LOGIN |
yes (prod) | the single GitHub login allowed to use the wiki |
WIKI_JWT_SIGNING_KEY |
recommended | stable random value so tokens survive restarts |
ANTHROPIC_API_KEY |
for the dream | the model calls in the consolidation pipeline |
WIKI_DREAM_MODEL (+ _TRIAGE/_DECIDE/_WRITE) |
no | dream model overrides |
WIKI_ROOT |
no | where the wiki lives (default /srv/wiki) |
WIKI_AUTH_DISABLED=1 |
dev only | run open, no secrets needed |
Provide these through your deployment's environment (a .env file, container
secrets, an orchestrator, etc.). They are configuration, not committed to the
repo.
Create a GitHub OAuth App (Settings -> Developer settings -> OAuth Apps) with:
- Homepage URL:
https://YOUR_DOMAIN - Authorization callback URL:
https://YOUR_DOMAIN/(GitHub accepts any subpath, so both/auth/callbackfor Claude and/ui/auth/callbackfor the console work). Secret names must not start withGITHUB_, which GitHub reserves.
- Transport: Streamable HTTP, MCP mounted at
/mcp, listening on:8765. - Data: the wiki lives under
WIKI_ROOT(default/srv/wiki), intended to be a persistent volume. The entrypoint seeds it fromseed/only if empty, thengit inits it, so restarts never clobber data. - Public URL:
https://YOUR_DOMAIN/mcp, behind a reverse proxy that terminates TLS (Caddy, nginx, Traefik, etc.).
memory-wiki/
Dockerfile
docker-entrypoint.sh # seed-if-empty + git init, then run the server
pyproject.toml
src/wiki_server/
server.py # FastMCP app: tools + /health
store.py, query.py # read / search / write over the markdown tree
paths.py # path validation under WIKI_ROOT, refuses private/
temporal.py # dated items
dream/ # the staged consolidation pipeline + scheduler
ui.py, prompts.py # web console and editable dream prompts
seed/ # initial wiki content, copied into the volume once
tests/
The fast way, with auth disabled (no OAuth needed; the dream needs an Anthropic key, but the read/write tools do not):
docker build -t memory-wiki .
docker run --rm -p 8765:8765 \
-e WIKI_AUTH_DISABLED=1 \
-v "$PWD/.localwiki:/srv/wiki" \
memory-wikiThen:
curl -s localhost:8765/health # -> {"ok": true}
npx @modelcontextprotocol/inspector
# connect to: http://localhost:8765/mcp
# call prime(), then read("self/identity.md")The local volume is seeded from seed/ on first run, so you get a working wiki
to poke at.
The logic (store, temporal items, the dream pipeline, path guard, prime) is covered by a fast pytest suite that runs against a throwaway wiki, with git commits and model calls stubbed. No API key or network needed. Runs in CI on every push.
pip install pytest # or: pip install -e ".[dev]"
pytest -qThe image is built from the included Dockerfile. A GitHub Actions workflow
(.github/workflows/ci.yml) runs the tests and, on main, builds and pushes
the image to GHCR. From there, deploy the container however you like: any Docker
host works, as long as the MCP endpoint sits behind a reverse proxy that
terminates HTTPS at your public URL and the env vars above are supplied.
Two things to remember in production:
- mount a persistent volume at
WIKI_ROOTso the wiki survives restarts; - point a DNS record for your domain at the host, and let the reverse proxy handle the certificate.
- In Claude.ai: Settings -> Connectors -> Add custom connector.
- URL:
https://YOUR_DOMAIN/mcp. - Claude redirects you to GitHub to log in and consent. Only the allow-listed GitHub account can use the tools.
- Ask Claude to call
prime(), thenread("self/identity.md").
Built so far: OAuth, remember() plus the lean read/search tools, temporal
items, the web console, and the staged dream (dry-run, execute, editable
prompts, per-stage model, automatic nightly schedule).
Still open: a weekly digest (a periodic summary of what changed and what is coming up). Optional later: notifications, and a "reorg" dream to fold residual duplicate pages.