Skip to main content
Glama
osaken55

mcp-memory-graph

by osaken55
README.md
# mcp-memory-graph

**A personal memory MCP server for Obsidian / markdown vaults — semantic + keyword + tag + time-aware co-occurrence hybrid retrieval, shared across 3 AI clients (Claude Code / Codex / agy).**

Built for **complex-systems thinkers** who refuse to delete old memories.
Every note stays forever; you just retrieve it better.

[![CI](https://github.com/osaken55/mcp-memory-graph/actions/workflows/ci.yml/badge.svg)](https://github.com/osaken55/mcp-memory-graph/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

---

## Why this exists

You started keeping a memory file alongside your AI assistant. It worked. Then your `MEMORY.md` index passed the 24KB read limit and your AI couldn't even load it. The obvious fix is "compress" — but that means **deleting things you might need next year**.

This server takes the other path: **keep everything, retrieve smarter**.

## The 4 principles

1. **Memory is permanent.** Old notes are not garbage — they're future re-discoveries waiting to happen.
2. **Separate inject from retrieve.** Boot-time context stays tiny; everything else is pulled on demand via MCP tools.
3. **Honor complex-systems thinking.** One keyword should pull 3–5 related memories by *meaning, structure, AND time*.
4. **Evolve without breaking.** Adding a new note never invalidates indexing. Old notes resurface naturally.

## How it retrieves

Four signals merge into a single hybrid score:

| Signal | Source | Weight (default) |
|---|---|---|
| Semantic | [Voyage AI](https://www.voyageai.com/) embeddings (default model: `voyage-multilingual-2`) | 0.5 |
| Keyword | `ripgrep --json` over markdown body (with Node.js fallback) | 0.3 |
| Tag | `metadata.type` + wikilinks + filename tokens | 0.2 |
| Co-occurrence | "Memories accessed together in the same session" (Phase 3) | 0.2 *(addition planned)* |

Weights are overridable per environment:

```bash
MEMORY_WEIGHTS="semantic=0.5,keyword=0.3,tag=0.2"
```

## Tools

| Tool | Purpose |
|---|---|
| `memory.search` | Hybrid retrieval with recency tie-breaker |
| `memory.get` | Read one memory + (optional) auto-prefetch top-2 related |
| `memory.related` | Related memories via wikilink, tag, semantic distance |
| `memory.recent` | Sorted by `last_accessed` (SQLite) |
| `memory.by_tag` | Filter by `metadata.type` |
| `memory.by_session` | **Cluster memories born in the same conversation session** (originSessionId) |
| `memory.list_archived` | Hybrid search without recency boost (surfaces older notes) |

## Install

```bash
git clone https://github.com/osaken55/mcp-memory-graph.git
cd mcp-memory-graph
npm install
npm run build
npm test
```

Default paths:

```text
MEMORY_DIR     = ~/.claude/projects/.../memory/
MEMORY_DB_PATH = ~/.claude/memory-mcp/cache.db        # SQLite is OUTSIDE the memory dir
VOYAGE_MODEL   = voyage-multilingual-2                # override with env
```

## Phase 0 — smoke test (no MCP client needed)

```bash
node dist/bin/probe.js
```

Reports how many `.md` files are readable, how many have/lack frontmatter, and which iCloud / Syncthing artifacts were excluded. **Read-only — zero writes.**

## Register with the 3 AI clients

### Claude Code (CLI / Plugin)

`~/.claude/settings.json`:

```json
{
  "mcpServers": {
    "memory-graph": {
      "command": "mcp-infisical-env",
      "args": ["--", "node", "/Users/junshu/Documents/Projects/mcp-memory-graph/dist/index.js"],
      "env": {
        "MEMORY_AUTO_PREFETCH": "true"
      }
    }
  }
}
```

### Codex (OpenAI Codex CLI / Desktop)

`~/.codex/config.toml` (or via `codex mcp add`):

```toml
[[mcpServers]]
name = "memory-graph"
command = "mcp-infisical-env"
args = ["--", "node", "/Users/junshu/Documents/Projects/mcp-memory-graph/dist/index.js"]
```

### agy (Antigravity CLI)

Use agy's MCP config or wrap in `agy-with-context`:

```bash
# Agy doesn't natively load MCP servers in CLI mode yet;
# in the Antigravity IDE the server registers via the same MCP config above.
```

All three clients hit the **same SQLite cache**. `last_accessed`, embedding cache, and co-occurrence stats are shared — your 3 AI workforce really has one shared memory.

## Why `voyage-multilingual-2` by default

The author's memory is roughly half Japanese, half English (Obsidian Vault notes from a Japanese small business owner). On benchmarks, `voyage-multilingual-2` outperforms `voyage-3.x` on mixed-language retrieval. Pure-English vaults should override with `VOYAGE_MODEL=voyage-3-large`.

## Comparison

| Axis | Typical mcp-memory | mcp-memory-graph |
|---|---|---|
| Storage | Internal DB | Existing flat markdown directory |
| Migration | Imports / rewrites | **Zero-destructive** — never edits your notes |
| Search | Keyword OR vector | Semantic + keyword + tag (+ co-occurrence) hybrid |
| Recency | Often implicit | SQLite `last_accessed`, **outside** memory dir |
| Multilingual | English-tuned | `voyage-multilingual-2` default |
| Failure handling | Vector API down = broken | Falls back to keyword + tag automatically |
| ripgrep absent | Crashes | Node.js `readline` + RegExp fallback |
| Sync artifacts (iCloud / Syncthing) | Pollutes index | Excluded by glob |
| Same-session clustering | None | `memory.by_session(originSessionId)` |
| Auto prefetch | Manual `related` call | Opt-in `MEMORY_AUTO_PREFETCH=true` |

## Roadmap

- **Phase 0** ✅ Probe — non-destructive read of every markdown file.
- **Phase 1** ✅ Hybrid skeleton, 7 tools, SQLite `last_accessed`, ripgrep + Node.js fallback, iCloud-exclude.
- **Phase 2** ✅ Voyage embedding cache, content-hash dedup, multilingual default model, semantic + keyword + tag merged.
- **Phase 3** ✅ Time-aware co-occurrence table (`co_access`), 4th signal in `memory.related`, in-memory ring buffer auto-records pairs from `memory.get` / `memory.search`.
- **Phase 4** ✅ `npx mcp-memory-graph init` zero-config bootstrap; Jaccard coefficients for wikilink / tag signals.
- *(next)* Local embedding fallback via `@xenova/transformers`; multi-tag arrays; tag taxonomy beyond `metadata.type`.

## Real-world numbers (from the author's vault)

Built on a real corpus of 130 personal markdown memories (mix of Japanese + English).

| Operation | Result |
|---|---|
| Phase 0 probe | 131 files / 123 with frontmatter / 0 errors |
| Phase 1 `memory.search("CTO")` | top-1 score 1.000, expected hit at top |
| Phase 2 Voyage indexing | 204 s for 130 files, ~$0.015 (0.26% of free tier) |
| Phase 2 semantic query "責任を取って判断する役割" (no keyword overlap) | top-1 = CTO role memory (score 0.464) |
| Phase 2 multilingual: English query → Japanese memory | top-1 score 0.555 |
| Phase 3 co-occurrence smoke | "memories from the same session cluster correctly" |

## License

MIT — see [LICENSE](./LICENSE).

## Acknowledgements

This server was designed in a 3-AI parallel review pattern that the project author has been refining for months:

- **agy** (Gemini 3.5 Flash High) wrote the Python design draft.
- **Codex** (GPT-5.x) wrote the TypeScript scaffold and ADRs.
- **Claude Code** (Opus 4.7) wrote the CTO integration review and added the 6 unique observations (Phase 0 probe, time-aware co-occurrence, originSessionId clustering, auto-prefetch, zero-config init, the name itself).
- **The owner** (オサケンさん) wrote the principles.

If three AIs and a complex-systems thinker can build a better personal memory, so can yours.