mcp server
agent-memory-supabase
Long-term memory for AI agents on plain Supabase Postgres. Hybrid vector + full-text + entity recall with RRF, temporal validity, write-time dedup. You own the SQL and the rows.
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- Node.js 20 or newer
{
"mcpServers": {
"agent-memory-supabase": {
"command": "npx",
"args": [
"-y",
"agent-memory-supabase"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 25, 2026Agent Memory on Supabase
Long-term memory for AI agents, built on plain Postgres. Vector search and full-text search fused with Reciprocal Rank Fusion, entity-grounded recall, temporal validity, and trigram-based dedup. No new database to run, no vendor lock-in, no opaque service between your agent and its memories. It is a table, a few functions, and a small client. You own the SQL and the rows.
import { MemoryStore } from "agent-memory-supabase";
const mem = new MemoryStore({
supabaseUrl: process.env.SUPABASE_URL,
supabaseKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
openaiKey: process.env.OPENAI_API_KEY,
});
await mem.store({ content: "User prefers figures in GBP.", memoryType: "preference" });
const hits = await mem.recall("what currency should I use?");
// [{ content: "User prefers figures in GBP.", memory_type: "preference", similarity: 0.83, ... }]
Why I built this
I didn't set out to build a memory product primarily. When Anthropic banned OpenClaw usage through subscriptions I decided to just build my own agent/harness/OS and build it a proper database memory to see if I could create something better than what Claw/Hermes ship with. I created a fresh version of the memory system in isolation and have open sourced it. That is this repo :)
Ok here's more detail on the memory (written by AI so forgive the tone):
I built the memory layer as Postgres you can read. It runs on Supabase
because Supabase is just Postgres with pgvector, pg_trgm, and row-level
security already in the box, which is exactly what a real memory system needs
and exactly what the hosted tools hide from you. After months of it running a
real life in production, 22,000+ memories deep, I extracted the core into this
template. The scars in the comments (why updated_at must not move on reads,
why one function overload silently broke dedup for months) are real, and they
are why the design is shaped the way it is.
What this does that other memory systems don't
Agent memory today mostly comes in two shapes. Frameworks like OpenClaw and
Hermes ship files-in-context: curated markdown (a MEMORY.md, daily
notes) loaded into the prompt, plus keyword search over past sessions.
Readable and simple, and I ran that way for a while, but it is flat: no
ranking, no dedup, no history, and it stops scaling the moment your memories
outgrow what fits in a prompt. On the other side are hosted memory
services (Mem0, Zep, Letta): real retrieval machinery, but running as a
black box between your agent and its memories.
This repo is a third shape: the retrieval machinery of the hosted services, implemented as plain Postgres you own and can read.
- Retrieval is a SQL function, not a service. Every ranking decision lives
in one readable PL/pgSQL function you can open,
EXPLAIN, profile, and tune. When recall goes wrong, it is a debugging session, not a support ticket. - Three recall lanes, not one. Pure vector search misses exact names, IDs, and rare keywords; pure keyword search misses paraphrase. This fuses vector, full-text, and a third entity lane that injects memories about people, projects, and tools named in the query even when neither of the other lanes finds them. Identity facts stop falling through the cracks.
- Facts have a timeline, not just an embedding. A
superseded_bychain plusvalid_from/valid_untilwindows means facts evolve without deletion, andmemory_history()replays how one changed over time. Most systems either overwrite (history lost) or append (contradictions pile up). - Dedup happens at write time, twice. A ≥95% semantic near-duplicate of the
same type updates in place instead of accumulating. A separate
pg_trgmpass catches "same template, changed values" snapshots that slip under the embedding threshold. Your store stays clean without a compaction job. - Memory types with teeth. Nine types (fact, decision, preference,
correction, pattern, learning, event, conversation, transcript), and the
type changes behaviour: corrections and preferences never decay in ranking,
transcripts stay out of keyword search, dedup never collapses a
decisioninto afact. - Honest scores. RRF plus small capped recency / importance / usage
factors decide the order, but the
similarityreturned to you is untouched cosine, so it stays meaningful as a threshold. Flipuse_blendedoff for a clean RRF baseline when you A/B retrieval. - Zero new infrastructure. If you have a Supabase project, you already run everything this needs. One schema file, RLS templates included, no hosted dependency beyond an embedding provider.
How it works
flowchart TD
subgraph WRITE["store()"]
W1["content + type + tags"] --> W2["embed<br/>(text-embedding-3-small)"]
W2 --> W3{"semantic near-dup?<br/>≥95%, same type"}
W3 -- yes --> W4["update existing row"]
W3 -- no --> W5{"trigram snapshot dup?<br/>(pg_trgm)"}
W5 -- yes --> W4
W5 -- no --> W6["insert new row"]
W6 -.-> W7["entity extraction<br/>(async, fails open)"]
end
W4 --> DB
W6 --> DB
W7 -.-> DB
DB[("memories table<br/>pgvector HNSW · tsvector GIN · pg_trgm")]
subgraph READ["recall()"]
R1["query"] --> R2["embed query"]
R2 --> R3["search_memories()"]
R3 --> L1["vector lane<br/>HNSW cosine, top 50"]
R3 --> L2["keyword lane<br/>full-text, OR-combined"]
R3 --> L3["entity lane<br/>named-entity injection"]
L1 --> F["Reciprocal Rank Fusion<br/>1 / (k + rank), k = 60"]
L2 --> F
L3 --> F
F --> B["capped blend:<br/>recency · importance · usage"]
B --> OUT["ranked memories<br/>(pure cosine similarity attached)"]
end
DB --> L1
DB --> L2
DB --> L3
One table holds everything. Writes go through dedup before they land; reads fan out across three candidate lanes and get fused back into one ranked list. The full ranking walkthrough is in How recall works.
Use it with Claude
The repo ships an MCP server, so Claude Code (or any MCP client) can use the
memory directly through five tools: memory_recall, memory_store,
memory_timeline, memory_history and memory_forget. The server also tells
the model when to recall and when to store, so there is no prompt to write.
The one-prompt install. Connect the official Supabase MCP to Claude Code
(claude mcp add --transport http supabase https://mcp.supabase.com/mcp, then
/mcp to log in), and say:
Give yourself long-term memory: follow AGENT_SETUP.md in https://github.com/reescalder/agent-memory-supabase
Claude clones the repo, applies the schema to your project, registers the
memory server and tells you to restart. AGENT_SETUP.md is
the runbook it follows, and it is short enough to read yourself first.
By hand. Do the Install steps below (the server reads the same
.env), then:
claude mcp add memory --scope user -- node /absolute/path/to/agent-memory-supabase/src/mcp-server.js
Env vars passed with claude mcp add -e KEY=value override .env. Optional:
MEMORY_PROJECT sets a default namespace for new memories, and
MEMORY_EXTRACT_ENTITIES=false skips the entity-extraction call.
Install
Prerequisites: Node 22+, a Supabase project (free tier is fine), and an
OpenAI API key. Embeddings use text-embedding-3-small; entity extraction uses
gpt-4o-mini. Running the whole quickstart costs well under $0.01.
git clone https://github.com/reescalder/agent-memory-supabase.git
cd agent-memory-supabase
npm install
(Not on npm yet. The clone is the install; src/client.js is a single
dependency-light file you can also just copy into your own project.)
Then, once:
- Run
sql/schema.sqlin the Supabase SQL editor. It is safe to re-run. It leaves the table RLS-enabled and server-only, so nobody holding your anon key can read or rewrite what your agent remembers. - Only if you need browser access, run
sql/rls.sqland pick a posture: per-user rows scoped toauth.uid(), or a deliberately public read-only corpus. cp .env.example .envand fill in your Supabase URL, service role key, and OpenAI key.
Heads up:
dotenvnever overrides variables already exported in your shell. If your terminal already has aSUPABASE_URLpointing at another project, that is where the quickstart will write. It prints the target host before touching anything, so check that line.
node examples/quickstart.js
How recall works
recall() embeds your query and calls the search_memories SQL function, which
runs three candidate lanes and fuses them:
- Vector lane: top 50 by cosine similarity over the HNSW index.
- Keyword lane: full-text matches on a generated
tsvector, OR-combined so natural-language queries match broadly, ranked by relevance then importance. - Entity lane: when the query names a known entity, that entity's top memories are injected even if neither of the first two lanes found them.
The three lanes are merged with Reciprocal Rank Fusion (1 / (k + rank),
k = 60), then nudged by small capped factors: recency (exponential decay,
floored so old-but-relevant memories lose at most 25%, and preferences /
corrections never decay), importance, and how often a memory has actually been
returned before. The similarity you get back is untouched cosine, so it stays
meaningful as a "how close is this" score; the fusion score is used only for
ordering. All of this is one readable PL/pgSQL function you can open and tune.
API
const mem = new MemoryStore({ supabaseUrl, supabaseKey, openaiKey });
// Store. Dedups against a >=95% same-type near-duplicate. Returns {action, memory}.
await mem.store({
content: "Decided to migrate memory retrieval to hybrid RRF.",
memoryType: "decision", // one of the nine types
project: "myapp", // optional namespace / tenant
tags: ["retrieval"], // optional
importance: 7, // optional; sensible per-type default otherwise
supersedes: "uuid-of-old-fact", // optional; closes the old fact's validity window
});
// Recall. Hybrid by default; returns rows with real cosine `similarity`.
await mem.recall("how do we rank memories?", {
project: "myapp",
matchCount: 10,
types: ["decision", "learning"], // optional type filter
namedEntities: ["Kern"], // optional entity grounding
});
// Recent entries, newest first.
await mem.timeline({ project: "myapp", limit: 20 });
// How a fact evolved over time (supersedes chain + validity windows).
await mem.history("retrieval");
// Forget: soft delete, the row stays for provenance but drops out of recall.
await mem.forget("uuid-of-memory");
Design choices worth knowing
- 1536-dim embeddings (OpenAI
text-embedding-3-small) by default. To use a different model, change thevector(1536)width inschema.sqland the model insrc/client.js; everything else is model-agnostic. - Service role, server side, RLS on from the first run. The client uses the
service_role key and is meant to run on a server;
schema.sqlenables RLS with no policies so everything else is locked out by default. Supabase hands the anon and authenticated roles SELECT/INSERT/UPDATE/DELETE on new public tables, and your anon key ships in the browser bundle, so "RLS later" means "anyone can rewrite my agent's memory today". For browser or multi-tenant use, add the posture you want fromsql/rls.sqlrather than turning RLS off. - Entity extraction is optional and non-blocking. It uses a cheap model and
fails open: a write never fails because entity extraction did. Pass
extractEntities: falseto the constructor to turn it off entirely. - Dedup is type-aware. A paraphrased
factcan collapse into an existingfact, but never overwrite adecision. This was a real bug once; the type filter is the fix.
Roadmap
- Optional cross-encoder reranker hook (the production system uses one; the template ships without it to stay dependency-light).
- Python client.
- Packaging as a Supabase Edge Function, so the MCP server can run remotely instead of on your machine.
License
MIT. Built by Rees Calder.