brain-engine
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
brain-engine
A memory server for AI agents. A vault is a folder of Markdown memories with YAML frontmatter. brain-engine indexes it with SQLite FTS5, builds a temporal knowledge graph of it with Graphiti, and serves both to agents over MCP.
Markdown files, committed to git
SOURCE OF TRUTH
│
┌──────────────┴──────────────┐
│ │
FTS5 Graphiti
document retrieval entities, facts and time
│ │
└──────────────┬──────────────┘
│
one long-lived local MCP service
(Streamable HTTP, 127.0.0.1:<port>/mcp)
│
┌─────────────┬───────┴──────┬──────────────┐
Claude Code Cursor Gemini CLI ChatGPT (later)Every client connects to the same running service. It is the only process that opens Graphiti's embedded FalkorDB Lite database. Graphiti and SQLite are indexes you can throw away and rebuild. The Markdown is what counts.
It runs on macOS and Linux with Python 3.12 or later.
A vault holds no server code. It has a brain.toml, its memory/ folder, a pyproject.toml that pins an engine tag, and whatever scripts it wants. A fix lands here once and reaches a vault when that vault bumps its pin.
Vault layout
my-vault/
├── brain.toml # name, instructions, port, kinds; see below
├── pyproject.toml # pins brain-engine
├── memory/ # the Markdown, committed to git
│ ├── notes/
│ ├── sessions/
│ │ └── transcripts/
│ └── <one folder per kind you add>
└── .brain/ # derived, per machine, ignored by git
├── index.sqlite3 # FTS5, plus one row per file (path, stat, id, status, hash)
├── graphiti.db # embedded FalkorDB Lite
├── graph-state.sqlite3 # memory id <-> Graphiti episode, with content hashes
├── metrics.sqlite3 # how long each operation took on this machine
└── service.lock # held by the one process that owns the graphAdd the state folder to the vault's .gitignore. examples/vault/ has a starting point.
Related MCP server: MCPSaver
brain.toml
name = "my-memory"
instructions = """
My long-term memory, shared by every agent I work with. Call search_memory first. ...
"""
port = 8766
state_dir = ".brain"
dated_kinds = ["decision", "session", "transcript"]
[kinds]
decision = "decisions"
person = "people"
project = "projects"Field | Default | Meaning |
| required | The MCP server name agents see. Also used in the state-lock message, CLI messages, each graph episode's source ( |
| required | The text the MCP server sends agents when they connect. The engine ships none. |
|
| Service port. |
|
| The Markdown folder, relative to the vault. |
|
| Index, graph, lock and metrics, relative to the vault. |
| see below | Memory kind to folder under |
|
| Kinds whose new filenames start with the date, for memories that record an event. |
|
| The OpenAI models Graphiti uses to extract entities and facts. |
remember defaults to kind note, and remember_session writes session and transcript. If kinds leaves one of those out, the engine adds it: note to notes/, session to sessions/, transcript to sessions/transcripts/. Map them yourself to use other folders. A misspelled field, a dated kind with no folder or a missing name stops the engine with an error naming it.
The graph models are set explicitly so that a graphiti-core upgrade can't switch them without anyone noticing. graphiti-core 0.30.2 defaulted to gpt-4.1-nano, and once OpenAI retired it every ingest failed with model_not_found. If you pick another model, check that the pinned graphiti-core sends it valid parameters. Some versions sent reasoning.effort: minimal to models that reject it.
Environment variables
The environment overrides brain.toml, which overrides the engine defaults. A blank value counts as unset. The engine reads only the process environment and never loads .env itself. Let your task runner do that, with set dotenv-load in a justfile for example.
Variable | Default | Meaning |
| current directory | The vault folder, when |
|
| The Markdown folder. It moves only the Markdown; the state stays in the vault. |
|
| Bind address. Keep it on localhost, since the service has no authentication. |
|
| Service port. |
|
|
|
|
| Written to each new memory's |
|
| Written to |
|
|
|
|
| Graphiti's models. |
| unset | Graphiti's LLM and embedding calls. Without it the service logs a warning and serves Markdown and FTS. |
Access mode and actor belong to the service, so every connected client shares them.
Installing in a vault
brain-engine isn't on PyPI. A vault installs a tag straight from GitHub with uv:
[project]
name = "my-vault"
version = "0"
requires-python = ">=3.12"
dependencies = ["brain-engine[graph] @ git+https://github.com/LiorHalfon/brain-engine@v0.1.0"]
[tool.uv]
package = falseuv sync
uv run brain-engine serveThe graph extra brings Graphiti and FalkorDB Lite. Without it the service runs on Markdown and FTS alone.
To upgrade, change the tag, run uv lock, and commit both files. Tags follow vX.Y.Z, and CHANGELOG.md says what each one changed.
Running a vault against a local checkout
To try an engine change in a real vault before tagging it, layer your checkout over the vault's environment for one command:
uv run --project ~/my-vault --with-editable ~/git/brain-engine brain-engine serve --vault ~/my-vaultThe vault's pyproject.toml and uv.lock don't change, so there is nothing to undo.
Running the service
brain-engine serve # Streamable HTTP on http://127.0.0.1:<port>/mcp
brain-engine search "query" # through the service if it runs, otherwise locally
brain-engine sync-graph # ingest new and edited Markdown into Graphiti
brain-engine reindex # rebuild the FTS5 index from the Markdown
brain-engine metrics # how long operations took on this machine; --days NEach command takes --vault DIR, then falls back to MEMORY_VAULT, then the current directory.
One process owns the graph
The service opens the Markdown store, the FTS5 index and FalkorDB Lite once at start-up and shares them across all clients. It holds service.lock in the state folder while it runs, so:
a second
serveon the same vault refuses to start;searchandsync-graphfind the running service through the lock and call it over MCP. With no service running, they work locally and hold the lock while they do;reindextouches only SQLite and is safe at any time.
Writes don't wait for the graph
A Graphiti ingest takes about 20 seconds of OpenAI calls, against about 40 ms for the Markdown write and FTS. While the service runs, remember, remember_session, edit_memory and correct_memory return once the Markdown file and FTS have the memory, with "graph": {"queued": true, "memory_id": ...}. One background worker ingests queued memories in write order, one at a time, because Graphiti resolves each episode's entities against the graph built so far. A memory edited again before its turn is ingested once, with its newest text.
For about 20 seconds after a write, search finds the memory through FTS but not yet through graph facts.
At start-up the service queues every memory
sync_memory_graphwould ingest. A memory whose text the graph already has is skipped without calling OpenAI, so this costs a few milliseconds per memory. It picks up memories pulled from git and anything still queued at the last stop.Stopping the service doesn't wait for an ingest in flight. The memory is already in Markdown and FTS, and the next start queues it again.
A failed ingest is logged and recorded in graph-state and
metrics, and the queue moves on. The next start retries it.If Graphiti didn't start, writes ingest right away and return the error, rather than queueing work that would fail where nobody sees it.
sync_memory_graph, and the CLI when no service runs, ingest right away and return counts.
Stopping
Ctrl-C, kill <pid> (SIGTERM) and closing the terminal that runs the service (SIGHUP) all stop it the same way, for serve and stdio alike. The service logs <name> stopped when it is done. A service started with SIGHUP ignored, as nohup does, keeps ignoring it.
Each connected client holds a
subscriptions/listenrequest open. The service ends those streams first, which is the spec's graceful close, and clients listen again when the service is back. uvicorn's 3-second timeout then only cuts off a tool call still running.FalkorDB Lite runs a
redis-serverprocess that writesgraphiti.dbonly on a timer. Closing the graph sends itSHUTDOWN SAVE, so the file is current and the process exits.A signal that arrives while the service is still opening the graph cancels the start and closes what was opened.
Some terminals don't send a signal the service can catch. Closing a tab in the Claude desktop app's terminal panel SIGKILLs the service and every process it started. FalkorDB Lite's redis-server is detached from the service, so it survives that. When the graph opens, the engine starts a small watcher, detached the same way, that checks on the service twice a second. Once the service is gone the watcher sends the redis-server
SHUTDOWN SAVE, sographiti.dbis saved and nothing is left running about a second later. When the service stops the server itself, the watcher just exits.
Older servers stopped cleanly only on Ctrl-C. After a kill or a closed terminal their redis-server kept running, and whatever killed it later, a logout for example, made it write its in-memory graph over graphiti.db. That graph could be older than the one on disk. Two things clean up after them:
On start, before opening the graph, the engine looks for running FalkorDB Lite redis-servers working in its state folder with its
graphiti.dbas their database. One with no unsaved changes is stopped without saving. If exactly one has unsaved changes, it holds writes from a service that died, so it saves and then stops. If more than one has unsaved changes, the engine refuses to start and names them, since guessing would lose data. It also refuses when a server for its graph doesn't answer, since it may still be saving. The log says what was stopped.Opening the graph checks graph-state against the episodes actually in the graph. A memory marked ingested whose episode is gone is marked
missing, and the start-up catch-up andsync-graphingest it again. The log counts them.
The engine also deletes FalkorDB Lite's graphiti.db.settings before opening, so it never reattaches to a server it didn't start. In a copied state folder that file names the original vault's server.
Graph viewer
While the service runs, http://127.0.0.1:PORT/graph shows the Graphiti graph.
Entities are circles sized by how many facts touch them. One line joins each pair of entities and carries every fact between them. The arrow shows the direction, and a line without an arrowhead has facts both ways.
Only labels that fit are drawn, so zoom in to see more. Hover a node to light up its neighbours and the names of its facts.
The chips at the top count entity kinds. Click one to highlight it. The relationship menu highlights the facts with one name.
Search finds an entity. Enter selects the best match and frames it with its neighbours.
Click a node or line to inspect it. A line lists each fact with its dates and the Markdown memories it came from.
Double-click a node to show only it and its neighbours. Drag a node to move it.
Show history adds invalidated and superseded facts as fainter lines.
The page is read-only and answers only when the Host header is localhost, which blocks DNS-rebinding pages from reading memories. It is a plain web page, not an MCP tool, so agents still see only the memory API. It renders with Sigma.js and lays the graph out with ForceAtlas2 from graphology, loaded as pinned ES modules from jsDelivr. The graph data never leaves the machine.
Timings
The service writes one row per operation to metrics.sqlite3: the operation (the MCP tool name, graph_ingest for one background ingest, or graph_view for /graph.json), the total time, the part spent in Graphiti and its OpenAI calls (graph_ms), the rest (store_ms), the number of searchable memories at the time, and for writes the content size. An operation that raised, or whose graph step failed and fell back, keeps the error. Recording never fails the operation. A background ingest that skipped an unchanged memory isn't recorded.
brain-engine metrics reports the last 30 days. It gives p50, p90 and max per operation with the store and graph split, then the medians grouped by vault size, so a slowdown that comes with growth shows up as rising rows. Ingests also get Graphiti time per KB, because ingest time grows with the memory's size and only a rise per KB means the graph itself is slowing down.
scripts/bench_store.py <vault> [sizes...] answers the same question ahead of time. It copies the vault's Markdown into a temporary folder, pads it with renamed copies of its own memories up to each size, and times search, get, remember, edit and verify with Graphiti stubbed out. It never touches the vault and makes no OpenAI calls.
Connecting clients
Claude Code
Add a .mcp.json to the vault, or to any project that should see the memory:
{
"mcpServers": {
"my-memory": {
"type": "http",
"url": "http://127.0.0.1:8766/mcp"
}
}
}Claude Code asks once to approve it. For every project, use claude mcp add --scope user --transport http my-memory http://127.0.0.1:8766/mcp.
Cursor
.cursor/mcp.json:
{
"mcpServers": {
"my-memory": {
"url": "http://127.0.0.1:8766/mcp"
}
}
}Enable it once under Cursor Settings, MCP.
Gemini CLI
gemini mcp add --scope user --transport http my-memory http://127.0.0.1:8766/mcpChatGPT
Not supported yet. ChatGPT can't reach 127.0.0.1, so it needs the service behind a tunnel or another remote MCP route, and that needs authentication first.
stdio
For a client that only speaks stdio:
uv run --project /path/to/vault brain-engine stdio --vault /path/to/vaultThis starts a separate server inside the client's process. It takes the same lock, so it can't run alongside serve or another stdio client. Prefer the HTTP service.
The memory model
Memory format
---
type: decision
id: mem_abc123
title: Memory architecture
tags:
- memory
status: draft
created_at: 2026-09-19T20:00:00Z
generated:
by: claude-code/local
at: 2026-09-19T20:00:00Z
verified_by_user: false
searchable: true
content_sha256: ...
sources:
- resource: conversation:123
---
Markdown remains the source of truth. Graphiti is a derived temporal index.Retrieval
Agents search first and read second. search_memory returns compact current facts from the graph plus short Markdown snippets from FTS. The agent picks the one to three memories that matter and reads them with get_memory, which pages large memories so a long transcript can't enter the prompt in one call. Transcripts saved through remember_session are searchable: false. They are kept, but out of normal search.
Duplicates
The store hashes normalized type + content before writing. An exact duplicate creates no file, no FTS row and no graph episode. Graph sync hashes the file body again, so hand edits are noticed even when the frontmatter hash is stale.
Graphiti resolves entities and deduplicates facts across episodes. The service also drops exact-duplicate facts before returning them.
Contradictions and time
Don't overwrite a memory because reality changed. "The office opens at 9" in September and "the office opens at 10" in October are two memories and two episodes, and Graphiti marks the first fact as no longer valid. Three tools keep this straight:
rememberrecords something new: an event, a decision, a changed fact.edit_memoryfixes the stored text itself, like a typo. It edits the file in place, clears verification and replaces the graph episode.correct_memoryrecords that a memory was wrong. It writes a new memory withcorrects: <old id>and marks the old onestatus: superseded,superseded_by: <new id>. The old episode stays in the graph so the history survives.
By default search_memory leaves out graph facts with invalid_at set, superseded memories, and graph facts whose every source memory is superseded, so a missed invalidation can't make an old source look current. include_history=true brings them back.
Provenance and verification
Each memory records who wrote it (generated.by) and, optionally, where it came from (sources). verified_by_user stays false until a person confirms the memory and an agent calls verify_memory, which writes verified.by: human:<MEMORY_HUMAN_ID>. Any edit clears it.
MCP tools
Always:
search_memory(query, limit=5, kind=None, include_history=false). Graph facts come from Graphiti's hybrid search, which needs OpenAI to embed the query. If that takes longer than 0.5 s, the facts come from the graph's keyword index alone, which is local and takes about 40 ms. If Graphiti is down, search returns FTS results and says why ingraph_error.get_memory(memory_id, offset=0, max_chars=8000).
With MEMORY_ACCESS=read-write: remember, remember_session, edit_memory, correct_memory, verify_memory, sync_memory_graph. Agents never get file-system write tools.
Suggested agent rules
Put rules like these in the vault's instructions or in the agent's own instructions:
Search memory when history or decisions can help. Never load the whole vault.
Prefer current facts unless the user asks about history.
Fetch full memories only after search points to them.
When something new happens, call
rememberrather than rewriting history. Useedit_memoryonly for errors in the text.Treat
verified_by_user=falseas useful but not confirmed.Ask before storing sensitive material. Suggest candidate memories instead of bulk-importing.
Changes from the per-vault servers
brain-engine replaces vault-specific servers that each ran their own copy of this code. If you ran one:
Its own command is gone. Use
brain-engine <command> --vault <dir>. MCP clients that connect over HTTP need no change, since the name, port and URL come frombrain.toml.MEMORY_ROOTused to move both the Markdown and the state folder, which sat next to it. It now moves only the Markdown, andstate_dirstays relative to the vault.The old servers found
memory/relative to their own source files. The engine is installed as a package, so it finds the vault through--vault,MEMORY_VAULTor the current directory.The state folder's files keep their names and formats. A vault that points
state_dirat its old folder keeps its index, graph and timings without re-ingesting anything. Keep the old server's name asnametoo, so clients see the same MCP server.
Development
just install # uv sync --extra graph --extra dev
just testThe tests need no network and no API key. They replace Graphiti with fakes where they can and start real FalkorDB Lite servers where the redis-server itself is under test. AGENTS.md has the code map.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Token-efficient MCP memory for Markdown vaults. Tiered search, GraphRAG, AI memories.
Serve a folder of Markdown notes as an MCP server: hybrid search, reading, and sourced answers.
One memory, every AI. A shared, user-owned markdown memory your AI clients read and write over MCP.
Private-by-default, local-first memory/context/task orchestrator for MCP apps and agents.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceMCP server that provides agentic memory management for markdown vaults, enabling hybrid search, governed writing, and maintenance of episodic, semantic, procedural, and working memories for LLM agents.MIT
- AlicenseNot gradedqualityDmaintenanceLocal MCP server for persistent, searchable agent memory using SQLite FTS5, replacing flat MEMORY.md files with efficient full-text search and workspace context caching.GPL 3.0
- AlicenseNot gradedqualityAmaintenanceLocal-first MCP memory server providing persistent, versioned, queryable memory for AI agents using JSON categories and SQLite FTS5, with optional fleet sync.2Apache 2.0
- AlicenseAqualityAmaintenanceAn MCP server that gives AI agents durable, temporal memory over local markdown vaults, with tools for searching, asserting facts, querying point-in-time state, and reinforcing useful knowledge.15167 npmMIT