Skip to main content
Glama
mbay-ODW
by mbay-ODW

memory-service

Self-hosted, versioned project memory for Claude Cowork (or any MCP client). One place for Claude to read and write structured notes across projects — searchable, versioned, editable by a human, and independent of any single chat session.

Why this exists

Cowork's built-in memory is scoped to a session and doesn't reliably survive the split between local-desktop sessions and cloud-run scheduled tasks — a fact learned the hard way, not a theoretical concern. If a cloud task researches something today, a local session tomorrow has no way to know. There's no version history, no way to search across everything Claude has learned, and no way for a human to look at what got stored without asking Claude to recite it.

memory-service is the fix: an explicit, external store. Every Cowork session — local, cloud, scheduled, interactive — talks to the same MCP server. Writes land in Postgres and a git repository in the same operation, so every change is both instantly queryable and permanently versioned. A human can browse, search, and edit the same data through a plain web UI, no prompt required.

Prior art

The individual ingredients here aren't new. WUPHF pairs a git-backed markdown wiki with a SQLite/BM25 index for search — the closest existing project to "git holds the real history, a database holds a fast index into it," just without MCP, pgvector, or a shared write path. Mem0's self-hosted stack is the closest on the other axis — Postgres+pgvector, an MCP server, a web dashboard — but versions nothing via git; history is a database table. DiffMem goes further than either: git as the only store, no database index at all, live git log/grep shell-outs at query time. And the "memory bank" convention used by several AI coding tools (Cline, Roo Code) — markdown files an agent is prompted to maintain inside the project repo — solves a related but different problem with no dedicated service, index, or MCP contract at all.

What doesn't seem to exist elsewhere is the specific combination: MCP tools and a plain human-editable web UI sharing one write path (so they can't drift apart), backed by a real git-commit-per-write history and a properly indexed hybrid (full-text + vector) Postgres search — rather than either git alone, a database alone, or a UI that's read-only.

Related MCP server: mem0-lite

Architecture

One FastAPI process serves three things from the same codebase, sharing one database connection pool and one write path:

┌─────────────────────────────────────────────────────┐
│                  FastAPI process                     │
│                                                       │
│   web UI (Jinja2+htmx)      MCP server (/mcp)         │
│         │                          │                  │
│         └──────────┬───────────────┘                  │
│                     ▼                                  │
│            app/services/*.py                          │
│      (the ONLY code that touches DB or git)            │
│                     │                                  │
│         ┌───────────┴───────────┐                      │
│         ▼                       ▼                      │
│    Postgres                  git repo                  │
│  (query + search)        (version history)              │
└─────────────────────────────────────────────────────┘

Single write path. Every mutation — whether it comes from an MCP tool call or a web form submission — goes through app/services/*.py and nowhere else. Neither the MCP layer (app/mcp/tools.py) nor the web routes (app/web/routes.py) touch the database or the git repo directly; they're both thin wrappers around the same service functions. That means the two surfaces can never drift apart in behavior, and there's exactly one place to look for how a write actually happens (app/services/entries.py's upsert_entry/update_entry).

Hybrid search, not just one or the other. Every entry gets both a Postgres tsvector (full-text, GIN-indexed) and a pgvector embedding (intfloat/multilingual-e5-small, HNSW-indexed), computed locally on CPU — no embeddings API call, no data leaving the box. A search merges both rankings with Reciprocal Rank Fusion, so an exact keyword match and a semantically related note that doesn't share any words both surface. See app/services/search.py.

Git is the real history; Postgres is the fast index into it. Every write does: DB transaction → render+commit a markdown file (one .md per entry, YAML frontmatter) → insert an entry_versions row recording the resulting commit hash → commit the transaction. If the git commit fails, the DB transaction rolls back — the two are never allowed to disagree about what the latest version is. entry_versions exists purely so "show me the last 5 changes" is an indexed SQL query instead of a git log shell-out; the commit hash it stores is how you get back to the actual git object if you need the full diff. See app/services/git_store.py.

Everything nests, nothing is hardcoded. Projects contain subtopics, subtopics can nest arbitrarily deep (kunde-mueller/vorgang-2026-08/...), and subtopic paths auto-create on first write — an agent doesn't need a separate "create subtopic" call before it can file a note under one. Projects don't auto-create (a project carries a sensitivity_level that has real access-control implications later, so creating one is a deliberate action — either a human in the web UI, or the one memory_create_project MCP tool).

MCP tools

Tool

Purpose

memory_search

Full-text + semantic search, optionally scoped to a project/subtopic and/or filtered by tags (OR across multiple tags; can browse by tag alone with an empty query). Every result includes its exact subtopic path.

memory_get

Current entries for a project or subtopic (call this before answering). Every result includes its exact subtopic path.

memory_upsert

Create or update an entry, identified by (subtopic, title) -- pass the subtopic from a prior memory_get/memory_search result to update it in place; guessing instead creates a separate entry, silently

memory_list_open

Entries flagged as needing follow-up

memory_history

Version history for one entry

memory_delete_entry

Permanently delete one entry (DB row + git file, one commit)

memory_check_sources

Batch dedup check for daily sync tasks (has this mail/message already been logged?)

memory_create_project

Create a new project — the only structural MCP tool; rename/delete are web-UI-only, human-confirmed actions

memory_link_entries

Record a direct, typed link between two entries (related_to, same_as, follow_up_of, mentions, supersedes, causes, fixes, contradicts) — the fix for "same real-world thing, filed under two titles" instead of creating a duplicate; supersedes also auto-flips the old entry to veraltet

memory_unlink_entries

Remove a specific link between two entries (idempotent)

memory_get_related

Every entry directly linked to this one, in either direction

memory_find_similar

Scan for likely-duplicate entries via embedding similarity (optionally scoped to one project); reports candidate pairs by default, only links (never merges) with auto_link=True

Relations are direct links only — no transitive graph traversal, no automatic entity extraction. They live in Postgres only, not mirrored into git frontmatter like tags/sources are, since a link to another entry would go stale if that entry is later renamed or deleted; the relation row's own created_at/created_by is audit trail enough. See app/services/relations.py.

Quickstart

docker compose up --build
# migrate, then seed 5 example projects with nested subtopics and sample entries
docker compose exec app alembic upgrade head
docker compose exec app python -m scripts.seed_dummy_data

Run tests (spins up its own memory_test database on the same Postgres):

docker compose up -d db
DATABASE_URL=postgresql+asyncpg://memory:memory_dev_password@localhost:5433/memory_test \
  python -m pytest

Repo layout

Path

What's there

app/db/models.py

SQLAlchemy models: projects, subtopics (self-referential), entries (tsvector + vector columns), entry_versions, sources, tags

app/services/

All business logic — entries.py, projects.py, search.py, embeddings.py, git_store.py, sources.py

app/mcp/tools.py

The 8 MCP tools, each a thin wrapper over services/*

app/web/

Jinja2 + htmx server-rendered UI — no SPA build step, no CDN dependencies (EasyMDE and htmx are vendored)

alembic/versions/

Schema migrations

tests/

pytest suite — service-layer, MCP-layer (via fastmcp.Client), and web-UI (via httpx.ASGITransport) tests

tasks/lessons.md

Real engineering gotchas hit and fixed while building this — async SQLAlchemy footguns, an MCP-client redirect bug, a Traefik routing collision. Worth a read if you're extending this.

Status

Built and running in production for one real deployment (Postgres + git-backed history + web UI + MCP server, behind Authelia OIDC via Traefik). Not yet hardened for multi-tenant use — row-level security by project_id is designed but not yet implemented (currently application-layer filtering only); see tasks/lessons.md and the design doc referenced in tasks/todo.md for what's done versus planned.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A database-backed MCP server that acts as a project memory bank, enabling AI assistants to store, retrieve, and search structured context like decisions, tasks, and architecture using SQLite and vector embeddings.
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    Provides self-hosted memory management for coding agents via MCP, with local vector storage and tools for adding, searching, updating, and deleting memories.
    8
    1
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    MCP server for local-first project memory, enabling AI agents to log, search, and retrieve structured project history, summaries, and per-file dossiers with client-specific configuration and diagnostics.
    15
    1
    MIT