Graph-Based AI Memory System with EcphoryRAG Retrieval and Leiden Clustering
Smriti is a Model Context Protocol (MCP) server that provides persistent, graph-based memory for LLM applications. It supports three database backends — LadybugDB (embedded), Neo4j, and FalkorDB — and uses EcphoryRAG-inspired multi-stage retrieval — combining cue extraction, graph traversal, vector similarity, and multi-hop association — to deliver human-like memory recall. Smriti uses the Leiden algorithm for automatic community detection, enabling cluster-aware retrieval that scales beyond thousands of memories.
- Graph-Based Memory — Engrams (memories) linked via Cues and Associations in a property graph
- EcphoryRAG Retrieval — Multi-hop associative recall with cue extraction, vector similarity, and composite scoring
- Leiden Community Detection — Automatic clustering of related memories using the Leiden algorithm with smart-cached resolution tuning, enabling cluster-aware scoring for efficient retrieval at scale
- Multi-Backend Support — LadybugDB (embedded, zero-config), Neo4j (enterprise graph DB), or FalkorDB (Redis-based graph DB)
- Multi-User Isolation — Per-file (LadybugDB), per-tenant property or per-database (Neo4j), or per-tenant property or per-graph (FalkorDB)
- Automatic Consolidation — Exponential decay, pruning of weak memories, strengthening of frequently accessed ones, and periodic Leiden re-clustering
- Flexible Backup — GitHub (system git) or S3 (AWS SDK) sync, plus noop for local-only
- Lazy HNSW Indexing — Vector and FTS indexes created on-demand when dataset exceeds threshold
- OpenAI-Compatible APIs — Works with any OpenAI-compatible LLM and embedding provider
- 3 MCP Tools —
smriti_store,smriti_recall,smriti_manage
graph TD
Client["MCP Client<br/>(Cursor / Claude / Windsurf / etc.)"]
Client -->|stdio| Server
subgraph Server["Smriti MCP Server"]
direction TB
subgraph Tools["MCP Tools"]
Store["smriti_store"]
Recall["smriti_recall"]
Manage["smriti_manage"]
end
subgraph Engine["Memory Engine"]
Encoding["Encoding<br/>LLM + Embed + Link"]
Retrieval["Retrieval<br/>Cue Match + Vector + Multi-hop<br/>+ Cluster-Aware Scoring"]
Consolidation["Consolidation<br/>Decay + Prune + Leiden Clustering"]
end
subgraph DB["Graph Database"]
direction LR
Graph["(Engram)──[:EncodedBy]──▶(Cue)<br/>(Engram)──[:AssociatedWith]──▶(Engram)<br/>(Cue)──[:CoOccurs]──▶(Cue)"]
DBType["LadybugDB | Neo4j | FalkorDB"]
end
subgraph Backup["Backup Provider (optional)"]
Git["GitHub (git)"]
S3["S3 (AWS SDK)"]
Noop["Noop"]
end
Store & Recall & Manage --> Engine
Encoding & Retrieval & Consolidation --> DB
DB --> Backup
end
LLM["LLM / Embedding API<br/>(OpenAI-compatible)"]
Engine --> LLM
The default recall mode performs multi-stage retrieval:
- Cue Extraction — LLM extracts entities and keywords from the query
- Cue-Based Graph Traversal — Follows
EncodedByedges to find engrams linked to matching cues - Vector Similarity Search — Cosine similarity against all engram embeddings (HNSW index when available, fallback to brute-force)
- Multi-Hop Expansion — Follows
AssociatedWithedges to discover related memories - Cluster-Aware Composite Scoring — Blends vector similarity (40%), recency (20%), importance (20%), and decay (20%), with hop-depth penalty and soft-bounded cross-cluster penalty (0.5x for hop results outside the seed cluster)
- Access Strengthening — Recalled engrams get their access count and decay factor bumped (reinforcement)
Smriti uses the Leiden algorithm — an improvement over Louvain that guarantees well-connected communities — to automatically detect clusters of related memories in the graph.
How it works:
- Runs automatically during each consolidation cycle
- Builds a weighted undirected graph from
AssociatedWithedges between engrams - Auto-tunes the resolution parameter using community profiling on the first run
- Uses a smart cache: the tuned resolution is reused across runs and only re-tuned when the graph grows by more than 10%
- Assigns a
cluster_idto each engram, stored persistently in the database - New engrams inherit the
cluster_idof their strongest neighbor at encode time
How it improves retrieval:
- The recall pipeline determines a seed cluster (most common cluster among direct-match results)
- Multi-hop results that cross into a different cluster receive a 0.5x score penalty (soft-bounded: they are penalized, not dropped)
- This keeps retrieval focused within the most relevant topic cluster while still allowing cross-topic discovery
Performance characteristics:
- Gracefully skips on small graphs (< 3 nodes or 0 edges)
- Clustering 60 nodes: ~40ms (first run with auto-tune), ~14ms (cached resolution)
- Per-user: each Engine instance maintains its own independent cache
Consolidation runs periodically (default: every 3600 seconds) and performs:
- Exponential Decay — Reduces
decay_factorbased on time since last access - Weak Memory Pruning — Removes engrams below minimum decay threshold
- Frequency Strengthening — Boosts decay factor for frequently accessed memories
- Orphaned Cue Cleanup — Removes cues no longer linked to any engram
- Leiden Clustering — Re-clusters the memory graph (smart-cached, skips if graph hasn't changed significantly)
- Index Management — Creates HNSW vector and FTS indexes when engram count exceeds threshold (50)
-
Go 1.25+ — For building from source
-
Git 2.x+ — Required for GitHub backup provider (must be in PATH)
-
GCC/Build Tools — Required for CGO (LadybugDB backend)
- macOS:
xcode-select --install - Linux:
sudo apt install build-essential - Windows: Use Docker (recommended) or MinGW
- macOS:
-
liblbug (LadybugDB shared library) — Runtime dependency for LadybugDB backend, downloaded automatically by
go-ladybugduring build. If building manually, grab the latest release from LadybugDB/ladybug:Platform Asset Library macOS liblbug-osx-arm64.tar.gz/liblbug-osx-x86_64.tar.gzliblbug.dylibLinux liblbug-linux-{arch}.tar.gzliblbug.soWindows liblbug-windows-x86_64.zipliblbug.dllThe shared library must be on the system library path at runtime (e.g.,
DYLD_LIBRARY_PATHon macOS,LD_LIBRARY_PATHon Linux, or alongside the binary on Windows). Docker and release binaries bundle this automatically. -
Neo4j 5.x+ — Required only when using
DB_TYPE=neo4j. Must have APOC and GDS plugins for vector search and full-text indexing. -
FalkorDB — Required only when using
DB_TYPE=falkordb. Runs on Redis protocol (default port 6379).
# Build
CGO_ENABLED=1 go build -o smriti-mcp .
# Run (minimal config)
export LLM_API_KEY=your-api-key
export ACCESSING_USER=alice
./smriti-mcpCursor (~/.cursor/mcp_settings.json):
{
"mcpServers": {
"smriti": {
"command": "/path/to/smriti-mcp",
"env": {
"LLM_API_KEY": "your-api-key",
"EMBEDDING_API_KEY": "your-embedding-key"
}
}
}
}Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"smriti": {
"command": "/path/to/smriti-mcp",
"args": [],
"env": {
"LLM_API_KEY": "your-api-key",
"EMBEDDING_API_KEY": "your-embedding-key"
}
}
}
}Windsurf (~/.codeium/windsurf/mcp_config.json):
{
"mcpServers": {
"smriti": {
"command": "/path/to/smriti-mcp",
"env": {
"LLM_API_KEY": "your-api-key",
"EMBEDDING_API_KEY": "your-embedding-key"
}
}
}
}Run directly without installing — similar to npx for Node.js:
{
"mcpServers": {
"smriti": {
"command": "go",
"args": ["run", "github.com/tejzpr/smriti-mcp@latest"],
"env": {
"LLM_API_KEY": "your-api-key",
"EMBEDDING_API_KEY": "your-embedding-key"
}
}
}
}Simple mode (single user):
{
"mcpServers": {
"smriti": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "/Users/yourname/.smriti:/home/smriti/.smriti",
"-e", "LLM_API_KEY=your-api-key",
"-e", "EMBEDDING_API_KEY=your-embedding-key",
"tejzpr/smriti-mcp"
]
}
}
}Multi-user mode:
{
"mcpServers": {
"smriti": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "/Users/yourname/.smriti:/home/smriti/.smriti",
"-e", "LLM_API_KEY=your-api-key",
"-e", "EMBEDDING_API_KEY=your-embedding-key",
"-e", "ACCESSING_USER=yourname",
"tejzpr/smriti-mcp"
]
}
}
}Note:
- Replace
/Users/yournamewith your actual home directory path- MCP clients do not expand
$HOMEor~in JSON configs — use absolute paths- The
.smritivolume mount persists your memory database- The container runs as non-root user
smriti
Build locally (optional):
docker build -t smriti-mcp .Then use smriti-mcp instead of tejzpr/smriti-mcp in your config.
Download pre-built binaries from the Releases page. Binaries are available for:
| Platform | Architecture | CGO |
|---|---|---|
| Linux | amd64 | Enabled (native) |
| macOS | arm64 (Apple Silicon) | Enabled (native) |
| Windows | amd64 | Enabled (native) |
Each release includes a checksums-sha256.txt for verification.
| Variable | Default | Description |
|---|---|---|
ACCESSING_USER |
OS username | User identifier (used for DB isolation) |
STORAGE_LOCATION |
~/.smriti |
Root storage directory (LadybugDB only) |
DB_TYPE |
ladybug |
Database backend: ladybug, neo4j, or falkordb |
| Variable | Default | Description |
|---|---|---|
LLM_BASE_URL |
https://api.openai.com/v1 |
LLM API endpoint (OpenAI-compatible) |
LLM_API_KEY |
(required) | LLM API key |
LLM_MODEL |
gpt-4o-mini |
LLM model name |
| Variable | Default | Description |
|---|---|---|
EMBEDDING_BASE_URL |
https://api.openai.com/v1 |
Embedding API endpoint |
EMBEDDING_API_KEY |
(falls back to LLM_API_KEY) | Embedding API key |
EMBEDDING_MODEL |
text-embedding-3-small |
Embedding model name |
EMBEDDING_DIMS |
1536 |
Embedding vector dimensions |
| Variable | Default | Description |
|---|---|---|
BACKUP_TYPE |
none |
none, github, or s3 |
BACKUP_SYNC_INTERVAL |
60 |
Seconds between backup syncs (0 = disabled) |
GIT_BASE_URL |
(empty) | Git remote base URL (required if github) |
S3_ENDPOINT |
(empty) | S3 endpoint (for non-AWS providers) |
S3_REGION |
(empty) | S3 region (required if s3) |
S3_ACCESS_KEY |
(empty) | S3 access key (required if s3) |
S3_SECRET_KEY |
(empty) | S3 secret key (required if s3) |
| Variable | Default | Description |
|---|---|---|
NEO4J_URI |
(required) | Bolt URI (e.g. bolt://localhost:7687) |
NEO4J_USERNAME |
(required) | Neo4j username |
NEO4J_PASSWORD |
(required) | Neo4j password |
NEO4J_DATABASE |
neo4j |
Database name (overridden by username in database isolation mode) |
NEO4J_ISOLATION |
tenant |
tenant (property-based, Community Edition) or database (per-DB, Enterprise Edition) |
| Variable | Default | Description |
|---|---|---|
FALKOR_ADDR |
localhost:6379 |
FalkorDB Redis address |
FALKOR_PASSWORD |
(empty) | FalkorDB password (if auth enabled) |
FALKOR_GRAPH |
smriti |
Graph name (overridden by {user}_smriti in graph isolation mode) |
FALKOR_ISOLATION |
tenant |
tenant (property-based) or graph (per-graph isolation) |
| Variable | Default | Description |
|---|---|---|
CONSOLIDATION_INTERVAL |
3600 |
Seconds between consolidation runs (0 = disabled) |
"Remember this" — Store a new memory. Content is automatically analyzed by the LLM, embedded, and woven into the memory graph. New engrams inherit the cluster_id of their most similar existing neighbor.
{
"content": "Kubernetes uses etcd as its backing store for all cluster data",
"importance": 0.8,
"tags": "kubernetes,etcd,infrastructure",
"source": "meeting-notes"
}| Parameter | Type | Required | Description |
|---|---|---|---|
content |
string | yes | Memory content |
importance |
number | no | Priority 0.0–1.0 (default: 0.5) |
tags |
string | no | Comma-separated tags |
source |
string | no | Source/origin label |
"What do I know about X?" — Retrieve memories using multi-stage EcphoryRAG retrieval with cluster-aware scoring.
{
"query": "container orchestration tools",
"limit": 5,
"mode": "recall"
}| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string | no | Natural language query (omit for list mode) |
limit |
number | no | Max results (default: 5) |
mode |
string | no | recall (deep multi-hop), search (fast vector-only), or list (browse) |
memory_type |
string | no | Filter: episodic, semantic, procedural |
Modes explained:
recall(default) — Full pipeline: cue extraction → graph traversal → vector search → multi-hop → cluster-aware composite scoringsearch— Vector-only cosine similarity. Faster but shallower.list— No search. Returns recent memories ordered by last access time.
"Forget this / sync now" — Administrative operations.
{
"action": "forget",
"memory_id": "abc-123-def"
}| Parameter | Type | Required | Description |
|---|---|---|---|
action |
string | yes | forget (delete memory) or sync (push backup) |
memory_id |
string | if forget | Engram ID to delete |
Smriti stores memories in a property graph with the following structure:
Node Tables:
Engram — id, content, summary, memory_type, importance, access_count,
created_at, last_accessed_at, decay_factor, embedding, source,
tags, cluster_id
Cue — id, name, cue_type, embedding
Relationship Tables:
EncodedBy — (Engram) → (Cue)
AssociatedWith — (Engram) → (Engram) [strength, relation_type, created_at]
CoOccurs — (Cue) → (Cue) [strength]
The cluster_id field on Engram nodes is managed by the Leiden algorithm. A value of -1 indicates the engram has not yet been assigned to a cluster (e.g., the graph is too small, or the engram has no associations).
Smriti supports three database backends with different storage and isolation models:
Each user gets an isolated embedded database file:
~/.smriti/
└── {username}/
└── memory.lbug # LadybugDB property graph database
The STORAGE_LOCATION env var controls the root. The ACCESSING_USER env var selects which user's DB to open. Backup providers sync the user directory to remote storage.
Two isolation modes controlled by NEO4J_ISOLATION:
tenant(default) — All users share one database. Each node gets auserproperty and all queries filter by it. Works on Neo4j Community Edition.database— Each user gets a separate Neo4j database. Requires Neo4j Enterprise Edition.
Two isolation modes controlled by FALKOR_ISOLATION:
tenant(default) — All users share one graph. Each node gets auserproperty and all queries filter by it.graph— Each user gets a separate graph (named{user}_smriti).
Schema migrations (e.g., adding cluster_id to existing databases) run automatically on startup.
smriti-mcp/
├── main.go # Entry point, server setup, signal handling
├── config/ # Environment variable parsing
├── llm/ # OpenAI-compatible HTTP client (LLM + embeddings)
├── db/ # Database backends (LadybugDB, Neo4j, FalkorDB), schema, indexes, migrations
├── memory/
│ ├── engine.go # Engine struct, consolidation loop
│ ├── types.go # Engram, Cue, Association, SearchResult structs
│ ├── encoding.go # Store pipeline: LLM extraction → embed → link → cluster inherit
│ ├── retrieval.go # Recall pipeline: cue search → vector → multi-hop → cluster scoring
│ ├── search.go # Search modes: list, vector-only, FTS, hybrid
│ ├── consolidation.go # Decay, prune, strengthen, orphan cleanup
│ └── leiden.go # Leiden clustering: graph build, auto-tune, smart cache, batch write
├── backup/ # Backup providers: noop, github (git), s3 (AWS SDK)
├── tools/ # MCP tool definitions: store, recall, manage
└── testutil/ # Shared test helpers
# Run unit tests
CGO_ENABLED=1 go test ./...
# Verbose with all output
CGO_ENABLED=1 go test -v ./...
# Specific package
CGO_ENABLED=1 go test -v ./memory/...
CGO_ENABLED=1 go test -v ./tools/...
# Leiden clustering tests only
CGO_ENABLED=1 go test -v -run "TestRunLeiden|TestNeedsRetune|TestDetermineSeedCluster" ./memory/E2E tests require real LLM/embedding services and are gated behind the integration build tag:
# LadybugDB E2E (no external DB required)
CGO_ENABLED=1 go test -tags integration -v -run "TestE2E_LadybugDB" ./memory/
# Neo4j E2E (requires running Neo4j instance)
NEO4J_URI="bolt://localhost:7687" NEO4J_USERNAME="neo4j" NEO4J_PASSWORD="yourpass" \
CGO_ENABLED=1 go test -tags integration -v -run "TestE2E_Neo4j" ./memory/
# FalkorDB E2E (requires running FalkorDB instance)
FALKOR_ADDR="localhost:6379" \
CGO_ENABLED=1 go test -tags integration -v -run "TestE2E_FalkorDB" ./memory/
# All E2E tests
CGO_ENABLED=1 go test -tags integration -v -run "TestE2E_" ./memory/All E2E tests require LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, EMBEDDING_BASE_URL, EMBEDDING_MODEL, and EMBEDDING_API_KEY environment variables.
Contributions are welcome! Please ensure:
- All tests pass (
CGO_ENABLED=1 go test ./...) - Code is properly formatted (
go fmt ./...) - New code includes the SPDX license header
See CONTRIBUTORS.md for the contributor list.
This project is licensed under the Mozilla Public License 2.0.
