Skip to main content
Glama

memser — Memory-as-a-Service MCP Server

An MCP server that exposes memory operations (store, recall, forget, consolidate, list) as tools, backed by a tiered storage engine, so any MCP-compatible agent (Claude Desktop, Claude Code, a Telegram bot, custom agents) can plug in for persistent memory.

Layers

MCP Interface Layer (app/server.py)     store_memory, recall_memory,
                                         forget_memory, list_memories,
                                         consolidate_memories
Memory Engine (app/memory_engine.py,    scoring, dedup, conflict
app/consolidation.py, app/scoring.py)   detection, decay scheduling
Storage Tiers
  - Working   (Redis, TTL)              app/redis_client.py
  - Episodic  (Postgres, JSONB-ish)     app/models.py (tier="episodic")
  - Semantic  (Postgres + pgvector)     app/models.py (tier="semantic")
Background Jobs (APScheduler)           app/jobs.py

Setup

cp .env.example .env          # adjust as needed
docker compose up -d          # Postgres+pgvector and Redis
pip install -e ".[dev]"       # or: pip install -r requirements.txt

# Only needed if not using docker-compose's auto-init:
python -m scripts.init_db

python -m app.server          # runs the MCP server over stdio

To try it with the MCP inspector instead of a real client:

mcp dev app/server.py

To wire it into Claude Desktop/Code, add to its MCP config:

{
  "mcpServers": {
    "memory-service": {
      "command": "python",
      "args": ["-m", "app.server"],
      "cwd": "/path/to/memser",
      "env": { "DATABASE_URL": "...", "REDIS_URL": "...", "OPENAI_API_KEY": "..." }
    }
  }
}

Without OPENAI_API_KEY, embeddings fall back to a deterministic local hash embedder (app/embeddings.py::HashEmbedder) — the server runs fully offline, but recall quality is lexical-overlap-only, not real semantic search. Consolidation summarization similarly falls back to naive dedup/join instead of an LLM call.

Tools

Tool

Purpose

store_memory(content, scope, tags?, source?, ttl_seconds?, actor?)

Write a memory. ttl_seconds → working tier (Redis); omitted → durable episodic tier (Postgres), with dedup/conflict resolution against existing memories in scope.

recall_memory(query, scope, k?, tags?, actor?)

Hybrid search: working tier + vector similarity over episodic/semantic, ranked by similarity + recency + frequency + confidence.

forget_memory(scope, memory_id?, tier?, tags?, actor?)

Cascading delete across Redis + Postgres, with an audit-log snapshot written before deletion.

consolidate_memories(scope, actor?)

Clusters episodic memories, LLM-summarizes each cluster into a semantic fact, marks originals superseded_by (kept, not deleted). Also runs on a schedule.

list_memories(scope, tier?, tags?, ...)

Audit/debug listing, no ranking.

scope is the tenancy boundary (user id, agent id, or session id) — nothing crosses scopes. Every tool call is written to memory_audit_log (action, actor, memory id, scope, detail).

Data model

See db/init.sql for the authoritative schema (memories, memory_audit_log) and app/models.py for the SQLAlchemy mapping. Key points:

  • memories.embedding is VECTOR(1536) (pgvector), indexed with an ivfflat cosine-distance index.

  • superseded_by chains episodic → semantic (via consolidation) and old-fact → new-fact (via conflict resolution) without ever deleting the superseded row — that's the audit trail.

  • forget_memory is the only thing that actually deletes rows, and it logs a full content snapshot to memory_audit_log first.

Key mechanisms

  • Conflict detection (app/memory_engine.py): before a durable store, the new embedding is compared against the scope's active memories. Above DUPLICATE_THRESHOLD cosine similarity, the write is treated as a duplicate (bumps access_count/confidence on the existing row instead of inserting). Above CONFLICT_THRESHOLD but below duplicate, the new memory is inserted and the older, similar ones are marked superseded_by the new row.

  • Consolidation (app/consolidation.py): greedy single-linkage clustering of a scope's episodic memories by cosine similarity, one LLM summarization call per cluster, one new semantic row per cluster, originals marked superseded (not deleted).

  • Scoring/decay (app/scoring.py): recall ranks by similarity, recency (exponential half-life), frequency (diminishing returns on access_count), confidence, weighted per .env. A background decay job (app/jobs.py::run_decay_sweep) shrinks confidence on stale, unaccessed episodic memories over time.

  • Scoped forgetting: forget_memory always requires a scope, and cascades to every tier a memory could live in (Redis working entry + Postgres row), logging who deleted what and when before the row is gone.

Background jobs

app/jobs.py runs two APScheduler interval jobs inside the same process as the MCP server (started/stopped via the FastMCP lifespan hook in app/server.py):

  • run_consolidation_sweep — every CONSOLIDATION_INTERVAL_MINUTES, consolidates every scope with at least CONSOLIDATION_MIN_EPISODIC un-consolidated episodic memories.

  • run_decay_sweep — every DECAY_INTERVAL_MINUTES, decays confidence on episodic memories untouched for DECAY_AFTER_DAYS.

Set ENABLE_SCHEDULER=false to disable both (e.g. run consolidation only via explicit consolidate_memories calls, or move this to Celery beat for a multi-process deployment — the job bodies are plain async functions and don't depend on APScheduler specifically).

Tests

pytest

Tests cover the pure logic (scoring math, hash-embedding determinism, clustering, summarization fallback) and don't require Postgres/Redis to be running. There's no integration test suite against a live database in this scaffold — add one with a pgvector/pgvector test container if you extend this.

Known scaffold limitations

  • cluster_by_similarity is O(n²) greedy single-linkage — fine for per-scope batches of tens–low-hundreds of episodic memories between consolidation runs; swap in a real clustering/ANN approach if scopes grow much larger before consolidating.

  • Conflict resolution always lets the newest similar memory supersede the older one(s); there's no merge-vs-flag-for-human-review path — add one if blind "newest wins" isn't safe for your use case.

  • Single-process scheduler (APScheduler) — fine for one server instance; move to Celery beat (as sketched in the original architecture doc) if you run multiple replicas and need a single consolidation/decay owner.