Local Brain MCP
by ejsinfuego
README.md
Local Brain MCP
===============
Local Brain MCP is a small MCP server that you **run locally** on your machine. It lets AI assistants store and retrieve personal memories on your own disk, and optionally use **semantic (embedding) search** for RAG‑style workflows.
You run the server on your computer (e.g. `python server.py`); Cursor and other MCP clients connect to it at `http://localhost:3000/mcp`. Optionally, you can expose it via something like Cloudflare Tunnel to use the same memory from other devices.
Features
--------
- **Local, transparent storage**
- SQLite database file on disk (`memory.db` by default).
- DB location is configured via **environment variables** (see “Database layout”). The `dbUrl` *tool argument* is intentionally ignored so all clients share the same configured DB.
- Simple schema: `memories` table with `title`, `content`, `tags`, `source`, timestamps, and IDs.
- **MCP tools**
- `save_memory` — insert a memory row (optionally generating an embedding).
- `update_memory` — patch `title` / `content` / `tags` / `source` on an existing row; optional embedding refresh when `content` changes.
- `delete_memory` — remove a row by id (embeddings removed with it).
- `fetch_memories` — RAG-style retrieval (`use_vector_search`, default on). A query is ranked by similarity, not by date. A blank query returns the newest saved rows first. There is no sort parameter. Each row includes `id`, `created_at`, `title`, `content`, `tags`, `source`, `mode` (`vector`, `keyword`, or `recent`), and `score`.
- `backfill_all_embeddings` — generate embeddings for older memories that don’t have one yet (after you configure an embedding provider).
- `doctor` — database path, embedding coverage, vector index, and whether the embedding API responds.
- **Embeddings / vector search**
- The server does not run an embedding model itself. Embeddings are **handled by OpenAI** (or optionally OpenRouter) via their APIs.
- Optional embeddings are stored in a separate `memory_embeddings` table.
- **OpenAI** is the default: set `OPENAI_API_KEY` and the server will use OpenAI’s embeddings API. Optionally, set `EMBEDDING_PROVIDER=openrouter` and `OPENROUTER_API_KEY` to use OpenRouter instead.
Quick start
-----------
1) **Install dependencies**
Create and activate a virtual environment, then install Python deps.
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
1) **Configure embeddings (optional but recommended)**
Embeddings are generated by **OpenAI’s API** (the server has no built‑in embedding model). Set `OPENAI_API_KEY` and the server will call OpenAI to create and store embeddings for semantic search.
- **Option A: OpenAI**
```bash
export EMBEDDING_PROVIDER=openai # default, can be omitted
export OPENAI_API_KEY=sk-...
export EMBEDDING_MODEL=text-embedding-3-small # optional override
```
- **Option B: OpenRouter**
```bash
export EMBEDDING_PROVIDER=openrouter
export OPENROUTER_API_KEY=sk-or-...
# Any OpenRouter embedding model (example shown):
export EMBEDDING_MODEL=openai/text-embedding-3-small
# Optional but recommended for OpenRouter analytics:
export OPENROUTER_SITE_URL="https://your-site-or-localhost"
export OPENROUTER_APP_NAME="Local Brain MCP"
```
1) **Run the MCP server**
From the project root:
```bash
source .venv/bin/activate
python server.py
```
You should see FastMCP start up and log something like:
- `Local Brain MCP` listening on `http://0.0.0.0:3000`
- MCP endpoint: `http://localhost:3000/mcp`
You can then point Cursor (or another MCP client) at `http://localhost:3000/mcp` as a remote MCP server.
Run with Docker Compose
-----------------------
If you prefer running this in Docker, use the included `docker-compose.yml`.
1) **Set env vars (optional, for embeddings)**
Create a `.env` file in the project root (or export env vars in your shell):
```bash
# .env example
EMBEDDING_PROVIDER=openai
OPENAI_API_KEY=sk-...
EMBEDDING_MODEL=text-embedding-3-small
```
2) **Build and start**
```bash
docker compose up -d --build
# or, on systems with the legacy CLI:
docker-compose up -d --build
```
3) **Use the MCP endpoint**
The server is available at:
- `http://localhost:3000/mcp`
4) **Stop the service**
```bash
docker compose down
# or:
docker-compose down
```
Notes:
- SQLite data is persisted in the Docker volume `memory_data`.
- Inside the container, the DB path is set to `MEMORY_DB_URL=/data/memory.db`.
Cursor integration note (local env auto-load)
---------------------------------------------
When you run `server.py` locally, it will also try to load environment overrides from `~/.cursor/mcp.json` (specifically the `mcpServers.local-brain-mcp.env` block), if present. This helps you keep your server configuration in one place.
Database layout
---------------
By default, the SQLite file is `~/.local/share/local-brain/memory.db`, outside any git checkout. Override it with an **absolute** path:
- Env vars: `dbUrl`, `DB_URL`, or `MEMORY_DB_URL` (first non‑empty wins).
Notes:
- The `dbUrl` **tool parameter is intentionally ignored** for path resolution.
- A relative path is rejected, including when the server starts. That keeps a second checkout from creating another database.
Tables:
- `memories`
- `id` (INTEGER, primary key)
- `created_at` (TEXT, ISO datetime, default `datetime('now')`)
- `title` (TEXT, nullable)
- `content` (TEXT, required)
- `tags` (TEXT, JSON‑encoded list of strings)
- `source` (TEXT, optional source identifier)
MCP tools
---------
**`save_memory`**
**Purpose**: Insert a new memory row (and optionally its embedding).
Parameters:
- `content` (str, required): main text content of the memory.
- `title` (str, optional): short title.
- `tags` (List[str], optional): arbitrary tags, stored as JSON.
- `source` (str, optional): where the memory came from (e.g. `"cursor"`, `"cli"`, `"web"`).
- `dbUrl` (str, optional): accepted for API compatibility, but ignored for database path resolution.
- `generate_embedding` (bool, optional, default `True`):
- If `True`, and an embedding API is configured (e.g. `OPENAI_API_KEY`), the server calls OpenAI (or OpenRouter) to generate an embedding and stores it.
Returns:
- A dict with `id`, `title`, `content`, `tags`, `source`, `embedded`.
- `embedded` is `true` when a vector was stored for this save. `false` means the text was kept and vector search will not see it until a later backfill.
**`update_memory`**
**Purpose**: Update fields on an existing memory (omit fields you do not want to change).
Parameters:
- `memory_id` (int, required): id of the row to update.
- `title`, `content`, `tags`, `source` (optional): new values; at least one must be provided.
- `generate_embedding` (bool, default `True`): when `content` changes, if `True` recompute the embedding; if `False`, drop the stored embedding so vector search cannot use stale vectors.
Returns: dict with `id`, `title`, `content`, `tags`, `source`, `embedded`. `embedded` is whether a vector exists after the update.
**`delete_memory`**
**Purpose**: Delete a memory by id.
Parameters:
- `memory_id` (int, required).
Returns: `{ "id": <int>, "deleted": <bool> }` (`deleted` is `False` if no row existed).
**`fetch_memories`**
**Purpose**: Retrieve memories relevant to a query, or the most recent rows when the query is empty.
Parameters:
- `query` (str, optional): text query. A non-blank query is ranked by similarity (`use_vector_search`) or keyword match. There is no sort or date parameter. If `query` is omitted or blank, results are the newest saved rows first (`created_at` descending).
- `limit` (int, optional, default `5`, max `50`): max number of results.
- `dbUrl` (str, optional): accepted for API compatibility, but ignored for database path resolution.
- `use_vector_search` (bool, optional, default `True`): RAG-style retrieval.
- `True` (default): embed the query and return memories ranked by semantic similarity; falls back to keyword search if embeddings are unavailable.
- `False`: keyword-only — SQL `LIKE` on `content` and `title`, ordered by recency.
- `fields` (list of str, optional): if set, each result includes only these keys. Allowed: `id`, `created_at`, `title`, `content`, `tags`, `source`, `mode`, `retrieval`, `score`. Omit for all fields. Useful to shrink MCP tool payloads (e.g. exclude `content` when listing or probing).
- `tags_any` (list of str, optional): if set, return only rows containing at least one of the provided tags (case-insensitive).
- `source_prefix` (str, optional): if set, return only rows where `source` starts with this prefix.
Returns:
- A list of dicts. By default each includes `id`, `created_at`, `title`, `content`, `tags`, `source`, `mode`, `retrieval`, and `score`.
- `mode` is `vector` (embedding similarity), `keyword` (text match, including when the embedding API is unavailable), or `recent` (no query). `retrieval` repeats that same value.
- `score` is cosine similarity for vector hits. It is `null` for keyword and recent results.
- With `fields`, only the requested keys are present.
**`doctor`**
**Purpose**: Check the database path, how many memories lack a vector, whether stored vectors match `EMBEDDING_MODEL`, which index is in use (`sqlite-vec` or Python), and whether a live embedding call succeeds.
Parameters:
- `ping` (bool, default `True`): send a short embedding request.
Using this as a RAG memory
--------------------------
Typical pattern for an AI assistant:
1. **On new long‑term information**
Call `save_memory` with:
- `content`: the text snippet you want to remember.
- `title` / `tags`: short descriptors.
- `generate_embedding=True` (default) so it’s indexed semantically.
2. **Before answering a user query**
Call `fetch_memories` with the user’s question (or a brief summary) as `query`. By default it uses RAG (semantic retrieval); use the top results as context when generating the answer.
3. **Fallback behavior**
If embeddings are not configured or not yet stored, vector search cleanly degrades to keyword search, so the tools remain usable.
4. **Hygiene**
Use `update_memory` to correct a row without duplicating it; use `delete_memory` to drop obsolete entries.
Embedding backfill (recommended after enabling embeddings)
---------------------------------------------------------
If you set an embedding provider/API key after you already have memories saved, run `backfill_all_embeddings` once to generate embeddings for older rows so semantic search works across your full history. If `EMBEDDING_MODEL` changes, run `python cli.py reembed` so older vectors are rebuilt and included in search.
Command line
------------
`cli.py` uses the same database as the server:
```bash
python cli.py list
python cli.py search "how do we start a campaign from a folder"
python cli.py search "exact error text" --keyword
python cli.py doctor
python cli.py reembed
python cli.py dashboard
```
`dashboard` serves a live graph at `http://127.0.0.1:8765`. Each memory is a node, and each line connects it to the two notes with the closest embeddings. The page polls `GET /events?after=<id>` every second and reads new rows from `retrieval_events`. Those rows are written by `_log_activity` inside `save_memory`, `update_memory`, `delete_memory`, `retrieve_memories` (which `fetch_memories` calls), `backfill_embeddings`, and `reembed_embeddings`. A save adds a node, an update moves it, a delete removes it, and a search lights up the matches.
Run `python server.py` from **this** checkout. The copy at `/Users/ej/Desktop/work/memory-mcp` does not write `retrieval_events`, so the graph stays still even though both processes use `~/.local/share/local-brain/memory.db`.
Development notes
-----------------
- The server is implemented in `server.py` using **FastMCP**.
- All schema creation is performed on connection; deleting `memory.db` will recreate an empty database on next start.
- Embeddings are handled by OpenAI (or OpenRouter); configuration is via environment variables only.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues