A Model Context Protocol (MCP) server that gives AI/LLM agents a durable brain memory — the ability to store, recall, search, connect, summarize, and forget information across sessions through standardized MCP tool calls.
Since v0.4.0 memory is modelled internally as a small knowledge graph:
- memories are the graph nodes (content, category, tags, importance),
- connections are directed links between memories (e.g.
related_to,caused_by,part_of), - details are extra facts attached to a single memory.
This makes recall precise — instead of only matching words, the server can walk the connections you build (multi-hop recall) and explain how two memories relate (shortest path). The tool vocabulary stays "memory"-oriented (no "entity" wording), and it is still just SQLite under the hood — zero extra dependencies.
The server runs in two modes:
- stdio (default) — the server is launched as a subprocess by an MCP
client (e.g. via
uvx brainmemory-mcp). - web — MCP over HTTP + Server-Sent Events (SSE) with
--web, so remote MCP-capable clients (Claude, IDE agents, etc.) can connect over the network.
Memory is persisted locally under ~/.brainmemory-mcp (a SQLite database).
Since v0.9.0 the tool surface is consolidated: every operation takes a list, so acting on one memory or fifty is the same call (a single item is just a list of one). Detail and link writes are unified into one mixed-operation batch tool per entity. The result is full CRUD over all three entities with 15 tools.
| Tool | Description |
|---|---|
store_memories |
Persist one or more memories (content, category, tags, importance). |
recall_memories |
Fetch one or more memories by id; opt-in include_details / include_links for the richer payload. |
search_memory |
Search-engine style: rank memories by relevance (BM25) for multi-word/long queries; also searches details; optional graph expand. |
list_memories |
List stored memories (most important & recent first). |
update_memories |
Modify one or more memories (only supplied fields change). |
forget_memories |
Delete one or more memories (now soft-deletes into trash for safety). |
edit_details |
Add / update / delete extra facts attached to memories — mixed ops in one batch. |
edit_links |
Create (link) / remove (unlink) directed connections — mixed ops in one batch. |
recall_related |
Multi-hop recall: memories connected to one memory, up to depth hops. |
connect_memories |
Shortest connection (path) between two memories. |
memory_map |
Return a map (nodes + links) of the memory graph. |
summarize_memories |
Summary statistics: totals, categories, top tags, connection stats, most-connected memories. |
export_graph_html |
Export the complete graph to a standalone interactive 3D HTML file at an absolute output_path. |
restore_memories |
Soft-delete trash, history, rollback, and trash purge management. |
transfer_memories |
Download/upload migration JSON inline across MCP servers, with optional absolute file paths and keyset pagination for large graphs. |
Every list-taking tool processes items independently and reports a per-item
status — one bad item never aborts the batch.
File operations are exposed through the same MCP tool registry in both stdio
and HTTP/SSE modes. export_graph_html takes an absolute output_path.
For migration between machines, transfer_memories(op="export") returns the
portable migration object in its data field (download), and
transfer_memories(op="import", data=...) accepts that object directly
(upload). This avoids incorrectly asking a remote HTTP server to read a path
from the client's filesystem:
{"op": "export"}
{"op": "import", "data": {"format": "brainmemory-export", "format_version": 1, "memories": []}, "on_conflict": "overwrite"}Server-local file workflows remain supported by supplying an absolute
output_path on export or an absolute input_path on import. Relative paths
(including ./file and ../file) and explicit . / .. path segments are
rejected. Parent directories are created for file exports.
{"output_path": "/absolute/workspace/memory-graph.html"}
{"op": "export", "output_path": "/absolute/workspace/brainmemory.json"}
{"op": "import", "input_path": "/absolute/workspace/brainmemory.json", "on_conflict": "skip"}Two independent servers (e.g. a local stdio server and a remote
HTTP/SSE server, or vice versa) share no filesystem, so migrating between
them goes through the calling agent's context — one giant export can be too
big for a huge memory graph. Add limit (and scope="memories",
scope="links", or scope="trash") to page through it instead:
{"op": "export", "scope": "memories", "limit": 200}Each call returns has_more / next_cursor at the top level; keep calling
with cursor=<next_cursor> until has_more is false, feeding each page's
data straight into {"op": "import", "data": ...} on the destination
server. Page through every scope="memories" batch first, then repeat
with scope="links" — an import never errors on a link whose endpoints
don't exist yet, it just skips it (reported in links_skipped), so
exporting links before their memories only under-imports links, it never
corrupts data. scope="all" (the default) stays a single unpaginated
full-graph export/import, unchanged from before; limit/cursor require a
single scope ("memories", "links", or "trash" — not "all").
Pagination uses a stable keyset cursor (plain base64url text — safe to
copy/paste through any client), so it stays O(page size) per call regardless
of how large the graph is.
Row count alone does not bound payload size: a limit of 100 can still be
too big if some memories hold large content (e.g. full book-text sections).
Start with a modest limit (15-25) for graphs with long-content memories and
raise it once you have confirmed pages stay comfortably within your client's
tool-result budget.
{"op": "export", "scope": "memories", "limit": 200, "cursor": "<next_cursor>"}
{"op": "export", "scope": "links", "limit": 500}
{"op": "export", "scope": "trash", "limit": 50}scope="trash" transfers exact soft-delete snapshots separately, preserving
memory IDs, deleted_at, embedded details, and embedded links without a
restore/forget workaround. Import trash pages with the same op="import"
(the payload's own "trash" key routes it automatically — no separate
import op); on_conflict="skip" is idempotent and "overwrite" replaces an
existing trash snapshot. category/tags filters and scope="all" are not
applicable to scope="trash" (trash rows carry no category/tag filtering).
edit_details — each item's op selects the operation:
{"items": [
{"op": "add", "memory_id": "<id>", "content": "config lives in /etc/nginx"},
{"op": "update", "detail_id": "<id>", "content": "corrected fact"},
{"op": "delete", "detail_id": "<id>"}
]}edit_links — connect/disconnect memories, mixed in one call:
{"items": [
{"op": "link", "from_id": "<a>", "to_id": "<b>", "relation": "depends_on", "weight": 0.9},
{"op": "link", "from_id": "<a>", "to_id": "<c>"},
{"op": "unlink", "from_id": "<a>", "to_id": "<d>"}
]}Re-linking the same from/to/relation updates the weight (upsert). Detail ids
are returned by the add op and by recall_memories(include_details=true).
Two read-only resources are exposed as JSON: brainmemory://stats (the summary)
and brainmemory://graph (the nodes + links map).
Migrating from v0.8.0 or earlier: the singular tools (
store_memory,recall_memory,update_memory,forget_memory,add_detail,link_memories,unlink_memories) and the v0.8.0 bulk names (store_memorieskept its name;add_details,link_memories_bulk,unlink_memories_bulkwere folded intoedit_details/edit_links) are replaced by the 15 tools above. The database is untouched — only the tool names/shapes changed, not the storage or graph model.
search_memory no longer needs a single keyword. It tokenises your query and
ranks memories by relevance, so full sentences work:
- Full-text + BM25 — a SQLite FTS5 index over content/tags/category/
details (kept in sync by triggers), ranked with BM25. Multi-word / long
queries match memories containing any (or, with
mode="all", every) term, with prefix + Porter stemming (syncmatchessyncing). - Graph spreading activation — with
expand=True(default), memories connected in the knowledge graph to a text hit are pulled in with a decayed score, so related context surfaces even without the query words. - Ranking blends text relevance with importance and recency. Each result
carries
relevance(0..1),match_type(text|related|list),matched_terms, anddistance(hops from a text hit). - Fallback — where a SQLite build lacks FTS5, search degrades to a tokenised
LIKEterm-coverage scorer, so it always works.summarize_memoriesreports the active engine (fts5-bm25orlike-fallback).
Example: search_memory("Burp Firefox proxy sync") returns the relevant
memories ranked, plus anything linked to them — in a single call.
From PyPI:
python3 -m pip install brainmemory-mcpFrom a local checkout:
python3 -m pip install .Both install the package and a console script named brainmemory-mcp.
For development (editable install):
python3 -m pip install -e ".[dev]"To build/publish a release, see docs/RELEASING.md.
Best for local MCP clients that launch the server themselves. Memory in
~/.brainmemory-mcp.
brainmemory-mcp
# Or without the console script
python3 -m brainmemory_mcp
# With a custom memory location
brainmemory-mcp --data-dir /path/to/memoryEnable with --web for remote / networked clients.
# Defaults: 127.0.0.1:8765, memory in ~/.brainmemory-mcp
brainmemory-mcp --web
# Custom host/port and memory location
brainmemory-mcp --web --host 0.0.0.0 --port 9000 --data-dir /path/to/memory
# Optional Bearer authorization (prefer the env var to avoid shell history)
BRAINMEMORY_KEY='replace-with-a-strong-secret' brainmemory-mcp --web
# Equivalent CLI form: brainmemory-mcp --web --key 'replace-with-a-strong-secret'When --key or BRAINMEMORY_KEY is set, every web request must include:
Authorization: Bearer replace-with-a-strong-secretThis protects both endpoints. Without a key, web mode remains unauthenticated for backward compatibility. Use HTTPS through a reverse proxy when exposing the server over a network; a Bearer key sent over plain HTTP is not encrypted.
Endpoints once running in web mode:
- SSE stream:
http://<host>:<port>/sse - Message POST:
http://<host>:<port>/messages/
| Option | Env var | Default |
|---|---|---|
--web |
BRAINMEMORY_WEB |
false (stdio) |
--host |
BRAINMEMORY_HOST |
127.0.0.1 |
--port |
BRAINMEMORY_PORT |
8765 |
--key |
BRAINMEMORY_KEY |
unset (authorization disabled) |
--data-dir |
BRAINMEMORY_HOME |
~/.brainmemory-mcp |
Configure the client to launch the server as a subprocess:
{
"mcpServers": {
"brainmemory": {
"command": "uvx",
"args": ["brainmemory-mcp"]
}
}
}If installed on your PATH, you can use "command": "brainmemory-mcp" with
"args": [] instead.
Start the server with --web, then point an SSE-capable client at the /sse
endpoint:
{
"mcpServers": {
"brainmemory": {
"url": "http://127.0.0.1:8765/sse",
"headers": {
"Authorization": "Bearer replace-with-a-strong-secret"
}
}
}
}Memories live in ~/.brainmemory-mcp/memory.db (SQLite, WAL mode) across three
tables:
memories— nodes:id,content,category,tags,importance(1–5),created_at,updated_at.memory_details— extra facts attached to a memory (cascade-deleted with it).memory_links— directed connectionssource_id -> target_idwith arelationandweight(cascade-deleted with either endpoint).
Search uses a SQLite FTS5 full-text index (memories_fts, kept in sync by
triggers) ranked with BM25, augmented by graph spreading activation. Graph
operations (multi-hop recall_related, shortest-path connect_memories, degree
centrality in summarize_memories) are computed with plain SQL + a little
Python — no external services or vector database required.
Nothing is ever silently deleted — removal only happens through
forget_memories or explicit delete ops in edit_details / edit_links.
When an older database is opened that lacks the newest schema (the graph
tables or the FTS index), it is backed up automatically to
~/.brainmemory-mcp/backups/ before the new objects are added.
MIT