A self-hosted AI memory layer with typed knowledge tables, hybrid search, and session handoff. Built for Onramp and any MCP-compatible AI client.
One Postgres database. One MCP endpoint. Every AI tool you use shares the same memory.
Inspired by Nate B. Jones's OB1 (Open Brain) and the broader Open Brain community. This is an independent implementation — no OB1 code is used. Built from scratch in Python, incorporating architectural lessons from community implementations.
OB1 established the vision: one database, one protocol, every AI tool shares the same memory. This project builds on that vision with a different language, different schema, and additional capabilities learned from real-world deployments.
| OB1 | OpenBrain MCP | |
|---|---|---|
| Language | TypeScript on Deno | Python (FastMCP) |
| Database | Supabase (hosted) | Self-hosted Postgres + pgvector |
| Schema | Single thoughts table |
Typed tables: rules, facts, incidents, tasks, documents |
| Embeddings | OpenRouter only | Ollama (default), OpenAI, OpenRouter |
| MCP tools | 4 | 12 (adds boot, recall, supersede, session lifecycle, browse, forget, update, rename_project) |
| Search | Vector cosine | Hybrid (vector + Postgres BM25 keyword) |
| Validation | SHA fingerprint dedup on capture | Write gate: headline/body word limits + semantic-similarity dedup |
| Session tracking | None | start_session / end_session with handoff notes |
| Memory lifecycle | Manual only | Reactivation tracking on facts (recall bumps the score; periodic decay is scaffolded but disabled by default), soft-delete via forget, supersede chain for rules |
| Audit | None | Append-only audit_log for every mutation |
| Deployment | Supabase Edge Functions | Docker / Onramp / any container host |
- 12 MCP tools:
capture,search,recall,boot,browse,stats,update,supersede,forget,rename_project,start_session,end_session - Typed memory tables so different memory types can have different lifecycles:
rules— immutable behavioral guidance, modified only viasupersedefacts— knowledge with a reactivation score that recall bumps upward; the periodic decay process is scaffolded (env knobs exposed) but disabled by default in this releaseincidents— postmortems, archivabletasks—open/blocked/done/staledocuments— large handoff specs, architecture docs, and project briefs (up to ~60KB). Capped at 10 active per project. Use for passing detailed plans between AI contexts (e.g. claude.ai → Claude Code)
- Write gate validates every capture: headline ≤15 words, body ≤400 words (≤15,000 for documents), semantic-similarity duplicate check (cosine threshold)
- Hybrid search blends pgvector cosine similarity with Postgres
tsvectorBM25 — configurable weight - Headline-only boot payloads with hard token cap so a session start doesn't burn 15K tokens loading memory bodies
- Session handoff — end one session with a note → next
boot/start_sessionsurfaces it - Audit log — every insert / update / supersede / delete recorded
- Ollama-first embeddings — local, free, no API key. OpenAI and OpenRouter supported as alternatives
- All knobs in
.envwith sensible defaults
If you run Onramp:
cd /apps/onramp
make enable-service openbrain
make edit-env openbrain # review auto-generated DB password and access key
make start-service openbrain
make logs openbrainThe service is then reachable at https://openbrain.<HOST_DOMAIN>/mcp with TLS via Traefik. The auto-generated OPENBRAIN_MCP_ACCESS_KEY in services-enabled/openbrain.env is what your MCP clients authenticate with.
You'll also need an embedding model in Ollama:
docker exec ollama ollama pull nomic-embed-textdocker network create openbrain
docker run -d --name openbrain-db --network openbrain \
-e POSTGRES_USER=openbrain \
-e POSTGRES_PASSWORD=changeme \
-e POSTGRES_DB=openbrain \
-v $PWD/pgdata:/var/lib/postgresql/data \
pgvector/pgvector:pg16
docker run -d --name openbrain --network openbrain -p 8080:8080 \
-e DATABASE_URL=postgres://openbrain:changeme@openbrain-db:5432/openbrain \
-e OPENBRAIN_OLLAMA_BASE_URL=http://host.docker.internal:11434 \
-e OPENBRAIN_MCP_ACCESS_KEY=$(openssl rand -hex 16) \
ghcr.io/crack-kitty/openbrain-mcp:latestThen point an MCP client at http://localhost:8080/mcp with the bearer token from your run command (see Connecting AI clients below).
OPENBRAIN_MCP_ACCESS_KEY is required — the server refuses to start without it. Generate a strong random value (e.g. openssl rand -hex 32).
Authenticate with the Authorization: Bearer YOUR_KEY header. This is the only method enabled by default.
Add to the mcpServers block in ~/.claude.json:
{
"mcpServers": {
"openbrain": {
"type": "http",
"url": "https://openbrain.example.com/mcp",
"headers": {
"Authorization": "Bearer YOUR_OPENBRAIN_MCP_ACCESS_KEY"
}
}
}
}Any client that speaks MCP over HTTP works. Send the access key in the Authorization header. The MCP endpoint is /mcp.
⚠️ Security trade-off. Passing the access key as a URL query parameter leaks it through browser history, web-server access logs, upstream proxies, monitoring/observability tools, and HTTPRefererheaders. Use this only if your client cannot send a customAuthorizationheader, and treat the key as compromised the moment anyone else sees the URL. Rotate it more aggressively than you would a header-based key.
Query-parameter auth is off by default. Enable it by setting:
OPENBRAIN_ALLOW_QUERY_KEY=1
Then connect with the key in the URL:
Settings → Integrations → Add more integrations... → scroll to the bottom → Add custom integration → paste this URL:
https://openbrain.example.com/mcp?key=YOUR_OPENBRAIN_MCP_ACCESS_KEY
Select No Authentication — the access key is embedded in the URL.
Settings → Connectors → Developer Mode → Add custom MCP server → paste the same ?key= URL. Select No Authentication.
All configuration is via environment variables. Defaults in parentheses.
| Variable | Default | Notes |
|---|---|---|
DATABASE_URL |
(required) | Postgres connection string with pgvector available |
OPENBRAIN_HOST |
0.0.0.0 |
Bind address |
OPENBRAIN_PORT |
8080 |
Listen port |
OPENBRAIN_MCP_ACCESS_KEY |
(required) | Access key — server refuses to start if unset. Pass as Authorization: Bearer header. |
OPENBRAIN_ALLOW_QUERY_KEY |
false |
Set 1 to also accept the key as ?key= query parameter (leaks via URLs/logs/referrers — see "Clients that can't send custom headers" in Connecting AI clients) |
| Variable | Default | Notes |
|---|---|---|
OPENBRAIN_EMBEDDING_PROVIDER |
ollama |
ollama / openai / openrouter |
OPENBRAIN_EMBEDDING_MODEL |
nomic-embed-text |
Must match the configured provider |
OPENBRAIN_EMBEDDING_DIMENSIONS |
768 |
Must match the model's output size |
OPENBRAIN_OLLAMA_BASE_URL |
http://ollama:11434 |
Used when provider=ollama |
OPENBRAIN_OPENAI_API_KEY |
(unset) | Used when provider=openai |
OPENBRAIN_OPENAI_MODEL |
text-embedding-3-small |
OpenAI embedding model |
OPENBRAIN_OPENROUTER_API_KEY |
(unset) | Used when provider=openrouter |
| Variable | Default | Notes |
|---|---|---|
OPENBRAIN_HYBRID_WEIGHT |
0.3 |
Keyword score weight (0 = pure vector, 1 = pure keyword) |
OPENBRAIN_DEDUP_THRESHOLD |
0.92 |
Cosine ≥ this on capture → reject as duplicate |
OPENBRAIN_MERGE_LOWER_THRESHOLD |
0.70 |
Lower bound of the smart-merge zone (reserved for future consolidation) |
| Variable | Default | Notes |
|---|---|---|
OPENBRAIN_HEADLINE_MAX_WORDS |
15 |
Hard cap on headline length |
OPENBRAIN_BODY_MAX_WORDS |
400 |
Hard cap on body length (all kinds except documents) |
OPENBRAIN_DOCUMENT_BODY_MAX_WORDS |
15000 |
Hard cap on document body length (~60KB) |
OPENBRAIN_DOCUMENT_MAX_ACTIVE |
10 |
Max active documents per project |
OPENBRAIN_DOCUMENT_EMBED_PREFIX_WORDS |
500 |
How many words of a document body to embed for search (headline is always fully embedded) |
The fields exist in the schema and the env knobs are wired through config.py, but the periodic decay/consolidation process is not yet running in this release. Recall still bumps a fact's decay_score upward; nothing currently decreases it.
| Variable | Default | Notes |
|---|---|---|
OPENBRAIN_DECAY_LAMBDA |
0.005 |
Reserved: per-day decay rate (consumed only when consolidation is enabled in a future release) |
OPENBRAIN_CONSOLIDATION_INTERVAL |
0 |
Reserved: minutes between consolidation passes (0 = disabled; no consumer in this release) |
| Variable | Default | Notes |
|---|---|---|
OPENBRAIN_BOOT_TOKEN_CAP |
2000 |
Hard token cap on the boot payload (minimum 200) |
OPENBRAIN_BOOT_BLOCKER_CAP |
5 |
Max BLOCKER rules in boot |
OPENBRAIN_BOOT_PATTERN_CAP |
5 |
Max PATTERN rules in boot |
OPENBRAIN_BOOT_TASK_CAP |
20 |
Max active tasks in boot |
OPENBRAIN_HANDOFF_MAX_CHARS |
4000 |
Max chars accepted by end_session(handoff_note=...). Larger handoffs are rejected; in boot(), a handoff that still exceeds the token cap after shrinking is truncated with a sentinel, then dropped entirely if even that won't fit. |
| Variable | Default | Notes |
|---|---|---|
OPENBRAIN_METADATA_LLM_PROVIDER |
ollama |
Reserved for richer auto-tagging on capture |
OPENBRAIN_METADATA_LLM_MODEL |
qwen2.5-coder:14b |
Same |
The schema separates memories by lifecycle, not by content. Behavioral rules and project facts are fundamentally different — they're written for different reasons, accessed at different times, and decay (or don't) at different rates. Stuffing them in one table forced compromises that fell over at scale in earlier Open Brain implementations.
OpenBrain MCP uses five typed tables:
rules— immutable, severity-classified (BLOCKER/PATTERN), modified only viasupersedeso the audit chain is preserved. Loaded into the boot payload.facts— knowledge withaccess_countand adecay_scorethat recall bumps upward. The periodic decay process is scaffolded (env knobs exist) but not yet active in this release, so today this is reactivation tracking rather than full decay. Not loaded at boot — fetched on demand viasearch+recall.incidents— postmortems and bug records, archivable after a quiet period.tasks— open / blocked / done / stale, surfaced in the boot payload while open.documents— large-format memories for architecture docs, project briefs, and handoff specs that exceed the normal 400-word body limit (up to ~60KB / 15,000 words). Capped at 10 active per project. Not loaded at boot (too heavy) — discoverable viasearch, retrievable viarecall. Only the headline and first 500 words of body are embedded for search; the full body is stored and returned onrecall.
A memory_index table mirrors headlines and embeddings across all five kinds for a single search query path. An HNSW index serves cosine search; a tsvector GIN index serves keyword search. Hybrid scoring blends the two with a configurable weight.
Every mutation lands in audit_log with a JSON snapshot of the row at the time of the change — useful when something looks wrong six months from now. The audit write is best-effort: it runs after the main mutation transaction commits, on a separate connection, so a process crash between the commit and the audit insert can leave a committed mutation with no audit row. For a personal memory service this is the right trade-off (the audit is for hindsight, not authorization); harden it if you're storing material that demands a true audit trail.
git clone https://github.com/crack-kitty/openbrain-mcp
cd openbrain-mcpNo containers needed — tests the write gate and validation logic directly:
uv run --with pytest pytest tests/Spins up an isolated Postgres + OpenBrain stack (separate from production), runs the full test suite, and tears it down:
./tests/run_integration.shThis uses docker-compose.test.yml which starts a throwaway pgvector database and a fresh OpenBrain instance on port 8081. Production is never touched.
MIT — see LICENSE.
- Nate B. Jones for creating OB1 and the Open Brain concept
- The Open Brain community for architectural patterns and real-world deployment lessons
- Built for the Onramp self-hosting framework