An MCP server that gives AI agents long-term memory. Store facts, recall them by meaning, search by keyword, and carry context across conversations — even after the chat window is long gone.
- Semantic recall — ask "what did we decide about X?" and get the right memory even when you use different words (powered by vector embeddings)
- Keyword search — BM25 full-text search via SQLite FTS5 for exact matches
- Tag filtering — organize memories with metadata tags
- Persistent — everything stored in SQLite, survives restarts
- Zero infrastructure — no external databases, no APIs, no GPU needed
- 7 ready-to-use MCP tools — works with Claude Desktop, Cursor, or any MCP client
# 1. Clone and install
pip install -r requirements.txt
# 2. Start the server
python server.pyThat's it. The server listens on stdio transport — the standard MCP protocol. Connect it to any MCP client and the 7 tools will appear automatically.
| Tool | What it does | Example |
|---|---|---|
memory_store(key, content, metadata) |
Save or update a memory | memory_store("user-name", "Called Alice", {"tag": "identity"}) |
memory_recall(query, limit) |
Find by meaning (vector search) | memory_recall("what's their name?", 3) |
memory_search(keywords, limit) |
Find by keyword (FTS5) | memory_search("Alice", 10) |
memory_get(id) |
Get a specific memory by ID | memory_get(1) |
memory_list(tag, limit) |
List all, optionally filtered by tag | memory_list("preferences") |
memory_forget(id) |
Delete a memory | memory_forget(3) |
memory_stats |
Show database statistics | memory_stats |
memory_store — Upserts by key. If key already exists, it updates the
content and re-embeds it. The metadata dict can hold anything (tags, notes,
timestamps). Example:
memory_store(
key="deploy-command",
content="Deploy with: ./deploy.sh staging",
metadata={"tag": "devops", "project": "backend", "priority": "high"}
)
memory_recall — Uses cosine similarity on 384-dim sentence embeddings. You don't need to match exact words. "What's our deployment process?" will find the same "deploy-command" entry above, even though the words differ.
memory_search — Uses SQLite FTS5 with porter stemming and BM25 ranking for traditional keyword search. Great for finding exact terms, code snippets, or names.
Add to your claude_desktop_config.json:
{
"mcpServers": {
"memory-hub": {
"command": "python",
"args": ["/absolute/path/to/PersistentMemory/server.py"]
}
}
}Replace /absolute/path/to/ with the actual path. Restart Claude — the 7 tools
will appear in your tool list.
In Cursor settings → Features → MCP Servers → Add new:
Name: Memory Hub
Type: command
Command: python /absolute/path/to/PersistentMemory/server.py
import asyncio
from mcp import ClientSession, StdioServerParameters
async def main():
async with ClientSession(
StdioServerParameters(command="python", args=["server.py"])
) as session:
await session.initialize()
result = await session.call_tool("memory_store", {
"key": "greeting",
"content": "Hello from Python!",
"metadata": {"tag": "test"}
})
print(result)
asyncio.run(main())You can also skip MCP entirely and use MemoryStore directly:
from memory_store import MemoryStore
ms = MemoryStore()
ms.store("fact", "Python is great", {"tag": "language"})
results = ms.recall("what language?")
print(results)Your prompt
│
▼ MCP Protocol (stdio)
┌──────────────────────┐
│ fastmcp server │ ← registers 7 tools
│ memory_store() │
│ memory_recall() │
│ memory_search() │
│ ... │
└─────────┬────────────┘
│
┌─────────▼────────────┐
│ MemoryStore │ ← business logic layer
└────┬────────────┬────┘
│ │
┌────▼────┐ ┌────▼────────────┐
│Embedder │ │ Storage │
│all- │ │ SQLite + FTS5 │
│MiniLM- │ │ vector BLOBs │
│L6-v2 │ │ cosine sim │
└─────────┘ └─────────────────┘
Each memory is converted to a 384-dimensional vector using
sentence-transformers/all-MiniLM-L6-v2. This is a lightweight model that runs
on CPU — no GPU required, no API calls. The vector captures the meaning of the
text, not just the words.
- Your query is converted to a vector
- All stored vectors are loaded from SQLite
- Cosine similarity is computed between query and every memory
- Top-K results sorted by similarity score (0 to 1)
SQLite FTS5 with porter unicode61 tokenizer provides BM25-ranked full-text
search — the same algorithm used by search engines.
PersistentMemory/
├── server.py # MCP server — entry point
├── memory_store.py # Business logic: connects tools → storage + embeddings
├── embeddings.py # SentenceTransformer model + cosine similarity
├── storage.py # SQLite CRUD + FTS5 search + vector read/write
├── requirements.txt
└── README.md
| File | Responsibility |
|---|---|
server.py |
Defines the 7 MCP tools via @mcp.tool(), runs the server |
memory_store.py |
Orchestrates store/recall/search/forget/list/stats |
embeddings.py |
Loads the model once, provides embed() and cosine_similarity() |
storage.py |
Raw SQLite operations, table creation, FTS5 triggers, query execution |
The server creates memories.db in the current working directory on first run.
- WAL mode for better concurrent read performance
- FTS5 virtual table auto-synced via triggers (insert/update/delete)
- Embeddings stored as pickled numpy arrays in the
embeddingBLOB column
# Reset everything
rm memories.db
python server.py # recreates tables freshCREATE TABLE memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
content TEXT NOT NULL,
metadata TEXT DEFAULT '{}', -- JSON
embedding BLOB, -- pickled numpy array
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE VIRTUAL TABLE memories_fts USING fts5(
key, content, metadata,
content='memories',
content_rowid='id',
tokenize='porter unicode61'
);| Problem | Likely fix |
|---|---|
ModuleNotFoundError: No module named 'sentence_transformers' |
Run pip install -r requirements.txt |
FastMCP() got unexpected keyword argument(s) |
Upgrade fastmcp: pip install --upgrade fastmcp |
sqlite3.OperationalError: no such table: memories |
Delete memories.db and restart |
| First run is slow | The embedding model downloads ~80MB on first use. Subsequent runs use a cached copy. |
| High memory usage | all-MiniLM-L6-v2 uses ~500MB RAM. This is normal for transformer models. |
- Python 3.10+ required for
str \| Noneunion syntax - First launch downloads
all-MiniLM-L6-v2(~80MB) from Hugging Face Hub - FTS5 is included in Python's
sqlite3module since Python 3.x — no separate extension needed - The server uses stdio transport only (no HTTP). This is the standard for MCP and works with all clients.