Back to MCP Servers

Keymem

Associative key-graph memory for LLM agents — recall by association, not vector similarity.

workplace-productivityllmagent
By donggyun112
3Updated 3 days agoTypeScriptMIT

Installation

npx -y keymem

Configuration

{
  "mcpServers": {
    "keymem": {
      "command": "npx",
      "args": ["-y", "keymem"]
    }
  }
}

How to use

  1. Run the installation command above (if needed)
  2. Open your Claude Code settings file (~/.claude/settings.json)
  3. Add the configuration to the mcpServers section
  4. Restart Claude Code to apply changes

keymem

npm version Node.js License: MIT

The associative memory layer for LLM agents — recall by association, not just similarity.

keymem demo — asking about the party cake surfaces Mina's peanut allergy via a shared key

Most agent memory is a vector store. It surfaces what sounds like your query — and misses everything your query is connected to.

keymem stores memories in a key graph instead. A search for "Newton" can still reach "strawberries" — Newton → apple → fruit → strawberry. The path lives in the graph, not in embedding space. It runs locally as an MCP server, so any MCP-compatible agent gets human-like associative recall with no external database.

Works with: Claude Desktop · Claude Code · any MCP-compatible LLM agent


Why associative memory?

Vector-store memory retrieves by embedding similarity. That works until the thing you need isn't similar to the words you typed:

Query: "Newton"
Similarity search finds: "Newton discovered gravity"  ✅
Similarity search misses: "user likes strawberries"   ❌

A person makes the leap anyway — Newton reminds them of the apple, apples are fruit, they like strawberries. keymem makes that same leap because the path exists in the key graph: Newton → apple memory → fruit key → strawberry memory. No embedding distance connects "Newton" and "strawberry"; a chain of shared keys does.

This is the core idea: memories are not islands ranked by distance. They are nodes in an N:M key/value graph that an agent can walk.


How it works

Key Space (concepts)         Value Space (memories)
[apple]   ────────┬─────────→      ↑ same memory
[gravity] ────────┘
                  │
[apple]   ────────┼─────────→ "apples are red fruit"
[fruit]   ──────┬─┘
[red]     ──────┤
                │
[fruit]   ──────┼─────────→ "user likes strawberries"
[strawberry]────┘

Memories live in a Value Space, reached through a separate Key Space — one memory reachable via many keys, one key leading to many memories.

recall("Newton") returns matching key clusters such as [Newton] and [apple], not memory content. The agent then navigates explicitly: read_key(apple) → select the Newton memory → read_memory(...) → discover its [fruit] key → read_key(fruit) → select the strawberry memory.

The default MCP flow is therefore Key → Memory → Key. Full memory content enters the model context only when the agent deliberately calls read_memory() — so broad concepts never flood the context window.


Quick Start

