Skip to main content
Glama
pavex

mcp-memory-graphdb

by pavex

Persistent Graph Memory for Claude & LLM Agents

A persistent graph-database memory server for Claude and other LLM agents, built on the Model Context Protocol (MCP) and powered by DuckDB.

Unlike flat key-value memory stores, mcp-memory-graphdb lets an AI agent remember people, projects, concepts and the relationships between them — as a real graph, stored locally in a single file, with a schema you control and can evolve safely over time.

If you're looking for a Claude memory MCP server, a persistent memory backend for LLM agents, or a lightweight embedded graph database for AI tooling, this project is built exactly for that.


Table of contents


Related MCP server: MCP Memory Server

Why a graph, not just key-value memory

Most memory servers for LLM agents store isolated facts: a note, a string, a timestamp. That's fine until the agent needs to answer something like "what projects is Pavel working on that use DuckDB?" — a question that's really about relationships, not isolated facts.

mcp-memory-graphdb stores two things: nodes (a person, a project, a concept, anything with a type and properties) and edges (a directed, typed relationship between two nodes — works_on, uses, knows, part_of, whatever your schema defines). The agent can then walk the graph with ordinary SQL — joins, filters, recursive traversals — instead of trying to reconstruct relationships from disconnected text snippets.

It runs on DuckDB, an embedded, file-based analytical database. No server process, no Docker container, no network port — just one .duckdb file on disk that the MCP server opens directly.

Features

  • Persistent across sessions — memory survives restarts, new conversations, even reinstalling the client. It's just a file.

  • Real relationships, not just facts — typed, directed edges between typed nodes, queryable with full SQL.

  • User-defined schema — you decide what kinds of things and relationships exist in your memory. Ships with a sensible default, fully customizable.

  • Safe schema evolution — adding new types is always safe. Renaming and removing types go through an explicit, validated migration process — never silent, never guessed.

  • Zero external dependencies at runtime — DuckDB is embedded; the whole server is one bundled JS file plus native bindings.

  • One-click installable — packaged as a .mcpb Desktop Extension for Claude Desktop.

  • MCP-native — exposes its tools and an onboarding setup prompt through the standard Model Context Protocol, so any MCP-compatible client can use it.

Requirements

Installation

Option A — one-click install via .mcpb

