Long-term memory MCP server for LLMs — hybrid search in a single SQLite file
Project description
rekal
Long-term memory for LLMs. One SQLite file, no cloud, no API keys.
rekal is an MCP server that gives Claude Code persistent memory across sessions. Memories are stored locally in SQLite and retrieved with hybrid search (BM25 keywords + vector semantics + recency decay). Nothing leaves your machine.
Session 1: "I prefer Ruff over Black" → memory_store(...)
Session 47: "Set up linting" → memory_search("formatting preferences")
← "User prefers Ruff over Black" (0.92)
Sets up Ruff without asking.
Install
pip install rekal
# or
uv tool install rekal
Requires Python 3.11+. On first run, rekal creates ~/.rekal/memory.db.
Setup
Three steps: add the MCP server, install the plugin, and disable built-in memory.
1. Add the MCP server — gives Claude Code the memory tools:
claude mcp add rekal rekal
2. Install the plugin — teaches Claude Code when to use those tools, and prevents conflicts with built-in memory:
claude plugin marketplace add janbjorge/rekal
claude plugin install rekal-skills@rekal
3. Disable built-in auto memory — add "autoMemoryEnabled": false to ~/.claude/settings.json:
{
"autoMemoryEnabled": false
}
Why is this required? Claude Code's instruction priority is system prompt > CLAUDE.md > MCP server instructions. Built-in memory lives in the system prompt and always wins — without disabling it, the agent ignores rekal and writes to a flat file with no search, no deduplication, no ranking. The plugin's SessionStart hook replaces the context injection auto memory normally provides, so you don't lose anything.
What if I forget? The plugin's
block-memory-writeshook will catch and block MEMORY.md writes as a safety net, but the agent wastes turns hitting the block. Disabling auto memory is cleaner.Can the plugin do this automatically? No — Claude Code doesn't allow plugins to modify user settings. This manual step is the only way.
What the plugin provides
Hooks (automatic, no user action needed):
| Hook | Event | What it does |
|---|---|---|
| session-start | SessionStart |
Reminds agent to call memory_build_context before doing anything |
| block-memory-writes | PreToolUse on Edit/Write |
Blocks writes to MEMORY.md, redirects to rekal tools |
Skills (user-invocable):
| Skill | Trigger | What it does |
|---|---|---|
rekal-init |
/rekal-init |
Scans codebase and bootstraps rekal with project knowledge |
rekal-save |
/rekal-save or auto on session end |
Deduplicates and stores durable knowledge from the conversation |
rekal-usage |
/rekal-usage |
Teaches agents how to use rekal effectively |
rekal-hygiene |
/rekal-hygiene |
Finds conflicts, duplicates, stale data — proposes fixes |
Tools
rekal exposes 16 MCP tools grouped into four categories.
Core — read and write memories:
| Tool | Purpose |
|---|---|
memory_store |
Store a memory with type, project, and tags |
memory_search |
Hybrid search across all memories |
memory_update |
Edit content, tags, or type of an existing memory |
memory_delete |
Remove a memory by ID |
Smart write — manage knowledge over time:
| Tool | Purpose |
|---|---|
memory_supersede |
Replace a memory while linking the old one as history |
memory_link |
Connect memories: supersedes, contradicts, or related_to |
memory_build_context |
One call that returns relevant memories + conflicts + timeline |
Introspection — explore what's stored:
| Tool | Purpose |
|---|---|
memory_similar |
Find memories similar to a given one |
memory_topics |
Topic summary grouped by type |
memory_timeline |
Chronological view with optional date range |
memory_related |
All links to and from a memory |
memory_health |
Database stats: counts by type, project, date range |
memory_conflicts |
Find memories that contradict each other |
Conversations — track session threads:
| Tool | Purpose |
|---|---|
conversation_start |
Start a conversation, optionally linked to a previous one |
conversation_tree |
Get the full conversation DAG |
conversation_threads |
List recent conversations with memory counts |
conversation_stale |
Find inactive conversations |
How it works
Storage
Everything lives in ~/.rekal/memory.db. Three subsystems share it:
- memories table — content, type, project, tags, timestamps, access counts
- FTS5 virtual table — full-text index over content+tags+project, auto-synced via triggers
- sqlite-vec virtual table — 384-dimensional vector index for semantic search
Memory links (supersedes, contradicts, related_to) are stored in a separate table. memory_supersede writes the new memory and creates a supersedes link in a single operation — old knowledge stays queryable with explicit lineage.
Embeddings
rekal uses fastembed with BAAI/bge-small-en-v1.5 (384 dimensions). Runs locally via ONNX — no API calls, no network. The model downloads once on first use (~50MB) and is cached.
Search
Every memory_search runs two parallel lookups, merges candidates, then scores:
score = w_fts × sigmoid(-BM25) ← keyword relevance (default 0.4)
+ w_vec × (1 - cosine_distance) ← semantic similarity (default 0.4)
+ w_recency × exp(-0.693 × days/half_life) ← recency (default 0.2, 30-day half-life)
Why three signals? Keywords miss synonyms ("deploy" vs "ship to prod"). Vectors miss exact identifiers. Recency alone buries important old knowledge. The blend covers all three failure modes.
Configurable weights. All weights and half-life are configurable at three levels:
| Priority | Source | Set by | Persists? |
|---|---|---|---|
| 1 (highest) | Per-search params | memory_search(..., w_fts=0.8) |
No — single query only |
| 2 | Database project config | memory_set_config(key, value, project) |
Yes — SQLite, across sessions |
| 3 | .rekal/config.yml |
Checked into version control | Yes — shared with team |
| 4 (lowest) | Hardcoded defaults | Built into rekal | Always: 0.4 / 0.4 / 0.2, 30-day half-life |
Layers resolve per-key independently. A .rekal/config.yml setting w_fts and a DB override for half_life combine — each key uses its highest-priority source.
# .rekal/config.yml
scoring:
w_fts: 0.6
w_vec: 0.3
w_recency: 0.1
half_life: 14.0
Why SQLite?
- Single file — copy, back up, version-control, or delete to start fresh
- Zero config — no daemon, no port, no connection string
- FTS5 built-in — BM25 ranking without an external search engine
- sqlite-vec extension — vector search in the same process, no separate vector DB
- Sub-millisecond — local disk I/O, no network round-trips
Troubleshooting
Agent still writes to MEMORY.md
- Check
autoMemoryEnabledisfalsein~/.claude/settings.json - Check the plugin is installed:
claude plugin listshould showrekal-skills
Agent doesn't call memory_build_context at session start
The SessionStart hook injects a reminder. If the agent ignores it, add to your project's CLAUDE.md:
Call memory_build_context before exploring the codebase.
Memories not being stored
Check the MCP server is running: claude mcp list should show rekal. If missing:
claude mcp add rekal rekal
Updating the plugin
Claude Code's plugin system may serve a stale cache after plugin install. If hooks or skills are missing after an update, clear the marketplace cache first:
rm -rf ~/.claude/plugins/marketplaces/rekal
claude plugin marketplace add janbjorge/rekal
claude plugin install rekal-skills@rekal
Architecture (for contributors)
Plugin (hooks + skills)
│
├── hooks/
│ ├── handlers/session-start.py ← SessionStart: inject context reminder
│ └── handlers/block-memory-writes.py ← PreToolUse: block MEMORY.md writes
│
└── skills/
├── rekal-init/ ← /rekal-init: bootstrap project knowledge
├── rekal-save/ ← /rekal-save: end-of-session capture
├── rekal-usage/ ← /rekal-usage: operational guide for tools
└── rekal-hygiene/ ← /rekal-hygiene: maintenance
MCP Server (rekal)
│ stdio (JSON-RPC)
│
mcp_adapter.py ← FastMCP server, lifespan, instructions
│
├── tools/core.py ─┐
├── tools/introspection.py│─ thin @mcp.tool() wrappers
├── tools/smart_write.py │
└── tools/conversations.py┘
│
sqlite_adapter.py ← all SQL lives here
│
├── SQLite (memories, conversations, tags, conflicts)
├── FTS5 (full-text index)
└── sqlite-vec (vector index)
Instruction flow (single source per concern):
| What | Where | Why |
|---|---|---|
| "Use rekal tools, not MEMORY.md" | MCP server instructions + PreToolUse hook | Instructions guide, hook enforces |
| "Call memory_build_context first" | SessionStart hook | Automatic, every session |
| "How to store/search/supersede" | MCP server instructions | Always present next to the tools |
| "Capture session knowledge" | rekal-save skill | Explicit trigger, detailed procedure |
| "Bootstrap project" | rekal-init skill | Explicit trigger |
| "Clean up database" | rekal-hygiene skill | Explicit trigger |
CLI
rekal serve # Run as MCP server (default)
rekal health # Database health report
rekal export # Export all memories as JSON
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rekal-1.9.0.tar.gz.
File metadata
- Download URL: rekal-1.9.0.tar.gz
- Upload date:
- Size: 154.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bead0e789c53090368d5e60be07f4c299aaf3b012851545bed58d4fdabbe6be2
|
|
| MD5 |
ec3fd2731f04836b1396912cf392eba8
|
|
| BLAKE2b-256 |
8e4332307f6b57e4037cf4c76cefcdc492200031f4acb8f41cb7403ba7dd33a4
|
Provenance
The following attestation bundles were made for rekal-1.9.0.tar.gz:
Publisher:
release.yml on janbjorge/rekal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rekal-1.9.0.tar.gz -
Subject digest:
bead0e789c53090368d5e60be07f4c299aaf3b012851545bed58d4fdabbe6be2 - Sigstore transparency entry: 1340558870
- Sigstore integration time:
-
Permalink:
janbjorge/rekal@e1224b246993db168b6b88deb477ce3288e05b8e -
Branch / Tag:
refs/tags/v1.9.0 - Owner: https://github.com/janbjorge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e1224b246993db168b6b88deb477ce3288e05b8e -
Trigger Event:
push
-
Statement type:
File details
Details for the file rekal-1.9.0-py3-none-any.whl.
File metadata
- Download URL: rekal-1.9.0-py3-none-any.whl
- Upload date:
- Size: 24.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bdba75a584d4ac6acb4811d190fbaf82d47689180757ce3b99349af1bb072ab6
|
|
| MD5 |
f23ab084095f8bdf3bbadb6be8a002d9
|
|
| BLAKE2b-256 |
94760b9826a649b842ddda97bdf08b31af301c169b34ff4a7a8ccff5bca07091
|
Provenance
The following attestation bundles were made for rekal-1.9.0-py3-none-any.whl:
Publisher:
release.yml on janbjorge/rekal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rekal-1.9.0-py3-none-any.whl -
Subject digest:
bdba75a584d4ac6acb4811d190fbaf82d47689180757ce3b99349af1bb072ab6 - Sigstore transparency entry: 1340558872
- Sigstore integration time:
-
Permalink:
janbjorge/rekal@e1224b246993db168b6b88deb477ce3288e05b8e -
Branch / Tag:
refs/tags/v1.9.0 - Owner: https://github.com/janbjorge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e1224b246993db168b6b88deb477ce3288e05b8e -
Trigger Event:
push
-
Statement type: