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
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):
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):
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
evolutiondb-brief --queue 3
evolutiondb-outbox list
evolutiondb-outbox show <id>
evolutiondb-brief --approve
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
evolutiondb-send-setup --channel gmail
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
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_ID or default_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:
| 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:
export EVOSQL_MCP_AUTH_TOKEN=$(openssl rand -hex 24)
evolutiondb-mcp-http
cloudflared tunnel --url http://127.0.0.1:8970
ngrok http 8970
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
cover chat.openai.com, chatgpt.com, claude.ai,
gemini.google.com. Override via EVOSQL_MCP_ALLOWED_ORIGINS
(comma-separated).
- Bound to 127.0.0.1 by default. The CLI warns if you flip to
0.0.0.0 without 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
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.
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
python -m mcp_server_evosql.scheduler status
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_PASSWORD accordingly. The server doesn't rotate secrets.