DEV Community

Cover image for A memory server remembers your conversation. That is not the same as knowing your code.
constantine makoid
constantine makoid

Posted on Originally published at celmis-labs.github.io

A memory server remembers your conversation. That is not the same as knowing your code.

Before publishing: set published: true, and check canonical_url — the article must
exist at that URL on the site first. Without it this competes with the original in search
instead of pointing at it. Tags are from the verified top-1,000 list; mcp was not in that
cache and is not used here.

A session ends. Your agent had worked out, over forty minutes, that the retry logic lives in
one service and the thing that gives up on it lives in another, that the queue name is spelled
two different ways, and that the person to ask about any of it left last year. Tomorrow you
open a new session and it knows none of that. Neither does your colleague's session. Neither
does the agent reviewing the pull request that comes out of it.

It is the same forty minutes a new engineer spends in week one, and the same forty minutes the
README would have saved if it were still true. It is why a manager asking "where is this up to"
has to interrupt someone who knows. The knowledge exists; it has nowhere to live but in people
and chat logs.

The reflex is to reach for memory. That reflex is worth interrogating, because there are two
different problems hiding under one word, and only one of them is what memory servers are for.

What MCP actually specifies

It helps to be exact, because "MCP memory" gets said as though it were a feature of the
protocol. It is not — and the current revision makes that harder to miss rather than easier.

Read the base protocol's own three-line summary in revision 2026-07-28: JSON-RPC message
format, stateless, self-contained requests, per-request capability negotiation. Servers
offer three features — Resources, Prompts and Tools. Clients offer one: Elicitation. Sampling
and Roots, which used to make that three, were deprecated in this same revision under SEP-2577,
along with Logging and Dynamic Client Registration; the migration note against Sampling reads
"integrate directly with LLM provider APIs".

There is no memory primitive and no persistence primitive. There is no memory extension
either — the official list is the two authorization extensions, MCP Apps, Skills over MCP, and
Tasks. Tasks is the one worth pre-empting, because it advertises "durable handles" and that
sounds adjacent: a task ID survives a disconnect so a client can resume polling a long-running
call, it carries a TTL, and what it holds is the status and eventual result of that one call.
It is durable in the sense a job ID is durable. It is not somewhere knowledge goes to live.

So every memory you have seen over MCP is a server implementing memory with ordinary tools.
Anthropic's own Memory MCP server keeps a knowledge graph in a JSONL file; other community
servers do the same job over different storage. Claude Code, separately from MCP altogether,
ships Auto Memory — a MEMORY.md per project, on by default since 2.1.59.

This is a compliment to those projects, not a criticism. They took a gap the protocol
deliberately left open and filled it with plain tools — exactly what the tool primitive is for.

What they remember, and why that is the right thing for them to remember

A memory server stores what was said. Your preferences. The decision you made on Tuesday and
the reason you gave. The fact that you want British spelling and no bullet lists. Entities,
relations and observations, accumulated from conversation.

Two properties of that content matter here.

It is personal. My conversational memory is a bad thing to hand to you. It contains my
half-formed conclusions, my shortcuts, and things I said and then revised. Sharing it is not
an unimplemented feature; it is a category error. The value is that it is mine.

It is stated. It is true because someone asserted it. Nothing in the repository can
contradict it, because it was never derived from the repository in the first place.

For remembering a conversation, both properties are correct. Now hold them against the other
problem.

Knowing the code has the opposite shape

"Where is this symbol used, in every repository, with file and line" is not personal. There is
exactly one right answer and everybody who has read access to those repositories is entitled
to the same one. Storing it per person means storing the same fact many times and being wrong
in a different way in each copy.

It is not stated, either. It is derived — a function of the current commit, and therefore
perishable in a way conversational memory is not. If I told you on Tuesday that I prefer
tabs, that is still true on Friday. If an agent noted on Tuesday that apply_refund had three
callers, that is a claim about a commit, and a merge on Wednesday can make it false without
anyone touching the note. A memory store cannot know that happened, because nothing said it.

That is the failure mode worth naming, and it is the same one that makes stale documentation
worse than none. A remembered fact about code does not decay into silence. It decays into a
confident wrong answer, indistinguishable in tone from a right one.

So: conversation is personal, stated and durable. Code knowledge is shared, derived and
perishable. Same word, opposite requirements. The fix for the second is not a better memory —
it is not memory at all. It is an index that is rebuilt from the code, that many callers read,
and that has a stated relationship to a commit.

One implementation, which you can read

I build Celmis and it is mine, so treat the description as a
worked example rather than a recommendation. It is a self-hosted platform for most of a
development cycle — the alert that arrives, the fix that goes out, the dependency and SBOM
evidence underneath — and what this article is about is one layer of it: the index. It is
AGPL-3.0, with a carve-out for ee/ that today holds no product code, and every claim below
names the file, so you can disagree with me by reading it.

