brain-mcp
Provides a local knowledge base for GitHub Copilot Chat, enabling it to recall lessons, patterns, and project context during coding sessions.
Integrates with a local Ollama server to optionally enable hybrid vector search, adding semantic similarity to keyword-based retrieval for lessons.
Click on "Install 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., "@brain-mcpsave a lesson: always validate user input"
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.
Brain MCP — a local knowledge base for AI coding assistants
Brain MCP is a local MCP (Model Context Protocol) server that acts as long-term memory for AI coding assistants such as GitHub Copilot Chat, Claude Code, or any other MCP-capable client. It stores lessons, reusable patterns, and project context in a local SQLite database on your machine.
Key point: everything stays on your computer. No cloud. No cost. The only (optional) network call is to a local embeddings server you run yourself (e.g. Ollama), and it is off by default — see Hybrid vector search.
Features:
Search you can ask in a sentence — five lexical retrievers over whole lessons and over passages, merged by weighted reciprocal rank fusion; plus vector search (sqlite-vec + a local embeddings server) when you opt in
Measured, not asserted — a committed corpus of judged queries, scored by
npm run evaland enforced in CI, so a ranking change produces a number rather than an impressionMCP resources — browse lessons and project summaries as
brain://resources, no tool calls neededExport/import — human-readable markdown or lossless JSON, with content-hash dedupe on import
Project scanner — indexes your code directory's tech stacks from metadata files
Soft-delete — archived lessons are never lost, always restorable
Architecture
MCP client (VS Code Copilot Chat, Claude Code, ...)
│
├── Sends JSON-RPC over stdio ──→ brain-mcp (Node.js process)
│ │
│ ├── SQLite DB (knowledge.db)
│ │ ├── lessons (lessons / insights)
│ │ ├── lessons_fts (full-text search, FTS5)
│ │ ├── lesson_chunks (passages — one paragraph each)
│ │ ├── lesson_chunks_fts (passage full-text index, FTS5)
│ │ ├── chunks_vec (passage vectors, sqlite-vec — optional)
│ │ ├── chunk_embeddings (embedding bookkeeping)
│ │ ├── lessons_archive (soft-deleted lessons)
│ │ ├── project_index (project scans)
│ │ └── patterns (architectural patterns)
│ │
│ ├── File system scanner
│ │ └── Reads your code directory
│ │ (package.json, README, composer.json)
│ │
│ └── OPTIONAL, opt-in via BRAIN_EMBEDDINGS_URL:
│ local embeddings server (Ollama)
│ http://localhost:11434 — the ONLY network call
│
├── The client uses the brain_* tools like any other MCP tool
└── ...and can browse brain://lessons/{id} & brain://projects/{name} resourcesFiles
File | Role |
| Server entry point — wires tools + resources to the MCP stdio transport |
| Tool definitions and presentation — DB setup, project scanner, the 12 MCP tools |
| Turns a question into FTS5 queries — tokenizing, stopwords, stemming |
| Retrieval — runs the retrievers, fuses them, returns rows (no formatting) |
| Splits lessons into passages and keeps that index in step |
| Proposes lessons that are about a tool rather than a project |
| Counts how often a trap has been recorded, from links a writer stated |
| recall@k, precision@k, MRR — so ranking changes produce numbers |
| Turns a startup crash (ABI mismatch, missing module) into instructions |
| Embeds the vector backlog on startup, without blocking the transport |
| Optional embeddings client (Ollama) + weighted reciprocal rank fusion |
| sqlite-vec vector index (loads the extension, degrades gracefully) |
| MCP resources: |
|
|
|
|
| Shared DB access + the tokenizer the hooks and |
|
|
|
|
| Registers/removes the hooks in Claude Code settings (merging, idempotent) |
| Imports Claude Code's |
|
|
|
|
|
|
| Semantic search for the hooks — embeddings, vec0, a file-backed breaker |
|
|
| Committed fixture corpus + judged queries, with CI thresholds |
| Test suites ( |
| Compiled JS (what your MCP client runs) |
| SQLite database with all knowledge (WAL mode, gitignored) |
| Dependencies: MCP SDK, better-sqlite3, sqlite-vec, zod (all pinned exact) |
| TypeScript config (ES2022, strict) |
Dependencies (minimal, pinned to exact versions)
@modelcontextprotocol/sdk— the official MCP protocol implementationbetter-sqlite3— native SQLite driver (fast, no async overhead)sqlite-vec— SQLite vector search extension (prebuilt binaries; optional at runtime — if it can't load on your platform, brain-mcp runs FTS5-only)zod— tool input validation
Requires Node.js >= 20.
Related MCP server: Mono Memory MCP
Installation
Recommended setup — read this first
A knowledge base only pays off if something writes to it and something reads it back. Register the MCP server alone and you get a passive store: the tools exist, but nothing reminds anyone to use them. In practice that means lessons trickle in at well under one per day and the agent starts most sessions blind to what it already learned.
Three steps close the loop:
# 1 — build
git clone <this repo> brain-mcp
cd brain-mcp
npm install
# 2 — build + register the lifecycle hooks in Claude Code
npm run setup
# 3 — register the MCP server itself (see the client sections below)
claude mcp add brain-mcp -- node "$(pwd)/dist/index.js"The database is created automatically on first run (default: data/knowledge.db inside the repo).
What the hooks do
npm run setup (or npm run hooks:install) registers these in ~/.claude/settings.json:
Hook | Script | Effect |
|
| Reads the database directly and injects this project's lessons — criticals first — before the first token. Also injects |
|
| Searches the FTS5 index for lessons that match what you just asked, across every project, and injects the top three in full. This is the hook that makes stored knowledge arrive at the moment it can change a decision — see Why relevance, and why not at session start. Stays silent when nothing matches, never repeats a lesson within a session, and records that a lesson was shown. |
|
| Watches Bash for the moment a mistake becomes visible: an undo command ( |
|
| If the session wrote nothing, asks once for a lesson — naming the specific incidents |
Why relevance, and why not at session start
SessionStart runs before anybody knows what the session is about. The best it
can do is guess, and for a long time the guess was: the twelve most recent
lessons of the open project, severity first, truncated to 220 characters each.
Measured against 297 stored lessons in August 2026, that guess meant:
Lessons that could ever be seen outside their own project | 6 of 297 (2%) |
Lessons a session in the largest project could see | 12 of 124 |
| 60 — so the twelve slots never reached |
Ranking | severity, then |
Recorded uses | none — nothing in the schema said a lesson had ever been read |
Writing was enforced by a blocking Stop hook; reading was one preview at
the start and nothing afterwards. The system was very good at capturing lessons
and close to inert at recalling them.
No amount of tuning SessionStart fixes that, because the problem is timing.
UserPromptSubmit is the first moment the task is known, so that is where the
search belongs — and the search itself already existed: brain_recall does it
well, it was simply left to the model's discretion, and a model does not know
what it does not know.
Two consequences worth stating plainly:
The current project wins ties, it does not win outright. A lesson about a bash trap learned in one repository is precisely the lesson that prevents the same mistake in another, and project-scoped recall is what made it invisible.
Silence is a feature. The hook stays quiet unless something genuinely matches. A memory system that answers every prompt with three vaguely related paragraphs teaches people to skim past the block that will one day matter.
Lessons now carry shown_count and last_shown_at, so "is any of this being
used?" is a query rather than an impression. Nothing writes updated_at when
they change — showing a lesson must not make it look freshly written, or it
would float to the top of the recency-ordered session digest and stay there.
Why capture at the moment, not at the end
The most valuable lesson is a mistake made and corrected mid-session, and that is exactly the one a session-end prompt misses. By the time the turn ends the evidence has scrolled away and the model reconstructs it from memory — or the session already recorded something unrelated, so the prompt never fires at all.
incident_watch.py optimises for precision over recall, because a hook that
nags gets disabled. It only fires on undo commands, where the base rate of "an
actual mistake happened" is close to 1 — nobody reverts unless something went
wrong. Routine commands (git status, git add, git diff, npm test) stay
silent. A single failed command stays silent too; it is usually a typo, not a
lesson. Both prompts ask for the same four-part shape — PROBLEM, CAUSE, FIX,
VERIFY — so the recorded lesson carries the mechanism and the check that would
catch it earlier, not just a description of the symptom.
Hooks cannot call MCP tools — a hook is a separate process, MCP is JSON-RPC inside the agent's session. So SessionStart opens the SQLite file read-only. That is also cheaper than a tool call: zero model round-trips, and the knowledge is simply present from the start.
All four hooks fail open. Any error exits 0 with no output, so a broken hook can never stop a session from starting or trap one in a loop. The Stop hook additionally guards against loops four ways: it respects stop_hook_active, blocks at most once per session (tracked by a per-session marker), stays quiet for sessions under BRAIN_HOOK_MIN_SECONDS, and never asks when the lesson count already grew.
Cross-cutting lessons
A lesson is stored against a project, and SessionStart normally injects only
the current project's. That leaves a gap: a critical lesson about tooling —
"this command silently overwrites a newer file" — is filed under whichever
project was open when it was learned, and is then invisible in every other
project, including the ones where the mistake would recur.
Nominate one or more projects whose criticals should follow you everywhere:
npm run hooks:install -- --global-projects tooling
# or several: --global-projects tooling,infraThe value is written into the hook command in your settings.json, not into
this repo — which projects are cross-cutting is a property of your knowledge
base, not of this engine. Those entries are tagged [category · project] in the
injection so they are not mistaken for something local, and capped at
BRAIN_HOOK_MAX_GLOBAL.
npm run hooks:status # show what is registered, write nothing
npm run hooks:install # idempotent — re-run after moving the repo
npm run hooks:uninstall # clean removal
node scripts/install-hooks.mjs --project # register in ./.claude/settings.json insteadThe installer merges into your settings: it identifies its own entries by the absolute path to this repo's hooks/ directory, so it leaves any other hooks you have alone, and --uninstall removes exactly its own. It backs up settings.json before the first change and never touches your database.
Setup is a deliberate opt-in rather than an automatic
postinstall.npm run setupwrites to~/.claude/settings.json— a file outside this project — and a package that modifies your global agent configuration as a side effect ofnpm installis not a package you should trust. One command, run knowingly.
Tuning the hooks (environment variables)
Variable | Default | Purpose |
|
| Lessons injected at session start |
|
| Hard cap on the injected block, so the hook can never balloon your context |
|
| Sessions shorter than this are never asked for a lesson |
| (empty — off) | Comma-separated projects whose |
|
| Cap on those cross-cutting entries |
Typical cost of the SessionStart injection is 600–850 tokens on a project with real history — roughly one avoided re-investigation pays for a month of it.
Optional — import existing Claude Code memory
Claude Code writes its own per-project memories as markdown under ~/.claude/projects/<project>/memory/. Those files have no search index; an agent finds them only when MEMORY.md happens to land in context. If you have accumulated any, move the content into the indexed store:
python3 scripts/import-claude-memory.py --dry-run
python3 scripts/import-claude-memory.pyThe markdown files are left in place — this copies content, it does not migrate. It is idempotent (keyed on source), so re-running updates changed files and skips the rest. Pass --code-root if your projects do not live in ~/code, and --client-group <folder> for folders holding client work.
Configuration (environment variables)
Variable | Default | Purpose |
|
| Directory that |
|
| Path to the SQLite database file |
| (unset — embeddings OFF) | Base URL of a local Ollama-compatible embeddings server, e.g. |
|
| Embedding model to request from that server |
|
| Timeout per embeddings request (AbortSignal) |
Hybrid vector search (optional)
By default brain_recall is pure SQLite FTS5 (keyword search, zero network). If you run Ollama locally you can add semantic search on top:
ollama pull nomic-embed-text # one-time, ~270 MB
export BRAIN_EMBEDDINGS_URL=http://localhost:11434
# optional: export BRAIN_EMBEDDINGS_MODEL=nomic-embed-text(or put those in the env block of your MCP client config). What changes:
brain_learnembeds each new lesson on write (callsPOST /api/embeddingson your local Ollama). If Ollama is down, the lesson is saved anyway and marked unembedded.brain_recallruns both retrievers — FTS5 and KNN over the sqlite-vec index — and merges them with reciprocal rank fusion (k=60). Each hit is annotated with which retriever(s) found it (matched: fts+vector). If the embeddings server is unreachable, recall silently falls back to FTS5-only — it never fails because embeddings are down.brain_reindexbatch-embeds any backlog of unembedded lessons (force: truerebuilds the whole index — use after switching models).brain_statusshows the embeddings mode (disabled / enabled / enabled-but-unreachable) and embedded/unembedded counts.
Embeddings are stored in a vec0 virtual table (sqlite-vec) inside the same knowledge.db. If the sqlite-vec extension cannot load on your platform, brain-mcp logs one warning and keeps running FTS5-only — the vector layer can never crash the server.
Endpoint contract: brain-mcp calls the classic Ollama embeddings route POST {BRAIN_EMBEDDINGS_URL}/api/embeddings with {"model": ..., "prompt": ...} and expects {"embedding": [...]}. Anything that speaks this API works (it does not use the OpenAI-compatible route).
VS Code (Copilot Chat)
Add the server to your MCP config (mcp.json — open it via Command Palette → "MCP: Open User Configuration"):
{
"servers": {
"brain": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/brain-mcp/dist/index.js"],
"env": {
"BRAIN_CODE_DIR": "/absolute/path/to/your/code/folder"
}
}
}
}Restart the MCP server (or VS Code) and the brain_* tools appear in Copilot Chat automatically.
Generic MCP clients (Claude Code, Claude Desktop, etc.)
Any client that supports stdio MCP servers works. For example, with Claude Code:
claude mcp add brain -e BRAIN_CODE_DIR=$HOME/code -- node /absolute/path/to/brain-mcp/dist/index.jsOr in a JSON-based client config:
{
"mcpServers": {
"brain": {
"command": "node",
"args": ["/absolute/path/to/brain-mcp/dist/index.js"],
"env": { "BRAIN_CODE_DIR": "/home/you/code" }
}
}
}The 12 tools
1. brain_learn — store a lesson
Takes scope: "global" for a lesson about a tool rather than a project — a
shell trap, a git behaviour, an API limit. Those recur everywhere, and filing
them under whichever project happened to be open is what made them invisible in
the repositories where the mistake actually repeats. UserPromptSubmit boosts
them. The default stays project.
Saves an insight, gotcha, or problem solution to the database.
When the agent should use it: after fixing a hard bug, discovering a gotcha, or finding the best approach to something.
brain_learn({
content: "Cloudflare D1 does not support ALTER TABLE ADD COLUMN IF NOT EXISTS — wrap it in try/catch",
category: "gotcha",
tags: ["cloudflare", "d1", "sql"],
project: "my-webapp",
severity: "important"
})Categories: bug-fix, architecture, performance, security, deployment, tooling, pattern, gotcha, best-practice, client, seo, i18n, testing, design, marketing, sales, workflow, business, financial, market, client-feedback
Severity: critical (never forget), important, info, tip
2. brain_recall — search the knowledge base
Search across the whole database, with optional category and project filters.
When the agent should use it: before starting work on a task — check for known issues and gotchas.
brain_recall({ query: "cloudflare deployment", project: "my-webapp" })Ask it in a sentence. The query is treated as a bag of terms, not as a phrase that must appear verbatim, and three retrievers run over those terms and are merged by weighted reciprocal rank fusion:
Retriever | Matches | Weight | What it is for |
| every term, anywhere in the lesson | 3 | precision — a lesson about exactly this |
| every term inside one paragraph | 3 | the only way past bm25's length penalty into a long lesson |
| any term | 1.5 | recall — the question spans several lessons |
| nearest passages, above a similarity floor | 1.5 | only when embeddings are enabled |
| any term in one paragraph | 1.2 | the passage index's recall arm |
| any stem | 0.6 | morphology — |
For a lesson longer than ~1000 characters, the response shows the passage that
matched rather than the first 1200 characters, with the whole text one
brain://lessons/{id} fetch away. The long lessons are the ones with the
evidence in them, and they are written as "PROBLEM — … CAUSE — … FIX —":
truncating from the top delivers the setup and cuts before the answer.
Results are annotated with the retrievers that found them (matched: all+any),
and the response names the terms actually searched for — including on a miss, so
a query that quietly reduced to two words is distinguishable from an empty base.
A hit carried only by shared words is also labelled with how many of them it
shares (thin: 1/5 terms), and a result set where every hit is thin carries a
caution. This is reported, not enforced: filtering on it was measured and
recall@5 fell from 100% to 79.4% with an eighth of all questions returning
nothing, because a real question spreads across lessons that each answer part of
it — a softer version of the implicit AND that made the base look empty to begin
with. The prompt hook, which speaks uninvited, does enforce it; the tool, which
was asked deliberately and whose caller can read the content, does not.
This is the fix for the bug that made brain-mcp look empty. FTS5 joins bare terms with an implicit AND, so
brain_recallused to demand a single lesson containing every word of the question. Asked"wp eval koszty zamówień backfill lipiec"against a base of 301 lessons it returned nothing — the same terms OR-ed returned 100. Punctuation was worse than useless:"how do I fix (kamar) orders?"raisedfts5: syntax errorout of the tool. Both are covered bytests/recall.test.ts.
Severity and scope act as tie-breakers only: a critical lesson and a global
one get a few percent, enough to order two equally relevant hits and never enough
to promote an irrelevant one.
3. brain_scan_projects — index your code directory
Automatically indexes every project in your code directory (BRAIN_CODE_DIR, default ~/code) — detects the tech stack from package.json, composer.json, Dockerfile, etc.
brain_scan_projects({})
// → Scanned 12 projects: my-webapp (React, Vite, Tailwind), my-api (Hono, CF Workers)...4. brain_project_context — project context
Fetches the full context of one project: stack, description, all lessons, and patterns.
brain_project_context({ project: "my-webapp" })5. brain_store_pattern — store a pattern
Saves a reusable architectural pattern with a code example.
brain_store_pattern({
name: "Pages Functions auth middleware",
pattern_type: "auth",
description: "Bearer token validation in an onRequest handler",
example: "export const onRequest: PagesFunction = async (ctx) => { ... }",
projects: ["my-webapp"]
})6. brain_status — dashboard
How many lessons, patterns, and projects are stored, broken down by category and project.
7. brain_forget — archive knowledge (soft-delete)
Archives outdated or incorrect lessons into lessons_archive — nothing is permanently lost. Requires confirm: true as a safety check; calling without it shows a preview of what would be archived.
8. brain_restore — restore archived lessons
Lists archived lessons and restores them back to the active set by ID.
9. brain_reindex — (re)build the vector index
Only useful with embeddings enabled. Embeds every lesson that has no vector yet (e.g. saved while Ollama was down, or imported). force: true drops the index and re-embeds everything — required after changing BRAIN_EMBEDDINGS_MODEL. Reports progress and aborts early if the endpoint keeps failing.
brain_reindex({}) // embed the backlog
brain_reindex({ force: true }) // full rebuild10. brain_export — export the knowledge base
format: "markdown"— human-readable, lessons grouped by category (plus patterns)format: "json"— lossless, re-importable withbrain_importWith
path— writes the file inside the data directory only (path-validated, symlink-safe; escaping paths are refused)Without
path— returns the export inline, capped at 64 KB
brain_export({ format: "json", path: "brain-backup.json" })11. brain_import — import a JSON export
Reads a brain_export JSON file (must live inside the data directory) and inserts its lessons and patterns. Duplicates are skipped by SHA-256 content hash, so importing the same file twice is a no-op. Imported lessons are not embedded yet — run brain_reindex afterwards if you use hybrid search.
brain_import({ path: "brain-backup.json" })MCP resources — browse without tool calls
Besides tools, brain-mcp exposes the knowledge base as MCP resources (resources/list + resources/read), so clients can browse it like documents:
URI | Content |
| One lesson as markdown (content, severity, tags, project, source) |
| Project summary: path, stack, status, and all related lessons |
Clients that support resource browsing (Claude Code @-mentions, VS Code, MCP Inspector) list up to the 200 most recent lessons and all indexed projects.
Security model
⚠️ The ONE network call (opt-in, off by default)
brain-mcp makes zero network calls out of the box. There is exactly one code path that can perform HTTP requests: the optional embeddings client, and it only exists if you set BRAIN_EMBEDDINGS_URL. When set, brain-mcp POSTs lesson/query text to {BRAIN_EMBEDDINGS_URL}/api/embeddings — intended to be a local Ollama instance on your own machine (http://localhost:11434). Nothing else is ever contacted, no telemetry, no cloud. Unset the variable and the network code path is dead again. Every request carries a short abort timeout, and every failure degrades to local-only FTS5 behavior.
What brain-mcp does
Reads files ONLY from your code directory (metadata:
package.json,README.md,composer.json)The scanner is confined to the scan root: symlinked directories are skipped, and every file read resolves symlinks first and refuses anything that lands outside
BRAIN_CODE_DIRFile reads are capped at 1 MiB per file — a giant file cannot exhaust memory
Writes ONLY to its SQLite database (local file) — plus
brain_exportfiles, which are path-validated (symlinks resolved) and confined to the data directory; escaping paths and overwriting the database file are refused.brain_importreads are confined the same way and size-cappedCommunicates ONLY over stdio (stdin/stdout with the client)
No HTTP server, no open ports
Sends nothing to the internet by default — the only network code is the opt-in local embeddings call described above, gated on
BRAIN_EMBEDDINGS_URLAll SQL queries use prepared statements (parameterized) — no string-interpolated SQL
Every tool's input is validated with Zod (length caps on all strings, bounded
limit, enum categories)LIKE queries use an ESCAPE clause (no SQL injection via wildcards)
brain_forgetrequires an explicitconfirm: true(Zod-enforced) — without it you only get a previewThe vector layer is fail-safe: if sqlite-vec can't load or the embeddings server is down, everything keeps working FTS5-only — the server never crashes because of it
All dependencies are pinned to exact versions; CI runs build + tests on Node 20 and 22
What brain-mcp does NOT do
Does not read source code contents (only project metadata)
Has no internet access unless you opt into local embeddings — and then it talks only to the one URL you configured (your own machine)
Does not store passwords, tokens, or API keys
Does not modify any files in your projects (exports go to its own data directory)
Does not run any system commands
The database
data/is gitignored — your knowledge never ends up in the repoSQLite WAL mode — safe for concurrent reads
Backup: just copy the
knowledge.dbfile
Working with brain effectively
Workflow: session start
New chat → brain is automatically available as an MCP tool
Tell the agent: "Check brain_project_context for my-webapp and brain_recall for known issues before starting"
The agent pulls the context and avoids repeating past mistakes
Workflow: during work
After solving a hard problem: "Save this to brain as a lesson"
Before a complex task: "Check brain for anything about [topic]"
Workflow: session end
"Save the key takeaways from this session to brain"
Prompt examples
You want to... | Say... |
Check known issues | "brain_recall: deployment issues my-webapp" |
Store a lesson | "Save to brain: D1 bindings require wrangler.toml config, not env vars" |
See what's stored | "Show brain_status" |
Get project context | "Give me the full brain_project_context for my-webapp" |
Index projects | "Run brain_scan_projects" |
Remove a wrong lesson | "brain_forget lesson #42" |
Pro tips
You don't have to call tools manually — the agent decides when to use
brain_*if you describe what you need in natural language.Quality > quantity — 50 valuable lessons beat 500 trivial ones. Store:
Solutions to problems that took >15 minutes
Gotchas specific to your tools
Architectural patterns you keep repeating
Decisions and their rationale (ADR-style)
Severity matters:
critical— could break productionimportant— will save hours of workinfo— useful, not criticaltip— nice to know
When the backend is not there
Semantic search is additive: every failure path degrades to the lexical retrievers rather than to an error. What matters is that it degrades quickly.
A refused connection fails in milliseconds. The case that costs is a backend which accepts the connection and never answers — a model loading under memory pressure, a laptop waking from sleep. Measured against a socket that accepts and hangs, every recall paid the full timeout and then returned exactly the lexical-only result it would have returned instantly: five questions, fifty seconds, nothing gained.
So the embedder sits behind a circuit breaker. A timeout counts double, because it already spent the whole budget — one hang is enough to pause vector search, while a single cheap failure is forgiven as a blip. After a minute one probe is allowed through, so a backend that comes back is noticed without anybody restarting anything. Five questions against a hung backend now cost one timeout instead of five.
Two other silent failures are made loud: a dimension mismatch after a model
change (every query matches nothing while brain_status still says "enabled")
names itself and tells you to run brain_reindex force:true, and npm run doctor pings the backend named in your MCP config rather than trusting that it
is up.
The vector index also heals itself. Passages are written on every brain_learn;
vectors only when a backend was configured and reachable at that moment, and
the two drift apart for ordinary reasons — a session started before embeddings
were configured, an offline laptop, a model mid-pull. On startup the server
embeds whatever backlog it finds, after the transport is connected so it answers
questions throughout, stopping after a few consecutive failures rather than
turning a dead backend into a thousand timeouts. "Run brain_reindex" is a fine
repair and a poor design: it needs somebody to notice, and the symptom of not
noticing is that some lessons are quietly unreachable by meaning while every
report says healthy.
When the same trap comes back
A trap recorded three times is evidence that reading it once did not stop it,
and this base contains several: git checkout -- after a mutation test appears
three times, and one of those lessons opens by saying it is the third — in
prose no query could count.
brain_learn takes repeats: <id> for that. The count follows the chain of
links and raises the lesson in future searches (bounded at ×1.3), and the result
shows 🔁 3× recorded.
Not severity, and not automatic — both were measured. Escalating repeats to
critical is self-defeating: the severity boost is inverse-frequency, so raising
the share of critical lowers the boost for everything including the repeat.
And counting recurrences from embedding similarity does not work at all, because
similarity is not transitive:
threshold 0.68 → largest cluster 205 lessons of 364 meaningless
threshold 0.75 → 28
threshold 0.85 → 3, and the known repeats (0.708–0.751) drop out entirelyThere is no threshold that catches the repeats this base actually contains and still says anything, and a chain-following counter was not even idempotent — it reported most of the base as a fourth occurrence, twice in a row. So the count is a claim somebody made and a reader can check. The detector still runs, purely to offer the pointer: "this looks like #267 (similarity 0.87) — if it is the same trap, record it with repeats: 267". A wrong suggestion costs a sentence; a wrong count is a claim about history nobody can verify.
Tuning the similarity floor
KNN always returns k neighbours, and nearest is not the same as near — without a floor the vector retriever answers every question, including the ones whose answer is nothing. Wiring vectors in without one was measured: precision@1 rose from 82% to 88% and the true-negative rate collapsed from 100% to 0%. Asked about Kubernetes, a base containing nothing about Kubernetes returned five lessons about bash and ABI mismatches, confidently.
Embeddings are normalised to unit length, so the floor is plain cosine similarity and means the same thing whatever model produces it. It still belongs to the pair of model and corpus, so re-derive it when you change models:
BRAIN_EMBEDDINGS_URL=http://localhost:11434 \
BRAIN_EMBEDDINGS_MODEL=bge-m3 npm run eval:sweepRead the table for a plateau rather than a peak — a threshold on the edge of a
cliff is overfitted to the query set. With bge-m3 the plateau runs 0.45–0.52,
which is why BRAIN_MIN_SIMILARITY defaults to 0.5.
Is it working?
Two commands answer that without guesswork.
npm run doctor # is the installation sound?
npm run eval # is the retrieval any good?doctor checks the things that have actually broken: a native module built for
a different Node ABI (which an MCP client reports as "could not connect"), a
config pointing at a directory that moved, bare node in a config, a stale
dist/, an index that has fallen behind, and whether the hooks are registered.
It is read-only — it prints what is wrong and what to run.
eval scores retrieval against tests/eval/: a committed corpus and judged
queries, including ones that must return nothing. The same thresholds run in
npm test, so a ranking regression fails the build instead of being discovered
months later by someone concluding the knowledge base is empty.
npm run eval:hook scores the prompt hook against the same judged queries.
It has its own ranking — a separate Python implementation — and for a long time
it had no thresholds at all, which is backwards: it fires on every sentence and
decides what an agent reads before it starts, while the tool waits to be asked.
The gap cost two real defects that only measurement found. Metrics are @3
because the hook shows three lessons; recall past the third slot describes a
list nobody sees.
lexical only + bge-m3
brain_recall recall@5 94.4% recall@5 100.0%
precision@1 59.3% precision@1 96.3%
MRR 0.740 MRR 0.975
brain hook recall@3 84.0% recall@3 96.9%
precision@1 85.2% precision@1 100.0%
MRR 0.864 MRR 1.000
both true negatives 100% true negatives 100%31 judged queries, four of which must return nothing — and two of those are built from vocabulary that is ordinary across the corpus, because that is the junk shape a small fixture otherwise cannot see. Both were wrong on the first attempt: their topics had semantic neighbours, so the vector arm answered them correctly and the set scored it as a failure. A negative has to be unrelated in meaning, not merely unanswered.
Two of the 21 queries are marked requiresSemantic: an English question against
a Polish lesson shares meaning and no words at all, and no amount of lexical
tuning reaches it. With embeddings on, every judged query is satisfied:
lexical only | + nomic-embed-text | + bge-m3 | |
recall@5 (all 21) | 92.1% | 92.1% | 100% |
precision@1 | 82.4% | 88.2% | 94.1% |
MRR | 0.897 | 0.924 | 0.961 |
true negatives | 100% | 100% | 100% |
nomic-embed-text is the obvious choice and it does not work here: it raised
precision and left the cross-lingual gap exactly where it was, because it is an
English-centric model and this base is written in two languages. Pick a
multilingual one.
Maintenance
Backup
cp data/knowledge.db ~/Backups/brain-$(date +%Y%m%d).dbOr ask the agent to run brain_export({ format: "json", path: "backup.json" }) — the JSON lands in data/ and can be re-imported (with dedupe) via brain_import on any machine.
For automated backups to a USB drive on macOS, see scripts/backup-to-usb.sh and the launchd template scripts/com.example.brain-mcp-backup.plist.
Rebuild after code changes
npm run build
# Your MCP client restarts the server automatically (or restart it manually)Pin the Node binary in your MCP config
better-sqlite3 is a native module: it is compiled against one Node ABI and
refuses to load under another.
Error: The module 'better_sqlite3.node' was compiled against a different
Node.js version using NODE_MODULE_VERSION 147. This version of Node.js
requires NODE_MODULE_VERSION 137.So "command": "node" in an MCP config is a coin flip: it resolves through
PATH, and with a version manager (fnm, nvm, asdf) that depends on which
shell happened to launch your client. The server then dies at startup — and it
dies before the MCP handshake, so the client reports a connection problem
rather than an ABI problem.
Give it an absolute path to the same Node you build with:
{
"mcpServers": {
"brain": {
"command": "/absolute/path/to/node",
"args": ["/absolute/path/to/brain-mcp/dist/index.js"]
}
}
}node -p "process.execPath" # the path to pin
npm rebuild better-sqlite3 # after any Node upgradeReset the database (start fresh)
rm data/knowledge.db
# The database is recreated on the next server startTests
npm test # the MCP server (49 tests)
npm run test:hooks # the hooks (45 tests, standard library only)
npm run test:all # bothThe hooks are tested separately and in Python, because that is what they are:
plain scripts with no dependencies, so the suite needs nothing beyond the
interpreter that runs them. They had no tests at all until August 2026 while
src/ had 39 — and they are the only mechanism by which anything stored here
ever reaches an agent.
A hook is a pure function of (stdin payload, database, cwd) → (stdout, exit
code). The suite covers the search and ranking, the per-session
no-repeat rule, the instrumentation (including that showing a lesson must not
touch updated_at), the Stop hook's four anti-loop guards, and — for every
hook — malformed JSON, an empty payload, a missing database, a corrupt database,
a read-only database and an unwritable state directory. No hook may ever be
the reason a session fails.
tests/hardening.test.ts covers the questions the happy path never asks: two
connections writing at once (a knowledge base shared by agents in separate
worktrees is the normal case, not an edge case), a lesson archived and the search
index not told, a round-trip of Polish prose with a fenced code block inside it —
run twice, because a round-trip that normalises something on the first pass looks
lossless from the second onwards — a truncated export file, and an embeddings
endpoint that is slow rather than dead.
Smoke test
npm run build
node scripts/smoke-test.cjsFAQ
Q: Does brain-mcp slow down my editor? Not noticeably. The server starts in <100 ms, uses ~30 MB RAM, and SQLite queries take <1 ms.
Q: Does my data go to the cloud?
No. Zero network calls by default — everything is local, stdio only. If you opt into hybrid search via BRAIN_EMBEDDINGS_URL, lesson text is sent to that one URL, which is meant to be an Ollama server running on your own machine.
Q: Do I need Ollama / embeddings?
No. Without them brain_recall uses SQLite FTS5 keyword search, exactly as before. Embeddings only add semantic ("fuzzy meaning") matching on top.
Q: What if the database gets corrupted? SQLite in WAL mode is very resilient. Worst case — delete the DB file and start fresh.
Q: Can I move brain to another machine?
Yes — copy the whole brain-mcp/ folder and update the paths in your MCP client config.
Q: Does the assistant use brain automatically? Yes, when it deems it useful. You can also ask explicitly: "check brain".
License
MIT — see LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityDmaintenanceA local MCP server providing persistent memory for AI coding assistants by storing and searching architectural decisions, patterns, and solutions. It also includes tools for git automation and mapping codebase expertise based on project history.MIT
- Alicense-qualityDmaintenanceA self-hosted MCP server that provides AI assistants with a shared, persistent SQLite-backed memory for storing and retrieving project context, decisions, and discoveries. It enables cross-session continuity and team-wide knowledge sharing to keep AI coding tools aligned and informed.3MIT
- AlicenseBqualityBmaintenanceMCP server that provides cross-session persistent memory for AI coding assistants using local vector database and semantic search, enabling automatic recall of project context, issues, and tasks.991Apache 2.0
- Flicense-qualityDmaintenanceA local MCP server that provides semantic memory storage and retrieval for coding and AI agents, enabling durable context across chat sessions.1314
Related MCP Connectors
Person-owned, portable AI memory as a remote MCP server, readable and writable by any MCP client.
Cloud-hosted MCP server for durable AI memory
An MCP server that gives your AI access to the source code and docs of all public github repos
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Marcin-Stanczyk/brain-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server