The easiest path. A .mcpb file is a Desktop Extension — a self-contained package that Claude Desktop can install with a double-click, no manual JSON editing required.

  1. Build the package yourself (there's no prebuilt release yet — see Packaging as a .mcpb extension), or grab mcpb/mcp-memory-graphdb.mcpb if someone already built it for you.

  2. Double-click the .mcpb file. Claude Desktop will open an install prompt.

  3. Optionally set a custom database name in the install dialog (see Choosing the database file).

  4. Confirm. The server is now available in every new conversation.

Option B — manual install (claude_desktop_config.json)

If you'd rather wire it in by hand, or you're using a different MCP client:

  1. Clone this repository and build it:

    git clone https://github.com/pavex/mcp-memory-graphdb.git
    cd mcp-memory-graphdb
    build.cmd   # Windows
    ./build.sh  # macOS / Linux

    This installs dependencies, bundles the server into dist/, copies the native DuckDB bindings next to it, runs the full test suite, and cleans up node_modules afterwards. dist/ is fully self-contained once it's done.

  2. Add the server to your client's MCP config. For Claude Desktop, that's claude_desktop_config.json:

    {
      "mcpServers": {
        "memory-graphdb": {
          "command": "node",
          "args": [
            "/absolute/path/to/mcp-memory-graphdb/dist/mcp.js",
            "/absolute/path/to/your/memory.duckdb"
          ]
        }
      }
    }
  3. Restart your client.

Choosing the database file

The server takes one optional argument: where to put the .duckdb file.

Argument

Result

(none)

.var/memory.duckdb inside the project folder

work

.var/work.duckdb inside the project folder — a named, separate memory

/absolute/path/to/file.duckdb

exactly that path, anywhere on disk

A path containing / or \ is always treated as a direct file location; anything else is treated as a short name and stored under .var/ next to the server. This lets you run multiple independent memories — e.g. one per project, or one personal and one work-related — just by pointing each MCP server entry at a different name or path.

The schema file (schema.yaml, see below) always lives in the same directory as the database file.

Multiple sessions sharing one database

Every Claude session starts its own server process, and DuckDB lets only one process hold a database file open at a time. The server therefore never keeps the file open: each tool call opens the database, runs as a single transaction and closes it again, which releases the file lock within a few tens of milliseconds. If another session holds the lock at that moment, the call waits and retries for up to 10 seconds before it returns a "database is busy" error. Schema changes made in one session are picked up by the others on their next tool call.

How it works

On first startup against a fresh database, the server:

  1. Creates the nodes and edges tables in the DuckDB file.

  2. Writes a default schema.yaml next to the database (this is schema revision 0 — see The default schema).

  3. Exposes a setup MCP prompt that walks the agent through reviewing and optionally customizing that schema with the user.

From then on, every conversation that connects to this server can read and write nodes and edges through the tools below, and the schema can be safely extended at any time through apply_schema. As the graph grows, dreaming provides a guided way to walk through it in batches and clean up duplicates or fill in missing relationships. And at the start of each new conversation, bootstrap loads whatever identity and context the agent has chosen to remember about itself and the user (see Bootstrap — session identity).

The tools

Tool

Purpose

add_node

Create a node — give it a type (must exist in the schema), optional labels (space-separated string or array; normalized and de-duplicated on write), and a free-form properties object. Properties are free-form. A new property key or label that resembles an existing one (note_career → note-career, fixes → bugfix, code-review → review) is stored but reported back in warnings, so the agent can reuse the established name; a new label sharing a whole word with existing ones (static-export → static-site) gets a weaker "related existing" hint. New, unrelated names produce no warning.

update_node

Merge new properties and/or replace labels on an existing node. Returns only what changed (id, type, labels, changed properties) plus the same warnings as add_node. With merge_from, merge duplicate nodes into it (see Dreaming).

delete_node

Delete a node. Any edges touching it are deleted automatically.

get_node

Fetch a single node by ID.

add_edge

Create a directed, typed edge between two existing nodes. The type must exist in the schema; both nodes must exist.

delete_edge

Delete a single edge by ID.

list_edges

List edges, optionally filtered by source_id, target_id and/or type.

search

Case-insensitive text search across node properties, optionally filtered by type.

get_schema

Return the current schema as YAML, including its revision number, plus the vocabulary in use: node counts per type and labels used at least twice with their counts (labels_total gives the number of all distinct labels). Types found in the data but missing from the schema are listed under unknown_types.

apply_schema

Apply additions, renames or removals to the schema (see below).

dreaming

Get a paginated batch of nodes with their immediate edges, for guided cleanup — merging duplicates and adding missing relationships (see below).

bootstrap

Load whatever identity, user context and working rules the agent has saved for itself, in one call (see below).

There is deliberately no raw query tool. Letting an agent run arbitrary SQL against the memory store is a bigger attack surface than the convenience is worth; the tools above cover the realistic range of what an agent needs to read and write memory safely.

Working with the schema

The schema defines what kinds of nodes and edges are allowed to exist — their names and descriptions, and for nodes, their expected properties. It's stored as a YAML file next to the database, and it's versioned: every change increments a revision number.

The default schema

A fresh database starts with schema.default.yaml — revision 0. It covers a reasonably general-purpose starting point: project, topic, technology, concept, person, article, note as node types, and uses, belongs_to, part_of, created_by, works_on, knows, relates_to, covers as edge types. It's meant to be customized, not used as-is forever — the setup prompt exists specifically to walk through that customization on first run.

Adding a new node or edge type

This is always safe and never requires anything special. Call apply_schema with just the new type — existing types don't need to be repeated:

nodes:
  event:
    description: "A dated occurrence or milestone"
    properties:
      name: string
      date: string

The nodes: and edges: sections in what you send to apply_schema are always treated as a patch, not a full replacement. Anything you don't mention is left exactly as it was.

Renaming a type

Renaming touches real data, so it has to be explicit. Add a migrations entry:

migrations:
  - rename_node: { from: technology, to: tool }

This relabels every existing node of that type in the database first, then updates the schema. No data is lost. You can combine this with a redefinition of the new name in the same call if its description or properties should change too.

The same pattern works for edges with rename_edge.

To fold one type into another type that already exists, say so explicitly with merge: true — otherwise renaming onto an existing type is rejected:

migrations:
  - rename_node: { from: architecture-decision, to: decision, merge: true }

All nodes of the source type move to the target, the target keeps its definition, and the source disappears from the schema. The source may also be a type that exists only in the data — get_schema lists such types under unknown_types (typically nodes written before type validation existed).

Removing a type

Removal is rejected if any node or edge of that type still exists in the database — there is no implicit data deletion:

migrations:
  - remove_node: { type: note }

If note still has nodes, rename them elsewhere first (or delete them individually), then remove the now-empty type.

Instructing an agent to manage the schema

If you're prompting an agent (Claude or otherwise) to manage this schema on your behalf, the short version is:

Call get_schema first to see the exact current state. To add a type, just include it under nodes: or edges: with a description — you don't need to repeat what already exists. To rename or remove a type, add an explicit migrations entry; this is the only way to change or delete an existing type, and omitting a type from the YAML never does it silently.

The apply_schema tool's own description carries this same guidance, and the built-in setup MCP prompt walks through it interactively — so in most cases you can simply ask the agent to "review the memory schema" or "add a new type to memory for X" and let it take it from there.

Dreaming — guided cleanup

Over time, any memory accumulates small problems: a node added twice under slightly different IDs, two relationships that should exist but were never written down. The dreaming tool exists to walk through the graph and fix exactly that — not automatically, but with an agent doing the reviewing.

Calling dreaming returns a batch of up to 10 nodes, ordered by type and then by normalized name (so "DuckDB", "duck-db" and "Duck DB" sit next to each other), along with a capped 1-hop view of each node's edges — what it connects to, and what connects to it. If a node has more edges than the cap, the response says how many more exist (out_more / in_more) without flooding the batch with all of them. The response also carries total, has_more and next_offset, so an agent can walk the entire graph one batch at a time by feeding next_offset back in.

A node whose normalized name matches another node of the same type — anywhere in the graph, not only in the current batch — carries a possible_duplicates list of their IDs. It is only a hint; the agent still decides.

The tool itself makes no decisions and changes nothing — it only surfaces data and a set of instructions. Merging is split between the agent and the server. The agent reads the duplicates and composes the merged properties itself, reconciling and extracting the information rather than concatenating text. It then calls update_node({ id, properties, merge_from: [duplicate ids] }). The server does the mechanical part in one transaction: it redirects every edge of the duplicates onto the kept node, drops edges that would become exact duplicates or self-loops, and deletes the duplicates. A missing relationship is added with add_edge. If a duplicate spans an entire node type — say a schema revision introduced tool as a better name for what used to be technology — that's a job for apply_schema's migration support instead (see Renaming a type), not for dreaming directly.

A typical pass looks like:

Use the dreaming tool to review the memory. Compare nodes batch by batch, merge anything that's clearly a duplicate, add any relationship that's obviously missing, and tell me what you changed. Continue with the next batch until there's nothing left.

In practice this has already caught real duplicate edges in this project's own memory — two identical uses relationships between the same two nodes, created a few sessions apart — found and cleaned up by an agent in a single dreaming pass.

Bootstrap — session identity

A fresh conversation has no memory of who it's talking to or how it talked last time, unless something tells it. bootstrap solves that with the simplest possible mechanism: it reads one node, _bootstrap, and hands its content straight back.

The server doesn't define what that node contains, doesn't validate it, and doesn't update it on its own. The agent owns it completely — typically a short block of plain text covering who the user is, what the agent's own role/name is, and any standing working rules worth repeating every session. The first time there's no _bootstrap node yet, the tool just says so and tells the agent how to create one with add_node.

A typical first message in a new conversation looks like:

Call bootstrap at the start of this conversation, then continue normally.

And updating it later is just as direct:

Update the bootstrap node — note that we're now using TypeScript strict mode by default.

Because bootstrap takes no arguments and the tool description itself says to call it first, most setups never need this spelled out explicitly — it's there the moment the agent looks at what tools are available.

Project structure

mcp-memory-graphdb/
├── schema.default.yaml        # revision 0 — the schema a fresh database starts with
├── manifest.json               # .mcpb / Desktop Extension manifest
├── build.mjs / build.cmd / build.sh    # bundle + copy native DuckDB bindings + test
├── mcpb.cmd / mcpb.sh           # package dist/ into a .mcpb file
├── src/
│   ├── mcp.js                  # server entry point — tool & prompt registration
│   ├── Config.js                # database path resolution from argv
│   ├── Datastore/
│   │   ├── InstallerDatastore.js   # creates the nodes/edges tables
│   │   └── GraphDatastore.js       # all node/edge CRUD + search + schema-migration primitives
│   ├── Schema/
│   │   ├── DefaultSchema.js        # loads schema.default.yaml
│   │   ├── SchemaManager.js        # the deterministic apply() process
│   │   └── SchemaValidator.js      # strict structural validation (Zod)
│   ├── Tools/                   # one file per tool group (incl. DreamingTool.js, BootstrapTool.js)
│   ├── Prompts/
│   │   └── OnboardingPrompt.js      # the "setup" MCP prompt
│   └── Utils/
│       └── Schemas.js            # Zod input schemas for every tool
└── test/
    ├── unit.js                  # datastore + schema manager unit tests
    └── integration.js           # full stdio JSON-RPC round-trip, src and dist

Development

Running tests

npm test

This runs test/unit.js (in-memory DuckDB, no real files touched) followed by test/integration.js against both src/ and dist/ — spawning the actual server process and talking JSON-RPC over stdio, exactly as a real MCP client would.

Building

node build.mjs

Bundles src/mcp.js with esbuild, copies the platform-specific native DuckDB bindings (duckdb.node plus the platform shared library) into dist/, and copies schema.default.yaml alongside. build.cmd / build.sh wrap this with a full install → build → test → cleanup cycle, ending in an audible beep so you don't have to watch the terminal.

Packaging as a .mcpb extension

node build.mjs --mcpb

or simply run mcpb.cmd / mcpb.sh, which do the full install → build → package → cleanup cycle in one go. The result is mcpb/mcp-memory-graphdb.mcpb, ready to double-click into Claude Desktop.

Roadmap

This is Phase 2 of the project — graph CRUD, a fully deterministic and validated schema system, and a guided dreaming/cleanup workflow. Open for the future:

  • Full-text search via DuckDB's FTS extension (current search uses a simple ILIKE).

Compatibility

Native DuckDB bindings are platform-specific. This project has been built and tested on Windows (x64). macOS should work out of the box once built there (DuckDB ships official bindings for darwin-x64 and darwin-arm64); Linux requires the matching @duckdb/node-bindings-linux-* package to be available for your architecture at build time.

License

MIT

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    A Model Context Protocol server that provides knowledge graph-based persistent memory for LLMs, allowing them to store, retrieve, and reason about information across multiple conversations and sessions.
    9
    142,129 npm
    2
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A simple memory storage server for Claude using the Model Context Protocol (MCP), enabling Claude to store and retrieve text memories across conversations.
    8 npm
    MIT