Shared memory for AI agents, as a graph in your own Postgres. Writes never call an LLM.
Copy the AI prompt to install this server into Claude Code, Cursor, or another agent β or use 1-click editor setup below.
One-click editor setup isnβt available for this listing yet β we donβt have a confirmed install command, and weβd rather show nothing than point your editor at the wrong package or host. Follow the projectβs own setup instructions, linked above.
Shared memory for AI agents, as a graph in your own database. What Claude Code learns, Cursor and Codex can recall. Every fact records who wrote it and when, and the server never calls a model to store one.
Your agents start every session from zero. The usual fix is a notes file you paste into context, which grows until it is mostly irrelevant to whatever you are asking. Echo Memory is the other shape: facts connected to each other, and a query that returns the few that matter. On the author's own store that is 96.7% less context for the same answer, with the answer still present 87.2% of the time across 1,190 questions.
Requires: Python 3.11+ and Docker (for the database).
quickstart starts the database, applies the schema, and prints the claude mcp add
line that registers it, filled in with the port it actually used. The Postgres image is
published, so nothing compiles.
Or use the hosted service and run no database at all:
Then once per machine, so an agent knows when to record and recall rather than only that the tools exist:
Restart your client afterwards. An MCP server is a long lived process that holds the code and config it started with, and an editable install does not change that.
The PyPI name is
echo-mem, notecho-memory. That name belongs to an unrelated hosted product. The import package and the CLI are bothecho_memory/echo-memory; only the distribution name differs.
Both steps, in that order, and neither is optional on a store that predates the version you just installed.
Skipping init-db fails loudly. A read against a schema older than the code raises an
undefined column, which is the good case: it stops rather than answering from something it
half understands.
Skipping reindex fails silently, which is worse. Derived data that the new code reads
differently is still the old data, and nothing errors. Concretely, on an upgrade to 0.5.3
the lexical corpus statistics are treated as stale until rebuilt, so BM25 stays off and
your queries keep ranking by ts_rank while the setting reports as on. reindex rebuilds
the embeddings and those statistics together. echo-memory health tells you afterwards.
| Tool | What it does |
|---|---|
write_episode | Store entities and the facts connecting them. No model call. |
query_memory | Hybrid vector and full text retrieval, fused by reciprocal rank. Full text ranks by BM25. Takes as_of to read the store as it stood at an instant. |
trace_cause | Causal chains through the entities a subject matches, not a ranked list. |
record_recall_save | Mark that a recalled fact saved re explaining something. Refuses a fact no read returned. |
get_audit_log | Every change to memory, with a plain language reason. |
pending_documents | Memory files this project wrote that the graph has not heard about. |
mark_ingested | Close one of those out. |
A graph, not a list. Entities are nodes and a fact is an edge between two of them. Two sessions that never knew about each other resolve onto the same entity by name, so the second inherits what the first learned.
Bounded retrieval, designed but not built. The plan is that old, rarely read memory
demotes into higher level summaries over time, with nothing discarded and every summary
still edged back to the facts it came from. None of it exists yet: there is no tiering,
no summarisation, and retrieval today walks every active fact in the scope. It is
described in docs/designs/ and listed below as v1c, and this paragraph
used to claim it in the present tense.
A ranked answer that says why each fact is in it. Every returned fact carries
score, its cosine similarity to the query, rank, its position, and matched, the
channels that found it. score is deliberately not the fusion number: reciprocal rank
values are sums of 1/(k+rank) and mean nothing from one query to the next, while a
similarity means the same thing every time, which is what a caller thresholding on it
needs. It is computed for every fact returned, including the ones only full text search
found, because which channel retrieved a fact is an implementation detail and has no
business reaching a field callers read as relevance.
The full text channel ranks by BM25. Postgres's ts_rank counts term occurrences and
nothing else: no inverse document frequency, so a word in every fact of a scope counts as
much as one in three; no saturation, so repetition scales linearly; no length
normalisation, so a long fact is punished for being long. On 324 real prompts the rank 3
and rank 4 scores were identical in 59% of them, and reciprocal rank fusion reads rank
position and never the score underneath it, so a channel ordered by scan order handed the
fusion a coin flip.
BM25 is computed in SQL rather than installed, so no extension is added to your database.
Corpus statistics are rebuilt amortised on write inside the transaction that already holds
the scope's lock, and by echo-memory reindex; a scope that has none falls back to
ts_rank rather than ranking on statistics it does not have. ECHO_MEMORY_LEXICAL_BM25=0
returns to ts_rank everywhere.
The history the store has always kept is readable. Every fact has carried t_valid
and t_invalid since the first migration, and the read path had only ever asked whether a
fact is current, so the store paid to keep the whole record of what a scope believed and
could not answer the first question a post mortem asks. query_memory takes as_of in
unix seconds and every channel honours it: vector, full text, the graph hop, the digest.
Supersession is what makes that more than a curiosity. Writing the same source, target and
relation again does not edit the old fact, it ends it: the old edge takes a t_invalid and
stays queryable. Nothing is ever rewritten, so the history is real rather than
reconstructed, which is the property the memory reconsolidation literature gives up and
the reason this project refuses that idea.
Provenance on every fact. Who wrote it, which tool, which project, when, and which reads returned it. A superseded fact is never deleted. It stops being drawn and stays reachable with its history.
Causal typing, and a walk over it. A fact can carry causal_hint: one of
caused_by, led_to, enabled_by, blocked_by or contradicts, set by the agent's
own read of what the session said and never inferred statistically. trace_cause walks
those links and returns chains rather than a ranked list, because the answer to "why did
this happen" is an ordered chain and similarity cannot produce one. "A led_to B" and "B
caused_by A" are one claim written from opposite ends, and both assemble into the same
chain.
A fact with no hint is associative, which is the default and usually correct. An empty
answer from trace_cause says which kind of empty it is: nobody recorded a cause here is
a different fact about a store than there are no chains.
Filling it in, for a store that predates it. Facts written before 0.5.0 carry no
hint, so on an existing store trace_cause has nothing to walk.
echo-memory infer-causal-hints re-reads the fact text already stored with your own
model (ECHO_MEMORY_LLM_API_KEY, ECHO_MEMORY_LLM_MODEL) and types the edges whose own
sentence states the relation. Dry run by default; --write applies what it printed and
--clear --write takes it back.
No reviews yet β be the first to share how this listing worked for you.
Showcase your server listing on GitHub or your project documentation. Embed this dynamic SVG badge to highlight official listing status and live engagement.
[](https://allmcps.com/mcp/echo-memory-2)<a href="https://allmcps.com/mcp/echo-memory-2"><img src="https://allmcps.com/api/badge/echo-memory-2?style=directory" alt="Echo Memory on AllMCPs" /></a>