Here is the wider loop, so the layer this article is about has something to be a layer
of. Thirty-four seconds, one real incident, every frame a screenshot of it:

It builds a tree-sitter symbol graph per repository and serves that same graph over MCP. Eight
languages have hand-written extractors — TypeScript, Vue, Python, Go, PHP, Java, C#, C++ — and
sixteen more (Ruby, Rust, Kotlin, Swift, Scala, Elixir, Dart, Lua, R, Solidity, OCaml, F#, Elm,
Gleam, Racket, Fortran) come from the grammar authors' own tags queries, registered at the
lowest priority so a real extractor always wins. Dockerfiles, Compose files, Helm charts and CI
workflows are matched by filename or path rather than suffix, and Kubernetes manifests by
sniffing the first few kilobytes for apiVersion and kind. Terraform is the ordinary case —
.tf.

The HTTP mount registers 23 tools, eighteen of which read and five of which write. A sample of
what the read half returns:

  • search_symbols — name, kind, file, line, signature and repo slug, for definitions matching a name across every repository in a project.
  • find_consumers — who calls a symbol, with repo, file and line. The list of what breaks.
  • get_api_surface — functions whose names look like HTTP handlers, with a route path guessed from the name by turning underscores into slashes, plus method, file and line. A name-convention heuristic over the symbol index and nothing more; it does not read route decorators, and the source says so.
  • get_architecture — the cached orientation summary for a repository, with the model that produced it and the timestamp, so you can see how old it is.
  • get_owner — top git-blame authors plus matched CODEOWNERS entries for a path. This is the one that answers "who do I ask" without asking.
  • list_accessible_repos and get_my_access — the agent asking what it is allowed to look at, and being told which path globs are denied.
  • start_integration_walk — an ordered checklist of the other tool calls, returned as data, for a client that would otherwise fire ten guesses.

Three things about that list are load-bearing.

It is the same index, not a copy. The MCP tools open settings.repo_graph_path(slug). So
does the retrieval layer answering a human's question in the web UI. So does the pull-request
reviewer. One graph file per repository, three readers. Celmis does hold its own clone and index
it — one copy, on your infrastructure, not one per agent or per session.

It is the same access rules, with one exception I will name. Every tool that returns code,
ownership or review content calls caller_access, which calls resolve_access from
src/access — the identical function behind the human REST endpoint and behind multi-repository
question answering. A repository you may not research is omitted and named in blocked_repos;
a denied path is filtered out of the matches, and search_symbols also reports how many it hid,
in hidden_symbol_count, though not every tool yet returns that count. The exception:
list_deprecations reads its table today without a workspace or access predicate. That is a
bug on my side, not a design, and it is being fixed. While I am being exact: the scope filter on
tools/list is listing hygiene, not authorisation — a scoped read client is not shown the
write tools, but the HTTP mount carries no per-call scope check, and a token with no scopes sees
everything. Writes are gated on the token resolving to a workspace, not on its scopes. Do not
treat a read scope as a boundary.

It has a stated relationship to a commit. A daily sweep runs git ls-remote — one network
round trip, no clone, no fetch — and compares the branch head with last_indexed_sha. It
reports three outcomes, not two: up to date, behind, and could not tell, because a check that
cannot reach the remote and renders as "no new changes" is worse than no check. Behind enqueues
an incremental pass that diffs last_sha..HEAD, drops the symbols for every touched file and
re-extracts them, rather than re-parsing the whole repository because one file moved.

The stdio server carries a raw-Cypher escape hatch too: it tokenises the query and rejects any
of eleven write keywords before running it — a denylist, not a parser, as its own docstring says.
It is not one of the HTTP mount's 23.

What this does not do

Celmis's MCP server has no memory. Zero hits for memory, persist, recall or remember across
src/mcp_server/. It will not remember that you decided to deprecate the old endpoint, or why.
That is conversational, it is yours, and a memory server is the right tool for it — run one
alongside, and let it keep the decisions while the index keeps the code.

The honest claim is narrower than "your agent finally has long-term memory", and better. The
next engineer's agent starts out knowing what yours knew about the code, because that knowledge
was never in your chat log to begin with. It is in an index, derived from the commit, readable
by every session under the same rules, and rebuilt when the commit moves.


I build Celmis — self-hosted, AGPL-3.0 (ee/ carve-out, empty
today), docker compose. Its pull-request reviewer placed 17th of 50 on the Martian Code Review
Bench offline set — seventeenth under all three judges, F1 between 42.7% and 47.5% depending on
who is judging. That is a deliberately unflattering number about one surface of the product and
it stays. The cross-repository capability described above contributed nothing to it: the
benchmark set is isolated single-repository pull requests and the graph came back empty on all
50, so nothing in that table is evidence for or against this path. Check it the way I would want
it checked — register two services, put them in one project, call find_consumers with a symbol
you know one calls in the other, and see whether the repo, file and line are right.

Top comments (0)