A persistent memory and a searchable map of your codebase — for Claude Code, Codex CLI, or any MCP client.
Coding agents forget everything between sessions and re-read whole files to
answer questions a grep-and-hope search could've answered in one shot.
memory-mcp fixes both: a branch-scoped note store your agents can read
and write to like a shared brain, and a tree-sitter-indexed map of your
codebase that returns exact file/line hits instead of forcing an agent to
pull entire files into context just to find one function.
Self-hosted. Free. Runs entirely on your machine — Qdrant, Memgraph, and Ollama in Docker, no API keys, no per-token embedding cost, no data leaving your machine.
- Why
- Features
- Quick start
- How it works
- Tools reference
- Branch-scoped memory
- Codebase indexing
- Wiring up a client
- Local development
- Project layout
- Roadmap
Two problems, one server:
Agents don't remember. Close the session and every hard-won insight — why a piece of code is shaped the way it is, what a past bug's actual root cause was, a decision that took twenty minutes to reach — is gone. The next session (or the other agent you're also using) starts from zero and burns tokens re-deriving what was already figured out.
Agents read too much to find too little. Ask "where's the retry logic" and a typical agent greps, guesses, and opens three whole files to confirm one function. That's tokens spent on file contents you never needed.
memory-mcp is two systems behind one MCP server: a branch-scoped note
graph (so Claude Code and Codex CLI can hand facts back and forth without
polluting main with scratch notes), and a semantic codebase index
built on real syntax trees, not regex — so search_code hands back exact
file, symbol, and line range instead of a pile of files to read manually.
- Persistent, branch-scoped memory — notes are tagged
repo+branch, so work onfeature/xdoesn't leak into or get diluted by other branches, whilemainstays the durable, promoted-on-purpose source of truth. - Shared across clients — Claude Code and Codex CLI (or any MCP client) read and write the same store. Hand off mid-task without re-explaining anything.
- Real code understanding, not regex —
index_codebasechunks along actual tree-sitter syntax boundaries (functions, classes, methods) across 12+ languages, not brace-counting heuristics. - Built for token efficiency —
search_codereturns compact hits (file, symbol, line range, score) by default, not full file text. Read only what you need, when you know you need it. - Graph-aware — every note and every code symbol lives in a Memgraph graph, so "what's connected to this" is one
get_entity_graphcall instead of manual cross-referencing. - Self-hosted and free — Qdrant + Memgraph + Ollama, all local Docker containers. No API keys, no per-token embedding bill, nothing leaves your machine.
- Two transports — stdio (default, zero config) or Streamable HTTP, for clients that can't spawn local processes or need to reach the server remotely.
- Safe by default — writes are explicit and branch-scoped,
mainis never auto-written or auto-pruned, and destructive operations (prune_scope) require confirmation and respect a staleness gate.
# 1. Set where your code lives (covers this once, for every repo underneath it)
echo "CODE_WORKSPACE=/Users/you" > .env
# 2. Bring up the stack
docker compose up -d
# 3. Pull the embedding model
docker exec -it $(docker compose ps -q ollama) ollama pull nomic-embed-text
# 4. Confirm everything's healthy
docker compose logs -f memory-mcp
# [memory-mcp] health check — qdrant: ok, memgraph: ok, ollama: okQdrant's memory_chunks and code_chunks collections (768-dim, Cosine)
are created automatically on boot. If you ever need to create them by
hand:
curl -X PUT http://localhost:6333/collections/memory_chunks \
-H "Content-Type: application/json" \
-d '{"vectors": {"size": 768, "distance": "Cosine"}}'Then wire it into your MCP client of choice — see
Wiring up a client below, or jump straight to
setup_guide.md for the full walkthrough (Claude Code,
Codex CLI, Claude Desktop, and generic HTTP clients).
┌───────────────┐ ┌───────────────┐
│ Claude Code │ │ Codex CLI │ ...any MCP client
└───────┬───────┘ └───────┬───────┘
│ │
└──────────┬─────────────┘
│ MCP (stdio or HTTP)
▼
┌────────────────┐
│ memory-mcp │ 8 tools, one server
└────────┬────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
┌───────────┐ ┌────────────┐ ┌──────────┐
│ Qdrant │ │ Memgraph │ │ Ollama │
│ vectors │ │ graph │ │ embeddings│
└───────────┘ └────────────┘ └──────────┘
Every write goes to both stores: a vector (Qdrant, for semantic search) and
a graph node (Memgraph, for relationships — RELATES_TO between notes,
DEFINES/IMPORTS between code symbols and files). A search does a vector
lookup first, then walks 1-2 hops in the graph from each hit to surface
what's connected — so you get both "what's semantically similar" and
"what's structurally related" in one call.
| Tool | What it does |
|---|---|
search_memory |
Semantic + graph search over notes, scoped to repo + branch (plus main). |
ingest_note |
Store a durable finding — embedded into Qdrant, added as a graph node. |
get_entity_graph |
Walk everything connected to a note or code symbol, up to N hops. |
promote_to_main |
Copy a branch-scoped note into main at merge time (never auto, never bulk). |
list_stale_scopes |
List branch scopes with no activity in STALE_BRANCH_DAYS+ days. |
prune_scope |
Delete a stale branch scope's notes. Refuses to run on active branches or main without an explicit override. |
index_codebase |
Tree-sitter-chunk a codebase into Qdrant vectors + a File/Symbol graph. Incremental by default. |
search_code |
Semantic code search — compact hits (file, symbol, line range, score), not whole files. |
Want your agents to actually use these efficiently instead of falling
back on Read/Grep out of habit? Copy
CLAUDE.md.example or
AGENTS.md.example into the repo you've indexed —
they're a ready-made set of usage rules for exactly that.
Every note carries repo, branch, source (claude/codex), and an
optional session_id. search_memory / get_entity_graph filter to a
repo + branch (plus main, unless include_main: false), so work on
feature/x doesn't see — or get diluted by — notes from other branches,
while still surfacing durable main-scoped knowledge.
Pass branch explicitly — this is the recommended path, not a fallback.
Claude Code and Codex CLI both run on your host, sitting in the repo's real
working directory, so getting the true current branch is one local command
away:
git branch --show-currentPass that result as branch on every search_memory / get_entity_graph /
ingest_note call. This has no dependency on how memory-mcp's container is
mounted, so moving/renaming folders on your host can't break it.
branch is technically optional on search_memory and get_entity_graph
(always required on ingest_note) — if omitted, memory-mcp falls back to
running that same git branch --show-current inside its own container,
against WORKSPACE_ROOT/<repo> (mirrors the CODE_WORKSPACE mount
index_codebase uses). Treat this as a convenience fallback, not something
to rely on: the container can't see your host's filesystem beyond that
mount, so if repo isn't a folder directly under it, or is checked out to
a different branch than what you actually have open, this doesn't error —
it silently returns the wrong branch, and your call would read or write
the wrong scope. It only errors when detection is unambiguously impossible
(detached HEAD, nothing mounted at that path, name mismatch).
Lifecycle:
| Event | What happens |
|---|---|
| Branch created | Nothing explicit — the first ingest_note on that branch name creates its scope. |
| During work | Both clients call search_memory / get_entity_graph scoped to the current branch before falling back to file search, and ingest_note durable findings as they go. |
| Branch merged | Review the branch's notes and call promote_to_main on the handful worth keeping (architecture decisions, root causes) — one at a time, never bulk. Scratch/debug notes are left behind. |
| Branch gone stale | Call list_stale_scopes to see branches with no ingest_note activity in 14+ days (STALE_BRANCH_DAYS), then prune_scope to delete them. Deliberately not keyed off git branch deletion — branches often survive locally after merge, so staleness (not git state) is the signal. |
prune_scope refuses to run on a branch active within the last
STALE_BRANCH_DAYS days unless you pass force: true, and can never touch
main. promote_to_main copies, it never moves — the original
branch-scoped note survives until its branch scope is eventually pruned.
Known gaps:
- Branch rename/rebase isn't handled — scope is keyed by branch name string, so a rename effectively starts a new scope. Rare enough to fix manually if it comes up.
- No auto-promotion, ever —
promote_to_mainis always an explicit, one-fact-at-a-time call. - Verify both clients see the tools before relying on this for hand-off:
/mcpin Codex, the equivalent check in Claude Code.
index_codebase needs filesystem access to the code from inside the
container. docker-compose.yml mounts CODE_WORKSPACE read-only at the
identical path inside the container as on your host — no path
translation. Set it once, in a root-level .env next to
docker-compose.yml (see .env.example), to a folder that covers
everywhere you keep code. Repos on your Desktop, in Documents, in a
"Projects" folder, etc. are all nested under your home directory anyway,
so pointing this at your home dir covers all of them with one mount:
# .env, next to docker-compose.yml
CODE_WORKSPACE=/Users/youThen, from an MCP client, rootPath is just the real path, no translation
needed:
// if CODE_WORKSPACE=/Users/you, and you want to index /Users/you/Desktop/my-app
{ "rootPath": "/Users/you/Desktop/my-app" }You never have to reconfigure the mount or restart the stack when you
start working on a different repo, as long as it's somewhere under
CODE_WORKSPACE — just pass whatever absolute path you're actually
sitting in. Anything outside that folder isn't visible to the container at
all (mount is read-only, scoped to that one folder).
Code truly outside your home directory (an external drive, /opt,
another volume) needs its own mount — a single mount can't span two
unrelated filesystem roots:
volumes:
- ${CODE_WORKSPACE}:${CODE_WORKSPACE}:ro
- ${CODE_WORKSPACE_2}:${CODE_WORKSPACE_2}:ro # e.g. /Volumes/externalindex_codebase walks the tree (respecting the root .gitignore plus
common junk dirs — node_modules, dist, .git, __pycache__, etc.),
splits each file into chunks along real syntax boundaries via tree-sitter
(functions, classes, methods — each method gets its own chunk, not folded
into its class — for JS/TS, Python, Go, Rust, Java, C#, Kotlin, Scala, C,
C++, PHP, and Ruby; fixed-window chunks as a fallback for anything else),
embeds each chunk, and writes:
- vectors + metadata into the
code_chunksQdrant collection - a
(:File)-[:DEFINES]->(:Symbol)graph in Memgraph, plus best-effort(:File)-[:IMPORTS]->(:File)edges for JS/TS and Python relative imports
It's incremental by default: re-running it skips files whose content hash
hasn't changed, and prunes graph/Qdrant entries for files deleted from
disk. Pass "incremental": false to force a full rebuild — needed the
first time you fix a bug in the indexing pipeline itself, since incremental
mode will otherwise skip files it already (incorrectly) processed.
Performance. Each file's writes are batched to a handful of
round-trips regardless of chunk count: one Ollama /api/embed call per
file (its chunks embedded together, not one HTTP request per chunk), one
Qdrant upsert for all of the file's chunks, and one Memgraph UNWIND call
for all of its symbols — instead of one network round-trip per chunk/
symbol, which is what made larger codebases slow to begin with. Files
themselves are also processed concurrently (INDEX_FILE_CONCURRENCY,
default 6). Tune these if indexing is still slow or if it's overwhelming
your Qdrant/Memgraph/Ollama containers:
| Env var | Default | What it controls |
|---|---|---|
INDEX_FILE_CONCURRENCY |
6 | Files processed at once. The main lever for overall indexing speed. |
EMBED_BATCH_SIZE |
64 | Chunks per /api/embed request. Lower if Ollama is memory-constrained. |
EMBEDDING_CONCURRENCY |
4 | /api/embed requests in flight at once (across a file's own sub-batches, and across concurrent files). |
Then search it:
{ "query": "how do we retry failed webhook deliveries", "repo": "my-app" }search_code returns compact hits by default — file path, symbol name,
kind, line range, score — without the chunk text, so the calling agent
reads only the lines it actually needs instead of pulling whole files into
context. Pass "includeText": true to get the chunk body inline when you
already know you want it. Results also include relatedFiles, pulled from
the IMPORTS graph edges of the top hits.
Chunking is tree-sitter-based, not heuristic. Chunk boundaries come from the real syntax tree — exact start/end of each function, class, and method — rather than regex/brace counting. A class produces a small "header" chunk (signature + any fields declared before the first method) plus one chunk per method, instead of one blob covering the whole class. Files whose language has no bundled grammar (or that fail to parse) fall back to fixed line-count windows with overlap, so nothing is silently dropped.
Known limitations:
- Version pinning is load-bearing.
web-tree-sitteris pinned to0.24.xinpackage.jsonbecausetree-sitter-wasms' prebuilt.wasmgrammars are compiled against an older tree-sitter ABI — newerweb-tree-sitterruntimes (0.25+) refuse to load them at all ("dylink metadata" error). If you ever bump either dependency, bump both together and re-verify with a throwaway parse before deploying — see the comment at the top ofsrc/codebase/treeSitter.ts. - Image size. The bundled grammars add roughly 50MB to
node_modules(all ~35 languagestree-sitter-wasmsships, even though only ~14 are wired up inlanguage.ts) — no native compilation needed, since it's pure WASM, but it's a real size cost in the Docker image. - Very large single symbols (e.g. a 1000-line function) are still capped at 400 lines; the overflow is swept up as a separate chunk rather than dropped.
- A few grammars don't expose field names for identifiers the way most
do (Kotlin) or bury the name in a declarator chain (C/C++) — those get
language-specific extraction logic in
language.tsrather than a genericchildForFieldName("name")lookup. Occasionally a chunk may come back unnamed if a construct doesn't match the expected shape. - No grammar is wired up for Go/Rust/Kotlin/Scala/etc. import statements
(import-edge resolution stays regex-based) — only handles relative
imports (
./foo,../bar, Python'sfrom .pkg import x) for JS/TS and Python. Other languages get indexed and are searchable, just withoutIMPORTSedges yet.
Claude Code and Codex CLI each register memory-mcp as their own MCP
server directly (no proxying) and both read/write the same Qdrant +
Memgraph instance.
Claude Code — .mcp.json, or claude mcp add:
{
"mcpServers": {
"memory": {
"command": "docker",
"args": ["exec", "-i", "memory-mcp", "node", "dist/main.js"]
}
}
}Codex CLI — ~/.codex/config.toml, or
codex mcp add memory-mcp -- docker exec -i memory-mcp node dist/main.js:
[mcp_servers.memory-mcp]
command = "docker"
args = ["exec", "-i", "memory-mcp", "node", "dist/main.js"]Both use stdio by default (zero config). An HTTP transport
(MCP_TRANSPORT=http or both) is also available for clients that can't
spawn local processes, or for reaching the server from another machine —
Claude Desktop needs a small bridge for it, stdio works there directly.
For the full walkthrough — every client (Claude Code, Codex CLI,
Claude Desktop, generic HTTP clients), both transports, and the gotchas
that come with each — see setup_guide.md. Verify
both clients see all eight tools before relying on this for hand-off
between them (/mcp in Codex, the equivalent check in Claude Code).
cd memory-mcp
cp .env.example .env # point at localhost instead of the compose service names
npm install
npm run dev # runs src/main.ts directly via tsxFor local dev, edit .env to use http://localhost:6333,
bolt://localhost:7687, and http://localhost:11434 instead of the
Docker Compose service hostnames, and run qdrant, memgraph, and
ollama via docker compose up -d qdrant memgraph ollama (skip building
memory-mcp itself). rootPath for index_codebase can then be any path
on your machine, and WORKSPACE_ROOT for branch auto-detection can point
anywhere git is on your PATH can reach — no mount needed at all.
docker-compose.yml
setup_guide.md # consolidated client setup (all clients, both transports)
CLAUDE.md.example / AGENTS.md.example # usage rules to copy into an indexed repo
memory-mcp/
Dockerfile
.env / .env.example
package.json / tsconfig.json
mcp-client-config.example.json # Claude Code / Claude Desktop, stdio
mcp-client-config-http.example.json # Claude Code, HTTP
codex-config.example.toml # Codex CLI, stdio + HTTP
src/
config.ts # env loading (incl. MCP_TRANSPORT/MCP_HTTP_PORT/MCP_HTTP_PATH)
types.ts # shared TS types for chunks/nodes/edges/scopes
embeddings.ts # Ollama embedding client
qdrant.ts # Qdrant client + memory_chunks collection + scope filters
graph.ts # Memgraph (Bolt/Cypher) client via neo4j-driver + indexes
scope.ts # (:Scope) node tracking for staleness (list_stale_scopes/prune_scope)
git.ts # branch auto-detection via `git -C <repo> branch --show-current`
healthcheck.ts # startup connectivity check
httpServer.ts # Streamable HTTP transport (stateless, fresh server/request)
util/
concurrency.ts # bounded-concurrency map helper
codebase/
walk.ts # gitignore-aware file discovery
language.ts # extension -> language, tree-sitter node-type/kind config
treeSitter.ts # web-tree-sitter runtime init + per-language grammar loading
chunker.ts # AST-based chunking (tree-sitter) + fixed-window fallback
imports.ts # relative-import resolution (JS/TS, Python) — regex, unchanged
hash.ts # content hashing for incremental reindex
qdrantCode.ts # code_chunks collection + search/upsert/delete
graphCode.ts # File/Symbol graph nodes + IMPORTS/DEFINES
tools/
searchMemory.ts
getEntityGraph.ts
ingestNote.ts
promoteToMain.ts
listStaleScopes.ts
pruneScope.ts
indexCodebase.ts
searchCode.ts
main.ts # MCP server entrypoint (stdio and/or HTTP, per MCP_TRANSPORT)
Not built yet, deliberately deferred rather than half-done:
- Telegram bot listener
- GitHub webhook sync (issues, PRs, commits)
- Auto entity extraction / cross-source auto-linking for notes
- Fully-typed Person/Issue/PR graph population (today's graph is mostly
generic
Notenodes fromingest_note, plus theFile/Symbolnodes fromindex_codebase) - Cross-repo memory —
repois required on every call today, but nothing yet aggregates or compares across repos - Automatic branch-delete hook to trigger
prune_scope— staleness-based cleanup covers this manually/periodically for now