keymem is an MCP server (a CLI), not a library. Run it with npx -y keymem (recommended — always the latest) or install the command globally with npm i -g keymem. Do not add it to your app with npm i keymem as a dependency: it bundles openai, zod, and the MCP SDK, so inside an existing project it just duplicates those trees (and can clash with your app's zod/ openai versions). The npm i keymem line npm shows on the package page is for libraries — it doesn't apply here.

# Optional global install (npx needs none). This puts a `keymem` command on PATH that
# MCP clients can spawn. Run bare, it starts a stdio MCP server and waits for a client —
# so point your MCP config at `keymem` (or just use `npx -y keymem` as shown below).
npm i -g keymem

Claude Desktop

Add to claude_desktop_config.json:

OpenAI embeddings:

{
  "mcpServers": {
    "keymem": {
      "command": "npx",
      "args": ["-y", "keymem"],
      "env": {
        "OPENAI_API_KEY": "your-openai-api-key"
      }
    }
  }
}

Local embeddings (no API key required) — bge-m3 recommended:

{
  "mcpServers": {
    "keymem": {
      "command": "npx",
      "args": ["-y", "keymem"],
      "env": {
        "EMBEDDING_BACKEND": "local",
        "LOCAL_EMBEDDING_MODEL": "bge-m3"
      }
    }
  }
}

bge-m3 (multilingual, recommended) auto-downloads ~570MB on first run, then caches. Omit LOCAL_EMBEDDING_MODEL for the lighter default (fast-multilingual-e5-large). Add "KEYMEM_RERANK": "true" to enable cross-encoder reranking (downloads a second model on first use).

Claude Code

# OpenAI embeddings
claude mcp add keymem -e OPENAI_API_KEY=your-key -- npx -y keymem

# Local embeddings (no API key required) — bge-m3 recommended (auto-downloads ~570MB on first run)
claude mcp add keymem -e EMBEDDING_BACKEND=local -e LOCAL_EMBEDDING_MODEL=bge-m3 -- npx -y keymem

That's it — recall and remember work immediately. The agent calls recall before its first reply, navigates with read_key/read_memory, and saves with remember.

For reliable proactive saving in Claude Code, add the following to ~/.claude/CLAUDE.md (MCP prompts are not automatically applied as persistent Claude Code instructions):

## keymem

- Before the first reply and whenever the topic changes, call `recall` silently with short noun-keyword queries.
- Before ending every reply, check whether this turn revealed a durable fact: a name, preference, decision, correction, project fact, or goal.
- If it did, call `remember` or `remember_batch` silently in the same turn with 3-6 diverse keys. A durable fact left unsaved is a bug.
- Use `correct` when existing information changes. Save nothing only when the turn revealed nothing durable.
- Never mention memory lookup or saving to the user.

For other MCP clients, include the memory_system_prompt MCP prompt in the agent's persistent system instructions.

Manual / Development

git clone https://github.com/donggyun112/keymem
cd keymem
pnpm install

Create .env:

OPENAI_API_KEY=your-openai-api-key
OPENAI_EMBEDDING_MODEL=text-embedding-3-small

Or use local embeddings (no API key required):

EMBEDDING_BACKEND=local
LOCAL_EMBEDDING_MODEL=fast-multilingual-e5-large  # default; best fit for Korean/multilingual keys
pnpm dev
# or:
pnpm build
pnpm start

Requirements:

  • Node.js 20+
  • pnpm for local development
  • OpenAI API key for OpenAI embeddings, or fastembed for local embeddings

Features

  • N:M key/value graph — memories and the concepts that index them are separate spaces, linked many-to-many. One memory is reachable through many keys; one key leads to many memories.
  • Agent-driven Key → Memory → Key navigation — the agent walks the graph deliberately instead of collapsing it into one opaque similarity search.
  • Associative multi-hop recall — reach memories no embedding distance would connect, by following chains of shared keys.
  • Depth system — every memory has a stability score 0.0 → 1.0. Frequently recalled facts deepen, stabilize, and decay slower.
  • Versioning, not overwriting — corrections preserve the full history (when a belief changed, and from what).
  • Key typesconcept keys match by similarity; name/proper_noun keys match exactly, so "동건" never matches "뉴턴" just for being short.
  • Cross-lingual key merging (IDF)파이썬 and Python collapse into one canonical cluster instead of fragmenting the key space.
  • Hebbian link learning — the path an agent actually traverses gets reinforced ("fire together, wire together"), so useful associations become easier to reach.
  • Hybrid retrieval (optional direct mode) — BM25 + dense + Reciprocal Rank Fusion, with depth/time modulation and configurable multi-hop expansion.
  • Cross-encoder reranking (opt-in) — bge-reranker-v2-m3 re-scores candidates in direct mode.
  • Local-first — all data in a local JSON graph; no external database. OpenAI or fully-local embeddings (auto-downloaded).

Depth System

Every memory has a depth score 0.0 → 1.0:

StageDepthBehavior
Shallow< 0.3Recent, unverified. Easy to update or forget.
Medium0.3–0.7Confirmed multiple times. Stable.
Deep> 0.7Well-established fact. Resists correction.

Depth increases +0.05 only when the agent confirms a fact with read_memory(). Key search, read_key(), and passive inject:true previews do not deepen memories. Deep memories decay slower over time. If you try to correct a deep memory, it resists — its depth stays higher even after supersede.

Key Types

Not all keys should behave the same. Names shouldn't match semantically — "동건" shouldn't match "뉴턴" just because they're both short Korean words.

TypeMatchingUse Case
concept (default)Embedding similarity ≥ threshold (0.28 OpenAI / 0.60 local)Topics, categories, attributes
nameExact match onlyPerson names
proper_nounExact match onlyBrands, places

Name/proper_noun keys also get an IDF penalty (×0.5) when they become hub keys connected to many memories, preventing them from polluting unrelated searches.

Versioning (not overwriting)

"user lives in Seoul"   (depth: 0.4 → weakened to 0.12, preserved)
        ↑ superseded by
"user moved to Busan"   (depth: 0.0, new)

keymem keeps the full history instead of overwriting on change. Every correction is traceable — when did the belief change, and from what session?

Key Merging

Add key "파이썬"  → finds existing "Python" (similarity 0.87 > threshold 0.85)
                 → reuses existing key instead of creating duplicate

Prevents key space fragmentation. The same concept across languages or phrasing stays unified.

Agent-driven Retrieval (default)

The default MCP API keeps Key Space and Value Space separate:

  1. recall(query) searches canonical keys and aliases. It returns key IDs, concept labels, match scores, linked-memory counts, hub status, and specificity — never memory content.
  2. read_key(key_id) returns ranked memory IDs and metadata, never content. Hub keys are paginated with limit/offset so broad concepts cannot flood context.
  3. read_memory(memory_id, via_key_id) returns the full memory plus every connected key cluster. Only this full read increases memory depth/access count; only the traversed via_key_id edge receives Hebbian reinforcement.
  4. The agent follows any returned key with another read_key() call, producing an explicit Key → Memory → Key graph walk.

Semantically merged keys are preserved as aliases on one canonical key cluster (for example Python + 파이썬). The recommended bge-m3 profile enables conservative short-key merging by default; override or disable it with KEYMEM_SHORT_KEY_MERGE. A key linked to at least three active memories is surfaced as a hub with is_hub, memory_count, and specificity metadata rather than being hidden by IDF. Override the hub threshold with KEYMEM_KEY_HUB_MIN_LINKS.

Direct Hybrid Retrieval (optional compatibility mode)

Set KEYMEM_DIRECT_RECALL=true to expose recall_memories(), a one-call memory retrieval path. Three signals run in parallel and are fused with Reciprocal Rank Fusion (RRF_K = 60):

  • BM25 (sparse): lexical full-text search over memory content (MiniSearch, fuzzy + prefix). Catches exact terms, names, and rare tokens that embeddings blur.
  • Dense Path A (key matching): query embedding → match keys → follow links → memories. Score = keySim × IDF × linkWeight, summed across all matching keys.
  • Dense Path B (content matching): query embedding → directly compare against memory content embeddings. Finds memories even when they weren't tagged with the right keys.

Sparse and dense rank lists are merged by RRF, then modulated by depth and time before configurable multi-hop expansion (hops=1–5, default 2). This compatibility tool is hidden by default so agents use explicit key navigatio

View source on GitHub