# Douglasjordan2 C0

> An external memory for LLMs: a bi-temporal knowledge graph with hybrid (keyword + vector) retrieval and a self-improving reflection loop. Benchmarked to beat flat vector RAG on corrections and time-versioned queries.

- **Type:** MCP server
- **Install:** `agentstack add mcp-douglasjordan2-c0`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [douglasjordan2](https://agentstack.voostack.com/s/douglasjordan2)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [douglasjordan2](https://github.com/douglasjordan2)
- **Source:** https://github.com/douglasjordan2/c0

## Install

```sh
agentstack add mcp-douglasjordan2-c0
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# c0

**An external memory for LLMs** — a bi-temporal knowledge graph with hybrid (keyword + vector) retrieval and a self-improving reflection loop.

[](https://github.com/douglasjordan2/c0/actions/workflows/ci.yml)
[](./LICENSE)
[](https://www.rust-lang.org/)

---

## Why

Language models are **stateless between sessions** and their training data **goes stale**. The usual fix — stuffing documents into a vector store — retrieves blobs of prose and has no notion of how knowledge changes over time.

c0 takes a different approach. It stores knowledge as a **graph of concepts and the relationships between them**, retrieves the relevant subgraph on demand, and tracks how each fact evolves. The result is a persistent, *correctable* memory layer you can query in natural language and grow as you work.

## How it works

```
query ──▶ ❶ exact match ─▶ ❷ keyword (BM25) ─▶ ❸ hybrid (BM25 + vector, fused by RRF)
                                                          │
                                                          ▼
                                          resolve to a concept node in Neo4j
                                                          │
                                                          ▼
                                     traverse the graph for related context  ──▶  answer
                                                          │
                                               (no match) ▼
                                          reflection loop: learn from the miss
```

- **Graph storage (Neo4j).** Knowledge lives as `Concept` nodes and typed relationships, not as text chunks — so retrieval can *traverse* from one idea to related ones.
- **Hybrid retrieval.** A tiered cascade: exact match → keyword (Lucene/BM25) → **hybrid**, which runs keyword *and* vector search and merges them with **Reciprocal Rank Fusion (RRF)**. Keyword nails exact names and identifiers; vectors catch synonyms and paraphrase; fusion gets the best of both without normalizing incompatible score scales.
- **Bi-temporal.** Every concept carries two independent timestamps — when it was *recorded* (transaction time) and when it is *true* (valid time) — so you can run point-in-time ("as-of") queries, **supersede** a concept when it evolves, or **invalidate** it with a causal audit trail. Nothing is deleted; it's time-bounded.
- **Self-improving reflection loop.** When a lookup finds nothing, the dead end is queued, and an LLM classifies it: **commit** a genuinely new, reusable concept, **discard** noise, or **queue** the uncertain ones for human review. Run it continuously (`c0 reflector run`) and c0 fills its own memory gaps as you work — [details below](#the-reflection-loop--c0s-learning-engine).

## See it in action

**Correct stale training data.** A pre-2026 model insists you create a Shopify app in Admin → "Develop apps". c0 walks its graph and overrides that with a patch — current knowledge wins.

**Hybrid retrieval — keyword + vector, fused by RRF.** One paraphrased question, three ways: keyword (BM25) misses it, vector understands intent, hybrid fuses both.

**Bi-temporal — ask "as of" any point in time.** The same question returns the era-correct answer: the Pages Router in 2022, the App Router today, with a dated supersession trail.

**Self-improving — a dead end becomes a new concept.** A lookup misses, the reflection loop classifies it, and c0 commits the new knowledge to its own graph — no human in the loop.

## Benchmark — does structured memory actually beat a vector store?

"It helps" is easy to assert. `c0 bench` makes it falsifiable: it seeds a
*synthetic* knowledge world (invented entities no model saw in training), then
answers the same questions three ways — a **bare model**, a **naive flat vector
store** (embed → cosine top-k), and **c0** — and grades every answer with an LLM
judge. Because the facts are invented, the score isolates what the *memory layer*
adds, not the model's prior knowledge.

10 questions, 4 categories, 3 trials each (majority vote):

| category       | bare model | flat vector RAG | + LLM reranker  |     c0     |
|----------------|:----------:|:---------------:|:---------------:|:----------:|
| simple recall  | 0/3 (0%)   |   3/3 (100%)    |   3/3 (100%)    | 3/3 (100%) |
| multi-hop      | 0/2 (0%)   |    1/2 (50%)    |    0/2 (0%)     | 2/2 (100%) |
| correction     | 0/2 (0%)   |   **0/2 (0%)**  |   **0/2 (0%)**  | 2/2 (100%) |
| temporal       | 0/3 (0%)   |   **0/3 (0%)**  |   **0/3 (0%)**  | 3/3 (100%) |
| **overall**    | 0/10 (0%)  |   4/10 (40%)    |    3/10 (30%)   | **10/10 (100%)** |

A vector store handles **simple recall** and not much else: it can't tell a
corrected fact from the stale one it replaced (correction) and has no notion of
an effective date (temporal) — and **adding an LLM reranker doesn't help**, because
reranking reorders passages without synthesizing the date/supersession metadata
that isn't in the text. Those are exactly what c0's temporal graph represents
natively.

```bash
c0 bench --seed --arms bare,flat_rag,flat_rerank,c0 --trials 3
```

Full methodology, the synthetic corpus, and honest limitations: **[BENCH.md](BENCH.md)**.

### Retrieval eval — is the *right* concept surfacing?

`c0 bench` measures end-to-end answer quality; `c0 eval` measures the narrower
thing the retrieval cascade is responsible for: given a natural-language query,
does the **right concept rank in the top _k_**? It scores the cascade over the
same synthetic fixture with the standard IR metrics — **recall@k** and **MRR** —
so a change to `HybridSearchConfig`, the RRF fusion, or the fulltext query builder
that silently degrades retrieval shows up as a number instead of a feeling.

```bash
c0 eval --seed --k 3                 # full cascade (exact → fulltext → hybrid, temporal)
c0 eval --seed --no-embeddings       # fulltext-only; no Ollama/API needed (the CI path)
c0 eval --judge                      # + opt-in LLM-as-judge context-relevance pass
```

The fulltext-only path is **local-first** (Neo4j only), so CI runs it as a gate
(`--min-recall`) and fails the build on a regression — no model dependency
required. Details and the golden set: **[EVAL.md](EVAL.md)**.

## Requirements

- **Rust** (2024 edition — 1.85+)
- **Neo4j 5** — a `docker-compose.yml` is included
- **[Ollama](https://ollama.com/)** for local embeddings *and* the reflection loop's classifier (defaults: `nomic-embed-text` for embeddings, `hermes3:8b` for classification) — the whole loop runs locally, no key required
- *(optional)* **Claude for the background LLM** — opt in with `[claude] enabled = true` to use Claude instead of a local model for classification/extraction, either via an **Anthropic API key** or your **Claude subscription** (the `claude` CLI). See [Configuration](#configuration)

## Quickstart

```bash
# 1. Start Neo4j (binds to localhost only)
docker compose up -d

# 2. Pull the embedding model
ollama pull nomic-embed-text

# 3. Build & install
cargo install --path .

# 4. Point c0 at Neo4j (defaults shown; the bundled compose uses no auth)
export NEO4J_URI="bolt://localhost:7687"
export NEO4J_USER=""        # empty for the bundled docker-compose
export NEO4J_PASSWORD=""
# export ANTHROPIC_API_KEY="sk-..."   # optional; only for [claude] enabled = true (defaults are local Ollama)

# 5. Create indexes (vector + fulltext), then a namespace
c0 migrate
c0 init --namespace my-project

# 6. Add knowledge and recall it
c0 add concept "reciprocal rank fusion" -d "Rank-based fusion of multiple result lists; score = weight/(k+rank)."
c0 add concept "hybrid search" -d "Keyword (BM25) + vector retrieval, fused by RRF." --force
c0 relate "reciprocal rank fusion" USED_BY "hybrid search"   # both endpoints must exist
c0 walk "reciprocal rank fusion"                             # traverses outgoing edges -> "hybrid search"
```

> `--force` on the second concept skips the *similar-concept* guard: closely related ideas
> often score as near-duplicates, and `relate` requires both endpoints to already exist.

## Core commands

| Command | What it does |
|---|---|
| `c0 walk ` | Recall: resolve a concept (exact → keyword → hybrid) and traverse for context |
| `c0 walk  --as-of ` | Point-in-time recall (bi-temporal) |
| `c0 search ` | Hybrid search without traversal (`--vector-only` / `--keyword-only`) |
| `c0 add concept  -d ""` | Add a concept (embedded on write) |
| `c0 add patch  --content ""` | Add a knowledge patch that corrects/augments a concept |
| `c0 relate   ` | Create a typed relationship |
| `c0 supersede  --with ` | Mark a concept evolved into a newer one |
| `c0 invalidate concept  --reason ""` | Retract a concept with a causal trail |
| `c0 describe  ""` | Update a description (and re-embed) |
| `c0 reflector run` | Run the learning loop: classify dead ends → commit new concepts (see [below](#the-reflection-loop--c0s-learning-engine)) |
| `c0 health --fix` | Check Neo4j / Ollama / indexes |
| `c0 audit enrich` | Reconnect orphaned concepts to nearest neighbours (`--dry-run`, `--rollback`) |
| `c0 export` · `c0 audit` · `c0 move` | Maintenance utilities |

Run `c0 --help` for the full set.

## More

Beyond the core commands, c0 includes a few subsystems worth knowing about (fuller docs are on the way):

- **Live sources** — `c0 link source add  --url ` fetches and embeds a page; `c0 fetch ` and `c0 link source search` retrieve over them, so external references stay fresh in the graph.
- **Triggers** — `c0 trigger add ` (or `--semantic`) decide when a prompt should consult c0; pair one with a [hook](#using-c0-with-claude-code) for hands-off recall.
- **Sessions** — index your assistant transcripts (build with `--features sessions`), then `c0 sessions search`, `c0 sessions resume`, and track spend with `c0 sessions cost`.
- **Raw queries & history** — `c0 find ""` runs Cypher directly against the graph; `c0 invalidation-chain ` reads a concept's causal history.
- **Maintenance** — `c0 backfill embeddings`, `c0 audit`, `c0 move`, `c0 export`, `c0 status`, `c0 config show`.

Run `c0  --help` for flags.

## Configuration

c0 reads connection details from the environment, with a per-namespace `.c0/config.toml` for local settings:

| Variable | Default | Purpose |
|---|---|---|
| `NEO4J_URI` | `bolt://localhost:7687` | Neo4j connection |
| `NEO4J_USER` / `NEO4J_PASSWORD` | empty | Neo4j auth |
| `ANTHROPIC_API_KEY` | — | Optional; used only when `[claude] enabled = true`. By default, classification & extraction run on a local Ollama model — no key needed |

Embedding host/model (Ollama) default to `http://localhost:11434` and `nomic-embed-text`, and are configurable.

### Security & threat model

The bundled `docker-compose.yml` runs Neo4j with **auth disabled** and binds its ports to
`127.0.0.1` only. This is fine for the intended default: a **single-user local machine**, where
the graph is trusted memory for one person.

Be aware of the tradeoff: with auth off, *any* local process or user on the machine can read or
rewrite the entire memory graph — and because this graph is explicitly positioned to **override the
model's training data**, poisoning it is high-value. If you share the machine, run untrusted local
code, or expose Neo4j beyond loopback, enable auth:

```bash
# Generate a password once and start Neo4j with it
export NEO4J_AUTH="neo4j/$(openssl rand -hex 16)"
docker compose up -d

# Point the CLI at the same credentials
export NEO4J_USER="neo4j"
export NEO4J_PASSWORD=""
```

### Background LLM: local, API, or your Claude subscription

The reflection loop, concept extraction, and session enrichment use a chat LLM. By default that's **local Ollama** — keyless and offline. To use Claude instead, set `[claude]` in `.c0/config.toml` to one of:

**Anthropic API** — billed per token, needs `ANTHROPIC_API_KEY`:

```toml
[claude]
enabled  = true
provider = "claude"
```

**Your Claude subscription, via the Claude Code CLI** — no API key, no per-token cost (it shells out to `claude -p`, using whatever that CLI is logged into):

```toml
[claude]
enabled  = true
provider = "claude-cli"
binaries = { claude = "claude" }   # path to your `claude` binary
```

Either way, you can route **per task** — e.g. local extraction but Claude classification — with `classification_provider`, `extraction_provider`, `enrichment_provider`, and `concept_extraction_provider`. (`droid`, `codex`, and `gemini` are supported as providers too.)

> **Unattended setups:** the `claude-cli` provider only works where that CLI is **authenticated**. Interactive/desktop use is fine, but a headless `cron`/`systemd` daemon needs the CLI logged in *in that environment* or classification will fail — for always-on servers the API key (`provider = "claude"`) is the robust choice.

## Running locally (modest hardware is fine)

c0's **core recall path — `walk` / `search` / `add` — needs almost nothing.** The only moving parts are Neo4j and a single small embedding model (`nomic-embed-text`, ~140M parameters, well under 1 GB), and both run comfortably **CPU-only**. No GPU, no cloud. A few gigabytes of free RAM cover the graph, the embedder, and Neo4j's page cache for a personal-scale knowledge base.

The heavier work is **optional and runs in the background**. The reflection loop, concept extraction, and session enrichment use a chat LLM — and **by default that's a local Ollama model** (e.g. `qwen2.5:7b`/`:14b` for extraction/enrichment, `hermes3:8b` for classification), so the whole thing runs keyless and offline. Opt into Claude (`[claude] enabled = true`) only if you want its higher-quality `haiku`/`sonnet` judgment — via the Anthropic API or your Claude subscription's `claude` CLI ([Configuration](#configuration)). Because none of this is on the recall hot path, **CPU inference is fine** — a slow background tick never affects how fast `walk` feels.

| If you want… | You need | Notes |
|---|---|---|
| **Core recall** (walk / search / add) | CPU + ~2–4 GB free RAM | Neo4j + `nomic-embed-text`. No GPU. |
| **+ run the loop's classifier on-device** (no API key) | ~8 GB RAM for a 7B model | local `qwen2.5:7b` instead of the API; background, so CPU speed is fine |
| **Faster / higher-quality local LLM** | a GPU (optional) | ~6 GB VRAM runs the embedder + a 7B model fast; 12–24 GB unlocks 14B–32B |

The short version: the embedding hot path is light enough for any laptop, and the only reason to add a GPU is to make the *optional* local LLM work faster — never to make c0 usable in the first place.

> **On a slow CPU-only host, watch the enrichment timeout.** Recall stays fast (embeddings are tiny), but enriching a *large* session can take minutes — and if a single call exceeds the per-request timeout (default 600 s) it fails with `TimedOut`. If you hit that: raise it with `C0_ENRICH_TIMEOUT_SECS`, do less per call (`C0_ENRICH_MAX_CONCEPTS`, `C0_ENRICH_TEXT_BUDGET`, or a smaller `--limit`), or use a faster/smaller model. And if you schedule several LLM jobs (enrich, extract, the reflector), serialize them — e.g. wrap each in a shared `flock` — so they don't dogpile Ollama's single-threaded queue, where a *waiting* request still burns its timeout.

> **How enrichment picks what to read.** Each session is enriched from a `C0_ENRICH_TEXT_BUDGET`-sized window (default 8,000 chars). Rather than the first N characters, c0 fills that budget with the *most representative* turns (ranked by similarity to the session's embedding centroid, boosting turns that ran tools and the opening turn), prefixed with a signal block of the files touched and commands run — where library and framework names show up most reliably. Set `C0_ENRICH_FULL=1` to instead map-reduce over the **entire** session in budget-sized chunks and merge the concepts (full coverage, more LLM calls — costs scale with session length).

## Using c0 with Claude Code

c0 pays off most when you treat the **graph as where knowledge lives** and keep your `CLAUDE.md` for **protocol**

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [douglasjordan2](https://github.com/douglasjordan2)
- **Source:** [douglasjordan2/c0](https://github.com/douglasjordan2/c0)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-douglasjordan2-c0
- Seller: https://agentstack.voostack.com/s/douglasjordan2
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
