Inspiration
Two weeks ago we shipped a public, MIT-licensed memory server for AI agents,
qwen-memory-mcp. It scoped every memory to a single user id. Shipping it taught
us the bug we built this project to kill.
Picture the failure: someone shares a secret with an agent in a private message. Later, the same person asks a question in a public channel. A memory system that filters by user id matches — same user — and repeats the secret to the channel. Nothing crashed. The code did exactly what it was written to do. The design was wrong, because it modeled who is asking and never who is listening.
The usual mitigation is to instruct the model not to leak. That is asking a language model to be a permission system. It isn't one. If the model can see a secret, the secret can come out.
What it does
scoped-memory-mcp is a standalone Model Context Protocol server that gives any
agent — Codex, Claude Desktop, a custom bot — memory isolated by conversational
scope. It exposes four tools: memory_write, memory_recall, memory_forget,
and memory_scopes.
A scope is a path: workspace / channel / {thread | dm}. The rule is one line:
A memory is visible from scope S if and only if its scope is S or an ancestor of S.
Facts flow down — a workspace fact is visible everywhere below it — but never up and never sideways. A DM secret is structurally invisible from the channel, because the channel's visible set simply does not contain the DM's path. The memory is absent, not filtered.
How we built it — Codex + GPT-5.6
We built this end to end in Codex, running GPT-5.6. The design came first: the scope model is the very first commit in the repository, before any code.
Codex did more than scaffold. It changed the design twice, and both changes are now security tests:
- It caught that matching scope keys by text prefix would let one workspace leak into another whose name merely started the same way, and replaced it with exact-set membership matching.
- It flagged that an unencoded identifier could forge a path separator and cross a scope boundary, and added safe per-segment encoding.
The recall pipeline is four stages, and the order is the point:
- Scope resolver (deterministic, before the model) — caller path to visible set
- Retrieval — candidates within the visible set only
- Ranker (the model step) — relevance, conflict resolution, compression
- Leak assertion (deterministic, after the model) — every returned item must be in the visible set, or the request hard-fails
The model is sandwiched between two deterministic gates. It never receives an out-of-scope memory, so it cannot leak one — and if the resolver ever had a bug, the assertion catches it before the caller does.
The ranking model is a swappable adapter, not the permission system. Three implementations sit behind one interface: a deterministic offline ranker (keyless default), GPT-5.6 over the OpenAI Responses API, and Qwen over Alibaba Model Studio. All three produce identical scope guarantees. That is the proof that isolation does not depend on the model.
Accomplishments
- Isolation enforced by deterministic code on both sides of the model.
- 47 tests, all green with no API key — including an adversarial suite that forces a compromised ranker to return an out-of-scope item and asserts the leak assertion catches it.
- A live run resolving a real contradiction ("we moved reporting from Postgres to Mongo") — the model picks the newer fact, explains the conflict, and compresses the answer, while the DM secret stays invisible.
npm ciin under 4 seconds. SQLite plus sqlite-vec, no Docker, no database service, no cloud infrastructure. A judge runs it in one command.
Challenges
Build Week API credits were exhausted, and we had no funded OpenAI billing, so the GPT-5.6 adapter is implemented and contract-tested against the Responses API with a mocked transport but was not exercised against the live API. We exercised the Qwen adapter live instead. We are explicit about this in the repository rather than implying a live GPT-5.6 run. It also demonstrates the thesis: the isolation guarantee held identically on a completely different model.
What we learned
Audience must be a first-class dimension of a memory primitive, resolved before retrieval and asserted after the model — not a filter bolted onto search, and not a prompt. The old version asked who is speaking. This one asks who can hear.
What's next for John CEO
Adopt it inside our own testing/staging agent as an MCP consumer (no coupling back into the module), semantic embeddings when a key is present, and membership-aware scopes as an opt-in layer on top of the lattice.
Log in or sign up for Devpost to join the conversation.