Skip to main content
Glama

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 graph

Add 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

name

required

The MCP server name agents see. Also used in the state-lock message, CLI messages, each graph episode's source (<name>:<path>) and the default actor (<name>/local).

instructions

required

The text the MCP server sends agents when they connect. The engine ships none.

port

8766

Service port.

memory_dir

memory

The Markdown folder, relative to the vault.

state_dir

.brain

Index, graph, lock and metrics, relative to the vault.

kinds

see below

Memory kind to folder under memory_dir. remember refuses kinds not listed.

dated_kinds

[]

Kinds whose new filenames start with the date, for memories that record an event.

graph_model, graph_small_model

gpt-6-luna

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

MEMORY_VAULT

current directory

The vault folder, when --vault isn't given.

MEMORY_ROOT

<vault>/<memory_dir>

The Markdown folder. It moves only the Markdown; the state stays in the vault.

MEMORY_HOST

127.0.0.1

Bind address. Keep it on localhost, since the service has no authentication.

MEMORY_PORT

port

Service port.

MEMORY_ACCESS

read-write

read-only serves only search_memory and get_memory.

MEMORY_ACTOR

<name>/local

Written to each new memory's generated.by.

MEMORY_HUMAN_ID

owner

Written to verified.by as human:<id> when a memory is verified.

MEMORY_GRAPH_ENABLED

true

false serves Markdown and FTS only.

MEMORY_GRAPH_MODEL, MEMORY_GRAPH_SMALL_MODEL

graph_model, graph_small_model

Graphiti's models.

OPENAI_API_KEY

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 = false
uv sync
uv run brain-engine serve

The 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-vault

The 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 N

Each 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 serve on the same vault refuses to start;

  • search and sync-graph find 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;

  • reindex touches 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_graph would 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/listen request 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-server process that writes graphiti.db only on a timer. Closing the graph sends it SHUTDOWN 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, so graphiti.db is 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.db as 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 and sync-graph ingest 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/mcp

ChatGPT

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/vault

This 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:

  • remember records something new: an event, a decision, a changed fact.

  • edit_memory fixes the stored text itself, like a typo. It edits the file in place, clears verification and replaces the graph episode.

  • correct_memory records that a memory was wrong. It writes a new memory with corrects: <old id> and marks the old one status: 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 in graph_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:

  1. Search memory when history or decisions can help. Never load the whole vault.

  2. Prefer current facts unless the user asks about history.

  3. Fetch full memories only after search points to them.

  4. When something new happens, call remember rather than rewriting history. Use edit_memory only for errors in the text.

  5. Treat verified_by_user=false as useful but not confirmed.

  6. 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 from brain.toml.

  • MEMORY_ROOT used to move both the Markdown and the state folder, which sat next to it. It now moves only the Markdown, and state_dir stays 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_VAULT or the current directory.

  • The state folder's files keep their names and formats. A vault that points state_dir at its old folder keeps its index, graph and timings without re-ingesting anything. Keep the old server's name as name too, so clients see the same MCP server.

Development

just install   # uv sync --extra graph --extra dev
just test

The 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

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP 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
  • A
    license
    Not graded
    quality
    D
    maintenance
    Local 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
  • A
    license
    Not graded
    quality
    A
    maintenance
    Local-first MCP memory server providing persistent, versioned, queryable memory for AI agents using JSON categories and SQLite FTS5, with optional fleet sync.
    2
    Apache 2.0