MCPG Config
All templates

Agent Shared Memory

beta

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.

Three agents writing and reading notes through one gateway into Postgres
Every note carries the verified caller as its author.

What you get

Four tools and one resource template, over a single table you own.

  • **memory.remember** — write or replace one note, addressed by namespace and key. 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. Marked destructive so 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 through resources/list.

Two things the tools do that are easy to get wrong by hand:

  • The author is the caller, not an argument. memory.remember fills the author column 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:
sql
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:

sql
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

  1. curl http://localhost:8787/health — the gateway answers before it accepts any MCP traffic.
  2. Connect a client and call tools/list. Five entries should appear. If the list is empty, the caller did not reach verified trust: check the audience in your token against Token audience.
  3. Call memory.remember with namespace: "smoke", key: "hello", value: "it works". You should get {"rows_affected": 1}.
  4. Call memory.recall with namespace: "smoke", query: "works". The note comes back with your own subject in author — that is the attribution working.
  5. Call memory.forget on 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_PASSWORD before starting the gateway.
  • **dev.mcpg.backend.sql is not baked into the published gateway images.** The config pulls it from ghcr.io/mcpg-dev/plugins/backend-sql at boot, so the gateway host needs registry access on first start, or a mirror configured through plugin_registry.default_registry.
  • **recall is 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 a tsvector column and change one statement — the tool contract does not have to move.
  • Notes are not evicted. Nothing here expires old rows. Add a scheduled DELETE on updated_at if the table should stay small.
Agent Shared Memory · MCPG Config