A PostgreSQL-backed MCP memory server with thermal decay, a knowledge graph, and 8-signal hybrid retrieval β a long-term memory for AI agents (e.g. Claude Code). Runs against any Postgres: a managed cloud instance (Supabase) or a local Docker container.
A reusable scaffold. Point it at your own Postgres, run the migrations, and wire it into your MCP client.
π οΈ Companion: claude-craft-kit β an opinionated Claude Code workflow harness (pre-tool guards Β· a tiered validation gate Β· a wrap-up gate) that wires this in as its optional long-term memory backend.
# Local Docker (easiest to try)
cp .env.example .env # configure credentials
make up # start Postgres + pg_cron (Docker)
make migrate # run Drizzle schema migrations
make dev # start the MCP server
# Managed Postgres (e.g. Supabase)
cp .env.supabase.example .env.supabase # add your pooler connection string
make dev-remote # start the MCP server against itThe server exposes these tools to an MCP client:
| Tool | Purpose |
|---|---|
remember |
Store a memory with tags, type, importance β auto-relates to top-3 FTS matches, dedup-checks |
recall |
8-signal hybrid search (FTS + trigram + temperature + importance + graph centrality + recency + access frequency) |
forget |
Delete a memory, cascade its relations, remove the synced markdown file |
update |
Partial update with automatic version snapshot before changes |
relate |
Create typed edges: related_to, supersedes, contradicts, elaborates, depends_on |
status |
Dashboard β tier/type breakdown, hottest/coldest memories, stale count |
graph |
Mermaid flowchart of the memory network |
traverse |
Multi-hop BFS graph traversal (depth 1β5, filterable by relation type) |
history |
Version chain for a memory (snapshots before each update) |
merge |
Combine duplicates β append observations, union tags, transfer edges |
conflicts |
List all contradicts edge pairs |
analytics |
Recall hit rate, top accessed, temperature distribution, events/day, graph density |
health |
Orphan count, stale count, dedup candidate pairs (similarity scores + proposed canonical), contradictions, type coverage |
user Β· project Β· decision Β· fact Β· pattern Β· feedback Β· reference
graph TD
CC[MCP Client] -->|MCP| SERVER[mcp-server.ts]
subgraph Core["Core Modules"]
SERVER --> RET[retrieve.ts<br/>8-signal hybrid scoring]
SERVER --> GRP[graph.ts<br/>BFS traverse, community]
SERVER --> INT[intelligence.ts<br/>Dedup, merge, versioning]
SERVER --> OBS[observability.ts<br/>Analytics, health, dedup pairs]
end
subgraph DB["PostgreSQL 17 + pg_trgm + pg_cron"]
PG[(Entities + Relations<br/>+ Versions + Events)]
CRON[pg_cron<br/>Nightly decay]
end
subgraph Sync["Optional"]
FS[file-sync.ts<br/>mirror memories to markdown]
CAN[events_canary.py<br/>event-freshness check]
end
RET -->|pooled SSL connection| PG
GRP --> PG
INT --> PG
OBS --> PG
SERVER --> FS
CAN -.->|polls events<br/>alert on silence| PG
style Core fill:#1e293b,stroke:#f97316,color:#fff
style DB fill:#1e293b,stroke:#22d3ee,color:#fff
style Sync fill:#1e293b,stroke:#a78bfa,color:#fff
| Signal | Weight | Source |
|---|---|---|
| Full-text rank | 0.20 | ts_rank on tsvector |
| Trigram similarity | 0.15 | pg_trgm |
| Tag match | 0.10 | Array overlap |
| Temperature | 0.15 | Thermal model |
| Importance | 0.10 | Auto-drifting (0.1β0.9) |
| Graph centrality | 0.15 | Relation edge count |
| Recency boost | 0.10 | Time since last access |
| Access frequency | 0.05 | Cumulative access count |
Weights are configurable in src/config.ts.
Memories have a temperature (0.0β1.0) that decays daily with a 0.85 multiplier:
- Pattern-aware decay β memories accessed 3+ days/week decay slower (access bitmap detection)
- Cascade bumps β accessing a memory warms its graph neighbors proportionally to edge weight
- Auto-importance drift β frequently accessed memories gain importance; neglected ones (60+ days) lose it
- Tier classification β HOT (>0.7), WARM (0.3β0.7), COLD (<0.3)
- Stale flagging β COLD memories untouched for 30+ days are marked stale
- PostgreSQL β primary store (entities, relations, versions, events)
- Markdown files (optional) β
file-sync.tsmirrors memories to a directory of.mdfiles (setMEMORY_PERSISTOR_DIR/CLAUDE_DIR), handy for agents with a file-based memory convention.
| Job | Schedule | Purpose |
|---|---|---|
memory-thermal-decay |
0 6 * * * UTC |
Nightly pattern-aware decay + importance drift + stale flagging |
memory-decay-startup-catchup |
@reboot (local Docker) |
Runs decay_catchup() on container start if last decay was >24h ago |
make help # show all targets
make test # unit tests (Vitest + pytest for scripts)
make test-integration # integration tests against real Postgres
make status # local DB + pg_cron status
make decay # run thermal decay (local)
make canary # events-pipeline freshness check (local)
make cron-status # pg_cron schedule and recent runs
make graph # Mermaid graph of memory network
make seed # import existing markdown memories (optional file-sync)
make clean # remove volumes and generated filesEach command has a -remote variant (dev-remote, status-remote, decay-remote,
canary-remote) that targets the connection in .env.supabase.
src/
mcp-server.ts # MCP tool definitions and handlers
retrieve.ts # 8-signal hybrid retrieval scoring
thermal.ts # Cascade bumps, pattern-aware decay, importance drift
graph.ts # BFS traversal, community detection, auto-relate
intelligence.ts # Dedup detection, confidence scoring, merge, versioning
observability.ts # Analytics, health metrics, dedup candidate pairs
events.ts # Fire-and-forget event logging
file-sync.ts # Optional dual-write to markdown files
schema.ts # Drizzle ORM schema (entities, relations, versions, events)
config.ts # Scoring weights, decay rates, tier boundaries
db.ts # Database connection (auto-SSL for remote)
import.ts # Seed script for existing markdown memories
scripts/
memory-decay.py # Python decay runner (Docker exec fallback)
events_canary.py # Event-freshness check (exits 1 if pipeline silent)
backfill-edges.ts # One-time auto-relate backfill
tests/
*.test.ts # Unit tests (Vitest)
*.py # Python tests (pytest)
integration/ # Integration suites against real Postgres
drizzle/ # Migration files
initdb/
01-pg-cron.sql # pg_cron setup, decay job, startup catchup function
docker-compose.yml # Local Postgres 16 + pg_cron + pg_trgm
LICENSE # MIT
Copy .env.example and adjust:
| Variable | Default | Purpose |
|---|---|---|
DATABASE_URL |
postgresql://postgres:postgres@localhost:5432/memory_persistor |
Postgres connection string |
POSTGRES_PASSWORD |
postgres |
Docker Compose Postgres password |
MEMORY_PERSISTOR_DIR |
project root | Base path for optional markdown mirror |
CLAUDE_DIR |
~/.claude |
Directory for optional markdown mirror |
Create .env.supabase with a pooler connection string (transaction mode, port 6543):
DATABASE_URL=postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
src/db.ts auto-detects a remote host and enables SSL. It respects an explicit
sslmode=... query param, so a no-SSL service container (CI) and a forced-SSL
managed instance can coexist.
GitHub Actions (.github/workflows/ci.yml, ubuntu-latest) on every push and PR:
npm ci β npm run build β Vitest unit suite β pytest (scripts) β gitleaks.
The integration suite (make test-integration) runs against a real Postgres and
is intended to be run locally / against your own instance.
Secrets scanning uses .gitleaks.toml (built-in rules + an allowlist for local
files and documentation placeholders).
- Runtime: Node.js 24+ (ESM) Β· Language: TypeScript 5.9 (strict)
- ORM: Drizzle ORM Β· MCP SDK:
@modelcontextprotocol/sdkΒ· Validation: Zod - Database: PostgreSQL 17/16 +
pg_trgm+pg_cron - Testing: Vitest (TS) + pytest (Python) Β· Container: Docker Compose
MIT β see LICENSE.