Skip to main content
Glama

shared-memory

CI License: MIT Python 3.10+ shared-memory MCP server

Listed on Glama: https://glama.ai/mcp/servers/fazerluga-creator/shared-memory

A portable, self-hosted semantic memory layer for LLM agents, exposed over the Model Context Protocol (MCP).

Point any number of agents — Claude Code, other MCP clients, or your own scripts — at one shared store and let them recall past decisions, notes, and conversations by meaning instead of grepping files.

  • One SQLite file. Text, a full-text index and vectors live together; no database service to run. ChromaDB is still available as an option.

  • Hybrid ranking. Vector similarity, plus exact matching of identifiers (order numbers, ABC-123 ids) that vectors alone miss, plus weights that put distilled notes above raw transcripts. See Search quality.

  • Local by default. Embeddings run on-device via fastembed; nothing leaves the host unless you opt into a remote API (OpenAI-compatible or Voyage AI).

  • One store, many agents. Every chunk carries an agent and source tag, so searches can be global or scoped to a single agent.

  • Seven MCP tools. memory_search, memory_add, memory_update, memory_forget, memory_stats, memory_links, memory_graph.

  • Bring your own content. Ships with a generic markdown indexer; writing an adapter for chat logs, tickets, or docs is a short script.

Why

Agents are stateless between sessions. The common fix is a short "always-loaded" memory file, but it can't scale — you can't paste everything you've ever decided into every prompt.

shared-memory is the second layer: an unbounded store you search on demand.

  • Layer A — a short, hand-curated memory file loaded into every prompt (you keep whatever you already use for this).

  • Layer B (this project) — unbounded, searched semantically. "What did we decide about X?" returns the relevant past chunk instead of a file dump.

Related MCP server: mnemo-mcp

Architecture

                 ┌────────────────┐
   agent A ─┐    │  MCP server    │   memory_search      ──▶  memory.sqlite
   agent B ─┼──▶ │  (FastMCP)     │   memory_add / update      chunks + FTS5 + vectors
   scripts ─┘    │                │   memory_forget / stats    (or ChromaDB, optional)
                 └────────────────┘
        ▲
        │ indexers / adapters
   your content (markdown, sessions, docs, …)
shared-memory/
├── shared_memory/
│   ├── server.py         # FastMCP server, the memory_* tools       → shared-memory-mcp
│   ├── index_markdown.py # generic adapter: index a folder of *.md  → shared-memory-index
│   ├── migrate.py        # copy a Chroma store into SQLite          → shared-memory-migrate
│   ├── embedder.py       # embeddings: local fastembed | OpenAI-compatible | Voyage AI
│   ├── store.py          # backend selection, shared helpers
│   └── store_sqlite.py   # SQLite backend: storage, hybrid ranking, Chroma-style API
├── mcp-server/server.py  # compatibility shim for 0.1/0.2 configs (removed in 1.0)
├── embedding.env.example # copy to embedding.env to customize (optional)
└── requirements.txt

Install

Requires Python 3.10+.

git clone https://github.com/fazerluga-creator/shared-memory.git
cd shared-memory
python -m venv .venv && source .venv/bin/activate
pip install -e .              # SQLite backend (default)
pip install -e ".[chroma]"    # also the ChromaDB backend / migration

Or without cloning:

pip install "git+https://github.com/fazerluga-creator/shared-memory.git@v0.3.0"

This installs three commands: shared-memory-mcp (the server), shared-memory-index (markdown indexer) and shared-memory-migrate.

Where data lives. From a clone (pip install -e .) the config and data stay in the repo folder: embedding.env, memory.sqlite. From a regular install they live in ~/.local/share/shared-memory/ ($XDG_DATA_HOME/shared-memory). SHARED_MEMORY_HOME moves both; SHARED_MEMORY_ENV / SHARED_MEMORY_DB set single files.


Optionally copy the config and tweak it (defaults are fine to start):

