memory-system

module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: AGPL-3.0

README

memory-system

Persistent, semantic long-term memory for AI agents, served over the Model Context Protocol.

CI Release Go


What it is

memory-system is a self-hostable MCP server that gives AI agents a durable, searchable long-term memory across sessions. Documents are stored as markdown, split into sections, embedded, and made retrievable through hybrid search — vector similarity fused with lexical ranking — backed by PostgreSQL and the pgvector extension. An agent stores what it learns as a self-contained note and recalls it later by meaning, not just keyword.

It ships as a single Go binary (module github.com/eliminyro/memory-system) that serves MCP over HTTP. On store, each section is embedded before any database write, so a provider failure never leaves a half-written document; on search, vector candidates ordered by cosine distance are fused with lexical candidates ranked by Postgres full-text search. A per-doc_type staleness model, an optional near-duplicate guard, and a retention sweep keep the corpus from rotting over time.

Every request passes through a Zanzibar-style, relationship-based authorization engine — a single decision point backed by relation tuples rather than hard-coded rules. Two independent authentication paths feed that engine: long-lived API keys (ideal for headless agents and CI) and OAuth 2.1 / OIDC (for humans signing in through an upstream identity provider). All data is tenant-scoped, with an optional shared "common" pool for knowledge every tenant can read; a caller's reads aggregate across their home tenant, the common pool, and any tenant they hold a direct grant on, while writes stay scoped to a single tenant.

Features

  • Hybrid retrieval — pgvector HNSW cosine search fused with a GIN-indexed tsvector lexical search, scored as 1 - (embedding <=> query_vector).
  • Pluggable embedding providersollama, gcp (Vertex AI), openai (any OpenAI-compatible /embeddings endpoint: OpenAI, Azure, vLLM, LM Studio, LocalAI, HuggingFace TEI), aws (Bedrock Titan / Cohere), and a deterministic fake provider for tests.
  • Dual authentication, one authz engine — API keys and OAuth 2.1 / OIDC both resolve to a unified Subject and are checked by the same relationship engine.
  • Per-tenant isolation — every document, key, and relation tuple is tenant-scoped, with a shared common pool for cross-tenant knowledge.
  • Aggregated reads across your tenantssearch_memory, list_documents, and get span the caller's readable tenant set (home tenant + common pool + any tenant they hold a direct viewer/member/manager/admin grant on), with each result labeled by its owning tenant and an optional tenant_id filter to narrow to one. Admins are not expanded to every tenant, and writes stay single-tenant.
  • Staleness & verification — per-doc_type freshness thresholds with off, advisory, or hard modes; stale sections can be withheld until re-verified.
  • Retention sweep — archives documents left unverified past a configurable multiple of their staleness threshold, then hard-deletes after a grace window (guarded against destructive misconfiguration).
  • Duplicate guard — an optional similarity check refuses near-duplicate writes, returning a similar_exists result so agents update instead of cloning.
  • Cleanup pipeline — a nightly scanner queues near-duplicate candidates and can post a per-scan summary to Telegram, plus a companion sweep that hard-deletes long-dead (revoked or expired) API keys past a configurable TTL so key listings stay tidy.
  • Admin CLI + web console — a memory-admin binary that operates directly against the database (no server required, ideal for bootstrap), plus a full web console at /ui: browse and edit memories (color-coded per owning tenant), manage tenants (create, rename, delete), members and per-document ACL grants, and API keys (create, rotate, revoke, purge), and run archive imports — every surface gated by the caller's authorization.
  • Self-service bootstrap & import — a one-shot first-run setup gated by a token the server generates and logs at boot (no env var to manage), available over HTTP or CLI, plus an async, job-based document-archive import (admin UI, HTTP, or CLI), so a fresh instance needs no direct database access.
  • Rich MCP tool surface — a regular tool set for agents and a privileged admin tool set, routed automatically by the caller's authorization.
  • Hardened HTTP edge — request-size limits, token-bucket rate limiting, strict CSP and security headers, unauthenticated health/readiness endpoints.

Quickstart

