Persistent knowledge base MCP server with pluggable search backends and graph relation.
2.4K
A Model Context Protocol server that gives AI agents persistent memory. Markdown files as source of truth, pluggable search backends, typed graph relations.
Website: engram-kb.org · Docker Hub: cylian/engram
Your agent manages the server. Recommended for Claude Code, ChatGPT Desktop, Cursor.
claude mcp add --transport stdio engram -- \
docker run -i --rm -v ./knowledge:/knowledge cylian/engram
Persistent server on the network. Share knowledge across multiple agents.
docker run -d --name engram \
-p 8192:8192 \
-v ./knowledge:/knowledge \
cylian/engram --transport sse
claude mcp add --transport sse engram http://your-host:8192/sse
Streamable HTTP. Session state lives in the server process: run one instance per URL.
docker run -d --name engram \
-p 8192:8192 \
-v ./knowledge:/knowledge \
cylian/engram --transport streamable-http
claude mcp add --transport http engram http://your-host:8192/mcp
Two backends ship out of the box. Switch with --backend or ENGRAM_BACKEND:
| Backend | Default | Description |
|---|---|---|
xapian | ✓ | Fast full-text search with configurable stemming |
sqlite | SQLite FTS5 — query your index with standard SQL tools |
Query syntax: with sqlite, tokens match literally (docker-proxy, port:8080 are safe) and a trailing * means prefix search. With xapian, boolean operators and wildcards apply, and a query with no result is retried with the spelling suggestion built from the index.
The backend is pluggable — see Custom Backend below.
| Tool | Description | Key Parameters |
|---|---|---|
remember | Create or update an entry (upsert). Same title updates it; a similar but different title returns {"conflict": true, "candidates": [...]} without writing — retry with entry_id or force. Returns size and warnings (structure, size over 512 B / 1 KB, index update failure, missing link target). | title, content, tags, entry_id, force |
recall | Read an entry with its graph relations (outgoing + backlinks). Returns size and last_modified. | entry_id |
search | Full-text search with optional tag filter | query, tags, limit |
list | Browse entries sorted by title | tags, limit |
tags | List all tags with entry counts | — |
forget | Delete an entry (file and index). Returns warnings if the index could not be updated. | entry_id |
rebuild | Rebuild search index from Markdown files | — |
Link entries with kb://uuid#type URLs in Markdown content:
This service runs on [Saturn](kb://a1b2c3d4-...#runs-on)
and depends on [PostgreSQL](kb://f9e8d7c6-...#depends-on).
recall returns both directions, plus size metadata:
{
"id": "a1b2c3d4-...",
"title": "My API Service",
"content": "...",
"tags": ["..."],
"size": 1024,
"last_modified": "2026-03-14",
"relations": {
"out": [{"type": "runs-on", "id": "e5f6...", "title": "Saturn"}],
"in": [{"type": "depends-on", "id": "b7c8...", "title": "Frontend App"}]
}
}
Like HATEOAS for knowledge — every response carries the links to navigate the graph.
Ask your agent:
"Remember that our API runs on port 8080 and depends on PostgreSQL 15."
Engram creates a Markdown file with a unique UUID, indexes it, and confirms. The agent can now recall this fact in any future session.
"What do we know about PostgreSQL?"
Engram searches across all entries by content, title, and tags. Results are ranked by relevance.
"What depends on PostgreSQL?"
If entries link to the PostgreSQL article with kb://uuid#depends-on, Engram returns all backlinks — showing every service that depends on it, without the agent having to search for each one.
Start Engram with SSE or HTTP transport. Multiple agents — even from different providers (Claude, ChatGPT, Copilot) — connect to the same server. What one agent remembers, all others can recall.
Agent A: "Remember that the deploy key rotates every 90 days."
Agent B: "When does the deploy key expire?"
→ Agent B finds the answer immediately.
Add this to your system prompt or project instructions to make your agent use Engram as a reflex, not an afterthought:
Engram is your persistent memory. Using it is mandatory, not optional.
Engram stores ZERO discoverable information. If you can derive it from
code, git history, configuration files, or existing documentation, it
does not belong in Engram. Engram captures decisions and their context,
diagnostics and their root causes, procedures learned the hard way —
the kind of knowledge that is lost when a conversation ends.
Before working on any topic: search Engram first. Always. Even if you think you know.
Before answering a question about infrastructure or architecture: search first.
Before proposing a solution: check if a past decision exists in Engram.
After resolving a diagnostic: remember the root cause and the fix.
After executing a procedure: remember the steps.
After making an architecture decision: remember the choice and the rationale.
After discovering something about the infrastructure: remember it.
All options have ENGRAM_* environment variable fallbacks. CLI args take priority.
| Option | Env var | Default | Description |
|---|---|---|---|
--data-path | ENGRAM_DATA_PATH | /knowledge | Root path for knowledge data |
--backend | ENGRAM_BACKEND | xapian | Search backend |
--language | ENGRAM_LANGUAGE | en | Stemmer language |
--transport | ENGRAM_TRANSPORT | stdio | MCP transport |
--host | ENGRAM_HOST | 0.0.0.0 | Listen address (SSE/HTTP) |
--port | ENGRAM_PORT | 8192 | Listen port (SSE/HTTP) |
Entries are Markdown files with YAML frontmatter in <data-path>/entries/:
---
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
title: Entry Title
tags: [infrastructure, postgresql]
---
Markdown content here...
The search index is a rebuildable cache in <data-path>/index/<backend>/. Delete it and rebuild — no data is ever lost.
Create backend/{name}/main.py with a class inheriting SearchBackend:
from backend import SearchBackend
class MyBackend(SearchBackend):
def index(self, entry):
"""Index or update an entry (upsert)."""
...
def unindex(self, entry_id):
"""Remove an entry from the index."""
...
def search(self, query_str, tags, limit):
"""Full-text search. Return [{id, score}]."""
...
def rebuild(self, entries):
"""Rebuild index from entries list. Return count."""
...
def get_relations(self, entry_id):
"""Return {out: [{type, id}], in: [{type, id}]}."""
...
Then use it with --backend {name}. Engram loads it automatically via importlib.
The constructor receives <data-path>/index/{name} as first argument, plus language= if it accepts that parameter. Set the class attribute needs_rebuild = True when your index was recreated empty (schema change): KnowledgeBase rebuilds it from the Markdown files at startup. Index failures raised by index() / unindex() are reported to the agent as warnings, never as errors.
FROM cylian/engram:latest
# Install Whoosh
RUN pip install --no-cache-dir whoosh==2.7.4
# Add backend
COPY whoosh_backend/ /app/backend/whoosh/
docker build -t engram-whoosh .
docker run -i --rm -v ./knowledge:/knowledge engram-whoosh --backend whoosh
Tests run on both backends from a virtualenv (the image ships without tests/):
python3 -m venv .venv
.venv/bin/pip install -r src/requirements.txt
.venv/bin/pip install coverage pytest
# Test (every test runs on xapian and on sqlite)
.venv/bin/coverage run -m pytest tests/ -v
.venv/bin/coverage report
# Build
docker build -t engram .
# Run locally (SSE)
docker run -d --name engram -p 8192:8192 -v ./knowledge:/knowledge engram --transport sse
Content type
Image
Digest
sha256:d39ec3cfb…
Size
52.3 MB
Last updated
about 1 month ago
docker pull cylian/engram