Graph Memory MCP
by river-ai-lab
README.md
# Graph Memory MCP
<p align="center">
<a href="https://github.com/river-ai-lab/graph-memory-mcp"><img src="https://img.shields.io/badge/status-active%20development-informational" alt="Status"></a>
<a href="https://github.com/river-ai-lab/graph-memory-mcp/actions/workflows/ci.yml"><img src="https://github.com/river-ai-lab/graph-memory-mcp/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.11+-blue.svg" alt="Python 3.11+"></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
</p>
**Graph memory for teams and their agents that doesn't turn into a dump.**
Self-hosted MCP server for agent developers, teams running agents, and heavy agent users. Agents decide what facts to write; the server keeps memory orderly—deduplicates on insert, marks outdated facts, surfaces contradictions, and enforces physical per-owner isolation. Your data stays with you. No LLM calls on the server, no external APIs. Embeddings computed locally.
<!-- Demo GIF: assets/demo/demo.gif (readme scenario). Seed first: assets/demo/seed.py -->
<p align="center">
<img src="assets/demo/demo.gif" alt="Search the release deadline, see both dates and the CONTRADICTS edge the agent marked, then the snapshot of the superseded beta" width="100%">
</p>
The GIF is a recording of the demo script running against a real server. Run it yourself with `uv run graph-memory-mcp --host 127.0.0.1 --port 8000`, then `uv run python assets/demo/seed.py` and `uv run python assets/demo/generate_demo.py --scenario readme`, and read [assets/demo/seed.py](assets/demo/seed.py) and [assets/demo/generate_demo.py](assets/demo/generate_demo.py) to see exactly what happens.
Other recordings: [docs/demos.md](docs/demos.md).
## Why Graph Memory MCP
- **Traversable graph**: Agents read edges (`get_context`, `get_trace`) to walk connections between facts—decisions link to affected services, new facts `FOLLOWS_FROM` old ones
- **Explicit outdating**: `mark_outdated` hides superseded facts; `CONTRADICTS` edges surface conflicts in `contradiction_pairs`; versioning tracks changes over time
- **Write-time deduplication**: Near-duplicates refused at insert using embeddings only (threshold 0.92), no LLM call, deterministic
- **Physical isolation**: Each `owner_id` gets its own FalkorDB graph or ArcadeDB database—no query-time filtering risks
## Quickstart
```bash
cp env.example .env
docker compose up -d arcadedb # Start ArcadeDB (default backend)
uv run graph-memory-mcp --host 127.0.0.1 --port 8000 # Start MCP server
claude mcp add --transport http graph-memory-mcp http://127.0.0.1:8000/mcp
```
See [full setup instructions](#setup) for Cursor and Claude Desktop.
**Learn more**: [Comparison with other systems](docs/comparison.md) · [FAQ](docs/memory_faq.md) · [Limitations](#limitations) · [Agent policy cheatsheet](docs/memory_policy_cheatsheet.md)
---
## How it works
Agent memory becomes noise over time: duplicate facts accumulate, contradictions are served as current truth, stale decisions look fresh, and context grows until retrieval becomes a dump. Every session starts from scratch, and teams end up with unstructured notes nobody trusts.
**Design principle**: The agent using the memory already knows what to write and how to structure it. The server does not run its own LLM to extract or rewrite facts; it enforces memory hygiene (dedup on write, outdated facts, contradictions, versions, isolation, token budget). Benefits: predictable, cheap, no hidden model calls, no double extraction.
## Before / After: Why a graph
A graph lets agents traverse connections, supersede decisions, and surface contradictions.
### Before: Decision made
```python
# Alice decides to use Component X for the API gateway
create_node(
text="API gateway uses Component X for routing.",
metadata={"project": "api", "created_by": "alice"},
owner_id="team_backend",
)
# → {"node": {"node_id": "a1b2c3...", "text": "API gateway uses Component X for routing.", "status": "active"}}
# Link decision to affected service
create_relation(
from_id="a1b2c3...",
to_id="d4e5f6...", # Service node for API gateway
relation_type="RELATED_TO", # Or USES: service USES this decision
owner_id="team_backend",
)
```
### Traverse connections
```python
# Get context: what decisions affect the API gateway?
get_context(node_id="d4e5f6...", owner_id="team_backend", depth=1)
# → Returns: API gateway node + connected decision + relation path
# Or: recall with context expansion
recall_context(
query="API gateway decisions", owner_id="team_backend", depth=1, compact=True
)
# → Returns snippets + paths showing decision→service links
```
### After: Decision superseded
```python
# Bob changes to Component Y
new_decision = create_node(
text="API gateway migrated to Component Y (Oct 2026).",
metadata={"project": "api", "created_by": "bob"},
owner_id="team_backend",
)
new_id = new_decision["node"]["node_id"] # Extract node_id from response
# Mark old decision outdated
mark_outdated(
fact_id="a1b2c3...", reason="Migrated to Component Y", owner_id="team_backend"
)
# Link new → old (new follows from the context of replacing old)
create_relation(
from_id=new_id,
to_id="a1b2c3...",
relation_type="FOLLOWS_FROM", # New decision follows from old context
owner_id="team_backend",
)
```
**Result**: `search("API gateway", owner_id="team_backend")` now returns Component Y decision. Old decision excluded by default; `include_outdated=true` retrieves history. The FOLLOWS_FROM edge preserves the replacement context.
**Note**: Custom relation types (e.g., `DEPENDS_ON`, `IMPLEMENTS`) can be added via `RELATION_ALLOWED_TYPES` in `.env`. Default allowlist (13, see [`relation_policy.py`](graph_memory_mcp/graph_memory/relation_policy.py)): `RELATED_TO`, `MENTIONS`, `SUMMARIZES`, `FOLLOWS_FROM`, `CONTRADICTS`, `EXTRACTED_FROM`, `SIMILAR_TO`, `NEXT_STEP`, `EXPOSES`, `CROSS_REF`, `RUNS_ON`, `USES`, `SERVED_BY`. Default mode: warn on unlisted types. `auto_link` writes `MENTIONS` (`AUTO_LINK_RELATION`), not a type named `AUTO_LINK`.
### Contradictions surfaced
```python
# Two conflicting facts stored
create_node(text="API rate limit is 1000 req/min.", ...) # id: x1
create_node(text="API rate limit is 500 req/min.", ...) # id: x2
create_relation(from_id="x1", to_id="x2", relation_type="CONTRADICTS", ...)
# Search returns both + flags the conflict
search(query="API rate limit", owner_id="team_backend")
# → {
# "results": [...],
# "contradiction_pairs": [
# {"from_id": "x1", "to_id": "x2", "relation_type": "CONTRADICTS"}
# ]
# }
```
## How agents should use this
- **Start every session** with `get_brief(owner_id)` (project anchor, recent facts, contradictions, stale count) → `search(query, compact=true)` for the task
- **Write one declarative fact per node**; no chat logs, no scratchpad, no secrets
- **Search before creating**; if `memory_duplicate` or `possible_duplicates` returned, do not write a third paraphrase
- **Fact became wrong** → `mark_outdated(fact_id, reason, owner_id="team_backend")` + `create_node(new_text)`, not `update_node`
- **After meaningful work** (GROW): Ground what changed → Record it → Orient (if recurring, add tags/relations) → Write with `metadata.project` and `created_by`
- **Temporary notes** get `ttl_days` (14-30); durable decisions have no TTL
- **Link facts** with `create_relation` using the default allowlist: `RELATED_TO`, `MENTIONS`, `SUMMARIZES`, `FOLLOWS_FROM`, `CONTRADICTS`, `EXTRACTED_FROM`, `SIMILAR_TO`, `NEXT_STEP`, `EXPOSES`, `CROSS_REF`, `RUNS_ON`, `USES`, `SERVED_BY` (see [`relation_policy.py`](graph_memory_mcp/graph_memory/relation_policy.py); default mode: warn). `auto_link` writes `MENTIONS`
**Full policies**: Load [`docs/memory_policies_for_LLM.md`](docs/memory_policies_for_LLM.md) or [`docs/memory_policy_cheatsheet.md`](docs/memory_policy_cheatsheet.md) in your agent's rules or context at session start. Put these operator constants in your own AGENTS.md ([`docs/AGENTS_example.md`](docs/AGENTS_example.md)).
## Use cases
### Team architecture decisions
**Before**: Architecture decisions live in Slack threads, meeting notes, and stale wiki pages. Finding "why did we choose Component X?" requires asking the team or searching multiple tools.
**With Graph Memory MCP**: When Alice documents "API gateway uses Component X" in Claude Desktop, the decision is stored with `create_node`. Three months later, Bob migrates to Component Y and marks the old fact outdated with `mark_outdated`, linking the new decision `FOLLOWS_FROM` the old. Any agent querying "what gateway component do we use?" gets only the current fact. `get_trace` walks back through the graph to show the full decision history.
### Coding agent across sessions
**Before**: Cursor or Claude Code agents rediscover the same information session after session—re-reading config files, re-asking "what port does staging use?"—because memory resets with each new chat.
**With Graph Memory MCP**: When the agent finds "staging database uses port 5433" in `config/staging.yaml`, it writes back with `create_node(text="The staging database uses port 5433.", metadata={"source": "config/staging.yaml"})` (the write-back rule in the [cheatsheet](docs/memory_policy_cheatsheet.md)). Next session, `search("staging database port")` retrieves it. No file re-reads, no extraction cost.
### Multiple agents sharing memory
**Before**: Three agents each encounter "retry policy: 3 retries" at different times, storing near-duplicate facts. Later, one agent finds "retry policy: 5 retries"—now two contradictory decisions coexist with no signal that they conflict.
**With Graph Memory MCP**: Write-time deduplication (embeddings, threshold 0.92) refuses the second and third paraphrase of "retry policy: 3 retries." When an agent writes "retry policy: 5 retries," it explicitly creates a `CONTRADICTS` edge. Any agent searching "retry policy" sees `contradiction_pairs` in results and can surface the conflict to the team.
### Separate clients and projects
**Before**: All memory is mixed in one index or database, requiring query-time filters (`user_id`, `project_id`) that risk leaks if filters are forgotten or misconfigured.
**With Graph Memory MCP**: Claude Desktop connects with `owner_id=team_backend`, Cursor with `owner_id=team_frontend`. Each owner gets a physically separate FalkorDB graph or ArcadeDB database—no shared tables, no query-time filtering. Self-hosted, so data stays on your infrastructure.
## When something else fits better
See [docs/comparison.md](docs/comparison.md) for detailed comparisons with Mem0, Graphiti, Zep, Letta, Cognee, and other memory systems.
## Limitations
- **No fact extraction on the server**: The agent decides what to write; raw chat logs are not auto-summarized (by design).
- **`search` has no `as_of` parameter yet**: Only `get_node` supports temporal queries; `search(as_of)` is planned in the roadmap.
- **Requires a running graph database**: ArcadeDB (default) or FalkorDB via Docker.
- **First model download is large**: the default fp32 ONNX weights for `intfloat/multilingual-e5-base` are about 1.1 GB. Quantized weights are not used. `EMBEDDING_BACKEND=torch` needs `uv sync --extra torch`.
- **Deduplication is similarity-based**: Near-duplicates are refused at insert using embeddings (threshold 0.92); may miss paraphrases below threshold or block distinct-but-similar facts.
- **Contradictions are not auto-detected**: The agent must create `CONTRADICTS` edges explicitly; they are surfaced in search results but not inferred semantically by the server.
- **Owner access is `OWNER_SCOPE`**: `client` (default) uses the tool argument `owner_id`. `header` ignores it and reads `X-Owner-Id` (loopback bind; the gateway sends `Authorization: Bearer $ADMIN_TOKEN` when that token is set). `token` requires a Bearer API key; `owner_id` must be one of the key's projects, and an empty value uses the first project. There is no multi-tenant admin UI.
## Setup
**1. Configure environment**
```bash
cp env.example .env
```
The defaults work with `docker-compose.yml` (ArcadeDB: `root` / `playwithdata`, port 2480; FalkorDB: password `falkordb123`, port 6379).
**2. Start the database**
```bash
docker compose up -d arcadedb
```
This starts ArcadeDB (port 2480, default backend). To use FalkorDB instead, set `GRAPH_BACKEND=falkordb` in `.env` and run `docker compose up -d falkordb`.
**3. Install and run**
From this repository:
```bash
uv sync
uv run graph-memory-mcp --host 127.0.0.1 --port 8000
```
Without a clone, the same long-running process comes from PyPI:
```bash
pip install graph-memory-mcp
graph-memory-mcp --host 127.0.0.1 --port 8000
```
The default install embeds with ONNX Runtime (`EMBEDDING_BACKEND=onnx`). To use the previous sentence-transformers path: `uv sync --extra torch` and set `EMBEDDING_BACKEND=torch`.
The server is now at `http://127.0.0.1:8000/mcp` (Streamable HTTP MCP).
**4. Configure your MCP client**
Add this to your MCP configuration file:
**Claude Code (CLI)**:
```bash
claude mcp add --transport http graph-memory-mcp http://127.0.0.1:8000/mcp
```
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
Claude Desktop launches only local (stdio) servers from `claude_desktop_config.json`, so bridge to the HTTP server with `mcp-remote`:
```json
{
"mcpServers": {
"graph-memory-mcp": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://127.0.0.1:8000/mcp"]
}
}
}
```
**Cursor** (`~/.cursor/mcp.json`):
```json
{
"mcpServers": {
"graph-memory-mcp": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}
```
See [examples/mcp_servers_config.json](examples/mcp_servers_config.json) for clients that read an mcpServers dict with a transport key (e.g. langchain-mcp-adapters).
**5. Test**
```bash
uv run python examples/http_client_usage.py
```
With `mcp>=2.3` the Python client is `mcp.client.streamable_http.streamable_http_client` (the old `streamablehttp_client` name is gone). Pass headers such as `Authorization` through an `httpx.AsyncClient`: `streamable_http_client(url, http_client=httpx.AsyncClient(headers=...))`.
## Features
- **`create_node`** / **`upsert_node`** with deduplication (embeddings-based refusal at threshold 0.92, no LLM call)
- **`search`** with hybrid ranking (cosine + identifier match + recency), `compact=true` for token-budgeted snippets
- **`get_brief`** for task warmup (project anchor, recent durable facts, open contradictions, stale count)
- **`mark_outdated`** / **`CONTRADICTS`** edges / **fact versioning** with optional snapshots
- **`get_context`** (subgraph walk from a node) / **`get_trace`** (paths between two nodes) / **`recall_context`** (search + expand in one call)
- **Entity and triplet graph** (`create_triplet`, `search_triplets`)
- **TTL facts** that stay out of default search until `include_ttl=true`
- **Auto-linking** (opt-in `auto_link=true` adds `MENTIONS` to similar Entity nodes)
- **Background jobs** (deduplication, archival) when `JOBS_ENABLED=true`
- **Backed by ArcadeDB** (default) or **FalkorDB** (set `GRAPH_BACKEND=falkordb`). Same MCP tools, same behavior.
## Documentation
- **[Comparison](docs/comparison.md)** — how this differs from Mem0, Graphiti, Letta, and others
- **[Agent policies](docs/memory_policies_for_LLM.md)** — what agents should store and how (load this in every session)
- **[API contract](docs/features.md)** — tool signatures and data model
- **[FAQ](docs/memory_faq.md)** — common questions and operational guidance
## Configuration
Settings are loaded from `.env` and environment variables. Defaults are in `graph_memory_mcp/config.py`.
Key settings:
- `GRAPH_BACKEND` — `arcadedb` (default) or `falkordb`
- `EMBEDDING_BACKEND` — `onnx` (default) or `torch` (optional extra)
- `EMBEDDING_MODEL` — default `intfloat/multilingual-e5-base`
- `RELATION_POLICY_ENFORCE` — `off` | `warn` (default) | `enforce`
- `RELATION_ALLOWED_TYPES` — comma-separated edge type allowlist
- `OWNER_SCOPE` — `client` (default), `header`, or `token`. See [Owners](#owners).
- `JOBS_ENABLED` — `false` by default; enable for archive and dedup background jobs
See `env.example` for all options. Details: [configuration](docs/configuration.md).
## Owners
The same server has three ways to choose `owner_id`. `client` stays the default, so a one-minute install does not change.
| Mode | Who chooses the owner | When to use it |
|------|------------------------|----------------|
| `client` | The tool argument `owner_id` | Trusted local or company deploy. Anyone who can call the server can name any owner. |
| `header` | `X-Owner-Id`, set by your gateway. The tool argument is ignored. The gateway sends `Authorization: Bearer $ADMIN_TOKEN` when that token is set. | The server listens on `127.0.0.1`. The gateway is the only client. |
| `token` | The API key's `projects`. `owner_id` must be one of them; omit it to use the first. | A small team on one process. No second service. |
An API key is a person's `name` plus the owner graphs (`projects`) they may use. Team setup: [docs/team-setup.md](docs/team-setup.md).
Token mode:
```bash
uv run graph-memory-mcp keys create --name alice --projects p1,p2
```
The command prints the key once. `keys.json` keeps the sha256 hash, name, and projects. Then:
```bash
OWNER_SCOPE=token
API_KEYS_FILE=./keys.json
```
A missing, unknown, or revoked key is HTTP 401. An `owner_id` that is not in the key's projects is HTTP 403 (or a tool error with `memory_forbidden`). `graph-memory-mcp keys set-projects <id> p1,p3` replaces the list. `keys list` prints id, name, projects, and created time, never the key. `keys revoke <id>` revokes by id. In token mode every write stores the key's `name` as `created_by` / `updated_by`. In `client` and `header` those fields come from `metadata.created_by` / `metadata.updated_by`. Jobs keep the owner they were started with.
`graph-memory-mcp --auth` is the token entry point (`server_auth.py`). It forces `OWNER_SCOPE=token` and refuses to start without `ADMIN_TOKEN`. Tool schemas omit `owner_id`; the server uses the key's first project. Issue keys with `keys create`, inspect them with `keys list`, and revoke with `keys revoke`. The default server is unchanged: set `OWNER_SCOPE=token` there when the agent must pass `owner_id` to choose among a key's projects.
Token mode does not start without `ADMIN_TOKEN` and a readable keys file. `/admin` takes the owner from the path, so in this mode it always requires that token. `client` and `header` still leave `/admin` open when `ADMIN_TOKEN` is empty.
Header mode belongs on `127.0.0.1`, with the gateway as the only client. The gateway sends `Authorization: Bearer $ADMIN_TOKEN` and `X-Owner-Id`. `/mcp` returns HTTP 401 when that token is set and the bearer is missing or wrong. Without `ADMIN_TOKEN`, the process starts on `127.0.0.1`, `::1`, or `localhost` only. Any other bind address exits. `ALLOW_INSECURE_HEADER=1` logs a warning and starts anyway.
`client` mode has no `/mcp` authentication, so it starts on loopback only. A bind such as `0.0.0.0` exits unless `ALLOW_INSECURE_CLIENT=1` is set (it logs a warning). For network access use `OWNER_SCOPE=token`.
`/internal/graph-size` in `client` and `header` requires `ADMIN_TOKEN` plus `X-Owner-Id`. `token` requires the API key; `X-Owner-Id` must be one of the key's projects, or the first project is used. `/metrics` stays open in every mode: handler, cache, and job counters, plus totals summed across owners. It does not name an owner.
## Running the databases
### ArcadeDB
```bash
cp env.example .env
docker compose up -d arcadedb
```
ArcadeDB runs on port 2480. Default credentials: `root` / `playwithdata`. Minimum version: 26.10 (the compose file pins `26.10.2`). 25.10.1 fails with `Index type 'LSM_VECTOR' is not supported`.
Test with ArcadeDB:
```bash
GRAPH_BACKEND=arcadedb uv run pytest -m arcade
```
### FalkorDB
To use FalkorDB instead of ArcadeDB, set `GRAPH_BACKEND=falkordb` in `.env`. Runs on port 6379, web UI at http://localhost:3000. Default password: `falkordb123`.
```bash
docker compose up -d falkordb
```
Test with FalkorDB:
```bash
GRAPH_BACKEND=falkordb uv run pytest -m "not arcade" -q
```
Test with ArcadeDB (default):
```bash
GRAPH_BACKEND=arcadedb uv run pytest -m arcade -q
```
### Vector indexes
The server uses two vector indexes (Fact and Entity). The first write for an owner creates both, and later writes reuse that per-owner cache. They are also created on `search`, `find_similar`, or Fact `auto_link` when missing, or when `AUTO_CREATE_INDEXES=true` is set.
**Changing the embedding model invalidates the stored corpus.** You must re-embed every node after changing `EMBEDDING_MODEL`, its dimension, or `EMBEDDING_QUERY_PREFIX` / `EMBEDDING_PASSAGE_PREFIX`. See [Embeddings](docs/configuration.md#embeddings).
## Server profiles
Profiles with the same tools; agents pick one and stay on it:
| Profile | Start | Provenance |
|---------|-------|------------|
| Default | `graph-memory-mcp` | Nested `source` object |
| Simple | `graph-memory-mcp --simple` | Flat (`ref`, `provenance_type`, `source_updated_at`, …); `upsert_node` requires `ref` |
| Extended | `graph-memory-mcp --extended` | Same as default, plus `description`, `shared_with_ids`, `collection_id`, and node types `Collection`, `User`, `Project` |
| Auth | `graph-memory-mcp --auth` | Same as default. `OWNER_SCOPE=token`. Schemas omit `owner_id`; the server uses the key's first project. |
See [docs/features.md](docs/features.md) for details.
## Graph Explorer
Local read-only web UI. **Open a memory**: enter a memory name and click **Load**, then search under **Ask a question** or open an item from the summary. An empty name shows "Enter a memory name, then click Load." and does not open `default`. **More tools** covers open by ID, find a path, similar wording, and how far to look. The toolbar is Fit, Arrange, Physics, zoom, text preview, **Names**, and **Details**. The map legend shows facts, entities, outdated items, contradicts, follows-from, and related. Details lead with the text, status, and validity. Timestamps, the id, and raw fields are under **More about this item**. Node text on the map is **On hover** by default, or **Always** (one short line). Always-mode labels hide when the map is zoomed out. The choice is stored in `localStorage` as `gm_node_preview`. The summary counts outdated facts. With `--auth`, the memory name shows the key's project and cannot be edited.
The shots below are `readme_demo` from [`assets/demo/seed.py`](assets/demo/seed.py) and the dense demo owner `demo_team`. Regenerate them with `node scripts/explorer_screenshots.mjs` while Explorer is running.



```bash
# Terminal 1 — MCP server
uv run graph-memory-mcp --host 127.0.0.1 --port 8000
# Terminal 2 — Explorer
uv run graph-memory-explorer --host 127.0.0.1 --port 8088
```
Open http://127.0.0.1:8088. Against `graph-memory-mcp --auth`:
```bash
GRAPH_MEMORY_API_KEY=<key> uv run graph-memory-explorer --auth --host 127.0.0.1 --port 8088
```
The owner field is then bound by the key. `--token` is the same as the env var.
## Multi-agent usage
Use `owner_id` to isolate or share memory:
- **Shared team memory**: both `agent_codegen` and `agent_debug` use `owner_id="team_platform"` and share all facts
- **Isolated per-agent**: `agent_codegen` uses `owner_id="agent_codegen"`, `agent_docs` uses `owner_id="agent_docs"`; neither sees the other's memory
Each `owner_id` lives in its own graph (FalkorDB) or database (ArcadeDB). Queries do not see other owners.
**Owner ID charset**: both backends accept `[A-Za-z0-9_@-]+`. `ARCADE_DATABASE` must match `^[A-Za-z][A-Za-z0-9_]*$`. Each ArcadeDB owner is an unquoted database `{ARCADE_DATABASE}_{owner_id}`.
See [docs/memory_policies_for_LLM.md](docs/memory_policies_for_LLM.md) for agent-facing guidance.
## Persistence and backups
### ArcadeDB
Data is the Docker volume `arcade-data` (the image user cannot write a root-owned `./data-arcade` bind). `docker compose down` keeps it; `docker compose down -v` deletes it. ArcadeDB admin endpoints support export/import:
```bash
# Export a database (requires authentication)
curl -u root:playwithdata http://localhost:2480/api/v1/export/memory_demo > backup.jsonl.gz
# Import (see ArcadeDB docs)
curl -u root:playwithdata -X POST http://localhost:2480/api/v1/import/memory_demo -F file=@backup.jsonl.gz
```
### FalkorDB
`docker-compose.yml` enables AOF persistence (`--appendonly yes --appendfsync everysec`). A container crash loses at most one second of writes.
Point-in-time backups:
```bash
./scripts/backup.sh
```
This runs `BGSAVE` and copies `dump.rdb` to `./backups` (keeps last 14).
### Logical per-owner backups (both backends)
Portable across instances:
```bash
curl http://127.0.0.1:8000/admin/export/{owner_id}
```
Supports `?limit=&offset=§ion=nodes|relations` for large graphs. Restore with `POST /admin/import`.
**ArcadeDB backups**: Data persists in the `arcade-data` volume. For a point-in-time copy, stop ArcadeDB and archive that volume. Logical export/import via the admin API works across backends.
## Development
### Tests
Default tests require FalkorDB running:
```bash
./scripts/test.sh
# or: docker compose up -d && uv run pytest -q
```
ArcadeDB tests:
```bash
uv run pytest -m arcade
```
Headless Explorer smoke (FalkorDB only). Skipped unless `EXPLORER_BROWSER=1`. Needs Explorer at `EXPLORER_URL` (default `http://127.0.0.1:8088`). The auth check uses `EXPLORER_AUTH_URL` (default `http://127.0.0.1:8091`) for an Explorer started with `--auth`. Playwright loads from `PLAYWRIGHT_MODULE` or `/tmp/node_modules/playwright/index.mjs`. CI excludes the marker (`not arcade and not browser`, and `arcade and not browser`).
```bash
EXPLORER_BROWSER=1 uv run pytest -q -m browser
```
### Pre-commit hooks
Hooks run via the project venv:
```bash
uv run pre-commit run --all-files
uv run pre-commit install # optional git hooks
```
## Status and roadmap
### Stable
These features are tested and in active use:
- Core memory operations (`create_node`, `search`, `mark_outdated`, relations, `get_context`, `get_brief`)
- Physical per-owner isolation (ArcadeDB databases, FalkorDB graphs)
- Write-time deduplication (embeddings, threshold 0.92)
- Vector search with hybrid ranking (cosine + identifier match + recency)
- Server profiles (default, `--simple`, `--extended`, `--auth`)
- TTL facts excluded from default search
- MCP over Streamable HTTP
- Compact recall with token budget
### Experimental
- Background jobs (deduplication, archival) when `JOBS_ENABLED=true`
- Versioning snapshots (`get_node as_of`, requires `versioning=true` on writes)
- Fact auto-linking (`auto_link=true` adds `MENTIONS` to similar entities)
See [docs/roadmap.md](docs/roadmap.md) for planned features and [CHANGELOG.md](CHANGELOG.md) for recent changes.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing guidelines, and pull request process.
## License
MIT — see [LICENSE](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessOutdated data