Agent Shared Memory
AI
Several agents working the same problem each keep their findings in their own context window, and none of them can see the others'. This template gives them one place to write notes down and read each other's back — a Postgres table behind four MCP tools, where every note is stamped with the agent that wrote it.
What you get
Four tools and one resource template, over a single table you own.
- **
memory.remember** — write or replace one note, addressed bynamespaceandkey. Writing the same key twice replaces the value; there is no duplicate to reconcile later. - **
memory.recall** — search one namespace for notes whose body or tags contain some text. Newest first, capped at 50 results. - **
memory.get** — fetch one note when the agent already knows the key. - **
memory.forget** — delete one note. Markeddestructiveso a client can ask before it happens. - **
memory://{namespace}/{key}** — the same notes as MCP resources, so a client that supports resources can attach a note to a conversation without spending a tool call. The namespace is browsable throughresources/list.
Two things the tools do that are easy to get wrong by hand:
- The author is the caller, not an argument.
memory.rememberfills theauthorcolumn from the gateway's verified principal. An agent cannot write a note under another agent's name, even if it tries. - Namespaces are a convention, not a wall. Any caller that reaches this gateway can read any namespace. If two teams must not see each other's notes, run two gateways, or add a policy plugin — do not rely on the namespace string.
What you need
- A PostgreSQL database the gateway can reach, and a role that can read and write one table. You will fill in four fields about it: PostgreSQL host and port, Database name, Database user, and Database password.
- An OpenID Connect provider your agents already authenticate against. You will fill in Identity provider issuer, Token audience, and JWKS endpoint. The template refuses anonymous callers, because a shared memory with no attribution is a shared memory nobody can audit.
- The table itself. Create it before first boot:
CREATE TABLE agent_memory (
namespace text NOT NULL,
key text NOT NULL,
value text NOT NULL,
tags text NOT NULL DEFAULT '',
author text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (namespace, key)
);
CREATE INDEX agent_memory_recall
ON agent_memory (namespace, updated_at DESC);Grant the role exactly what the tools use, and nothing else:
GRANT SELECT, INSERT, UPDATE, DELETE ON agent_memory TO mcpg_memory;How it works
Every tool is one dev.mcpg.backend.sql binding running one statement. There is no ORM, no migration runner and no schema inference — the SQL in the config is the SQL that runs, and you can read all of it in one screen.
Values reach the statement as bound parameters (:namespace, :key, …), never by string interpolation, so a note whose body contains '; DROP TABLE is stored as text and nothing else. memory.recall builds its ILIKE pattern in SQL — '%' || :query || '%' — for the same reason.
The author column is filled by a param_exprs entry that evaluates context.principal_id: a CEL expression over the request's identity, computed by the gateway after the token is verified. The caller has no way to reach it.
The resource template carries a list_query alongside its read query. The read query returns MCP resources/read contents directly (row_mode: resource_contents), so the gateway does not have to guess how to wrap the row.
The first five minutes after it boots
curl http://localhost:8787/health— the gateway answers before it accepts any MCP traffic.- Connect a client and call
tools/list. Five entries should appear. If the list is empty, the caller did not reachverifiedtrust: check the audience in your token against Token audience. - Call
memory.rememberwithnamespace: "smoke",key: "hello",value: "it works". You should get{"rows_affected": 1}. - Call
memory.recallwithnamespace: "smoke",query: "works". The note comes back with your own subject inauthor— that is the attribution working. - Call
memory.forgeton the same key to clean up.
Notes
- This template is self-host only, deliberately. The SQL backend opens its connection pool and health-checks it while the binding registers, which happens before per-call credential resolution. A managed-cloud render would hand the driver a connection string that still carried an unresolved token, and the gateway would fail to boot. Run it yourself, and export
PG_PASSWORDbefore starting the gateway. - **
dev.mcpg.backend.sqlis not baked into the published gateway images.** The config pulls it fromghcr.io/mcpg-dev/plugins/backend-sqlat boot, so the gateway host needs registry access on first start, or a mirror configured throughplugin_registry.default_registry. - **
recallis a substring search, not a ranked one.** It is honest about what it is: good enough for tens of thousands of notes, and the wrong tool for millions. When you outgrow it, add atsvectorcolumn and change one statement — the tool contract does not have to move. - Notes are not evicted. Nothing here expires old rows. Add a scheduled
DELETEonupdated_atif the table should stay small.