Engram
About
Self-hosted MCP server giving AI agents persistent memory — Markdown source of truth, hybrid BM25+embedding search, typed graph relations.
Details
- Author
- veronchenko
- Categories
- AI, Knowledge Base, Other
Jump to
Setup
Install Engram in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/veronchenko/engram-memory
Follow the installation instructions in the repository README, then restart your MCP client.
Inkwell — Persistent Knowledge Base MCP Server
Docker Hub:foreigndmitryi/inkwell-memory·Website:veronchenko.github.io/inkwell-memory
Formerly Engram.Renamed in 0.14.0 — there are a dozen unrelated projects called "Engram" and the name had stopped being findable. Not affiliated with any of them, nor with the Engram keyboard layout. See thechangelogfor what the rename breaks.
- Concept
- Features
- Comparison
- Measured Against a Plain Markdown Wiki
- Design Patterns
- Architecture
- Quick Start
- Search
- Tools
- Dashboard
- Graph Relations
- Usage Examples
- Prompt Your Agent
- Configuration
- Storage Format
- Development
- License
Agent conversations end and take their context with them. Inkwell is the piece that survives: a knowledge base an agent searchesbeforeacting and writes toafterresolving something non-obvious, so the next session — same project or a different one — starts with what was already learned instead of re-deriving it.
It deliberately storeszero discoverable information. If a fact can be pulled from code, git history, config files, or existing docs, it does not belong in Inkwell — that's what greps and re-reads are for. What belongs is the kind of knowledge a conversation would otherwise lose: a decision and the alternatives it ruled out, a bug's root cause and fix, a procedure learned the hard way, a preference stated once that should hold from then on.
Two things keep the base usable as it grows:
- Atomicity— one entry, one fact.rememberwarns (non-blocking) on Markdown headers, more than 3 paragraphs, or content past 512 B/1 KB, pushing multi-fact dumps back into separate linked entries instead of a wall of text no search will rank well.
- The graph, not a pile— entries link to each other viakb://uuid#typereferences, so related facts (a hub project, its features, a diagnostic tied to one of them) stay navigable both ways instead of living as isolated rows.
- Hybrid search— SQLite FTS5 (BM25, Porter stemming) fused with cosine similarity over local Model2Vec embeddingsandan IDF-weighted exact-match channel over title/tags via Reciprocal Rank Fusion; finds entries by meaning or by a literal proper noun BM25/embeddings alone would dilute among lexically-similar distractors, with zero cloud dependency
- Typed graph relations—kb://uuid#typelinks between entries, resolved both directions (outgoing + backlinks) on everyrecall, with an optional second hop (hops=2) to see how two entries connect through an intermediate one
- Schema-enforced entry types—hub,decision,diagnostic,feature,procedure,integration,pattern,snippet,preference,idea— declared inschema.jsonand exposed to the client as an enum, so an invalid type can't be written; filterable on search/list
- doctorintegrity pass— one schema-driven check over the Markdown files for dangling and superseded links, undeclared types, missing template fields, supernodes, and tag/type collisions
- part_ofstructural membership— links a detail entry (decision,diagnostic,feature,procedure,integration, ...) to its hub, enforced per type by the schema; filterable onsearch/list, grouped alongsidekb://back-links in a hub'srecalldigest
- Bi-temporal versioning—remember(..., supersede=True)creates a new version instead of overwriting; old versions stay in history (include_superseded=True) instead of being lost
- Duplicate detection & link suggestions—remembermatches near-identical titles to avoid duplicate entries, and returnssuggested_links(embedding-similarity matches) so related facts get cross-referenced instead of orphaned
- Atomicity guardrails— non-blocking warnings on structural anti-patterns (headers, >3 paragraphs, oversized content) so the base stays one-fact-per-entry as it scales
- Web dashboard— force-directed graph view, hybrid search, and a CRUD panel over the same knowledge base the MCP tools use (seeDashboard)
- Three transports— stdio (agent-managed), SSE, streamable-http — so the same server works for a single local agent or a shared multi-agent deployment
- Markdown as source of truth— the SQLite index is a rebuildable cache; delete it andrebuild, no data is ever lost
Inkwell trades automatic extraction (Mem0, Zep) for an agent-curated, atomic, explicitly-linked knowledge base — no LLM-driven ingestion pipeline, no graph database dependency, and the on-disk Markdown stays human-readable and diffable.
Inkwell was benchmarked against the same knowledge base packaged as an ordinary Markdown wiki — one file per topic, organized in folders by project and category (decision, diagnostic, feature, procedure, ...), each project with an index page and cross-links between related pages, browsed withRead/Grep/Glob/Bash. Same content, two ways of finding it — for equivalent fact coverage, Inkwell'ssearch/recallused:
32 questions (single-hop, multi-hop, negative, cross-lingual, supersede) over5,070 entries— 70 curated facts across 4 fictional projects plus 5,000 real-text distractor entries, so retrieval has to work at a scale that doesn't fit in an agent's context:
wiki/ ├── Ledgerbird/ ) ├── Pipewren/ ) 4 curated projects — 70 real ├── Snipfox/ ) decisions/diagnostics/features/ ├── Featherstore/ ) procedures/integrations/snippets │ ├── README.md <- project index page, links to every entry below │ ├── decision/ │ │ ├── offline-engine-duckdb-over-spark.md │ │ ├── online-value-serialization-msgpack.md │ │ └── ... (2 more) │ ├── diagnostic/ │ │ ├── redis-memory-doubling-from-ttl-less-deprecated-feature-groups.md │ │ └── training-serving-skew-from-tz-naive-event-timestamps.md │ ├── feature/ (4 entries) │ ├── integration/ (2 entries) │ ├── procedure/ (1 entry) │ └── snippet/ (1 entry) ├── _shared/ cross-project patterns & preferences └── haystack-project-00000.../00199/ 200 distractor projects x 25 pages │ = 5,000 real-text (Wikipedia) pages ├── README.md <- same index-page shape as a real project ├── decision/ (2 entries) ├── feature/ (3 entries) ├── idea/ (7 entries) ├── pattern/ (4 entries) ├── procedure/ (1 entry) └── snippet/ (2 entries)
Every category folder is a flat list of one-file-per-entry, and every project folder (real or distractor) has its ownREADME.mdindex page linking to all of them — structurally identical, so the wiki arm can't tell curated fact from distractor by shape alone.
An agent that already knowswhereto look doesn't need togrep, re-read, and re-grepits way there — the efficiency edge holds for equivalent fact coverage. Retrieval ranking has also been improving: adding an IDF-weighted exact-match channel (below) moved MRR from 0.851 to 0.857 and recall@5 from 0.851 to 0.869, with no regression across any language.
- Reciprocal Rank Fusion— BM25 and embedding rankings are computed independently and merged by rank position rather than raw score, avoiding the need to normalize incomparable similarity metrics.
- Rebuildable cache over source of truth— the SQLite index is fully derived from the Markdown files (rebuildregenerates it from scratch); the database is never the only copy of a fact.
- Bi-temporal versioning—supersedewrites a new entry and points the old one at it viasuperseded_by, instead of overwriting in place, so history stays queryable (include_superseded).
- HATEOAS-style graph traversal— everyrecallresponse carries its own outgoing/incomingkb://links, so navigating the knowledge graph doesn't require a separate query per hop.
- Shared domain layer, two transports— the dashboard's REST API and the MCP tools both call the sameKnowledgeBase/SQLiteBackendmethods, so there is exactly one code path for writes regardless of which surface triggered them.
Markdown files ---> Search index ---> MCP ---> Agent (source of truth) (SQLite FTS5 + (server.py) (Claude Code, Model2Vec, ChatGPT, ...) rebuildable cache)
Markdown files in<data-path>/entries/are the only source of truth. The SQLite index (search_backend.py) is a disposable cache built from them — BM25 + Model2Vec embeddings fused via Reciprocal Rank Fusion, plus thekb://relation graph — and can always be regenerated withrebuild.server.pyexposes that index to an agent as MCP tools (remember/recall/search/...); the dashboard is an alternate entry point at the same layer, hitting the sameKnowledgeBase/index directly over REST instead of MCP, soremember/deletebehave identically whether called by an agent or edited by hand in the browser.
- src/server.py— MCP tool definitions (remember,recall,search,list,tags,forget,rebuild,doctor), one process, stdio/SSE/streamable-http transport
- src/database.py—KnowledgeBase: Markdown + YAML frontmatter CRUD, UUID assignment, duplicate detection, bi-temporal supersede logic
- src/schema.json+src/schema.py— the entry taxonomy and its per-type rules as data, plus the loader that turns them into theentry_typeenum the MCP client validates against
- src/doctor.py— the schema-driven integrity pass shared by thedoctortool,rebuild's warnings, andremember's conformance check
- src/search_backend.py—SQLiteBackend: BM25 (FTS5) fused with Model2Vec cosine similarity via Reciprocal Rank Fusion, pluskb://relation extraction/graph traversal; embeddings are computed lazily on write and stored as a BLOB column
- src/dashboard/— a second, optional process (app.pyFastAPI REST +/api/graph,static/index.htmlvanilla-JS canvas graph,__main__.pyits own uvicorn entry point) reusing the sameKnowledgeBase
- plugins/inkwell-hooks/— a self-contained plugin installable in both Claude Code and Codex (.claude-plugin/plugin.json,SessionStart/Stop/SessionEnd+ aPreToolUsesearch-before-remember gate, plus a Claude-Code-onlyinkwell-project-onboarderagent) that mechanically enforces the search-first, remember-after workflow instead of relying on a system prompt alone; listed asinkwell-hooksin both the repo-root.claude-plugin/marketplace.json(Claude Code) andplugins/marketplace.json(Codex)
In Docker, the MCP server and the dashboard run as two processes in one container (docker-entrypoint.sh), sharing the same/knowledgevolume; the container exits if either process dies.
Your agent manages the server. Recommended for Claude Code, ChatGPT Desktop, Cursor.
claude mcp add --transport stdio inkwell -- \ docker run -i --rm -v ./knowledge:/knowledge foreigndmitryi/inkwell-memory
Persistent server on the network. Share knowledge across multiple agents.
docker run -d --name inkwell \ -p 8192 \ -v ./knowledge:/knowledge \ foreigndmitryi/inkwell-memory --transport sse docker port inkwell 8192 # host port Docker assigned claude mcp add --transport sse inkwell http://your-host:<port>/sse
-p 8192(host port omitted) has Docker pick a free ephemeral host port instead of failing when a fixed port like 8192 is already taken by another container — check the actual assignment withdocker port. Use-p 8192:8192instead if you need the host port to stay fixed.
docker run -d --name inkwell \ -p 8192 \ -v ./knowledge:/knowledge \ foreigndmitryi/inkwell-memory --transport streamable-http docker port inkwell 8192 # host port Docker assigned claude mcp add --transport http inkwell http://your-host:<port>/mcp
One server, several teams, each isolated to its own data folder and API key. SetINKWELL_MULTI_TENANT=1(requiresINKWELL_PUBLIC_URLandINKWELL_ADMIN_API_KEY, and a network transport — never stdio);src/app.pythen serves MCP, the admin UI, and the dashboard from one process. Provision a team withinkwell add-team <name>(run viadocker exec, seesrc/cli.py) to get back its API key, then point each team's agent at/mcpwith that key as a bearer token:
claude mcp add --transport http inkwell https://your-host/mcp \ --header "Authorization: Bearer <TEAM_API_KEY>"
Or as a rawmcpServersconfig (Claude Desktop and other clients):
{ "mcpServers": { "inkwell": { "type": "http", "url": "https://your-host/mcp", "headers": { "Authorization": "Bearer <TEAM_API_KEY>" } } } }
TeamTokenVerifier(src/server.py) hashes the token and looks it up inadmin.dbon every request — no caching, so a revoked key stops working immediately.
Hybrid: SQLite FTS5 (Porter stemming, BM25) fused with cosine similarity over localModel2Vecembeddings (minishlab/potion-multilingual-128M, no cloud dependency) via Reciprocal Rank Fusion — so a query that shares no literal words with an entry can still find it by meaning. Falls back to keyword-only if the embedding model can't load. The index is still a single SQLite file you can query with standard SQL tools.
The entry taxonomy and its per-type rules live inschema.json, not in Python: which frontmatter fields a type requires, which template body fields it should carry, whether search may boost it by usage, and whetherrecalldigests its back-links.entry_typeis exposed to the MCP client as an enum built from that schema, so an undeclared type is rejected before the call reaches the server.
Resolution order, first found wins:<data-path>/schema.json, then the packagedsrc/schema.json. A user filereplacesthe packaged one outright — the two are never merged, so copy the default and edit it. The schema is read once at startup, so editing it requires restarting the server. Entries written by hand can still carry an undeclared type;doctorreports those.
A web UI for visually exploring and hand-editing the same knowledge base the MCP tools use — no protocol duplication, every action goes throughKnowledgeBase.
- Force-directed graphof all entries and theirkb://relations — click a node to inspect it, click a legend chip to filter by type (matches stay lit, others dim)
- Hybrid searchover the same BM25 + semantic index as thesearchMCP tool, with tag andentry_typefilters
- CRUD panelto create, edit, supersede, or delete entries without touching Markdown files by hand — outgoing/incoming graph relations shown alongside the fields
- Dark, dense, no-chrome interface — a maintenance tool, not a consumer app (seePRODUCT.md/DESIGN.md)
Disabled by default (the container only runs the MCP server). Enable it as a second process in the same container:
docker run -d --name inkwell \ -e INKWELL_ENABLE_DASHBOARD=1 \ -p 8192 -p 8193 \ -v ./knowledge:/knowledge \ foreigndmitryi/inkwell-memory --transport sse docker port inkwell 8193 # dashboard host port
Or run it standalone locally:python -m dashboard(seeConfigurationforINKWELL_DASHBOARD_HOST/INKWELL_DASHBOARD_PORT).
Link entries withkb://uuid#typeURLs in Markdown content:
This service runs on Saturn and depends on PostgreSQL.
recallreturns 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"}] } }
LikeHATEOASfor knowledge — every response carries the links to navigate the graph.
"Remember that our API runs on port 8080 and depends on PostgreSQL 15."
Inkwell 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?"
Inkwell searches across all entries by content, title, and tags. Results are ranked by relevance.
If entries link to the PostgreSQL article withkb://uuid#depends-on, Inkwell returns all backlinks — showing every service that depends on it, without the agent having to search for each one.
Start Inkwell 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 Inkwell as a reflex, not an afterthought:
Inkwell is your persistent memory. Using it is mandatory, not optional. Inkwell stores ZERO discoverable information. If you can derive it from code, git history, configuration files, or existing documentation, it does not belong in Inkwell. Inkwell 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 Inkwell 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 Inkwell. 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.
A system prompt is easy to forget mid-session.plugins/inkwell-hooks/ships a self-contained plugin (SessionStart,Stop,SessionEnd,PreToolUsehandlers, plus a Claude-Code-onlyinkwell-project-onboarderagent) that mechanically nudges the agent to search Inkwell before starting work and reminds it toremembernon-trivial changes before finishing, instead of relying on it recalling this section unprompted. Works in both Claude Code and Codex — install with/plugin marketplace add <this-repo>then/plugin install inkwell-hooks@inkwell-memory(Claude Code), orcodex plugin marketplace add <this-repo>/pluginsthencodex plugin add inkwell-hooks@inkwell-memory(Codex) — seeplugins/inkwell-hooks/README.mdfor what each hook does and the full install flow.
Disable Claude Code's built-in auto memory
Claude Code ships its own automatic memory (MEMORY.mdunder~/.claude/projects/<project>/memory/, loaded every session). Running it alongside Inkwell means two systems writing overlapping notes and competing for the agent's attention, which gets in the way more than it helps. Turn it off insettings.json:
{ "autoMemoryEnabled": false }
Or via environment variable (takes precedence over the setting and the/memorytoggle):CLAUDE_CODE_DISABLE_AUTO_MEMORY=1. SeeClaude Code's memory docsfor details.
All options haveINKWELL_*environment variable fallbacks. CLI args take priority.
Entries are Markdown files with YAML frontmatter in<data-path>/entries/:
--- id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 title: Entry Title tags: [infrastructure, postgresql] type: decision resource: /path/to/relevant/file --- Markdown content here...
typeis required on everyremembercall (a dedicated field, not part oftags) — it classifies the entry (hub,decision,diagnostic,procedure,preference,snippet, ...) and is filterable viasearch/list.resourceis optional: a canonical file/folder path the entry describes. Legacy entries written before these fields existed still read fine.
The search index is a rebuildable cache at<data-path>/index/inkwell.db. Delete it andrebuild— no data is ever lost.rebuildalso reports schema-conformance warnings (missingtype, malformedresource) across existing entries.
# Build docker build -t inkwell . # Test (separate Dockerfile — pytest/tests/ never ship in the production image) docker build -f tests/Dockerfile -t inkwell-test . docker run --rm inkwell-test # Run locally (SSE) docker run -d --name inkwell -p 8192 -v ./knowledge:/knowledge inkwell --transport sse
mem0-mcp-server — exposes Mem0 persistent semantic memory as an MCP HTTP server; supports add/search/read/update/delete operations and semantic search for agent memory.
Long-term memory system for AI agents with semantic search, context management, and multi-format storage.
A self-hosted, secure, feature-rich memory system for AI agents and assistants. Provides intelligent fact extraction and deduplication, with an artifact store for detailed content.
Persistent memory for AI agents with Ebbinghaus forgetting curve decay, hybrid BM25 + vector + knowledge graph retrieval, temporal reasoning, and a local dashboard. 89.4% Recall@5 on LongMemEval.
Persistent cognitive memory for Claude Code. Cloud-based semantic search, Ai-powered extraction, project scoping, and compaction recovery.
Persistent memory layer for AI agents with semantic search, consolidation, and cross-session intelligence via MCP.
Local-first agent memory: a plain-Markdown Obsidian vault is the source of truth, with a rebuildable DuckDB index for hybrid BM25 + vector + graph recall.
Local Work Model for AI agents that learns from real outcomes.
Adaptive MCP memory system for AI applications. Learns which retrieval strategies work for your data, scores results using cognitive science models, builds a knowledge graph automatically, and validates every parameter change against real query history before adopting it. Patent pending.
Auditable, self-improving knowledge & memory for AI agents over MCP — citation-enforced answers and a replayable why-trace, self-hosted on Postgres.
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.