```bash
cp embedding.env.example embedding.env

The first run downloads the local embedding model (a few hundred MB) once. With a remote provider (voyage, openai_compatible) nothing is downloaded.

Docker

docker build -t shared-memory .
docker run -i --rm -v shared-memory-data:/data shared-memory

The container speaks MCP over stdio and needs no credentials: the SQLite store lives in /data and embeddings run locally (the model is downloaded on the first search or add). Mount /data to keep the memory between runs.

fazerluga-creator/shared-memory MCP server

Quick start

Index some markdown and search it:

# 1. ingest a folder of notes
shared-memory-index ./notes --agent notes

# 2. run the MCP server (stdio transport)
shared-memory-mcp
# or over HTTP on 127.0.0.1:8000 (read the Security section first)
shared-memory-mcp --transport http

Or use the library directly:

from shared_memory import Embedder, get_client, get_collection

col = get_collection(get_client())
emb = Embedder()

col.add(
    ids=["note::1"],
    embeddings=[emb.embed_document_one("Ship the beta on Friday, feature-flag the new UI.")],
    documents=["Ship the beta on Friday, feature-flag the new UI."],
    metadatas=[{"agent": "notes", "source": "manual"}],
)

res = col.query(query_embeddings=[emb.embed_one("when is the beta?")], n_results=3)
print(res["documents"])

MCP tools

Tool

Signature

Returns

memory_search

(query, top_k=5, agent_filter=None, source_filter=None, since=None)

[{id, text, metadata, score}, ...] sorted by relevance

memory_add

(text, metadata=None)

id of the added chunk

memory_update

(id, text=None, metadata=None, replace_metadata=False)

{id, changed, metadata}; the id is kept, unknown ids raise

memory_forget

(ids=None, agent_filter=None, source_filter=None, dry_run=True, expect_count=None)

{matched, deleted, dry_run, sample}

memory_stats

()

{total, by_agent, by_source, collection, backend}

memory_links

(id, type=None, limit=50)

[{from_id, to_id, type, score, created_at}, ...] either direction

memory_graph

(filter=None, limit=100)

{nodes, edges}; at most 200 nodes and 500 edges

memory_forget is a dry run unless you pass dry_run=False; deleting by filter also requires expect_count equal to the number of matches, and a call with no ids and no filters is refused.

Example memory_search result:

[
  {
    "id": "notes::markdown::decisions.md::003",
    "text": "decisions.md › Release: ship the beta on Friday, feature-flag the new UI.",
    "metadata": {"agent": "notes", "source": "markdown", "kind": "note",
                 "timestamp": "2026-10-01T09:12:00+00:00"},
    "score": 0.7134
  }
]

Filters:

  • agent_filter — restrict to a single metadata.agent value.

  • source_filter — restrict to a metadata.source value (e.g. markdown, sessions).

  • since — ISO timestamp lower bound on metadata.timestamp. On the SQLite backend it is applied before ranking, so a narrow window on a large store still returns the matches inside it.

On SQLite, memory_add also writes cross-links (failures are logged and do not fail the insert):

  • similar — hybrid-search neighbours, top 5, score ≥ SHARED_MEMORY_LINK_THRESHOLD (default 0.75), excluding the new chunk itself.

  • tag — other chunks that share a [[topic]] marker in the text, at most 20.

memory_links lists those rows. memory_graph returns the same edges as {nodes, edges} for a UI or a prompt. filter is a substring of the chunk id, text, or metadata.agent; direct neighbours are included until the cap. Deleting a chunk removes its links. The chunks table itself is unchanged.

scripts/link_backfill.py rebuilds similar links for chunks that were stored before this existed. It is a dry-run unless you pass --apply. --llm confirms each new pair through OmniRoute at http://127.0.0.1:20128/v1/chat/completions (API key from OMNIROUTE_API_KEY or a local file, never hardcoded) and stores type=llm. The script does not install cron.

Connect to Claude Code / any MCP client

Add the server to your client config (paths are examples):

{
  "mcpServers": {
    "shared-memory": {
      "command": "/path/to/shared-memory/.venv/bin/shared-memory-mcp"
    }
  }
}

Security

The server has no authentication. Anyone who can reach it can read every memory, add to it, rewrite it and delete it. Run it over stdio, or bind the HTTP transport to 127.0.0.1 (FastMCP's default), and never expose it to the internet or to a network you do not fully control. shared-memory-mcp --transport http refuses a non-loopback --host unless you also pass --allow-unauthenticated-remote. If several machines need it, put it behind a VPN or an authenticating reverse proxy.

Memories tend to collect secrets that were pasted into chats. Treat the database file like a password store: restrictive file permissions (chmod 600) and encrypted backups. With a remote embedding provider every chunk is sent to that provider; filter secrets out in your indexer before that if it matters.

Configuration

All settings are read from embedding.env (see embedding.env.example). Paths are overridable by environment variable:

Variable

Default

Purpose

SHARED_MEMORY_ENV

./embedding.env

location of the env file

SHARED_MEMORY_BACKEND

sqlite

sqlite or chroma (see below)

SHARED_MEMORY_DB

./memory.sqlite

SQLite database file

SHARED_MEMORY_LINK_THRESHOLD

0.75

minimum hybrid score for a similar link

SHARED_MEMORY_DATA_DIR

./chroma_data

ChromaDB persistence directory

EMBEDDING_PROVIDER

local

local (fastembed), openai_compatible or voyage

EMBEDDING_MODEL

MiniLM multilingual (voyage-4 for voyage)

embedding model id

EMBEDDING_COLLECTION

shared_memory

collection name (display only on SQLite)

To route embeddings through a remote OpenAI-compatible /v1/embeddings endpoint, set EMBEDDING_PROVIDER=openai_compatible and fill in EMBEDDING_BASE_URL / EMBEDDING_API_KEY. For Voyage AI set EMBEDDING_PROVIDER=voyage and EMBEDDING_API_KEY (or VOYAGE_API_KEY).

Vectors are tagged with the model that produced them and search ignores vectors of any other model, so changing the provider or model means re-indexing (shared-memory-migrate --reembed does this for a Chroma store).

Upgrading from 0.1.x (Chroma)

If an existing chroma_data directory is found and no memory.sqlite, 0.2.0 keeps using Chroma and logs a warning, so nothing disappears after an upgrade. To move to SQLite:

pip install -e ".[chroma]"
shared-memory-migrate   # reuses stored vectors, no API calls
# check memory_stats / a few searches, then set SHARED_MEMORY_BACKEND=sqlite

Search quality

On the SQLite backend a chunk's score is its cosine similarity times a weight for metadata.kind — note 1.0, doc 0.97, transcript 0.90, anything else 1.0 — plus 0.3 when the query contains an exact token (a number of 4+ digits, a word with a digit, ABC-123) that the chunk contains verbatim. Chunks with metadata.superseded: true are multiplied by 0.9: still findable, just below the current version. Without a query vector (the embedding API is down) search falls back to BM25 full-text ranking instead of failing.

Measured on a private 32-question benchmark over a real 25k-chunk multi-agent memory (right file among the top 3 results):

setup

top-3

ChromaDB + local MiniLM (0.1.x)

16 / 32

Voyage voyage-4, vectors only

26 / 32

Voyage + kind weights + exact tokens + superseded (0.2.0)

30 / 32

The numbers come from one corpus; treat them as a direction, not a guarantee.

Upgrading from 0.2.x

The lib package is now shared_memory: change from lib.store import ... to from shared_memory.store import ... (or from shared_memory import ...). Client configs that run mcp-server/server.py keep working through a shim; switch them to the shared-memory-mcp command when convenient. Data files in a clone stay where they were.

Agents without access to the server: a file bus

The MCP server is meant to stay on a private machine (see Security). Agents that cannot reach it, such as a chat assistant on a phone or a hosted agent with only a Google Drive or Dropbox connector, can still share work through a plain shared folder used as a message bus. One setup that works in practice:

Agent Bus/
├── 00 Welcome          # what this is, which files to read first
├── 01 Rules            # task and answer formats, what never goes here (secrets, personal data)
├── 01_Inbox            # one file per task: task_id, from, to, priority, goal, expected result
├── 02_Outbox           # one file per answer: task_id, agent, status done|blocked|needs_review
├── 03_Shared Memory    # confirmed facts and decisions only, one file per decision
└── 05_Logs             # traces, daily snapshots

Rules that keep it sane: every task and answer carries a task_id; silence from another agent is never treated as agreement; a record is a proposal, not permission to act on anything irreversible; secrets stay on the server and the bus carries only references. An agent that does have server access (for example Claude Code on the same machine) bridges the two: it reads the inbox, does the work with memory_search / memory_add, and writes confirmed facts back as files. shared-memory does not ship a bus; the folder layout above is a convention, not code.

Writing your own indexer

shared_memory/index_markdown.py is the template. An adapter needs to:

  1. discover source items,

  2. turn each into (text, metadata) chunks — always set agent and source, an ISO timestamp if you want since filtering, and kind (note / doc / transcript) if you want the ranking weights,

  3. call add_chunks(collection, embedder, ids, texts, metadatas, upsert=True).

Use a stable, deterministic id scheme so re-runs upsert instead of duplicating.

Design notes

  • Embeddings are computed by us, not by the store. Vectors are passed explicitly on add/query, so the store stays decoupled from embedder availability.

  • No vector index on SQLite. Search is a brute-force cosine over an in-memory numpy matrix: 25k chunks x 1024 dims is ~100 MB and a few milliseconds per query, with nothing to corrupt or rebuild. The matrix is reloaded when another process writes to the file.

  • Chunking (markdown): split by ## headings; fall back to paragraphs; soft-cap 8000 chars at word/newline boundaries. Paths in ids/metadata use POSIX form for cross-OS stability.

  • Timestamps are ISO-UTC strings throughout; the SQLite backend normalizes them on write, so Z, +02:00, naive and unix-epoch inputs compare correctly.

  • Search-time hygiene: results are de-duplicated by content hash and short known-error strings are dropped. Scores are clamped to [0, 1].

  • Stats: memory_stats pages through metadatas to bound peak memory.

Roadmap

  • TTL / forgetting policy for high-volume sources.

  • Optional PII filter at index time.

  • Session-end hooks to auto-index new content.

  • More example adapters (chat logs, issue trackers).

Contributing

Issues and PRs welcome — see CONTRIBUTING.md. New indexer adapters (chat logs, issue trackers, docs) are a great place to start.

License

MIT.

Available Tools

5 tools
memory_addMemory AddC

Add an ad-hoc memory chunk. Returns the generated id.

Inputs: text (required), metadata. Returns: result

memory_forgetMemory ForgetA

Delete memory chunks by explicit ids or by agent/source filter.

Deletion is irreversible, so there are two safety catches: dry_run is on by default, and deleting by filter requires expect_count equal to the number of matches, so an over-broad filter cannot wipe the collection by mistake. A call with no ids and no filters is refused.

Inputs: ids, dry_run, agent_filter, expect_count, +1 more. Returns: object

memory_statsMemory StatsA

Return collection statistics: total count, breakdown by agent/source.

Inputs: no named parameters. Returns: object

memory_updateMemory UpdateA

Update an existing memory chunk in place; the id is kept.

Keeping the id means references to it stay valid. An unknown id raises, so a typo never silently turns into a new chunk.

Inputs: id (required), text, metadata, replace_metadata. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 5 tools

    Disambiguation5/5

    Each tool maps to a distinct lifecycle operation on a single resource: add, search, update, forget, stats. There is no meaningful overlap — search retrieves, stats aggregates, and the write operations are cleanly separated.

    Naming Consistency5/5

    All five tools follow a strict memory_<verb> snake_case pattern with clear, conventional verbs (add, search, update, forget, stats). No mixed conventions or vague names.

    Tool Count5/5

    Five tools is a tight, well-scoped surface for a memory store: write, read, mutate, delete, and observe. Every tool earns its place with no redundancy or filler.

    Completeness4/5

    The set covers the full CRUD lifecycle plus stats, and search likely substitutes for a direct get-by-id. Minor gaps remain around bulk operations and explicit id-based retrieval, but core workflows are covered without dead ends.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Self-hosted semantic memory for AI agents. Save worklogs, decisions, and notes via MCP, then recall them across sessions by meaning rather than keyword. Backed by Postgres + pgvector with local embeddings (multilingual-e5-base).
      1
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      A portable self-hosted memory layer for AI tools, storing context, memories, and handoffs for access from any MCP-compatible client.
      48 npm
      MIT