A local MCP server that gives code agents (Claude Code, Antigravity, and any other MCP client) a shared, persistent graph memory, backed by Kuzu — an embedded database purpose-built for graph storage and graph search — plus a live web dashboard showing the memory graph and an activity log.
Typical use: long multi-session work where context must survive — e.g. writing a long story with many interlocking plot threads. The agent stores characters, chapters, plot threads and decisions as entities, attaches facts to them, and links them with typed relations (foreshadows, suspects, part_of, …). Next session — even from a different agent — it recalls and expands that graph instead of starting cold.
Claude Code ──stdio──► bridge (src/index.js) ─┐
├─HTTP─► daemon (src/daemon.js)
Antigravity ──stdio──► bridge (src/index.js) ─┘ ├─ Kuzu graph DB (data/graph.kuzu)
├─ MCP endpoint (POST /mcp)
Browser ── http://127.0.0.1:7688 ──────────────────────┴─ Dashboard + API
Both agents launch the same tiny stdio bridge. The first bridge to start spawns one shared background daemon; every later bridge just connects to it. That is what lets multiple agents use one memory graph at the same time — an embedded DB allows only a single writer process, and here only the daemon ever touches the database. The daemon keeps running after agents disconnect (the dashboard stays available); stop it any time with npm run stop.
The storage engine is Kuzu (v0.11) — an embedded graph database, i.e. a library inside the daemon process, no separate DB server to install. It uses a property-graph model queried with Cypher, and is columnar and vectorized under the hood, optimized precisely for graph search: multi-hop joins, relation traversal, pattern matching. The schema is two node tables (Entity, Observation) and two relationship tables (HAS_OBSERVATION, RELATES).
All data lives in one folder: data/graph.kuzu (plus data/activity.jsonl for the log history). Back up = copy the folder; reset = delete it (with the daemon stopped).
- Node.js ≥ 20 (tested on Node 24, Windows 11)
npm installalready done in this folder (deps:@modelcontextprotocol/sdk,kuzu,zod)
On this machine everything below is already configured. Restart Claude Code / Antigravity (or reload their MCP servers) and the
graf-memorytools appear.
User scope (available in every project) — one command:
claude mcp add --scope user graf-memory -- node "C:/Users/Kuba/Desktop/GrafMCP_For_WorksSpace/src/index.js"Or per-project: copy .mcp.json into the project root. Verify with /mcp inside Claude Code — you should see graf-memory with 12 tools.
Open the Agent panel → ⋯ → MCP Servers → Manage MCP Servers → View raw config, and add the graf-memory block (on this machine the file is C:\Users\Kuba\.gemini\antigravity\mcp_config.json):
{
"mcpServers": {
"graf-memory": {
"command": "node",
"args": ["C:/Users/Kuba/Desktop/GrafMCP_For_WorksSpace/src/index.js"]
}
}
}Then click Refresh in the MCP servers panel.
Point it at node <this folder>/src/index.js over stdio, or — if it supports Streamable HTTP — directly at http://127.0.0.1:7688/mcp (the daemon must already be running for the HTTP variant).
| Tool | What it does | Example arguments |
|---|---|---|
remember |
Store facts about an entity (creates it if new) | { "entity": "Aria Voss", "entity_type": "character", "observations": ["Captain of the Kestrel"] } |
connect |
Create a directed, typed relation between two entities | { "from": "Chapter 1", "to": "The Betrayal", "relation_type": "foreshadows" } |
recall |
Keyword search over names, types and facts | { "query": "consortium" } |
expand |
Graph traversal: entity + everything within N hops (1–3) | { "entity": "Aria Voss", "depth": 2 } |
find_nodes |
Search nodes by name only (case-insensitive "like") — returns each match's node id | { "query": "aria" } |
search_facts |
Search nodes by fact content — returns matching nodes with the specific facts that matched | { "query": "consortium" } |
get_node |
Fetch one node by numeric id or exact name, with its facts and relations | { "id": 7 } or { "name": "Aria Voss" } |
tree |
Hierarchical tree rooted at an entity, following relations out/in/both |
{ "entity": "Chapter 1", "depth": 2 } |
read_graph |
Overview of the whole memory: all entities and relations | {} |
forget |
Delete an entity (by name or node_id), a relation, or a single fact |
{ "entity": "Chapter 1" } or { "node_id": 7 } |
list_workspaces |
List every workspace in use, with entity counts | {} |
cleanup |
Remove exact-duplicate facts and orphaned records | { "workspace": "wh40k-titanfall" } |
recall answers "what do I know about X?"; expand answers "what is connected to X?" — that second question is what a graph database is optimized for, and why this beats a flat notes file for interlocking stories. find_nodes is the narrow name-only lookup, search_facts is its mirror over fact content (which nodes mention something), get_node pulls one node straight by its id/name, and tree reads a structure top-down (chapters → scenes → beats). (recall is the broad one — it spans names, types and facts at once.)
Every tool except list_workspaces also takes an optional workspace argument — see Workspaces below. Omit it and the tool uses this project's automatic workspace (its folder name).
The same read-only views are also exposed as MCP resources, addressable by URI. Clients that support resources (e.g. ReadMcpResourceTool, or a resource picker) can pull graph context by URI without a tool round-trip. All URIs use the graf:// scheme and mirror the tools:
| URI | Returns | Equivalent tool |
|---|---|---|
graf://workspaces |
Every workspace with an entity count | list_workspaces |
graf://workspaces/<workspace>/graph |
Full overview of one workspace (entities + relations) | read_graph |
graf://workspaces/<workspace>/entity/<name> |
One entity with its facts and direct relations | recall (single hit) |
graf://workspaces/<workspace>/expand/<name>?depth=<1-3> |
An entity plus everything within N hops (depth optional, default 1) |
expand |
<workspace> and <name> are path segments, so URL-encode anything with spaces or slashes (B1.5 The Unintended Shadow → B1.5%20The%20Unintended%20Shadow). Example — the depth-2 neighborhood of chapter B1.5 in the the-frontier workspace:
graf://workspaces/the-frontier/expand/B1.5%20The%20Unintended%20Shadow?depth=2
The first three are resource templates plus one static resource (graf://workspaces); all responses are application/json with the same body shape the matching tool returns.
Open http://127.0.0.1:7688 (it starts automatically with the daemon; light and dark theme follow your OS).
- Workspace chooser (first screen) — the dashboard always shows exactly one workspace. On load it asks which one: pick from the list (with entity counts), open a new/empty one by name, or 🗑 delete a workspace right there. Reopen it any time with the Workspaces… button in the header; the header select switches directly.
- 🗑 Delete workspace button (header) — deletes the current workspace with everything in it (entities, facts, relations), after a confirmation. Also available per-row in the workspace chooser. There is no undo — see Backup & restore.
- + New node button (header) — create a node (name, type, workspace, optional first fact) straight from the dashboard.
- Clean up button (header) — runs the
cleanupmaintenance pass against the current workspace and reports how many duplicate/orphaned records it removed. Same thingnpm run cleanupdoes from the terminal. - Graph view — force-directed layout of the memory. Node color = entity type (legend top-left), node size grows with fact count. Click a legend row to hide/show that entity type — a quick filter for large graphs (hidden types show struck-through). Drag nodes, pan the background, zoom with the mouse wheel, hover for a tooltip, click to inspect. Auto-fits the whole graph until you pan/zoom/drag; the Fit button (top-right of the canvas) snaps back.
- Activity log — live feed of every tool call and every dashboard edit, with arguments, duration and errors. Also appended to
data/activity.jsonl. - Entities — filterable list of everything stored; shows each entity's workspace when viewing All workspaces.
- Details — the selected node's id, facts (with ids) and relations, plus a full node editor: rename the node or change its type (✎ Edit), delete it (🗑 Delete node), add a fact / click a fact to edit it / delete a fact (×), and add or remove relations to other entities. Every edit is written through
POST /api/editand shows up in the activity log. - Tree — at the bottom of Details, a collapsible tree rooted at the selected node, following relations
out/in/both(toggle). Click any node in the tree to jump to it. This is the dashboard twin of thetreetool.
Everything the server does lands in the Activity log (dashboard, with filter chips), in data/activity.jsonl, and on the daemon's stderr. Log kinds:
| Kind | What it records |
|---|---|
server |
daemon lifecycle (start, shutdown) |
agent |
an MCP client connected — the line names the agent, e.g. graf-bridge (Claude Code, pid 1234) |
tool |
every tool call: name, duration, arguments, and a preview of the result |
error |
failed tool calls, with the message |
debug |
every raw MCP request method — only when GRAF_MCP_DEBUG=1 |
note |
lines injected from outside via POST /api/log |
To check the connection works: restart the agent, open the dashboard → Activity log → Agents filter — a client connected: graf-bridge (Claude Code…) (or (Antigravity…)) line appears the moment the agent starts the server. Then ask the agent to call any tool and watch the tool line arrive with its arguments and result.
Verbose mode — add GRAF_MCP_DEBUG to the agent's MCP config to also see every MCP request (mcp initialize, mcp tools/list, mcp tools/call, …):
"graf-memory": {
"command": "node",
"args": ["C:/Users/Kuba/Desktop/GrafMCP_For_WorksSpace/src/index.js"],
"env": { "GRAF_MCP_DEBUG": "1" }
}Scripting endpoints: GET /api/logs?after=<seq> returns entries as JSON; POST /api/log with {"kind":"note","message":"..."} writes a line into the log (handy to mark test runs). The agent side of the pipe (bridge stderr: daemon spawning, reconnects) shows up in the agent's own MCP logs — in Claude Code check /mcp → graf-memory.
Tell your agent (Claude Code or Antigravity) something like:
Use the graf-memory tools while we write this story. Store every character, location, chapter and plot thread as entities with facts, and connect them (introduces, foreshadows, pays_off, appears_in, suspects…). Before writing each chapter,
expandthe chapter's characters and threads to depth 2 and stay consistent with what's stored. After each chapter, store what changed.
What this looks like in tool calls:
remember {entity: "Aria Voss", entity_type: "character", observations: ["Captain of the salvage ship Kestrel", "Hides a debt to the Consortium"]}remember {entity: "The Betrayal", entity_type: "plot_thread", observations: ["Someone on the crew sells the route to pirates", "Must pay off by chapter 9"]}connect {from: "Chapter 1", to: "The Betrayal", relation_type: "foreshadows"}- Next session, possibly in the other agent:
recall {query: "betrayal"}→expand {entity: "The Betrayal", depth: 2}— and the agent has the thread, who's involved, and where it was planted.
Suggested entity types (free-form, they just color the dashboard): character, location, plot_thread, chapter, event, decision, fact. The same pattern works for code projects: module, bug, decision, requirement.
Agents only use tools they're reminded of. Add this to CLAUDE.md (Claude Code) or your rules/AGENTS.md (Antigravity):
## Persistent memory
You have graph memory via the graf-memory MCP tools.
- Always pass workspace: "wh40k-titanfall" on every graf-memory call for this project.
- Start of a task: `recall` keywords from the request; `expand` (depth 2) the most relevant hit.
- When you learn a durable fact, decision, or plot point: `remember` it on the right entity.
- When two things are related: `connect` them with a snake_case verb.
- Never store secrets or credentials. Use `forget` when something becomes wrong.
- After a long session, call `cleanup` to remove any duplicate facts you left behind.| Env var | Default | Meaning |
|---|---|---|
GRAF_MCP_PORT |
7688 |
Daemon port (MCP endpoint + dashboard) |
GRAF_MCP_DB |
<this folder>/data/graph.kuzu |
Kuzu database location |
GRAF_MCP_WORKSPACE |
(the launch folder's name) | Overrides the automatic per-folder workspace. Set it to a fixed name to pin a project's workspace, or to default to restore the old single shared memory. |
Set them in the agent's MCP config (env block) — the bridge passes them to the daemon it spawns. Different port + different DB path = a second, fully independent memory (e.g. one per project).
One daemon, one database, one dashboard — but memory is split into named workspaces so unrelated projects (a novel, a codebase, another novel) don't mix entities or collide on names. Two different workspaces can each have their own entity called "Chapter 1" without conflict; everything is tagged, not duplicated.
Each agent's workspace is chosen automatically, at startup, from the folder it was launched in — the bridge uses the launch directory's name (its cwd basename) as the workspace and injects it into every tool call that doesn't name one. So a project is isolated by default with no per-call workspace argument and no CLAUDE.md boilerplate: an agent working in C:/dev/wh40k-titanfall automatically reads and writes the wh40k-titanfall workspace, and its memory never leaks into another project's.
Consequences:
- Different folders → separate memory. Two projects can't see or overwrite each other's entities, even though they share one daemon and database.
- Same folder → shared memory. Claude Code and Antigravity launched in the same project folder land in the same workspace, so they still collaborate on one graph — just scoped to that project.
- Override with
GRAF_MCP_WORKSPACEin the agent's MCPenvblock: set a fixed name to pin the workspace regardless of folder, or set it todefaultto restore the old behavior where everything shares onedefaultworkspace. - Per-call override still wins. Any tool call that passes an explicit
workspaceuses that one, ignoring the folder default.
You can still address workspaces by hand when you want to:
remember {entity: "Julius", entity_type: "character", observations: [...], workspace: "wh40k-titanfall"}recall {query: "julius", workspace: "wh40k-titanfall"}expand {entity: "Julius", depth: 2, workspace: "wh40k-titanfall"}
Pass workspace: "all" on recall, read_graph, find_nodes, search_facts or get_node to search across every workspace at once (not valid on write tools, nor on expand/tree — they need one concrete workspace to start from). list_workspaces shows what already exists.
Pinning a workspace name explicitly: if you don't want the folder-name default, either set GRAF_MCP_WORKSPACE in the MCP config, or put the workspace name in that project's CLAUDE.md / AGENTS.md (e.g. Always pass workspace: "wh40k-titanfall" to graf-memory tools.). There's no per-connection "current workspace" state on the daemon (it's stateless per request) — the workspace travels with each call; the bridge's folder-name injection is what makes that automatic without you repeating it.
The dashboard always shows one workspace at a time: the chooser on load (or the Workspaces… button) picks which, and workspaces can be deleted from there or with the header's 🗑 Delete workspace button. (Tools can still pass workspace: "all" — only the dashboard's all-at-once view is gone.)
Workspaces share one Kuzu database file — enough for keeping projects from colliding, but a bad forget or a corrupted database still affects everyone. For a project that needs a genuinely separate database (its own file, its own backup schedule, its own crash blast radius), give it its own port + DB path in its .mcp.json instead:
{
"mcpServers": {
"graf-memory": {
"command": "node",
"args": ["C:/Users/Kuba/Desktop/GrafMCP_For_WorksSpace/src/index.js"],
"env": {
"GRAF_MCP_PORT": "7690",
"GRAF_MCP_DB": "C:/path/to/that-project/.graf-memory/graph.kuzu"
}
}
}
}That project gets its own daemon, database and dashboard (http://127.0.0.1:7690), fully separate from the shared one. The same env block works in Antigravity's mcp_config.json.
The daemon can run as a container instead of a local process (Dockerfile, docker-compose.yml). The image runs src/daemon.js with the MCP endpoint + dashboard on port 7688 and the database on a named volume graf-data — memory survives container rebuilds.
On this machine there is no Docker Desktop; Docker lives inside WSL Debian, so prefix commands with
wsl -d Debian --. The imagegraf-mcpis already built and was tested end-to-end (tool calls over HTTP + data persisted across a container restart).
# build and start (from this folder; inside WSL use the /mnt/c/... path)
docker compose up -d --build
# check
curl http://127.0.0.1:7688/api/health
node test/http-check.js write # MCP round-trip against the container
# stop / logs
docker compose down
docker logs -f graf-memoryHow agents connect to the containerized daemon — two options:
- Keep the stdio configs unchanged. The bridge health-checks port 7688 first and uses whatever daemon answers — including the container. Caveat: if the container is down, the bridge silently starts a local daemon and your writes land in
data/instead of the Docker volume — two diverging memories. Userestart: unless-stopped(already in the compose file) to avoid that. - Connect over HTTP directly (no bridge, container must be running):
- Claude Code:
claude mcp add --scope user --transport http graf-memory http://127.0.0.1:7688/mcp - Antigravity raw config:
"graf-memory": { "serverUrl": "http://127.0.0.1:7688/mcp" }
- Claude Code:
WSL caveat: with Docker inside WSL (no Docker Desktop), the container only runs while the WSL VM does — Windows shuts the VM down a minute after its last console closes, killing the container (it comes back automatically on the next wsl command thanks to the restart policy). If you want the containerized daemon always-on, keep a WSL window open, set vmIdleTimeout in .wslconfig, or just use the local (non-Docker) daemon — that is the more practical default on this machine.
Long sessions — especially an agent re-storing the same fact across many remember calls — can leave exact duplicates behind. The cleanup tool (and the dashboard's Clean up button, and npm run cleanup) removes:
- duplicate facts — the same text stored twice on the same entity, keeping the earliest copy
- orphaned fact records — an
Observationleft with no owning entity (defensive; normal deletes always cascade, this only catches the abnormal case)
It never touches distinct facts, entities, or relations — safe to run any time. Scope it to one workspace or omit to clean everything:
npm run cleanup # clean every workspace
npm run cleanup -- wh40k-titanfall # clean just that oneIf an agent has been through a very long or chaotic session, running this afterward (or asking the agent to call the cleanup tool itself) is good hygiene. It's not a substitute for backups — see below.
The database is a working file, not an archive — a runaway agent can forget things it shouldn't (this has happened). Snapshot it periodically, especially after a long or heavily-automated session:
npm run backup # writes backups/memory-<timestamp>.json
npm run stop # restore needs exclusive access to the DB
npm run restore -- backups/memory-2026-07-17T22-47-22-411Z.jsonbackup reads the live daemon over HTTP (no need to stop it). restore writes directly to the database files, so the daemon must be stopped first; start it again afterward with npm start or by reconnecting an agent.
| Action | Command |
|---|---|
| Start manually (foreground) | npm start |
| Stop | npm run stop |
| Test everything end-to-end | npm test |
| Snapshot memory to JSON | npm run backup |
| Restore a snapshot (daemon stopped) | npm run restore -- <file> |
| Remove duplicate/orphaned records | npm run cleanup [-- <workspace>] |
| Reset memory completely | npm run stop, then delete the data/ folder |
You normally never start the daemon yourself — the first agent that connects does.
- Tools don't appear in the agent — restart the agent / refresh its MCP servers; check the config path points at
src/index.js; runnpm testhere to confirm the server itself is healthy. - Dashboard "daemon offline" — no daemon running; connect an agent or run
npm start. daemon did not become ready— something else owns port 7688 (netstat -ano | findstr 7688); either free it or setGRAF_MCP_PORTto another port in both agents' configs.- Database locked / won't open — a stray daemon is holding it:
npm run stop(or kill thenodeprocess from the dashboard footer's pid), then retry. - After updating this code — bridges auto-restart an outdated daemon on next connect (version check); or just
npm run stop.
GrafMCP to lokalny serwer MCP dający agentom (Claude Code, Antigravity) wspólną, trwałą pamięć grafową na bazie Kuzu (baza zoptymalizowana pod wyszukiwanie grafowe) + dashboard na http://127.0.0.1:7688 (podgląd grafu i log działań na żywo).
Na tym komputerze wszystko jest już skonfigurowane — wystarczy zrestartować Claude Code i Antigravity. Agent dostaje 12 narzędzi: remember (zapisz fakty), connect (połącz encje relacją), recall (szukaj po nazwach, typach i faktach), expand (pobierz sąsiedztwo w grafie), find_nodes (szukaj węzłów po nazwie), search_facts (szukaj węzłów po treści faktów), get_node (pobierz węzeł po id lub nazwie), tree (drzewo relacji), read_graph (całość), forget (usuń — encję, także po node_id), list_workspaces (lista przestrzeni roboczych), cleanup (usuń duplikaty). Agenci uruchomieni w tym samym folderze dzielą jedną pamięć; uruchomieni w różnych folderach mają pamięć oddzielną.
Przykład — długa historia z wieloma wątkami: poproś agenta, by każdą postać, rozdział i wątek zapisywał jako encję z faktami i łączył je relacjami (foreshadows, appears_in…), a przed pisaniem kolejnego rozdziału wywoływał expand na bohaterach rozdziału. Zarządzanie: npm start / npm run stop / npm test; pełny reset = stop + usunięcie folderu data/.
Baza danych: Kuzu — wbudowana (embedded) grafowa baza z językiem Cypher; wszystkie dane w folderze data/. Przestrzeń robocza jest wybierana automatycznie z nazwy folderu, w którym startuje agent (cwd), więc każdy projekt ma osobną pamięć bez podawania parametru workspace — dwa projekty mogą mieć encję o tej samej nazwie bez konfliktu. Można to nadpisać zmienną GRAF_MCP_WORKSPACE (stała nazwa albo default, by wrócić do jednej wspólnej pamięci) albo podając workspace w wywołaniu. Węzły można też tworzyć i edytować bezpośrednio w dashboardzie (przycisk + New node, edycja nazwy/typu/faktów/relacji w panelu Details, widok Tree). Pełna fizyczna izolacja (osobna baza) nadal możliwa przez własny GRAF_MCP_PORT/GRAF_MCP_DB. Duplikaty faktów po długiej sesji usuwa npm run cleanup lub przycisk Clean up w dashboardzie. Wersja w kontenerze: wsl -d Debian -- docker compose up -d --build (sekcja Docker).