memolayer
v0.2.2
Published
Client installer for memolayer — a shared, curated memory layer for coding agents over MCP. Registers the MCP server and wires the auto-recall hook.
Maintainers
Readme
memolayer
A universal, tool-agnostic shared memory layer for coding agents, delivered over MCP.
memolayer stores curated, durable engineering knowledge about a codebase — decisions
(with rationale), conventions, gotchas, and patterns — and serves it to any coding agent
through the Model Context Protocol. Unlike a flat CLAUDE.md / AGENTS.md, it offers selective
retrieval (hybrid vector + keyword), a memory lifecycle, provenance, conflict detection, and live
cross-agent sharing.
This repository currently implements Phase 0 (skeleton): a working backend where remember
stores a memory with an embedding and recall returns relevant memories ranked above irrelevant
ones. See ../../IMPLEMENTATION_PLAN.md for the full roadmap (Phases 1–3).
Status: Phases 0–2 deployed on the VPS and verified (2026-07-14).
- Phase 0/1 (agent-facing MCP):
node packages/mcp-server/test/acceptance.mjs(12/12).- Phase 2 (human curation): the
apiservice (127.0.0.1:3200) serves the Palimpsest React/Vite dashboard at/dashboard, with session-cookie auth and the full curation surface (review queue, browser, side-by-side conflict resolution).AUTO_ACCEPT=false— agent captures now land aspendingand become retrievable only after a curator approves them. Admin login is seeded fromADMIN_EMAIL/ADMIN_PASSWORDin.env. The dashboard is bound to127.0.0.1— reach it via SSH-L 3200:127.0.0.1:3200, Tailscale, or a TLS reverse proxy (setCOOKIE_SECURE=truewhen serving over HTTPS).
Architecture (Phase 0)
Three containers on a single VPS, orchestrated with Docker Compose:
| Service | Image | Exposure | Role |
|--------------|---------------------------------------------------|-------------------------|------|
| db | pgvector/pgvector:pg16 | 127.0.0.1:5432 | Postgres 16 + pgvector: relational + vector store |
| embeddings | ghcr.io/huggingface/text-embeddings-inference | 127.0.0.1:8080 | Local BAAI/bge-m3 (1024-dim, multilingual) over HTTP |
| mcp | built from packages/mcp-server | 127.0.0.1:3100※ | MCP Streamable-HTTP server: recall + remember |
Only the mcp service is ever exposed publicly — behind nginx/Caddy + TLS + a bearer token.
The DB and embedding service stay bound to 127.0.0.1 / the internal Docker network.
※ This VPS remaps the MCP host port to
3100because3000is already in use by another service. The container still listens on3000internally; only the host binding changed.
Prerequisites on this VPS
Two host-level prerequisites are not yet in place and require sudo (run them yourself):
# 1) Docker Engine + Compose plugin (not currently installed)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER" # then log out/in so `docker` works without sudo
# 2) 2 GB swap file — OOM insurance during HNSW index builds / embedding bursts
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile \
&& sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # persist across rebootsDeploy
cd ~/projects/memolayer
# 1) Secrets
cp .env.example .env
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=$(openssl rand -hex 32)/" .env
sed -i "s/^MCP_BEARER_TOKEN=.*/MCP_BEARER_TOKEN=$(openssl rand -hex 32)/" .env
# 2) Build & start (the embedding model downloads ~2.2 GB on first start)
docker compose up -d --build
docker compose logs -f embeddings # wait for "Ready" before testing
# 3) Sanity-check the stack
docker compose ps
docker compose exec db psql -U memolayer -d memolayer -c '\dt' # memories table exists
# 4) Seed ~10 varied memories, then smoke-test recall/remember
docker compose exec mcp node dist/seed.jsConnect a coding agent
Register the server as a remote HTTP MCP server (example: Claude Code):
claude mcp add --transport http memolayer http://127.0.0.1:3100/mcp \
--header "Authorization: Bearer $(grep MCP_BEARER_TOKEN .env | cut -d= -f2)"For remote clients, put nginx/Caddy + TLS in front of 127.0.0.1:3100 on a public hostname and
point the client at https://<host>/mcp. Confirm the firewall exposes only 80/443 + SSH — never
5432, 8080, or 3100.
Tools
recall{ query, scope?, limit=8, types? }→ compact ranked hits[{ id, type, scope, content (truncated), score }]. Hybrid retrieval (vector + keyword) overstatus='active'memories in the requested scope plusglobal.remember{ content, type, scope='global', rationale?, client?, session_id? }→{ id, status, conflict? }. Embeds the content, runs a conflict pre-check (cosine > 0.85 against active same-type memories), and inserts. In Phase 0 (AUTO_ACCEPT=true) new memories areactiveimmediately; from Phase 2 they land aspendingfor curation.
Repository layout
memolayer/
├── docker-compose.yml
├── .env.example
├── db/migrations/0001_init.sql # runs on first DB init only
└── packages/mcp-server/
└── src/
├── index.ts # HTTP transport, auth, tool + prompt registration
├── db.ts # pg pool + query helpers
├── embeddings.ts # HTTP client for the embedding service
├── retrieval.ts # hybrid (vector + keyword) ranking
├── seed.ts # inserts ~10 varied memories for sanity checks
├── types.ts # TS types matching the schema
└── tools/{recall,remember}.tsOperational notes
- Migrations in
db/migrations/run only on an empty data dir (via/docker-entrypoint-initdb.d). For later schema changes, add a migration runner (node-pg-migrate/drizzle-kit) — do not rely on the init folder beyond0001. - Embeddings are disposable: the raw
contentis always the source of truth; a vector can be recomputed. Queries are embedded with the same model as stored memories, by construction. - The MCP server retries its DB and embeddings connections on startup, so
docker compose upis safe even though the embedding model takes minutes to download on first boot. - Embeddings memory (8 GB box): bge-m3's model warmup allocates O(seq²) attention buffers, so a
high
--max-batch-tokens(e.g. 16384) makes warmup spike multiple GB and OOM-kill the container in a restart loop. It's pinned to2048with amem_limit: 4gsafety cap; steady-state RSS is ~2.3 GB. Memories are short facts, so 2048 is ample — raise both together only if you truly need to embed very long documents, and watchdocker stats.