The bundled docker-compose.yml brings up the full stack — the memory server, PostgreSQL (pgvector/pgvector:pg17), and Ollama for embeddings — on the API-key path. The server is published on host port 8090 (mapped to the container's 8080).

# 1. Start Postgres, Ollama, and the memory server (published on http://localhost:8090)
docker compose up -d

# 2. Load the embedding model into Ollama (matches OLLAMA_MODEL=nomic-embed-text, 768-dim)
docker compose exec ollama ollama pull nomic-embed-text

Postgres stays on the internal compose network, but the server image also ships the memory-admin binary at /memory-admin. Bootstrap the first tenant and API key by exec-ing it inside the running container — it inherits the container's DATABASE_URL:

# 3. Create a tenant (note the returned id) and issue a key (plaintext shown ONCE)
docker compose exec memory-mcp /memory-admin tenant create --name quickstart
docker compose exec memory-mcp /memory-admin key issue --tenant <TENANT_ID> --label quickstart
# -> key: mmcp_XXXXXXXX...   (copy it now; it is never retrievable again)

memory-admin is also a standalone binary (grab it from a release archive or go build ./cmd/admin) — point it at any reachable Postgres via DATABASE_URL.

Finally, generate an MCP client config and point a client at the server:

docker compose exec memory-mcp /memory-admin setup --url http://localhost:8090 --token mmcp_XXXXXXXX...

That emits a ready-to-paste mcpServers block. The server answers MCP at http://localhost:8090/mcp with Authorization: Bearer mmcp_...; unauthenticated calls receive a 401 JSON challenge. Health lives at /~/health.

Container image

Prebuilt multi-arch images (linux/amd64, linux/arm64) are published on every release to GHCR (canonical) and mirrored to Docker Hub — pull from whichever you prefer:

docker pull ghcr.io/eliminyro/memory-system:latest   # GHCR — no pull rate limits
docker pull eliminyro/memory-system:latest           # Docker Hub mirror

Tags are :vX.Y.Z (semver), :latest, and :<git-sha>. The docker-compose.yml above builds from source for a hackable dev loop; to run a pinned release instead, swap build: . for image: ghcr.io/eliminyro/memory-system:latest.

Configuration

All configuration is via environment variables (parsed by caarlos0/env; see internal/config/config.go). Defaults are chosen so a stock Ollama deploy works out of the box.

Variable Default Notes
DATABASE_URL postgres://memory:memory@localhost:5432/memory?sslmode=disable PostgreSQL DSN (pgvector required).
SERVER_ADDR :8080 HTTP listen address.
LOG_LEVEL info Log verbosity.
EMBEDDING_PROVIDER ollama ollama | gcp | openai | aws | fake.
EMBEDDING_DIMENSIONS 768 Must equal the dimension your model emits (startup guard enforces it).
OLLAMA_URL http://localhost:11434 Ollama server URL.
OLLAMA_MODEL nomic-embed-text Ollama embedding model.
GCP_PROJECT (unset) Required when EMBEDDING_PROVIDER=gcp.
GCP_LOCATION us-central1 Vertex AI region.
GCP_EMBEDDING_MODEL text-embedding-005 Vertex AI embedding model.
OPENAI_BASE_URL https://api.openai.com/v1 API root exposing /embeddings.
OPENAI_API_KEY (unset) Optional; sent as a bearer token when set.
OPENAI_EMBEDDING_MODEL (unset) Required when EMBEDDING_PROVIDER=openai.
AWS_REGION (unset) Required when EMBEDDING_PROVIDER=aws.
AWS_EMBEDDING_MODEL (unset) Required when EMBEDDING_PROVIDER=aws.
ADMIN_ALLOWED_EMAILS (unset) Bootstrap seed only — emails granted system:memory#admin at startup.
MEMORY_RESET (unset) Boot-only break-glass flag — clears the admin key(s) and re-arms bootstrap; never touches tenants/documents/memories.
IMPORT_MAX_UPLOAD_BYTES 33554432 (32 MiB) Cap on an uploaded import archive at POST /api/admin/import; exempt from MAX_REQUEST_BYTES.
IMPORT_WORKER_CONCURRENCY 1 Concurrent goroutines draining the async import job queue.
MAX_REQUEST_BYTES 1048576 Request-body cap; 0 disables.
RATE_LIMIT_RPS 20 Token-bucket rate over the auth/write surface; <= 0 disables.
RATE_LIMIT_BURST 40 Bucket burst; must be >= 1 when RPS > 0.
RATE_LIMIT_TRUSTED_PROXY_DEPTH 0 Number of trusted reverse-proxy/CDN hops in front for client-IP rate-limit keying. 0 (default) trusts none: X-Forwarded-For is ignored and the key is RemoteAddr (unspoofable). Behind a proxy/CDN, set it to your trusted-hop count so the client IP is read from the right X-Forwarded-For entry; leaving it 0 there makes all traffic share one bucket.
RETENTION_MULTIPLIER 3 Archive after this × the doc_type staleness threshold; must be >= 1.
RETENTION_DELETE_GRACE_DAYS 30 Hard-delete this many days after archiving; must be >= 1.
CLEANUP_ENABLED true Enable the nightly near-duplicate / retention scanner.
CLEANUP_INTERVAL_HOURS 24 Scan interval (shared by the dead-key sweep below).
DEAD_KEY_TTL_DAYS 7 Nightly sweep hard-deletes API keys dead (revoked or expired) longer than this many days, so retired keys stop cluttering listings. 0 disables it — keys are then removed only via the console's manual Delete.
TELEGRAM_BOT_TOKEN (unset) Optional — post a per-scan cleanup summary.
TELEGRAM_CHAT_ID (unset) Optional — target chat for the summary.
MEMORY_DEFAULT_OPTS (safe bundle) Per-tenant toggle defaults, applied at tenant-create time only (existing tenants keep their settings). The built-in default is the safe bundle staleness=hard,duplicate_guard=true,cleanup_scan_enabled=true; override to loosen, e.g. staleness=off,duplicate_guard=false,cleanup_scan_enabled=false. See docs/administering.md.
MEMORY_SELF_SERVICE_POLICY open Global default for the self-service gate (open | admin_only) over per-tenant settings editing and API-key creation. open lets a tenant's manager/owner self-serve; admin_only raises both to system-admin. A per-tenant override (set by an admin) takes precedence.
AUTHLET_MASTER_KEY (unset) 32-byte hex key encrypting OAuth signing material at rest.
MEMORY_MCP_GOOGLE_CLIENT_ID (unset) OAuth opt-in (set with the secret).
MEMORY_MCP_GOOGLE_CLIENT_SECRET (unset) OAuth opt-in (set with the client id).
MEMORY_UI_CLIENT_ID (unset) Public PKCE client id used by the web UI.
PUBLIC_BASE_URL (unset) External origin (e.g. https://mem.example.org). Required when the OAuth path is enabled.
SIGNUP_ALLOWED_DOMAINS (unset) Self-serve signup gate (OAuth path). Empty/unset means PUBLIC signup — any verified identity your IdP authenticates can self-provision a tenant. Set a comma-separated domain allow-list (e.g. example.com,corp.example.com) to lock signup to your org. Fails closed (403); never affects already-provisioned users.

Setting both MEMORY_MCP_GOOGLE_CLIENT_ID and MEMORY_MCP_GOOGLE_CLIENT_SECRET opts into the OAuth 2.1 / OIDC path; setting one without the other is a fatal boot error. When the OAuth path is enabled, PUBLIC_BASE_URL must be an absolute http(s) origin with no path. Leaving the Google client vars unset keeps /mcp API-key-only.

Warning — signup is PUBLIC by default. With the OAuth path enabled, the first verified login from an identity that has no existing membership auto-provisions a personal tenant for that user (they become its admin). If SIGNUP_ALLOWED_DOMAINS is unset or empty, this is public — anyone your IdP will authenticate (for Google, any Google account) can self-provision. To lock a private instance to your organization, set SIGNUP_ALLOWED_DOMAINS to your domain(s). The gate does not affect already-provisioned users, and the first admin is separately gated by the bootstrap token. See docs/administering.md.

Embedding providers
EMBEDDING_PROVIDER Env vars Example model Typical dim Credentials
ollama OLLAMA_URL, OLLAMA_MODEL nomic-embed-text 768 none
gcp GCP_PROJECT, GCP_LOCATION, GCP_EMBEDDING_MODEL text-embedding-005 768 Application Default Credentials
openai OPENAI_BASE_URL, OPENAI_API_KEY, OPENAI_EMBEDDING_MODEL text-embedding-3-small 1536 (configurable) OPENAI_API_KEY (optional for local servers)
aws AWS_REGION, AWS_EMBEDDING_MODEL amazon.titan-embed-text-v2:0 1024 standard AWS credential chain
fake EMBEDDING_DIMENSIONS none (deterministic; tests only)

EMBEDDING_DIMENSIONS must match what the model emits — a startup guard freezes the embedding identity (provider + model + dimension) and refuses to run a populated corpus whose stored dimension differs. See docs/embedding-providers.md for per-provider detail, including Azure/vLLM/TEI base URLs and output-dimension truncation.

Administration

Two equivalent administrative surfaces exist:

  • memory-admin CLI — operates directly against the database via DATABASE_URL, so it works even with no server running. This is the only way to bootstrap the first tenant and key. Holding DATABASE_URL is equivalent to full control — treat the CLI as an operator tool.
  • MCP admin tools — the same operations exposed to an already-privileged MCP client (list_tenants, create_tenant, update_tenant, delete_tenant, create_api_key, list_api_keys, revoke_api_key, rotate_api_key, grant_user, list_users, update_user_role, revoke_user).

Admin status is pure relationship state: a principal is an admin iff it holds the global grant system:memory#admin. ADMIN_ALLOWED_EMAILS only seeds that grant at startup. Keys are issued as opaque mmcp_... tokens, stored only as a SHA-256 hash, and shown in plaintext exactly once. Rotation issues a replacement and can hold the predecessor valid for a grace window for zero-downtime swaps. A small admin web page is served at /ui, gated on bootstrap state: 404 before an admin exists, a static "OAuth not configured" notice once bootstrapped without OAuth wired up, and the real knowledge browser once both hold — it never shows a login or setup form itself.

A fresh instance can bootstrap itself instead of requiring direct database access: on first boot, while no admin exists yet, the server generates a one-time bootstrap token and logs it at WARN (read it with docker logs; it is regenerated every boot until an admin exists and is never persisted or logged again afterward). GET /bootstrap serves a dedicated setup page: paste the logged token and pick a mode. The page always shows OAuth status and offers two modes — MCP tokens (provision the admin Bearer key) and OAuth (additionally seed a founding admin email). POST /bootstrap provisions the first tenant and admin, returning the plaintext admin key exactly once (always, in either mode); the whole /bootstrap surface 404s once an admin exists. When OAuth is configured, the optional founding admin email is granted system:memory#admin (the same authority as the founding key), so the operator signs in at /ui via OAuth with full admin rights. With OAuth enabled the server also auto-registers the /ui OAuth client on boot (from MEMORY_UI_CLIENT_ID, defaulting to memory-ui, and PUBLIC_BASE_URL), so browser login works with no manual database access. (Configuring the OAuth provider from the page itself, persisted to the DB, is planned.) memory-admin bootstrap provisions the same way straight from the CLI with no token needed (it talks to the database directly and is inherently privileged). An operator-only MEMORY_RESET=1 boot flag re-arms bootstrap by clearing the admin key(s) — it never deletes tenant data, and the next boot logs a fresh token. Loading a corpus works the same way: upload an archive from the admin UI, POST /api/admin/import (multipart, a zip, processed asynchronously by a background worker), or run the memory-import CLI over a local directory. See docs/administering.md for the full first-run bootstrap, break-glass reset, document import, user-management, and key-lifecycle runbook.

Connecting a client

memory-admin setup emits the standard mcpServers JSON without writing your token to disk:

memory-admin setup --url https://mem.example.org --token mmcp_your_token_here
# or: MEMORY_URL=... MEMORY_TOKEN=... memory-admin setup

Paste the emitted block into any MCP client that supports HTTP transport.

When OAuth is configured, a static key is optional: an MCP client that supports OAuth can connect with just the server /mcp URL — it discovers the authorization server from the protected-resource metadata, registers dynamically, and completes the PKCE login flow, so there is no token to mint or store. Use a static mmcp_... key as a Bearer token for clients (or CI) that don't do OAuth. The /ui "Connect an MCP client" panel shows both paths. See docs/connecting-clients.md for client examples, and docs/memory-usage-prompt.md for a persona-free, ready-to-paste system-prompt block that teaches an assistant to recall before acting, store durable learnings, and update instead of duplicating.

Architecture

The server is one HTTP listener. /mcp carries the MCP Streamable-HTTP transport; a DualAuth router inspects the bearer token (a three-segment JWT is verified by the OAuth path, anything else falls through to API keys), writes a unified Subject, and the MCP transport calls the authorization engine's Check. Application code lives under internal/ (config, database, repository, service, mcp, authz, auth, authletas, middleware, cleanup, retention, staleness); entry points are cmd/server, cmd/admin, and cmd/import. Data is stored in Postgres with a vector(N) embedding column per section, an HNSW cosine index, a GIN lexical index, and a relation_tuples table for the authorization graph. See docs/architecture.md for the full component map, data-flow walkthroughs, and the dual-auth model.

Building from source

Requires Go (see the version in go.mod).

go build ./...          # build all binaries (cmd/server, cmd/admin, cmd/import)
go test ./...           # unit tests

Integration tests are behind a build tag and need a live PostgreSQL (with pgvector). Point TEST_DATABASE_URL at it and serialize packages so parallel migrations don't race:

export TEST_DATABASE_URL='postgres://memory:memory@localhost:5432/memory?sslmode=disable'
go test -tags=integration -p 1 ./...

Releases are automated: on a green CI run on master, the release workflow computes the next version, tags it, builds and pushes a multi-arch image to ghcr.io/eliminyro/memory-system (mirrored to Docker Hub when its credentials are configured), and runs goreleaser to produce the memory-mcp, memory-import, and memory-admin binaries, archives, checksums, SBOMs, and a GitHub Release.

Contributing

Contributions are welcome.

  • Use Conventional Commits (feat:, fix:, refactor:, chore:, …) — release versioning is derived from commit history.
  • Code must be gofmt-clean and pass golangci-lint run.
  • Keep tests green: go test ./... and, where relevant, the integration suite above. CI runs gofmt, golangci-lint, unit tests, integration tests, a build, and a boot smoke test on every pull request.

License

Licensed under the GNU Affero General Public License v3.0. See LICENSE.

Note the AGPL's §13 network-use clause: if you run a modified version of this software and let others interact with it over a network, you must offer those users the corresponding source of your modified version. Hosting a fork is fine — publishing its source is the condition.

Directories

Path Synopsis
cmd
admin command
Command memory-admin is a privileged offline admin CLI.
Command memory-admin is a privileged offline admin CLI.
import command
server command
internal
authletas
Package authletas wires authlet into memory-system: UserResolver, AS construction, JWKS/PRM handlers, and dual-auth middleware.
Package authletas wires authlet into memory-system: UserResolver, AS construction, JWKS/PRM handlers, and dual-auth middleware.
authletstore
Package authletstore is memory-system's GORM-backed implementation of authlet/pkg/storage; tables match internal/migrations/<ts>_authlet_tables.sql.
Package authletstore is memory-system's GORM-backed implementation of authlet/pkg/storage; tables match internal/migrations/<ts>_authlet_tables.sql.
authz
Package authz is a Zanzibar-style relationship-based authorization engine: relation tuples (object#relation@subject) evaluated against a fixed, non-user-editable namespace by a recursive Check.
Package authz is a Zanzibar-style relationship-based authorization engine: relation tuples (object#relation@subject) evaluated against a fixed, non-user-editable namespace by a recursive Check.
authzseed
Package authzseed populates the RBAC engine's relation tuples out-of-band: pure constructors (single source of truth) + idempotent Backfill/BootstrapAdmins writers.
Package authzseed populates the RBAC engine's relation tuples out-of-band: pure constructors (single source of truth) + idempotent Backfill/BootstrapAdmins writers.
cleanup
Package cleanup runs the server-side near-duplicate scanner and its Telegram notifier: on a fixed interval, walk every tenant, upsert pending queue entries.
Package cleanup runs the server-side near-duplicate scanner and its Telegram notifier: on a fixed interval, walk every tenant, upsert pending queue entries.
mcp
panicguard
Package panicguard provides a deferred recover for long-lived background goroutines.
Package panicguard provides a deferred recover for long-lived background goroutines.
retention
Package retention holds the pure time-window math for the retention sweep (sweep in internal/cleanup, SQL in internal/repository) — unit-testable without a DB.
Package retention holds the pure time-window math for the retention sweep (sweep in internal/cleanup, SQL in internal/repository) — unit-testable without a DB.
staleness
Package staleness decides whether a section's content is withheld: verified older than its doc_type threshold AND making claims that rot (code paths, symbols).
Package staleness decides whether a section's content is withheld: verified older than its doc_type threshold AND making claims that rot (code paths, symbols).
version
Package version is the single source of truth for the server version, used by the MCP server identity and the /~/version endpoint.
Package version is the single source of truth for the server version, used by the MCP server identity and the /~/version endpoint.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL