Persistent long-term memory for Claude Desktop / Claude Code, backed by EvolutionDB.
Persistent long-term memory for Claude Desktop / Claude Code, backed by EvolutionDB.
EvolutionDB Long-Term Memory · v1.1.3
by Alptekin
mcp-server-evolutiondb
A Model Context Protocol server
that gives Claude Desktop / Claude Code persistent long-term
memory backed by EvolutionDB. Anything Claude decides to remember
during a conversation is written to a real database; in any future
session — same window or weeks later — Claude can search it back
without you having to repaste context.
Install
pipx install mcp-server-evolutiondb
# or: pip install --user mcp-server-evolutiondb
The package installs the mcp-server-evolutiondb console entry-point
(also aliased as mcp-server-evosql). It speaks the PostgreSQL wire
protocol over psycopg, so installation is pure-Python — no C
toolchain, no libevosql-memory.so to build. EvolutionDB still has
to be running somewhere reachable; docker compose up -d in the
main repo is the easiest
way.
┌──────────────────┐ ┌─────────────────────┐ ┌────────────────┐
│ Claude Desktop │ stdio │ mcp-server-evosql │ TCP │ EvolutionDB │
│ (or Claude │ ◀──────▶│ (this package) │ ◀──────▶│ (port 9967) │
│ Code) │ JSON- │ │ │ │
│ │ RPC │ save_memory │ │ MEMORY STORE │
│ │ 2.0 │ search_memory │ │ ENTITY STORE │
│ │ │ recent_memories │ │ │
│ │ │ forget │ │ │
│ │ │ list_tags │ │ │
└──────────────────┘ └─────────────────────┘ └────────────────┘
Why
The default Claude experience is stateless — every new chat starts
from scratch, so you waste tokens re-explaining who you are, what
project you're on, what your preferences are. Plug this server in
and the model:
- saves preferences / decisions / facts during natural conversation,
- searches them back the next time you ask something related,
- forgets entries on demand,
- never sees the user_id that pins the namespace (we override it
server-side, so the model can't accidentally fragment the
namespace by inventing IDs across sessions).
Token math: 100 chats × 3,000 tokens of pre-loaded context (~$0.90
on Sonnet) → 100 chats × ~250 tokens of just-relevant facts pulled
on demand (~$0.26). Roughly 3.5× cheaper inputs without losing
context fidelity.
What's exposed to Claude
All under one evolutiondb-memory MCP server. Memory (the core):
| Tool | Purpose |
|---|---|
save_memory |
Persist a fact + optional tags |
search_memory |
Hybrid semantic + keyword search (before answering) |
recent_memories |
Last N saved facts (most-recent-first) |
forget |
Delete by key |
list_tags |
All distinct tags in use, with counts |
show_profile · expand_episode · feedback · restore_memory · set_language |
retrieval/profile extras |
Personal assistant + action loop (over your synced mail/chat — see below):
| Tool | Purpose |
|---|---|
daily_brief |
Who's waiting on you, what you owe, your day |
suggest_reply |
Draft a reply for an open loop (grounded) |
queue_reply |
Queue a drafted reply for approval (sends nothing) |
list_pending_replies |
Review the outbox |
approve_send |
The only tool that delivers (opt-in, gated) |
reject_reply |
Cancel a queued reply |
outbox_audit · send_scheduled |
the send trail + flush due scheduled sends |
Each call's user_id is overridden server-side from the
MCP_USER_ID env var — stops the model from drifting the namespace
across "user" / "default_user" / your name etc.
The assistant action loop
Beyond memory, the server runs a human-in-the-loop action loop over the
sources you sync in (gmail / teams / outlook / slack / imessage, via the sibling
*-sync connectors):
read (your mail/chat) → understand (open loops, self-model)
→ suggest (draft a reply) → queue → APPROVE → send → loop resolved
It is read + suggest by default — nothing is ever sent until you opt in and
approve each reply. Drive it from Claude (the tools above) or from one CLI:
evolutiondb-brief # who's waiting on you, what you owe
evolutiondb-brief --queue 3 # draft + queue replies for the top 3 (nothing sent)
evolutiondb-outbox list # review what's queued
evolutiondb-outbox show <id> # preview exactly what would go out
evolutiondb-brief --approve # deliver (dry-run unless sending is enabled)
Turning on sending (deliberately)
Sending is off by default and the sync connectors stay read-only. To enable it,
ask the onboarding helper what a channel needs — it prints the exact env + the
one-time consent step, and writes nothing:
evolutiondb-send-setup # status: enabled? channels? guards?
evolutiondb-send-setup --channel gmail # the env to enable gmail + its auth step
Sending only happens when both EVOSQL_SEND_ENABLED=1 and a per-channel send
scope (gmail.send / Mail.Send / ChatMessage.Send / chat:write) are set,
and is wrapped in safe-default guards: an undo window, a rate cap, dedup
(no double-reply), and a queryable audit trail. Full design + the invariants:
docs/adr/ADR-004-action-loop-send-approval.md.
Install + run
1. Bring up EvolutionDB
cd /path/to/evolutiondb
docker compose up -d
Zero-Docker alternative: set EVOSQL_EMBEDDED=1 and the MCP server spawns its
own EvolutionDB on first connect (against a per-user data dir, reaped on exit) —
no docker compose step. If no evosql-server binary is found
(EVOSQL_SERVER_BINARY / PATH / dev checkout), it auto-fetches the prebuilt
one for your platform from the GitHub release (checksum-verified, cached) — so a
plain pip install + EVOSQL_EMBEDDED=1 needs neither Docker nor a binary.
Pre-fetch with evolutiondb-embedded-fetch; disable auto-fetch with
EVOSQL_EMBEDDED_AUTOFETCH=0. An already-running instance still wins, so it's
safe to leave on.
2. Build the SDK once
make -C client/libevosql-memory
3. Configure Claude Desktop
Open the config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Drop in the entry from
examples/claude_desktop_config.json,
substituting the absolute paths for your machine. Quit + restart
Claude Desktop.
You'll see a small 🔌 / hammer icon in the bottom-right of the
chat composer once evolutiondb-memory is connected.
4. Talk normally
Say "remember that I take my espresso single-shot, no sugar"; Claude
will run save_memory(...). Days later open a new chat, ask "what
do I drink?" — Claude runs search_memory(...) and recalls.
One-command setup for all supported hosts
pip install mcp-server-evolutiondb puts a small evolutiondb-mcp-setup
helper on $PATH. It autodetects every MCP host installed on the
machine and merges the evolutiondb-memory server into each of
their config files in one go:
evolutiondb-mcp-setup
# [setup] Claude Desktop: wrote /Users/you/Library/Application Support/Claude/claude_desktop_config.json
# [setup] ChatGPT Desktop: not detected — skipping
# [setup] Gemini CLI: wrote /Users/you/.gemini/settings.json
The merge is idempotent — running it twice changes nothing the
second time. Existing mcpServers entries are preserved; only the
evolutiondb-memory block is overwritten.
Useful flags:
--dry-run— show what would change, write nothing.--client claude-desktop— only configure one host (repeatable).--user-id NAME— override the sticky namespace (default:$MCP_USER_IDordefault_user).--port 5433— non-default EvolutionDB PG-wire port.
Override config paths via env vars when the auto-detected location
is wrong for your install: EVOSQL_CLAUDE_CONFIG,
EVOSQL_CHATGPT_CONFIG, EVOSQL_GEMINI_CONFIG.
Per-host manual config
If you'd rather hand-edit, every host accepts the same JSON shape —
the mcpServers block is part of the MCP spec, not specific to any
vendor:
{
"mcpServers": {
"evolutiondb-memory": {
"command": "uvx",
"args": ["mcp-server-evolutiondb"],
"env": {
"EVOSQL_HOST": "127.0.0.1",
"EVOSQL_PORT": "5433",
"EVOSQL_USER": "admin",
"EVOSQL_PASSWORD": "admin",
"EVOSQL_DATABASE": "evosql",
"MCP_USER_ID": "your-handle",
"MCP_STORE_PREFIX": "mcp"
}
}
}
}
File paths:
| Host | macOS | Linux / WSL | Windows |
|---|---|---|---|
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json |
~/.config/Claude/claude_desktop_config.json |
%APPDATA%\Claude\claude_desktop_config.json |
| Claude Code | ~/.claude/mcp.json |
~/.claude/mcp.json |
%USERPROFILE%\.claude\mcp.json |
| ChatGPT Desktop | ~/Library/Application Support/ChatGPT/mcp.json |
~/.config/ChatGPT/mcp.json |
%APPDATA%\ChatGPT\mcp.json |
| Gemini CLI | ~/.gemini/settings.json |
~/.gemini/settings.json |
%USERPROFILE%\.gemini\settings.json |
After editing, restart the host (or for Gemini CLI, just run the next
command — it re-reads its config on every launch).
Remote HTTP transport (web ChatGPT, Gemini app, etc.)
Web-only MCP hosts cannot spawn a local process and so cannot use
stdio. They speak the spec's "streamable HTTP" transport instead.
Run the same server as an HTTP listener and point a tunnel at it:
# 1. Start the HTTP listener (default port 8970, path /mcp).
export EVOSQL_MCP_AUTH_TOKEN=$(openssl rand -hex 24)
evolutiondb-mcp-http
# 2. Expose it through a tunnel. Pick whichever you already use.
cloudflared tunnel --url http://127.0.0.1:8970
# → outputs https://random-words.trycloudflare.com
# OR
ngrok http 8970
# → outputs https://random.ngrok-free.app
In the web host's "MCP Connectors" or "Custom MCP server" panel,
register:
- URL:
https://your-tunnel-host/mcp - Authorization:
Bearer <the token you just generated>
The server enforces three guards when bound to anything other than
loopback:
- Every request must carry the bearer token.
Origin(when present) must be on the allow-list. The defaults
coverchat.openai.com,chatgpt.com,claude.ai,
gemini.google.com. Override viaEVOSQL_MCP_ALLOWED_ORIGINS
(comma-separated).- Bound to 127.0.0.1 by default. The CLI warns if you flip to
0.0.0.0without a token.
Identical tool surface as stdio — save_memory, search_memory,
recent_memories, forget, list_tags. New tools land in both
transports at once because both go through the same MCPServer.handle.
Configuration
| Env var | Default | Purpose |
|---|---|---|
EVOSQL_HOST |
127.0.0.1 |
DB host |
EVOSQL_PORT |
9967 |
EVO port |
EVOSQL_USER |
admin |
DB user |
EVOSQL_PASSWORD |
admin |
DB password |
MCP_USER_ID |
default_user |
Sticky namespace for every tool call |
MCP_STORE_PREFIX |
mcp |
Catalog object prefix |
EVOSQL_PYTHON_SDK |
(auto-discovered) | Override path to the Python ctypes binding |
EVOSQL_MEMORY_LIB |
(auto-discovered) | Override path to libevosql-memory.dylib/so |
EVOSQL_EMBEDDING_PROVIDER |
none |
openai, local, or none. Turns search_memory into hybrid semantic+keyword |
EVOSQL_EMBEDDING_MODEL |
provider default | Override embedding model (e.g. text-embedding-3-large) |
OPENAI_API_KEY |
— | Required when provider is openai |
Context-layer boosts (all opt-in)
These layer extra signal onto retrieval. Each is off by default so the plain
hybrid search is unchanged; turn them on per deployment.
| Env var | Default | Purpose |
|---|---|---|
EVOSQL_SALIENCE_BOOST |
0 |
Weight (0-1) for the per-row salience score (recency × sender activity × thread depth × feedback) |
EVOSQL_GRAPH_BOOST |
0 |
Weight (0-1) for knowledge-graph spreading activation — relational queries reach rows that never name the queried entity |
EVOSQL_PROFILE_BOOST |
0 |
Weight (0-1) for the user interest profile — biases results toward the cluster the query points at |
EVOSQL_DECAY |
0 |
1 to fade old, unused rows (archived, never deleted; include_archived digs them back up) |
EVOSQL_GRAPH_BUILD |
1 |
Build graph edges inline on save (cheap; needed before the graph boost helps) |
EVOSQL_ENTITY_EXTRACT |
1 |
Extract entities inline on save |
salience, profile, and decay read data the background scheduler
maintains, so run it for them to have any effect:
python -m mcp_server_evosql.scheduler run # hourly/daily/weekly jobs
python -m mcp_server_evosql.scheduler status # last run + failure ratio
It also embeds + entity-extracts new rows, recomputes salience, refreshes the
interest profile, regenerates episode summaries, and runs the decay pass.
Semantic search
search_memory runs in keyword mode by default. Setting
EVOSQL_EMBEDDING_PROVIDER=openai (with OPENAI_API_KEY) or
EVOSQL_EMBEDDING_PROVIDER=local flips it into hybrid mode: new
saves are tagged with a dense vector and queries are ranked by a
weighted mix of cosine similarity (0.7) and substring overlap (0.3).
Rows saved before you enabled embeddings continue to score on
keyword overlap only, so the switch is non-destructive — older
memories don't disappear, they just rank lower against semantically
strong matches.
The local provider needs an extra install:
pip install 'mcp-server-evolutiondb[embeddings-local]'
Tests
cd client/mcp-server-evosql
python3 tests/test_mcp.py
Eight cases — initialize handshake, tools/list discovery, save+search
round-trip, tag-filtered search, recent ordering, forget, list_tags
aggregation, and the "user_id can't be hijacked from the LLM side"
isolation case. Each test spawns the server as a real subprocess and
talks JSON-RPC, so framing bugs that an in-process unit test would
hide get caught.
Inspect the database directly
While Claude is using the server, open another terminal and:
docker compose exec evosql evosql-cli -W admin
Then:
SELECT mem_namespace, mem_key, mem_value FROM __mem_mcp_mem;
ENTITY RANK FROM mcp_ents;
Everything Claude has decided to remember is right there as
queryable rows — no opaque blob storage.
Wire format
Newline-delimited JSON-RPC 2.0 over stdio (no Content-Length
headers — that's the LSP variant; MCP uses plain \n-delimited).
The server speaks protocol version 2024-11-05.
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2024-11-05","capabilities":{}}}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"save_memory",
"arguments":{"fact":"loves jazz","tags":["preference"]}}}
Errors come back as {"jsonrpc":"2.0","id":N,"error":{"code":..,"message":..}}
or as a tools/call result with isError: true.
Known limitations
- Single-process EvolutionDB connection. The server holds one
Connection — the SDK contract is one-per-thread, and MCP stdio is
inherently single-threaded so this is fine. - No streaming responses. Tool results return as single JSON
blobs. Larger memories (>100 facts) take ~50 ms to serialise; the
protocol can stream but Claude's tool-use UI doesn't render
partial responses anyway. - Authentication via env-vars only. If you expose the server to
another machine (which you shouldn't — it's stdio), set
EVOSQL_PASSWORDaccordingly. The server doesn't rotate secrets.