agentic-box/memora
Give your AI agents persistent, collective memory — with deduplicating absorb, supersession lineage, semantic search, and a graph UI. Speaks MCP.
About agentic-box/memora
agentic-box/memora is an open-source project on GitHub, mainly written in Python. Give your AI agents persistent, collective memory — with deduplicating absorb, supersession lineage, semantic search, and a graph UI. Speaks MCP. It currently holds 727 stars and 0 forks with 0 open issues, and was last pushed on an unknown date (repository created unknown).
Project Overview
AI Homed tracks it on the AI Agent Memory board.
GitHub Repository Details
README
Memora
"You never truly know the value of a moment until it becomes a memory."
Give your AI agents persistent collective memory
An MCP memory layer for agents: structured storage, semantic retrieval, graph relations, and source-backed cross-session context.
Absorb agent work into durable graph memory, then use memory_digest(topic) to retrieve relevant memories, TODOs/issues, related edges, and source IDs.
Features · Preview · Install · Usage · Config · Multi-DB · Containers · Live Graph · Cloud Graph · Chat · Semantic Search · Documents · LLM Dedup · Linking · Neovim
Features
Core Storage
- 💾 Persistent Storage - SQLite with optional cloud sync (S3, R2, D1)
- 🗄️ Multi-database routing - One process serves many stores; a workspace reaches its own at
/mcp/(see Multi-database routing) - 📂 Hierarchical Organization - Section/subsection structure with auto-hierarchy assignment
- 📦 Export/Import - Backup and restore with merge strategies
- 🧬 Absorb - Feed facts in; an LLM classifies each against the store (duplicate / update / contradiction / related / new), skips duplicates, links relations, and consolidates related facts — with
dry_runpreview - 🌱 Supersession Lineage - Updates supersede old knowledge instead of deleting it; retrieval follows the chain to the current version by default (
followmodes:active,latest,full_history) - 🗞️ Topic Digest -
memory_digest(topic)bundles relevant memories, open TODOs/issues, related edges, and source IDs into one retrieval
- 🔍 Semantic Search - Vector embeddings (TF-IDF, sentence-transformers, OpenAI)
- 🎯 Advanced Queries - Full-text, date ranges, tag filters (AND/OR/NOT), hybrid search
- 🔀 Cross-references - Auto-linked related memories based on similarity
- 🤖 LLM Deduplication - Find and merge duplicates with AI-powered comparison
- 🔗 Memory Linking - Typed edges, importance boosting, and cluster detection
- 📄 Structured Documents - Store markdown documents as searchable fragment trees (claims, plan items, references, risks)
- 🔒 Fragment Integrity - Guards against accidental delete/merge/absorb of document fragments
- 🔍 Granular Search - Individual claims and findings are semantically searchable while the full document remains retrievable as a unit
- ⚡ Memory Automation - Structured tools for TODOs, issues, and sections
- 🕸️ Knowledge Graph - Interactive visualization with Mermaid rendering and cluster overlays
- 🌐 Live Graph Server - Built-in HTTP server with cloud-hosted option (D1/Pages)
- 💬 Chat with Memories - RAG-powered chat panel with LLM tool calling to search, create, update, and delete memories via streaming chat
- 📡 Event Notifications - Poll-based system for inter-agent communication
- 📊 Statistics & Analytics - Tag usage, trends, and connection insights
- 🧠 Memory Insights - Activity summary, stale detection, consolidation suggestions, and LLM-powered pattern analysis
- 📜 Action History - Track all memory operations (create, update, delete, merge, boost, link) with grouped timeline view
Preview
Install
Two paths. pip is a local stdio child the client spawns. A container is a detached HTTP service you start with up; with MEMORA_DATABASES it serves multiple stores from one process. The LaunchAgent supervises the proxy, not the container — after a host restart the listener can come back while its upstream is still stopped. If you are running memora as a service, the container path is the install.
pip (local / stdio)
pip install memora-mcp
The PyPI package is memora-mcp (bare memora on PyPI is an unrelated project). Includes cloud storage (S3/R2) and OpenAI embeddings out of the box.
# Optional: local embeddings (offline, ~2GB for PyTorch)
pip install "memora-mcp[local]"
Latest development version straight from git
pip install "git+https://github.com/agentic-box/memora.git"
Then spawn it from .mcp.json with "command": "memora-server" (see Configuration).
Container (HTTP service)
Default runtime is Apple's container CLI. Every container operation scripts/memora-instance.sh performs (build, up, status, logs, down) uses $MEMORA_CONTAINER_BIN (default container). The generated proxy process does not; it hardcodes container list.
Before the first build:
1. Install Apple's container CLI (signed pkg from its GitHub releases). It needs a Mac with Apple silicon running macOS 26 — Apple does not support older macOS versions for container.
2. Start the runtime — Apple's documented first command, which also installs a kernel if none is configured:
container system start
3. Clone this repo and cd into it:
git clone https://github.com/agentic-box/memora.git
cd memora
4. Copy the instance template. It ships with INSTANCE=myinstance so the later build/up/proxy lines match without renaming. Edit PORT and a backend (STORAGE_URI, VOLUME, or MEMORA_DATABASES):
cp instances/example.env instances/myinstance.env
5. Create the credential file and install the proxy the LaunchAgent will run. cred_args() requires a .mcp.json whose mcpServers.memora.env holds CLOUDFLARE_API_TOKEN (D1 access) and the embedding/LLM keys — up dies if that file is missing. The script looks for ~/.config/memora/credentials.mcp.json if that file exists, otherwise ~/repos/agentic-box/.mcp.json. Set CRED_SOURCE in the instance file to pick a path. Separately, proxy renders a plist whose executable is $MEMORA_PROXY_BIN (default ~/.local/libexec/memora/memora_proxy.py) and whose logs live in $MEMORA_LOG_DIR (default ~/.local/var/log) — nothing creates either on a fresh clone.
mkdir -p ~/.config/memora ~/.local/libexec/memora ~/.local/var/log
cp scripts/memora_proxy.py ~/.local/libexec/memora/
# real values; any key is fine, an absent file is not
# the default umask is permissive -- chmod 600 keeps other local accounts out
cat > ~/.config/memora/credentials.mcp.json <<'JSON'
{"mcpServers":{"memora":{"env":{"CLOUDFLARE_API_TOKEN":"REPLACE","OPENAI_API_KEY":"REPLACE"}}}}
JSON
chmod 600 ~/.config/memora/credentials.mcp.json
That JSON is the minimal correct config: both the LLM and embeddings use the
default OpenAI host with a real OpenAI key. Do not add
OPENAI_BASE_URL pointing at OpenRouter without the embedding pair from
Embeddings — OpenRouter has no embeddings
endpoint, every embed call 404s, and memora silently falls back to TF-IDF
keyword bags while looking healthy.
Then:
./scripts/memora-instance.sh build myinstance # tags IMAGE from myinstance.env (memora-pilot if IMAGE is unset)
./scripts/memora-instance.sh up myinstance # runs that same IMAGE
./scripts/memora-instance.sh proxy myinstance # render the LaunchAgent; run the printed launchctl
up does not publish a host port. The listener the workspace connects to is the proxy. proxy only renders a macOS LaunchAgent and prints the launchctl commands — it does not load the service. Run those printed commands.
The printed workspace URL is always http://127.0.0.1:/mcp (the registry default). For a non-default store, append / yourself — a bare /mcp on a registry silently binds MEMORA_DEFAULT_DB:
{"mcpServers": {"memora": {"type": "http", "url": "http://127.0.0.1:/mcp/"}}}
Proxy rationale, credentials, instance files, and MEMORA_CONTAINER_BIN: Container Deployment.
Usage
The server runs automatically when configured in Claude Code. Manual invocation:
# Default (stdio mode for MCP)
memora-server
With graph visualization server
memora-server --graph-port 8765
HTTP transport (alternative to stdio)
memora-server --transport streamable-http --host 127.0.0.1 --port 8080
Configuration
Claude Code
Add to .mcp.json in your project root:
Local DB:
{
"mcpServers": {
"memora": {
"command": "memora-server",
"args": [],
"env": {
"MEMORA_DB_PATH": "~/.local/share/memora/memories.db",
"MEMORA_ALLOW_ANY_TAG": "1",
"MEMORA_GRAPH_PORT": "8765"
}
}
}
}
Cloud DB (Cloudflare D1) - Recommended:
{
"mcpServers": {
"memora": {
"command": "memora-server",
"args": ["--no-graph"],
"env": {
"MEMORA_STORAGE_URI": "d1:///",
"CLOUDFLARE_API_TOKEN": "",
"MEMORA_ALLOW_ANY_TAG": "1"
}
}
}
}
With D1, use --no-graph to disable the local visualization server. Instead, use the hosted graph at your Cloudflare Pages URL (see Cloud Graph).
Cloud DB (S3/R2) - Sync mode:
{
"mcpServers": {
"memora": {
"command": "memora-server",
"args": [],
"env": {
"AWS_PROFILE": "memora",
"AWS_ENDPOINT_URL": "https://.r2.cloudflarestorage.com",
"MEMORA_STORAGE_URI": "s3://memories/memories.db",
"MEMORA_CLOUD_ENCRYPT": "true",
"MEMORA_ALLOW_ANY_TAG": "1",
"MEMORA_GRAPH_PORT": "8765"
}
}
}
}
Codex CLI
Add to ~/.codex/config.toml:
[mcp_servers.memora]
command = "memora-server" # or full path: /path/to/bin/memora-server
args = ["--no-graph"]
env = {
AWS_PROFILE = "memora",
AWS_ENDPOINT_URL = "https://.r2.cloudflarestorage.com",
MEMORA_STORAGE_URI = "s3://memories/memories.db",
MEMORA_CLOUD_ENCRYPT = "true",
MEMORA_ALLOW_ANY_TAG = "1",
}
Environment Variables
| Variable | Description |
|------------------------|-----------------------------------------------------------------------------|
| MEMORA_DB_PATH | Local SQLite database path (default: ~/.local/share/memora/memories.db) |
| MEMORA_STORAGE_URI | Storage URI: d1:/// (D1) or s3://bucket/memories.db (S3/R2). Used when MEMORA_DATABASES is unset. |
| MEMORA_DATABASES | JSON object {name: uri} mapping each store this process serves. Names are one URL path segment (/mcp/): letters, digits, -, _, . only. Duplicate keys, empty values, unsafe names, or non-objects refuse to start rather than silently picking a store. Unset = single-store (legacy). See Multi-database routing. |
| MEMORA_DEFAULT_DB | Registry name a bare /mcp uses. Required when the registry has more than one database; with exactly one name, that name is the default. A value not in the registry refuses to start. |
| CLOUDFLARE_API_TOKEN | API token for D1 (d1:// URI). CF_API_TOKEN is accepted as an alias. |
| MEMORA_CLOUD_ENCRYPT | Encrypt the local file before uploading to S3/R2. Unset/false = off; 1/true/yes = on. |
| MEMORA_CLOUD_COMPRESS| Compress the local file before uploading to S3/R2. Unset/false = off; 1/true/yes = on. |
| MEMORA_CACHE_DIR | Local cache directory for an S3/R2-synced database. Unset: the backend picks a cache path. |
| MEMORA_ALLOW_ANY_TAG | Allow any tag without validation against allowlist (1 to enable) |
| MEMORA_TAG_FILE | Path to a JSON file containing an array of allowed tags, e.g. ["plan", "memora/issues"] |
| MEMORA_TAGS | Comma-separated list of allowed tags |
| MEMORA_HOST | Bind address for HTTP transports (default 127.0.0.1). Overridable with --host. |
| MEMORA_PORT | Bind port for HTTP transports (default 8000). Overridable with --port. |
| MEMORA_GRAPH_PORT | Port for the knowledge graph visualization server (default: 8765) |
| MEMORA_TRANSPORT | stdio (default), sse, or streamable-http. An unknown env value falls back to stdio; --transport still rejects unknown values. Multi-database routing and the session guard run only on streamable-http. |
| MEMORA_TOOL_PROFILE | Tool subset exposed to clients: full (default, all 43), leader (19), agent (12). Unset/empty = full; an unknown value refuses to start. See Tool Profiles. |
| MEMORA_MAX_SESSIONS | Hard ceiling on concurrent MCP sessions (default 128). 0 disables. A creation rate plus an idle timeout is not a bound — a client that keeps session ids alive can grow without limit at the creation rate. Invalid values refuse to start. Streamable-HTTP only. |
| MEMORA_MAX_INIT_PER_MIN | New sessions admitted per minute (default 120). 0 disables. Invalid values refuse to start. Streamable-HTTP only. |
| MEMORA_MAX_INIT_BODY_BYTES | Maximum initialize request body accepted/buffered (default 65536, minimum 1024). Larger requests receive 413. Invalid values refuse to start. Streamable-HTTP only. |
| MEMORA_SESSION_IDLE_TIMEOUT | Seconds before an abandoned valid session is reaped (default 1800). 0 disables. Invalid values refuse to start. Streamable-HTTP only. |
| MEMORA_HEALTH_TOKEN | Bearer token for detailed /health/db bodies (names, counts, error text). Unset: only a loopback peer sees detail; everyone else gets aggregate status. FastMCP custom_route() is unauthenticated even when MCP auth is configured. HTTP transports only (memora.health is imported for SSE/streamable-http, not stdio). |
| MEMORA_HEALTH_TTL | Seconds a readiness snapshot may be served before a refresh is due (default 10, cap 3600). Must be > 0. Invalid values refuse to start. HTTP transports only — a malformed value does not abort stdio. |
| MEMORA_HEALTH_TIMEOUT| Bound on one refresh pass and on each store probe (default 15, cap 300). Must be > 0. HTTP transports only. |
| MEMORA_HEALTH_REFRESH_INTERVAL | How often the server refreshes readiness on its own (default 15, cap 3600). 0 = poll-only. Without this, a proxy deployment has no loopback caller and the alert surface stays unknown while every database is fine. When periodic refresh is enabled, interval + timeout must be < MEMORA_HEALTH_MAX_STALE. HTTP transports only. |
| MEMORA_HEALTH_MAX_STALE | Age after which a cached per-database result may no longer be reported ready (default 60, cap 3600). Must be >= MEMORA_HEALTH_TTL. HTTP transports only. |
| MEMORA_STALE_DAYS | Two consumers, two defaults, same name: memory_insights treats an open TODO/issue as stale after 14 days; the graph UI greys closed items after 30 days. Set the variable to override both. |
| MEMORA_EMBEDDING_MODEL | Embedding backend: openai (default), sentence-transformers, or tfidf |
| SENTENCE_TRANSFORMERS_MODEL | Model for sentence-transformers (default: all-MiniLM-L6-v2) |
| MEMORA_EMBEDDING_API_KEY | Embedding provider API key (atomic with base URL — see below) |
| MEMORA_EMBEDDING_BASE_URL | Embedding provider base URL (atomic with API key — see below) |
| MEMORA_EMBEDDING_STRICT | Recommend 1. Fail hard on embedding errors instead of silent TF-IDF. Without it a broken endpoint keeps answering while every vector becomes a keyword bag (how 756 memories degraded unnoticed). |
| OPENAI_API_KEY | LLM only (dedup/chat) when MEMORA_EMBEDDING_* is set. Embeddings fall back to this key only if both MEMORA_EMBEDDING_API_KEY and MEMORA_EMBEDDING_BASE_URL are unset |
| OPENAI_BASE_URL | LLM base URL (OpenRouter, Azure, etc.). Same atomic fallback rule as the key — not an embeddings URL when you use a split config |
| OPENAI_EMBEDDING_MODEL | Model id for the openai embedding backend. Must exist on the embedding host (default text-embedding-3-small is OpenAI-only; Cloudflare needs e.g. @cf/baai/bge-m3) |
| MEMORA_LLM_ENABLED | Enable LLM-powered deduplication comparison (true/1/yes; default: true) |
| MEMORA_LLM_MODEL | Model for deduplication comparison and, if unset, for query rewrite and local chat (default: gpt-4o-mini). Pick a plain chat/instruct model, not a reasoning model — a reasoning model was measured at 12-17s per absorb classification call (1000-1700 reasoning tokens it does not let OpenRouter's reasoning.effort/reasoning.max_tokens bound), vs 3-4s for gpt-4o-mini on the same calls. |
| MEMORA_ABSORB_CONCURRENCY | Worker count for memory_absorb's concurrent classify phase (default 4; non-numeric or <1 falls back to 4; 1 disables the thread pool and classifies sequentially). Only the per-fact LLM classification call is parallelized — embeds and searches stay sequential (cheap, and conn isn't safe to touch from worker threads: sqlite3 connections are thread-affine by default and D1Connection carries mutable session-token state). |
| MEMORA_LLM_TIMEOUT | Seconds the OpenAI client waits (default 60, floored at 1). A non-numeric value falls back to 60. |
| MEMORA_REWRITE_MODEL | Model for RAG query rewriting in the graph chat panel. Unset/empty uses MEMORA_LLM_MODEL. |
| MEMORA_VECTOR_SCAN_PAGE_SIZE | Rows per page when loading embeddings from D1 (default 1000; non-numeric or <1 falls back to 1000; hard ceiling 10000). At the default, a store under 1000 rows returns the entire corpus plus every embedding in one D1 response, which raced Cloudflare's 30s per-request ceiling and made memory_absorb fail outright. Use 100 on D1 (the instance script already injects that). Paging is a mitigation, not the fix: absorb reads the corpus once per call and reuses a process-local cache keyed on the DB's monotonic embedding_change_epoch. |
| MEMORA_REBUILD_CHUNK_SIZE | Rows per batch in memory_rebuild_embeddings (default 32; non-numeric or <1 falls back to 32). Each batch is one compute_embeddings_batch call and one conn.commit(), instead of one embed call and one commit per row — on D1 this collapses the lease-heartbeat round trips from two per row to two per batch, which dominated wall time far more than the per-row commits did (D1's commit() is already a no-op). |
| CHAT_MODEL | Model for the local graph chat panel. Unset/empty falls back to MEMORA_LLM_MODEL. (The deepseek/deepseek-chat default is Cloudflare Pages wrangler.toml, not this process.) |
| MEMORA_CLOUD_GRAPH_ENABLED | true/1/yes to notify the hosted graph of writes (default off). |
| MEMORA_CLOUD_GRAPH_WORKER_URL | Worker base URL for those broadcasts (POST /broadcast). Unset: broadcasts are skipped. |
| MEMORA_CLOUD_GRAPH_DEBOUNCE | Seconds to batch rapid writes before broadcasting (default 1.0). |
| MEMORA_CLOUD_GRAPH_SYNC_SCRIPT | Path captured at startup (default: memora-graph/scripts/sync.sh if that file exists). The current write path does not execute this script — D1 is the source of truth and only the worker broadcast runs. |
| AWS_PROFILE | AWS credentials profile from ~/.aws/credentials (useful for R2) |
| AWS_ENDPOINT_URL | S3-compatible endpoint for R2/MinIO |
| R2_PUBLIC_DOMAIN | Public domain for R2 image URLs |
Tool Profiles (MEMORA_TOOL_PROFILE)
All 43 MCP tools register unconditionally, so every agent session is injected with the full ~12,700-token tool schema even when most tools are never called. MEMORA_TOOL_PROFILE exposes a subset per deployment so a gated tool is genuinely absent — missing from tools/list AND undispatchable (call_tool returns unknown-tool, not a hidden execution). The profile is applied and attested at startup; the active profile and exposed tool count are logged to stderr.
| Value | Tools | Use |
|-------|-------|-----|
| full (default) | all 43 | Direct stdio use; every existing deployment is byte-for-byte unchanged |
| leader | 19 | The agent set plus memory_create_section, memory_store_document, memory_get_document, memory_tags, memory_delete, memory_digest, memory_list |
| agent | 12 | The read/create surface a worker agent needs: memory_absorb, memory_semantic_search, memory_hybrid_search, memory_list_compact, memory_get, memory_related, memory_link, memory_stats, memory_create, memory_create_issue, memory_create_todo, memory_update |
- Unset / empty =
full. No existing deployment changes behaviour. - An unknown value aborts startup with a message naming the valid values. It never silently falls back to
full— a typo must not re-expose destructive maintenance tools (memory_rebuild_embeddings,memory_delete_batch) to every worker. Fail closed. memory_listis inleaderbut notagent. It was excluded from both while it cost 163-174s on a D1 store againstmemory_list_compact's 0.22s; #973 fixed that (now ~1.1s). It stays out ofagentbecause a worker's read surface is deliberately narrow, not for speed.- The leader/agent boundary is data in
memora/tool_profile.py(two frozensets). Editing it is one line, not a sweep of 43 decorators. - The prune deletes from FastMCP's private
_tool_manager._toolsdict, somemorapinsmcp>=1.27,<1.28(the audited minor) and runs a startup attestation through the low-level registered MCP request handlers (_mcp_server.request_handlers[ListToolsRequest]/[CallToolRequest]— the actual dispatch callable real client requests use, not theFastMCP.list_tools/call_toolPython helpers) that refuses to start if the installed SDK routes listing/dispatch elsewhere (private-implementation drift). The pin is the static guard; the attestation is the runtime backstop. Bumping the upper bound requires