Install
$ agentstack add mcp-douglasjordan2-c0 ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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
Conceptnodes 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.
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.
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.ymlis included - Ollama for local embeddings and the reflection loop's classifier (defaults:
nomic-embed-textfor embeddings,hermes3:8bfor classification) — the whole loop runs locally, no key required - (optional) Claude for the background LLM — opt in with
[claude] enabled = trueto use Claude instead of a local model for classification/extraction, either via an Anthropic API key or your Claude subscription (theclaudeCLI). See [Configuration](#configuration)
Quickstart
# 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 --urlfetches and embeds a page;c0 fetchandc0 link source searchretrieve 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), thenc0 sessions search,c0 sessions resume, and track spend withc0 sessions cost. - Raw queries & history —
c0 find ""runs Cypher directly against the graph;c0 invalidation-chainreads 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:
# 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:
[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):
[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
- Source: douglasjordan2/c0
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.