# Nexus-MCP

> Unified, local-first MCP server combining hybrid search (vector + BM25), a static code graph with call edges, and persistent semantic memory in a single process. Designed for AI coding agents via the Model Context Protocol. About 90MB RAM idle and 460MB with the embedding model loaded, no API keys, no cloud dependencies. Open source under the MIT license.

This file contains the complete project documentation for Nexus-MCP, concatenated for LLM consumption.

---

# Section 1: README

# Nexus-MCP

[![PyPI version](https://img.shields.io/pypi/v/nexus-mcp-ci)](https://pypi.org/project/nexus-mcp-ci/)
[![PyPI downloads](https://static.pepy.tech/badge/nexus-mcp-ci)](https://pepy.tech/projects/nexus-mcp-ci)
[![PyPI downloads (pypistats)](https://img.shields.io/endpoint?url=https%3A%2F%2Fjaggernaut007.github.io%2Fnexus-mcp%2Fdownloads-badge.json)](https://pypistats.org/packages/nexus-mcp-ci)
[![Python 3.10–3.12](https://img.shields.io/badge/python-3.10--3.12-blue)](https://pypi.org/project/nexus-mcp-ci/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](#license)
[![CI](https://github.com/jaggernaut007/Nexus-MCP/actions/workflows/publish.yml/badge.svg)](https://github.com/jaggernaut007/Nexus-MCP/actions/workflows/publish.yml)
[![Glama MCP server](https://glama.ai/mcp/servers/jaggernaut007/Nexus-MCP/badges/card.svg)](https://glama.ai/mcp/servers/jaggernaut007/Nexus-MCP)

**Hybrid search + code graph + semantic memory in a single local MCP server. About 90 MB idle and 460 MB with the embedding model loaded ([measurements](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/MEMORY.md)).**

<!-- mcp-name: io.github.jaggernaut007/nexus-mcp -->

Nexus-MCP is a local code-intelligence MCP server for Claude Code, Codex, Cursor and any [Model Context Protocol](https://modelcontextprotocol.io) client. It gives AI agents precise, token-efficient answers about your codebase without cloud dependencies: no API keys, no data egress, no subscriptions.

**Not sure yet? Measure it on your own code first.** It is free, runs offline, calls no LLM and takes a few minutes:

```bash
uvx nexus-mcp-ci eval            # the current folder
uvx nexus-mcp-ci eval --example  # or the Nexus-MCP source, if you have no project at hand
```

You get a verdict ("Strong fit", "Likely fit" or "Weak fit") and the tokens an agent needs to reach the right file, with and without Nexus-MCP. Nothing leaves your machine. See [Evaluate on your project](#evaluate-on-your-project).

**Ready?**

```bash
pip install nexus-mcp-ci
claude mcp add nexus-mcp -- nexus-mcp-ci
```

### Results at a glance

<!-- Generated from benchmarks/published/results.json by scripts/publish_results.py. Do not edit. -->
<!-- results:start -->
| Project | Cost: Grep and Read → Nexus-MCP | Turns | Correct answers |
|---|---:|---:|---:|
| Private Python project (460 files) | $0.571 → $0.446 (**-22%**) | 55 → 35 | 36 → 35 of 36 |
| [Django](https://github.com/django/django) (3,000 files) | $0.894 → $0.863 (**-3%**) | 93 → 71 | 34 → 36 of 36 |

Live runs: Claude Code, Claude Sonnet, headless (claude -p), 12 questions per project, 3 runs each, version 2.1.0 (2026-10-28). The same code can vary by about 20% in cost with 3 runs per question (measured 2026-10-28). Search hit@1 on the four retrieval suites: `jobscout` 0.81, `nexus_mcp` 0.80, `shop_repo` 0.50, `flask` 0.64.

`nexus-mcp-ci eval` (the free offline check, no LLM) on 3 projects:

| Project | Verdict | Median tokens to the right file | Right file first |
|---|---|---:|---:|
| [Chromium base/](https://github.com/chromium/chromium/tree/599789097af11286fca074380e8a3634cea2fa25/base) (3,638 files) | Strong fit | 17,998 → **950** | 40% → 70% |
| [pallets/click](https://github.com/pallets/click) (91 files) | Strong fit | 24,774 → **1,301** | 27% → 97% |
| Nexus-MCP source (59 files) | Likely fit | 5,385 → **1,234** | 70% → 93% |

Nexus-MCP and GitNexus on [Chromium base/](https://chromium.googlesource.com/chromium/src/+/d8ebdd25980e38badccc265758aa508e6592000e/base/) (C++, 4,243 files): 8 tasks, 3 runs each, 2026-11-04.

| Condition | Version | Tokens (median) | Cost per task (median) | Correct |
|---|---|---:|---:|---:|
| Grep and Read | built-in tools | 62,450 | $0.057 | 24 of 24 |
| Nexus-MCP | 2.2.0 with the C++ parser fixes of 2.2.1 | 82,900 | $0.062 | 24 of 24 |
| GitNexus | 1.6.12 | 164,192 | $0.085 | 24 of 24 |

Nexus-MCP used fewer tokens than GitNexus on 8 of 8 tasks and cost less on 6; GitNexus cost $0.002 less on one task, and one task was a tie. Grep and Read cost less than both on this C++ code. Nexus-MCP cost less than Grep and Read on 3 of 8 tasks (the two needle tasks and WeakPtr). Each condition had the same agent, model, prompt and built-in tools, and each tool had its MCP server and its always-loaded instructions ([method](https://github.com/jaggernaut007/Nexus-MCP/blob/main/benchmarks/README.md#methodology)).

All numbers: [benchmarks page](https://jaggernaut007.github.io/nexus-mcp/benchmarks.html) · [JSON](https://jaggernaut007.github.io/nexus-mcp/benchmarks.json) · [CSV](https://jaggernaut007.github.io/nexus-mcp/benchmarks.csv).
<!-- results:end -->

---

## The Problem It Solves

An agent that explores a codebase with Grep and Read pays for every call: each call is one more
turn, and each turn reads the whole conversation again. Nexus-MCP answers a question about code
in fewer calls. `search` returns the whole code of its top results, `find_symbol` returns the
code of several names in one call, and `graph` returns every text reference beside the call
edges.

The north star of the project is lower token use with the same accuracy. It is measured, not
estimated: see [Token efficiency](#token-efficiency) and `benchmarks/`.

---

## Evaluate on your project

Decide with your own numbers. The quick check is free, offline and calls no LLM:

```bash
uvx nexus-mcp-ci eval                                   # the current folder
uvx nexus-mcp-ci eval path/to/project                   # another folder
uvx nexus-mcp-ci eval https://github.com/pallets/flask  # a public repo (shallow clone)
uvx nexus-mcp-ci eval --example                         # the Nexus-MCP source, offline
```

It asks about 30 questions made from your own code and compares Grep and Read with one
Nexus-MCP search. The right answer is the file that defines the symbol, from the parser, never
from the Nexus-MCP ranking. On this repo's source (59 files) it gave "Likely fit": 1,234 median tokens to reach the right file against 5,385, and the right file first in 93% of
questions against 70%. On Chromium's `base/` (3,638 files of C++, Java and Python) it gave
"Strong fit": 950 median tokens against 17,998 (95% fewer), and the right file first in 70% of questions against 40%. On [pallets/click](https://github.com/pallets/click) (91 files, from its
URL) it gave "Strong fit": 1,301 median tokens against 24,774 (95% fewer), and the right file first in 97% of questions against 27%. On a tiny project Grep and Read win, and the report says
so.

Want the real agent cost? The agent tier runs your own Claude Code or Codex login on tasks from
your code, with and without Nexus-MCP. It shows the cost first and asks:

```bash
nexus-mcp-ci eval --agent claude                       # Claude Sonnet
nexus-mcp-ci eval --preset efficient --agent claude:haiku,claude:sonnet
```

Results are saved in `~/.nexus-mcp/evals/` (`eval list`, `eval show <id>`). `eval share <id>`
posts the numbers (no code, no paths) to the community table, only after you say yes. Full
guide: [docs/EVALUATE.md](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/EVALUATE.md). In Claude Code, the plugin adds
`/nexus-mcp:evaluate`.

---

## Privacy and Data

Your code stays on your machine. The server makes no network calls and has no telemetry.

- **In:** the package from PyPI, and the embedding model from Hugging Face once (set
  `NEXUS_OFFLINE=1` after that). `eval <url>` also runs a `git clone`.
- **Out:** nothing, unless you run `eval --agent` (prompts and code go to Anthropic or OpenAI
  through your own CLI) or `eval share` (numbers only). Both ask first.
- **Stored:** the index, memories and graph in `<project>/.nexus/` (it holds code: add it to
  `.gitignore`), the model in `~/.cache/huggingface/`, eval results in `~/.nexus-mcp/evals/`.

```bash
nexus-mcp-ci data where              # every location and its size
nexus-mcp-ci data clear --all        # dry run; add --yes to delete
```

Every location, the shell commands that do the same, and the uninstall steps:
[docs/PRIVACY.md](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/PRIVACY.md).

---

## Quickstart (60 seconds)

You need [uv](https://docs.astral.sh/uv/getting-started/installation/). Nothing else.

**Ask your agent.** Open your agent (Claude Code, Codex, Cursor, Gemini CLI) in your project
and say:

> Install Nexus-MCP from https://github.com/jaggernaut007/Nexus-MCP

The agent reads [`llms-install.md`](llms-install.md) and runs one command. You can run the
same command yourself:

```bash
uvx nexus-mcp-ci setup
```

`setup` finds the MCP clients on your machine, shows each change, and asks before it writes
anything. It adds the server and a short skill that tells the agent when to use each tool.
Add `--dry-run` to see the changes only, `--client codex` to pick one client, and
`--uninstall` to remove what it wrote. Then reload your client (or start a new session) and
ask something like "where do we retry failed payments?". The agent calls `search`, and
`index` once, the first time in a project.

The server sends its own usage instructions when it connects, so the agent knows which tool
answers which question even without the skill.

Other ways in, each with the same server:

- **VS Code:** [Install in VS Code](https://vscode.dev/redirect/mcp/install?name=nexus-mcp&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22nexus-mcp-ci%22%5D%7D)
- **Cursor:** the "Add to Cursor" button on the [website](https://jaggernaut007.github.io/nexus-mcp/#install)
- **Claude Desktop:** download `nexus-mcp.mcpb` from the [latest release](https://github.com/jaggernaut007/Nexus-MCP/releases/latest) and open it
- **Gemini CLI:** `gemini extensions install https://github.com/jaggernaut007/Nexus-MCP`
- **Docker:** `docker run -i --rm -v "$PWD:/workspace" -v nexus-data:/data ghcr.io/jaggernaut007/nexus-mcp`
- **GitHub Actions:** [`uses: jaggernaut007/Nexus-MCP@v2`](docs/GITHUB-ACTION.md)

All channels, and what each needs: [docs/DISTRIBUTION.md](docs/DISTRIBUTION.md).

### Claude Code plugin

The plugin registers the server and the same routing skill. It runs the server through
`uvx`, so it needs only uv, no `pip install`:

```
/plugin marketplace add jaggernaut007/Nexus-MCP
/plugin install nexus-mcp@nexus-mcp
```

Use `setup` or the plugin for Claude Code, not both, or the server is registered twice.
Tool names differ by route: `mcp__nexus-mcp__search` after `setup`,
`mcp__plugin_nexus-mcp_nexus-mcp__search` through the plugin. The rest of this README
writes bare names (`search`) and you do not need the prefix in prompts.

### Optional: project instructions

If you want the rules in your own project file, a short block is enough:

```markdown
## Code Navigation

The nexus-mcp server is connected. For questions about how this codebase works:
- No setup call is needed; run `index` only if a tool says the project is not indexed.
- Use `search` to find code, `find_symbol` for symbols, `map` for structure.
- Use `graph` with `transitive=true` before you change a shared function. The call graph
  is static, so also use `search` for dynamic call sites.
- Use plain grep or read when you already know the file or the exact string.
```

The full routing guide is in [docs/AGENT_ROUTING.md](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/AGENT_ROUTING.md).

---

## How It Works

### Indexing Pipeline (8 steps)

```
Source files
    │
    ├─ Step 1: Discover ──────── walk tree, filter by ext/size/.gitignore
    │
    ├─ Step 2: Parse symbols ─── tree-sitter (parallel ThreadPool)
    │           extracts: functions, classes, methods
    │           captures: name, signature, docstring, line_start/end, language
    │
    ├─ Step 3: Parse graph ────── ast-grep (sequential for consistency)
    │           extracts: containment edges, import edges, callee names of each function
    │                     (indexing/call_resolver.py makes CALLS edges after the last batch;
    │                      no inheritance edges yet)
    │           output: UniversalGraph — nodes: modules, functions, classes;
    │                                    edges: CONTAINS, IMPORTS, CALLS
    │
    ├─ Step 4: Transfer graph ── populate rustworkx PyDiGraph
    │           O(1) node lookup by name, Rust-backed traversal
    │
    ├─ Step 5: Chunk ──────────── Symbol → CodeChunk
    │           deterministic IDs: SHA256(file_path + symbol_name + line)
    │           avoids duplicate inserts on incremental reindex
    │
    ├─ Step 6: Embed ──────────── bge-small-en: 384-dim
    │           lazy-loaded, unloaded after indexing (try/finally)
    │           GPU/MPS auto-detected; falls back to CPU
    │
    ├─ Step 7: Store ──────────── write to LanceDB `chunks` table (12-col PyArrow schema)
    │           rebuild native FTS (Tantivy) index after write
    │
    └─ Step 8: Cleanup ────────── unload model, persist metadata (mtimes for incremental)
                                  save rustworkx graph to SQLite (warm-start recovery)
```

**Incremental reindex:** mtime-based — only changed files are re-processed. Corrupt index detection triggers automatic full rebuild.

### Search Pipeline

```
search("how does auth work")
         │
         ├─► vector_engine.search(query, n=30)  ← cosine similarity on 384-dim (bge-small-en) embeddings
         │                                         "auth" finds "verify_credentials", "token_check"
         │
         ├─► bm25_engine.search(query, n=30)    ← Tantivy FTS on same LanceDB table
         │                                         fast exact-keyword matching
         │
         ├─► graph_engine.boost(query, n=30)    ← structural relevance score
         │                                         hub symbols (high in/out degree) boosted
         │
         └─► fusion.merge(v_results, b_results)
                  │
                  │  Reciprocal Rank Fusion: score = Σ weight_i / (k + rank_i)
                  │  default weights: vector=0.5, bm25=0.3
                  │
                  └─► token_budget.truncate()   ← summary / detailed / full
                           │
                           └─► Top-N chunks, scored, formatted
```

### Technology Stack

| Layer | Technology | Decision Rationale |
|-------|-----------|-------------------|
| **Vector store** | LanceDB | mmap disk-backed → ~20–50 MB overhead vs ChromaDB's in-memory model. Native Tantivy FTS means one store for both vector and BM25. ([ADR-002](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-002-lancedb-over-chromadb.md)) |
| **Embeddings** | bge-small-en | Lightweight (384-dim, no trust_remote_code). Lazy-load/unload keeps RAM flat after indexing. ([ADR-003](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-003-onnx-runtime-over-pytorch.md)) |
| **Graph engine** | rustworkx PyDiGraph | Rust-backed, O(1) node lookup, PageRank + centrality algorithms. Thread-safe with RLock. ([ADR-006](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-006-rustworkx-graph-engine.md)) |
| **Symbol parser** | tree-sitter 0.21.3 | 25+ languages, incremental parsing, AST-level symbol extraction with metadata. Parallel via ThreadPool. ([ADR-005](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-005-dual-parser-strategy.md)) |
| **Graph parser** | ast-grep | Structural matching for containment and import edges (call edges are resolved after indexing; inheritance edges are not extracted yet). Sequential run for graph consistency. ([ADR-005](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-005-dual-parser-strategy.md)) |
| **Chunking** | Symbol-based | One chunk per function/class. Deterministic SHA256 IDs prevent duplicate inserts. ([ADR-008](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-008-code-chunk-strategy.md)) |
| **Persistence** | SQLite + LanceDB | Graph in SQLite (warm-start recovery), vectors+FTS in LanceDB, mtimes in JSON. Zero-config. |
| **MCP framework** | FastMCP 2.0 | Stdio transport, automatic tool registration, schema generation. |

---

## Token Efficiency

Two live runs, 12 questions each, with Claude Sonnet, 3 runs for each question and condition
(72 runs each) and the prompt cache warm for both conditions. **The result depends on the
project.**

**Latest (commit `a67e5b4`, `find_symbol` shows an outline of a long class):** the numbers are
in [Results at a glance](#results-at-a-glance), generated from
`benchmarks/published/results.json`.

Only the nexus condition ran again for this commit; the Grep and Read numbers are the stored
runs below. On django nexus is cheaper in total but still dearer on 8 of 12 questions (the short
lookups). The one wrong answer on the private project came from a run that did not call
`find_symbol`. With 3 runs for each question the same code can vary by about 20% in cost
(measured 2026-10-28), so the change against the runs before the outline is inside the noise;
all three recent nexus runs on the private project cost less than Grep and Read ($0.446 to
$0.531 against $0.571), and on django nexus is about equal ($0.863 to $0.973 against $0.894). Details:
`docs/EVALS.md`. The sections below are the runs before the outline.

**Private 460-file Python project** (2026-10-08, commit `91a338e`):

| | Grep and Read | Nexus-MCP |
|---|---:|---:|
| Turns (sum of the median of each question) | 55 | 34 (-38%) |
| Output tokens | 11,917 | 8,881 (-25%) |
| Cost | $0.571 | $0.459 (**-20%**) |
| Correct answers | 36 of 36 | 36 of 36 |
| Prompt on the first turn | 12,700 tokens | 16,300 tokens |

Nexus is cheaper on 7 questions, equal on 3 and dearer on 2 (the two impact questions, by
$0.005 and $0.004). The saving comes from fewer turns and from smaller tool results than the
whole-file reads of Grep and Read (see the cost split below). Per question numbers are in
`docs/EVALS.md`. The same questions on 2026-10-07 (before exact names, code in `find_symbol`,
the smaller tool list and the warm-up call) gave nexus 6% more cost than Grep and Read.

**Django, a public repository** (3,000 files; 2026-10-27, commit `3fa8810`):

| | Grep and Read | Nexus-MCP |
|---|---:|---:|
| Turns (sum of the median of each question) | 93 | 78 (-16%) |
| Output tokens | 32,291 | 27,513 (-15%) |
| Cost | $0.894 | $0.973 (**+8.9%**) |
| Correct answers | 34 of 36 | 35 of 36 |

On django nexus uses fewer turns but costs more. It is cheaper on the questions where Grep and
Read need 18 to 24 turns (`SQLCompiler.as_sql` -21%, `URLResolver.resolve` -10%) and dearer on
short lookups that take 2 or 3 turns. Django is a repository that the model knows from
training, so part of this measures the model. One ground truth was corrected after the run (a
question asked for a value, and the scorer also required a file name); details in
`docs/EVALS.md`.

**Why: the cost split of the traces.** The parts below add up to the reported cost of each run.

| Change, nexus against Grep and Read | django | jobscout |
|---|---:|---:|
| Cache read (the prompt, on each turn) | +$0.011 | -$0.002 |
| Cache write (tool results and the first message) | **+$0.111** | -$0.078 |
| Output | -$0.042 | -$0.033 |
| **Total** | **+$0.079** | **-$0.112** |

- The 3,600 extra prompt tokens on each turn cost little: a cache read is $0.20 per million.
- Tool results are written to the cache at $4 per million, 20 times the read price. So the size
  of a result matters more than the size of the prompt.
- On jobscout the agent does not know the code and reads whole files (10,000 characters for each
  Read). A `find_symbol` result of 9,700 characters replaces a Read and saves a turn.
- On django the agent knows the code and reads short line ranges (3,400 characters for each
  Read). A `find_symbol` result of 9,200 characters is larger than the read that it replaces. A
  class lookup sends up to 6,000 characters of the class body: `CharField` cost $0.023 with nexus
  and $0.013 with Grep.

This led to the outline for a class in `find_symbol` (methods and lines, not the body). With it,
nexus on django went from $0.973 to $0.863 (see "Latest" above). Details and the other
candidates are in `docs/EVALS.md`.

The saving is large on a project that the model does not know and small on one that it knows.
Run `benchmarks/` on your own project before you rely on it.

---

## The 8 Tools

**New in v2.3.0 (no breaking change):** `uvx nexus-mcp-ci setup` installs the server and the
skill into your MCP clients, new install routes cover Claude Desktop, Gemini CLI, VS Code,
Cursor, Docker and GitHub Actions, and the license is now MIT. The MCP tools do not change.

**New in v2.2.0 (no breaking change):** `nexus-mcp-ci eval` measures Nexus-MCP on your own
code ([Evaluate](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/EVALUATE.md)), and
`nexus-mcp-ci data` shows and clears every stored file ([Privacy](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/PRIVACY.md)).
The MCP tools do not change.

**v2.1.0 breaking change:** `explain` and `health` are removed from the MCP tool list, and
several rarely used parameters are removed from the schemas (see [CHANGELOG](https://github.com/jaggernaut007/Nexus-MCP/blob/main/CHANGELOG.md)).
`find_symbol` now returns the code with callers and callees.

**v2.0.0 breaking change:** `find_callers`/`find_callees`/`impact` merged into
`graph`, `overview`/`architecture` merged into `map`, and `remember`/`recall`/`forget`
merged into `memory` — see [CHANGELOG](https://github.com/jaggernaut007/Nexus-MCP/blob/main/CHANGELOG.md) for the old→new mapping and
[ADR-017](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-017-tool-consolidation.md) for why. Fewer, richer tools route
better under MCP Tool Search than many thin ones.

### Discovery & Indexing

| Tool | Use When |
|------|----------|
| `index(path)` | First action in any session. Supports comma-separated multi-folder paths. Incremental by default, reports progress as it runs, and starts a debounced auto-reindex watcher (`NEXUS_AUTO_WATCH`) when it finishes. |
| `status()` | Check index health: symbol count, chunk count, memory usage, engine availability, and a `stale`/`staleness_warning` pair if files changed since the last index. |
| `map(detail)` | **Replaces `ls` + manual browsing.** `detail="summary"` (files/languages/quality/top-modules, was `overview()`), `"architecture"` (layers/dependencies/classes/entry points/hub symbols, was `architecture()`), or `"full"` for both. |

### Search

| Tool | Use When |
|------|----------|
| `search(query, limit)` | Primary code discovery. A query can hold exact names (`parse_config RetryPolicy`): their definitions come first. The top 3 results carry the whole symbol; the others show the signature. Falls back to live grep if results are sparse. Returns a `warning` if the index looked stale (a background reindex starts). |

### Graph Analysis

| Tool | Use When |
|------|----------|
| `find_symbol(symbol_name, exact)` | Look up one or more symbols by name (several names in one call): the code of each, with its callers and callees by name. `exact=False` for fuzzy matching. |
| `graph(symbol_name, direction, transitive)` | `direction="callers"` (who calls this, was `find_callers`) or `"callees"` (what this calls, was `find_callees`). **`transitive=True`** (was `impact()`) gives the transitive change blast radius before a refactor (a lower bound: static edges only). It requires `direction="callers"`; `direction="callees"` with `transitive=True` returns an error. Call edges are static and name-based, so use `search` for call sites the graph cannot see (see [Known Limitations](#known-limitations)). |
| `analyze(path)` | Code quality: cyclomatic complexity, cognitive complexity, code smells, dependency metrics. |

### Memory

| Tool | Use When |
|------|----------|
| `memory(action, content, query, tags, memory_id)` | `action="store"` to persist a decision or note across sessions; `"search"` for semantic retrieval; `"delete"` to remove by ID or tag. With `NEXUS_PERMISSION_LEVEL=read` only `search` is allowed. |

---

## Install

### From PyPI (recommended)

```bash
pip install nexus-mcp-ci

# GPU (CUDA) support — adds ONNX CUDA execution provider
pip install nexus-mcp-ci[gpu]

```

### From Source

```bash
git clone https://github.com/jaggernaut007/Nexus-MCP.git
cd Nexus-MCP
./setup.sh           # creates venv, installs, verifies
# or
pip install -e ".[dev]"
```

**Python 3.10–3.12 supported.** Python 3.13+ is not yet supported by the current dependency stack, and the packaged Glama/Docker build uses Python 3.12 for compatibility. Optional: `rg` (ripgrep) for 100% search coverage fallback on unindexed files.

> The deprecated `jina-code` model requires ONNX Runtime. If you see ONNX/Optimum errors:
> ```bash
> pip install "sentence-transformers[onnx]" "optimum[onnxruntime]>=1.19.0"
> ```
> The default `bge-small-en` model needs neither ONNX nor `trust_remote_code`.

---

## MCP Client Setup

### Claude Code

```bash
# Minimal
claude mcp add nexus-mcp -- nexus-mcp-ci

# GPU embeddings
claude mcp add nexus-mcp -e NEXUS_EMBEDDING_DEVICE=cuda -- nexus-mcp-ci

# Virtualenv install — pass the full binary path
claude mcp add nexus-mcp -- /path/to/.venv/bin/nexus-mcp-ci
```

### Claude Desktop

`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):

```json
{
  "mcpServers": {
    "nexus-mcp": {
      "command": "nexus-mcp-ci",
      "args": [],
      "env": {
        "NEXUS_EMBEDDING_DEVICE": "cpu"
      }
    }
  }
}
```

### Cursor / Windsurf / Cline / Any MCP Client

```json
{
  "nexus-mcp": {
    "command": "nexus-mcp-ci",
    "transport": "stdio"
  }
}
```

---

## Agent Integration Patterns

### CLAUDE.md boilerplate (drop into project root)

```markdown
## Code Intelligence — nexus-mcp

1. **Session start**: no setup call. Run `index` only if a tool says "No codebase indexed"
2. **To find code you cannot name**: `search`, before reading files one by one
3. **To see symbols you can name**: `find_symbol`, with all the names in one call
4. **Before refactoring**: `graph` with `transitive=true` to assess blast radius. The call graph is static (no dynamic dispatch), so also use `search` to find call sites.
5. **For project orientation**: `map` with `detail="summary"` or `detail="architecture"`
6. **When you know the exact file or string**: plain grep or read is fine
```

### Typical agent tool-call sequence

```
# Session start
status()               → "indexed: True, 8,412 chunks, 1,203 symbols, 87 MB"

# Code discovery
search("JWT token validation", limit=10)
  → auth/jwt.py:42  validate_token()         score=0.94
  → auth/middleware.py:18  require_auth()    score=0.87
  → tests/test_auth.py:91  test_valid_jwt()  score=0.81

# Symbols you can name
find_symbol("validate_token, require_auth")
  → the code of each, file and lines
  → callers: [post_order, ...], callees: [...]

# Pre-refactor safety check
graph("validate_token", direction="callers", transitive=True)
  → total_impacted: N          # a lower bound: dynamic calls are not visible

# Locate call sites with search instead
search("validate_token(", mode="bm25")
  → auth/middleware.py:18  require_auth()  ...
  → auth/login.py:57  login_required()     ...
```

### Multi-folder monorepo indexing

```python
# Index multiple roots in one call — processed sequentially, shared engines
index(path="packages/api/src,packages/shared/src,packages/cli/src")

# Or use the paths parameter for additional roots
index(path="packages/api/src", paths="packages/shared/src,packages/cli/src")
```

---

## Configuration

All settings via `NEXUS_` environment variables:

| Variable | Default | Description |
|----------|---------|-------------|
| `NEXUS_EMBEDDING_MODEL` | `bge-small-en` | `bge-small-en` (384-dim). `jina-code` is deprecated |
| `NEXUS_EMBEDDING_DEVICE` | `auto` | `auto` (CUDA → MPS → CPU), `cuda`, `mps`, `cpu` |
| `NEXUS_STORAGE_DIR` | `.nexus` | Index storage directory |
| `NEXUS_AUTO_WATCH` | `true` | Auto-reindex on file change via a debounced watcher, started after `index()` |
| `NEXUS_AUTO_RESTORE` | `true` | A new server process reattaches to the stored index, so `status` is already `indexed: true` |
| `NEXUS_WARM_START` | `true` | Attach the index and load the embedding model in the background while the server starts, so the first `search` does not wait about 7 s. The model then uses its memory (about 370 MB) from the start of each session |
| `NEXUS_STALENESS_CHECK_INTERVAL` | `15` | Seconds between `status()`/`search()` staleness checks (throttled, not per-call) |
| `NEXUS_IDLE_TIMEOUT` | `10800` | Stop the server after this many seconds with no tool call (3 hours). `0` turns it off |
| `NEXUS_EXIT_ON_PARENT_EXIT` | `true` | Stop the server when the client process that started it is gone |
| `NEXUS_SHUTDOWN_TIMEOUT` | `10` | Longest wait for a running index at shutdown; the process then exits |
| `NEXUS_MAX_FILE_SIZE_MB` | `10` | Skip files larger than this |
| `NEXUS_CHUNK_MAX_CHARS` | `4000` | Max chars per code chunk |
| `NEXUS_EMBEDDING_BATCH_SIZE` | `32` | Chunks per embedding batch. Lower it to cut peak RAM while indexing |
| `NEXUS_INDEX_FILE_BATCH_SIZE` | `50` | Files per streaming indexing batch |
| `NEXUS_GRAPH_MAX_DEPTH` | `10` | Max depth for transitive graph queries |
| `NEXUS_MAX_MEMORY_MB` | `350` | Memory budget target |
| `NEXUS_SEARCH_MODE` | `hybrid` | `hybrid`, `vector`, or `bm25` |
| `NEXUS_FUSION_WEIGHT_VECTOR` | `0.5` | Vector score weight in RRF |
| `NEXUS_FUSION_WEIGHT_BM25` | `0.3` | BM25 score weight in RRF |
| `NEXUS_PERMISSION_LEVEL` | `full` | `full`, `read`, or `restricted` |
| `NEXUS_RATE_LIMIT_ENABLED` | `false` | Enable per-tool token-bucket rate limiting |
| `NEXUS_AUDIT_ENABLED` | `true` | Structured audit logging with correlation IDs |
| `NEXUS_TRUST_REMOTE_CODE` | `true` | Needed only by the deprecated jina-code; set `false` with bge-small-en |
| `NEXUS_LOG_LEVEL` | `INFO` | Logging level |
| `NEXUS_LOG_FORMAT` | `text` | `text` or `json` |

The Python package defaults to `bge-small-en`. The `Dockerfile`, `docker/Dockerfile.stdio` and `glama.json` use the same default. `jina-code` still loads, with a warning, so an existing index keeps working. It is deprecated and will be removed in a future major release.

### Embedding Models

| Model | Key | Dims | Max Seq | Backend | `trust_remote_code` |
|-------|-----|:----:|:-------:|---------|:-------------------:|
| BGE Small EN v1.5 (default) | `bge-small-en` | 384 | 512 | PyTorch | No |
| Jina Embeddings v2 Code (deprecated) | `jina-code` | 768 | 8,192 | ONNX | Yes |

**After changing model, re-index.** Embeddings from different models are incompatible.

---

## Comparison

### vs. other code-intelligence MCP servers

Features only, from each project's own documentation. We make no performance claims about
other tools: run `nexus-mcp-ci eval` on your code instead.

| Feature | Nexus-MCP | Serena | claude-context |
|---|:---:|:---:|:---:|
| How it finds code | hybrid: vectors + BM25 | language servers (LSP) | hybrid: vectors + BM25 |
| Where the index lives | local LanceDB | none (live LSP) | a vector database (Milvus or Zilliz Cloud) |
| Embeddings | local model, no API key | none | an embedding provider (OpenAI by default; Ollama possible) |
| Call graph | static, 6 languages | LSP references | no |
| Project memory | yes | yes | no |
| Edits code | no (read only) | yes | no |
| Measure it on your repo | `nexus-mcp-ci eval` | — | — |

Other servers in this space include codebase-memory-mcp, code-graph-rag, code-index-mcp and
ast-grep-mcp. Check each project's docs. Corrections to this table are welcome.

### vs. Other MCP Servers

| Feature | Nexus-MCP | Sourcegraph MCP | Greptile MCP | GitHub MCP | tree-sitter MCP |
|---------|:---:|:---:|:---:|:---:|:---:|
| Fully local / private | ✅ | ❌ infra required | ❌ cloud | ❌ cloud | ✅ |
| Semantic (vector) search | ✅ | ❌ keyword only | ✅ LLM-based | ❌ | ❌ |
| Keyword (BM25) search | ✅ | ✅ | — | ✅ | ❌ |
| Hybrid fusion (RRF) | ✅ | ❌ | ❌ | ❌ | ❌ |
| Code graph (call/import) | static: containment, imports, calls | ✅ SCIP | ❌ | ❌ | ❌ |
| Semantic memory (persistent) | ✅ 6 types | ❌ | ❌ | ❌ | ❌ |
| Change impact analysis | static callers only | partial | ❌ | ❌ | ❌ |
| Token-budgeted responses | ✅ 3 levels | ❌ | ❌ | ❌ | ❌ |
| Languages | 25+ | 30+ | many | many | many |
| Cost | **Free (MIT)** | $$$ | $40/mo | $10–39/mo | Free |
| API keys required | **No** | Yes | Yes | Yes | No |

### vs. AI Code Tools

| Capability | Nexus-MCP | Cursor | Copilot @workspace | Cody | Continue.dev | Aider |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| IDE-agnostic | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| MCP-native | ✅ | partial | ❌ | ❌ | ✅ client | ❌ |
| Fully local | ✅ | partial | ❌ | partial | ✅ | ✅ |
| Hybrid search | ✅ | unknown | unknown | keyword | yes | ❌ |
| Code graph | partial (containment + imports) | unknown | unknown | ✅ SCIP | basic | ❌ |
| Semantic memory | ✅ persistent | ❌ | ❌ | ❌ | ❌ | ❌ |
| Token-budgeted output | ✅ | — | — | — | — | — |
| Source available | ✅ MIT | ❌ | ❌ | partial | ✅ | ✅ |
| Cost | **Free (MIT)** | $20–40/mo | $10–39/mo | $0–49/mo | Free | Free |

---

## Development

```bash
git clone https://github.com/jaggernaut007/Nexus-MCP.git
cd Nexus-MCP
pip install -e ".[dev]"

pytest -v                    # all tests (-m "not slow" skips the performance ones)
pytest -m "not slow"         # skip performance benchmarks
pytest tests/test_hybrid_search.py  # single module
ruff check .                 # lint
```

### Project Structure

```
src/nexus_mcp/
├── server.py              # FastMCP entrypoint — 10 thin tool wrappers (permission guard, rate limit, audit), graceful shutdown
├── core_api.py            # Transport-agnostic tool logic + input validation; importable without MCP
├── config.py              # Settings (NEXUS_ env prefix)
├── state.py               # Global singleton SessionState
├── core/
│   ├── models.py          # Symbol, ParsedFile, CodebaseIndex, Memory
│   ├── graph_models.py    # UniversalNode, Relationship
│   ├── interfaces.py      # IParser, IEngine protocols
│   └── exceptions.py      # NexusException hierarchy
├── parsing/
│   ├── treesitter_parser.py   # Symbol extraction (parallel)
│   ├── astgrep_parser.py      # Structural graph extraction (sequential)
│   ├── language_registry.py   # 25+ language definitions
│   └── file_watcher.py        # Debounced watchdog for live reindex
├── engines/
│   ├── vector_engine.py   # LanceDB cosine similarity search
│   ├── bm25_engine.py     # LanceDB native FTS (Tantivy)
│   ├── graph_engine.py    # rustworkx PyDiGraph with RLock
│   ├── live_grep.py       # rg/grep fallback for unindexed or sparse results
│   ├── fusion.py          # Reciprocal Rank Fusion
├── indexing/
│   ├── pipeline.py        # 8-step indexing pipeline
│   ├── embedding_service.py   # ONNX Runtime, GPU/MPS auto-detect
│   ├── parallel_indexer.py    # ThreadPool over files
│   └── chunker.py         # Symbol → CodeChunk with deterministic IDs
├── memory/
│   └── memory_store.py    # LanceDB-backed memory, TTL, 6 types
├── analysis/
│   └── code_analyzer.py   # Cyclomatic/cognitive complexity, smells
├── formatting/
│   ├── token_budget.py    # Token counting and truncation
│   └── response_builder.py  # Structured responses
├── persistence/
│   └── store.py           # SQLite graph persistence
├── security/
│   ├── permissions.py     # READ/MUTATE/WRITE tool categories
│   └── rate_limiter.py    # Token-bucket, per-tool, thread-safe
└── middleware/
    └── audit.py           # Structured audit logs, correlation IDs, field redaction
benchmarks/                # Token-efficiency benchmark harness (ADR-018)
self_test/                 # End-to-end demo of the 8 tools and the Python API
plugin/                    # Claude Code plugin: registers the server + routing skill
```

### Adding a New Tool

1. Add the logic as a plain function in `core_api.py`
2. Validate input there with the `validate_*` helpers (`validate_path`, `validate_symbol_name`, `validate_query`)
3. Add a thin wrapper in `server.py` decorated with `@mcp.tool()` and `@_audited`, and call `_guard("<tool>")` first
4. Add the permission category to `security/permissions.py`
5. Write tests in `tests/`
6. Update `self_test/demo_mcp.py` to exercise the tool

### Adding a New Language

1. Add entry to `parsing/language_registry.py` with the tree-sitter grammar
2. Add structural patterns to `parsing/astgrep_parser.py` for call/import extraction
3. Add test fixtures in `tests/fixtures/`

---

## Self-Test

Verify your installation exercises the 8 tools and the Python API end-to-end:

```bash
python self_test/demo_mcp.py                   # built-in sample project
python self_test/demo_mcp.py /path/to/project  # your own codebase
```

Expected output: the 8 tools and the Python API exercised with pass/fail per tool and a summary.

---

## Known Limitations

- **Sequential graph parsing**: ast-grep runs sequentially (not parallel) to keep the call graph consistent. This is the main indexing bottleneck on large codebases.
- **bge-small-en uses PyTorch**: The default model runs on PyTorch, not ONNX.
- **No incremental graph updates**: Graph is rebuilt in full on incremental reindex (only vector/BM25 are incremental at the chunk level).
- **No SSE transport**: Only stdio transport is currently supported.
- **Language coverage**: 25+ languages get tree-sitter symbol extraction. ast-grep structure (functions, classes, imports) covers Python, JavaScript, TypeScript, Go, Java, and Rust; other languages have no graph structure.
- **Call edges are static and name-based.** `graph()` and the `callers`/`callees` fields of `find_symbol()` come from parsing, not runtime tracing. Edges exist for Python, JavaScript, TypeScript, Go, Java and Rust. A call resolves only when the callee is in the same file, in an imported file, or has a unique name in the index. A name shared by several definitions gets no edge, so results are a lower bound. Dynamic dispatch, monkey-patching, callbacks and reflection do not show up, so treat `graph(transitive=True)` as a lower bound on blast radius.
- **Complexity is approximate**: cyclomatic complexity counts branch points per function. Only Python counts `and`/`or`, and only Python functions get docstrings on graph nodes.
- **Auto-reindex has a detection lag**: with the file watcher enabled (default), edits are picked up after a short debounce, and `status()`/`search()` run a throttled staleness check as a backstop — not an instant, per-call guarantee of freshness.

---

## Architecture Decision Records

Key decisions are documented in [docs/adr/](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/):

| ADR | Decision |
|-----|----------|
| [ADR-001](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-001-single-mcp-consolidation.md) | Merge two MCP servers into one |
| [ADR-002](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-002-lancedb-over-chromadb.md) | LanceDB over ChromaDB |
| [ADR-003](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-003-onnx-runtime-over-pytorch.md) | ONNX Runtime over PyTorch for embeddings |
| [ADR-004](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-004-bge-small-default-model.md) | bge-small-en as default embedding model |
| [ADR-005](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-005-dual-parser-strategy.md) | Dual parser: tree-sitter + ast-grep |
| [ADR-006](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-006-rustworkx-graph-engine.md) | rustworkx for graph algorithms |
| [ADR-007](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-007-lancedb-schema-design.md) | 12-column PyArrow schema for LanceDB |
| [ADR-008](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-008-code-chunk-strategy.md) | Symbol-based chunking with deterministic IDs |
| [ADR-009](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-009-indexing-pipeline-architecture.md) | 8-step indexing pipeline |
| [ADR-010](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-010-graph-tools-api-design.md) | Graph tools API: serialization, ambiguity handling |
| [ADR-011](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-011-hardening-decisions.md) | Graceful shutdown, corruption recovery, JSON logging |
| [ADR-012](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-012-tool-permission-model.md) | READ/MUTATE/WRITE permission categories |
| ~~[ADR-013](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-013-pydantic-schemas.md)~~ | Pydantic v2 I/O schemas — superseded by ADR-016 (never wired in, deleted) |
| [ADR-014](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-014-rate-limiting.md) | Token-bucket rate limiting (off by default) |
| [ADR-015](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-015-auto-watch-and-staleness-detection.md) | Auto-watch + throttled staleness detection |
| [ADR-016](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-016-remove-unused-pydantic-schemas.md) | Removal of unused Pydantic schemas (supersedes ADR-013) |
| [ADR-017](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-017-tool-consolidation.md) | Tool consolidation 15→10, action-aware permission categories |
| [ADR-018](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-018-token-efficiency-benchmark.md) | Token-efficiency benchmark harness (`benchmarks/`) |

---

## Documentation

Start at the [docs index](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/README.md).

- [Evaluate on your project](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/EVALUATE.md) — The free offline check, the agent tier, saving and sharing results
- [Privacy and data](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/PRIVACY.md) — What goes in and out, where data is stored, how to remove it
- [Benchmarks](https://jaggernaut007.github.io/nexus-mcp/benchmarks.html) — Every published number, with JSON and CSV
- [Installation Guide](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/INSTALLATION.md) — Prerequisites, client-specific setup, troubleshooting
- [Architecture](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/ARCHITECTURE.md) — Data flow, component design, memory budget analysis
- [Usage Guide](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/USAGE_GUIDE.md) — Full tool reference with examples
- [Developer Guide](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/DEVELOPER_GUIDE.md) — Contributing, adding tools/engines/languages
- [Research Notes](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/RESEARCH.md) — Library evaluations and technology deep-dives

---

## Acknowledgments

Nexus-MCP consolidates two earlier open-source projects:

- **[CodeGrok MCP](https://github.com/shreyasjagannath/CodeGrok_mcp)** by [rdondeti](https://github.com/rdondeti) (Ravitez Dondeti, MIT) — Contributed the symbol extraction pipeline, embedding service, parallel indexer, core data models, and memory retrieval system.
- **[code-graph-mcp](https://github.com/entrepeneur4lyf/code-graph-mcp)** by [entrepeneur4lyf](https://github.com/entrepeneur4lyf) — Contributed the ast-grep structural parser, rustworkx graph engine, complexity analysis, and relationship extraction.

Source files retain "Ported from" attribution in their module docstrings. See [ADR-001](https://github.com/jaggernaut007/Nexus-MCP/blob/main/docs/adr/ADR-001-single-mcp-consolidation.md) for the consolidation rationale.

---

## License

[MIT](LICENSE). Free to use, copy, modify and distribute, for any purpose, including at work.

Versions 2.0.1 to 2.2.3 were released under the PolyForm Noncommercial License 1.0.0. Anyone who
obtained one of those versions keeps the terms they got. Versions 0.1.0, 0.1.1 and 2.0.0 were MIT.
Every new release is MIT.

If Nexus-MCP saves you tokens at work, a star or a sponsorship is welcome. Neither is required.

---

# Section 2: Evaluate on your project

# Evaluate Nexus-MCP on your code

Measure Nexus-MCP on your own project before you adopt it. The first check is free, runs offline
and calls no LLM. Your code does not leave your machine.

```bash
uvx nexus-mcp-ci eval
```

The command indexes the current folder in a temporary place, asks about 30 questions made from
your own code, and compares two ways to reach the right file:

- **Grep and Read**: the way an agent works without Nexus-MCP.
- **Nexus-MCP**: one `search` call, plus file reads only when the search misses.

It prints a verdict ("Strong fit", "Likely fit" or "Weak fit"), a table, and a "Data in this run"
block. The first run downloads the embedding model once (about 130 MB).

## Choose what to evaluate

| Command | What it reads | Network |
|---|---|---|
| `nexus-mcp-ci eval` | the current folder | none (after the model download) |
| `nexus-mcp-ci eval path/to/project` | that folder | none |
| `nexus-mcp-ci eval https://github.com/pallets/flask` | a shallow clone of a public repo | `git clone` |
| `nexus-mcp-ci eval --example` | the installed Nexus-MCP source (59 files) | none |

Use a local path for private code: it never leaves the machine. Use a public URL when you want a
result that other people can repeat (the run records the commit).

`uvx` needs Python 3.10 to 3.12. If your default Python is newer, run
`uvx --python 3.12 nexus-mcp-ci eval`. `pipx run nexus-mcp-ci eval` and
`pip install nexus-mcp-ci && nexus-mcp-ci eval` also work.

## Read the verdict

| Verdict | Rule |
|---|---|
| Strong fit | Right file in the top 3 at least 10 points more often, and at least 30% fewer tokens |
| Likely fit | Right file in the top 3 at least as often, and at least 15% fewer tokens |
| Weak fit | Anything else, or a project with fewer than 50 files |

A "Weak fit" is a valid result. On a small or very well named project, Grep and Read are cheap,
and the report says so. On an 18-file toy project and on a synthetic 119-file project,
Grep and Read used fewer tokens.

The numbers are a token proxy (characters divided by 4), not a bill. The agent tier below
measures the real cost.

Measured results (2026-11-03, 30 to 40 questions each):

| Project | Files | Verdict | Median tokens: Grep and Read → Nexus-MCP | Right file first |
|---|---:|---|---:|---:|
| [Chromium `base/`](https://github.com/chromium/chromium/tree/599789097af11286fca074380e8a3634cea2fa25/base) | 3,638 | Strong fit | 17,998 → 950 | 40% → 70% |
| [pallets/click](https://github.com/pallets/click) | 91 | Strong fit | 24,774 → 1,301 | 27% → 97% |
| Nexus-MCP source (`--example`) | 59 | Likely fit | 5,385 → 1,234 | 70% → 93% |

Indexing took about 25 minutes for Chromium `base/`, 1 to 2 minutes for click and about 1 minute for the Nexus-MCP source, on a laptop. The source of these numbers is
`benchmarks/published/results.json`.

Question kinds:

- `concept`: the first sentence of a docstring, with the name removed (Python only).
- `described`: the words of the name and of its class.
- `lookup`: the exact name. This kind measures tokens only.

The right file is the file that defines the symbol, from the parser. It never comes from the
Nexus-MCP ranking.

## The agent tier (paid, opt-in)

```bash
nexus-mcp-ci eval --agent claude                       # Claude Sonnet, Grep and Read against Nexus-MCP
nexus-mcp-ci eval --agent claude:haiku,claude:sonnet   # two models
nexus-mcp-ci eval --agent codex:gpt-5-codex            # Codex, experimental
nexus-mcp-ci eval --preset efficient --agent claude:haiku,claude:sonnet
```

- It runs your own `claude` or `codex` CLI, with your own login, on tasks made from your code.
- It sends the prompts and the code that the agent reads to Anthropic or OpenAI.
- It shows a cost estimate and asks before it starts. It stops starting runs at `--max-usd`
  (default $5).
- Conditions: `baseline` (Grep, Glob and Read), `mcp` (the server), `nexus` (the server and its
  skill). Choose with `--conditions baseline,mcp,nexus`.
- Presets: `quick` (Claude Sonnet, 4 tasks), `matrix` (every `--agent` model), `efficient`
  (adds `mcp`, then names the cheapest model and setup whose accuracy is not below the baseline
  of the same model).
- One run per task has about 20% cost noise. Treat a small difference as a tie.

Run `nexus-mcp-ci eval doctor` first. It checks ripgrep, the cached model, the `claude` and
`codex` CLIs and the results folder.

## Saved results

Every run is saved in `~/.nexus-mcp/evals/<run-id>/` (`report.md`, `report.json`, `env.json`)
and listed in `history.jsonl`.

```bash
nexus-mcp-ci eval list
nexus-mcp-ci eval show <run-id>
nexus-mcp-ci eval show <run-id> --json
```

Add `--out DIR` to copy the report into your project. Set `NEXUS_EVAL_HOME` to keep results
somewhere else.

## Share a result (optional)

```bash
nexus-mcp-ci eval share <run-id>
```

It prints the exact payload: versions, size band, languages and the numbers. It holds no paths,
no code, no question text and no local project name. A public repo URL and commit are included
only for a URL source. It posts nothing unless you answer yes. With a logged-in `gh` it opens an
issue on `jaggernaut007/Nexus-MCP`; otherwise it prints the issue-form link. Accepted results
appear in the community table of the [benchmarks page](https://jaggernaut007.github.io/nexus-mcp/benchmarks.html).
There is no telemetry.

## Adopt

If the verdict is good:

```bash
claude mcp add nexus-mcp -- nexus-mcp-ci
```

Run the eval with `--keep-index` first, and the index in `.nexus/` is ready when the server
starts. Add `.nexus/` to `.gitignore`: the index holds code snippets.

## Remove everything

```bash
nexus-mcp-ci data where
nexus-mcp-ci data clear --evals --yes
```

See [PRIVACY.md](PRIVACY.md) for every storage location and the shell commands that do the same.

## Codex and other agents

Codex reads `SKILL.md` files. Copy the evaluate skill to use it from Codex:

```bash
mkdir -p ~/.agents/skills/nexus-evaluate
cp plugin/skills/evaluate/SKILL.md ~/.agents/skills/nexus-evaluate/SKILL.md
```

In Claude Code, install the plugin and run `/nexus-mcp:evaluate`.

---

# Section 3: Privacy and data

# Privacy and data

Nexus-MCP processes your code on your machine. The server makes no network calls. It has no
telemetry and needs no API key.

## What crosses the network

Only the flows below cross the network. Each one is named in the CLI output before it starts.

| Flow | Direction | When | What | How to stop it |
|---|---|---|---|---|
| Package install | in | `pip install` or `uvx`, once | the wheel from PyPI | install from a local wheel |
| Embedding model | in | the first index, once | `BAAI/bge-small-en-v1.5` from Hugging Face (~130 MB) | set `NEXUS_OFFLINE=1` after the first download, or copy the model cache |
| Repo URL source | in | `eval <url>` only | `git clone --depth 1` of that public repo | use a local path |
| Agent tier | **out** | `eval --agent` only, after you answer yes | the prompts and the code that the agent reads, sent through **your own** `claude` or `codex` CLI to Anthropic or OpenAI | do not use `--agent`; the quick tier sends nothing |
| Share | **out** | `eval share`, after you answer yes to the exact payload | allow-listed numbers only: no paths, no code, no question text | do not run `share` |

Defaults that protect you:

- `HF_HUB_DISABLE_TELEMETRY=1` is set unless you set it yourself.
- `NEXUS_OFFLINE=1` also sets `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1`. The model load then
  fails clearly if the model is not cached.
- A test (`tests/test_evaluate_quick.py::test_quick_tier_makes_no_network_connection`) blocks
  every socket connection and runs the quick tier. It must pass.

## What is stored, and where

| Data | Default location | Holds your code? | Change it with |
|---|---|---|---|
| Project index: code chunks, vectors, BM25 (LanceDB `chunks`) | `<project>/.nexus/lancedb/` | **yes**, code snippets | `NEXUS_STORAGE_DIR` |
| Memories (LanceDB `memories`) | `<project>/.nexus/lancedb/` | only what the agent stores | `NEXUS_STORAGE_DIR` |
| Code graph (SQLite) | `<project>/.nexus/graph.db` | symbol names and paths | `NEXUS_STORAGE_DIR` |
| Embedding model | `~/.cache/huggingface/hub/` | no | `HF_HOME` |
| Audit log | stderr (your MCP client log); a file only if you set one | tool arguments, with secrets redacted | `NEXUS_AUDIT_LOG_FILE`, `NEXUS_AUDIT_ENABLED` |
| Eval results | `~/.nexus-mcp/evals/` | no: numbers only; question text stays local | `NEXUS_EVAL_HOME` |
| Eval index | the OS temp folder, deleted at exit | yes, while the eval runs | `--keep-index` writes it to `<project>/.nexus/` |
| Eval clones | `~/.cache/nexus-mcp/eval/repos/` | the public repo | `NEXUS_EVAL_CACHE` |
| `uvx` package cache | the `uv` cache folder | no | `uv cache clean nexus-mcp-ci` |

The index holds code. Add `.nexus/` to `.gitignore` (or to `.git/info/exclude`, which is never
committed). `nexus-mcp-ci data where` warns when `.nexus/` is not ignored.

## See and remove your data

```bash
nexus-mcp-ci data where                     # every location, with its size
nexus-mcp-ci data export-memories           # write memories to nexus-memories.json first
nexus-mcp-ci data clear --memories          # dry run: lists what it would delete
nexus-mcp-ci data clear --memories --yes    # delete only the memories table
nexus-mcp-ci data clear --all --yes         # index, memories, graph, eval results, clones, model
```

- `clear` is a dry run unless you add `--yes`.
- `clear` refuses a folder that does not look like a Nexus-MCP store.
- Stop your MCP client first: `clear` cannot see a server that runs in another process.
- Targets: `--index`, `--memories`, `--graph`, `--evals`, `--clones`, `--models`, `--all`.

Shell commands that do the same, if you prefer not to run our code to remove our data:

```bash
rm -rf .nexus
rm -rf ~/.nexus-mcp ~/.cache/nexus-mcp
rm -rf ~/.cache/huggingface/hub/models--BAAI--bge-small-en-v1.5
uv cache clean nexus-mcp-ci
```

## Uninstall

1. Remove the server from your client: `nexus-mcp-ci setup --uninstall --yes`,
   `claude mcp remove nexus-mcp`, or `/plugin uninstall nexus-mcp@nexus-mcp`.2. Remove the package: `pip uninstall nexus-mcp-ci`.
3. Remove the data: `nexus-mcp-ci data clear --all --yes` before step 2, or the shell commands
   above after it.

---

# Section 4: Installation Guide

# Nexus-MCP Installation Guide

[![jaggernaut007/Nexus-MCP MCP server](https://glama.ai/mcp/servers/jaggernaut007/Nexus-MCP/badges/card.svg)](https://glama.ai/mcp/servers/jaggernaut007/Nexus-MCP)
[![jaggernaut007/Nexus-MCP MCP server](https://glama.ai/mcp/servers/jaggernaut007/Nexus-MCP/badges/score.svg)](https://glama.ai/mcp/servers/jaggernaut007/Nexus-MCP)

## Quickest way: one command

You need [uv](https://docs.astral.sh/uv/getting-started/installation/). Then ask your agent
"Install Nexus-MCP from https://github.com/jaggernaut007/Nexus-MCP", or run:

```bash
uvx nexus-mcp-ci setup
```

`setup` finds your MCP clients (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, Gemini CLI,
Codex, Cline), shows each change, and asks before it writes. It adds the server and a short skill
that tells the agent when to use each tool. Useful flags: `--dry-run` (show only), `--client codex`
(one client), `--yes` (do not ask), `--no-skill`, `--uninstall`. Reload your client afterwards.

Other ways in: the Claude Code plugin, a Claude Desktop extension, a Gemini CLI extension, install
links for VS Code and Cursor, a Docker image and a GitHub Action. See
[DISTRIBUTION.md](DISTRIBUTION.md). The rest of this page is the manual route.

---

## Prerequisites

- **Python 3.10, 3.11, or 3.12** (Python 3.13+ is not yet supported by tree-sitter-languages)
- **pip** (comes with Python)
- **ripgrep (rg)** (recommended for 100% search coverage fallback)

Check your Python version:

```bash
python3 --version
```

If you have multiple Python versions, ensure you use 3.10 or later.

---

## Install from PyPI (recommended)

The simplest way to install Nexus-MCP:

```bash
pip install nexus-mcp-ci
```

With optional extras:

```bash
# With GPU (CUDA) support
pip install nexus-mcp-ci[gpu]
```

After installing, the `nexus-mcp` command is available globally:

```bash
nexus-mcp
```

> **Virtual environment recommended:** While `pip install nexus-mcp-ci` works globally, using a virtual environment avoids dependency conflicts:
> ```bash
> python3 -m venv ~/.nexus-mcp-venv
> source ~/.nexus-mcp-venv/bin/activate
> pip install nexus-mcp-ci
> ```

---

## Install from Source (for development)

### Quick Start (Setup Script)

```bash
# Clone the repository
git clone https://github.com/jaggernaut007/Nexus-MCP.git
cd Nexus-MCP

# Run setup script (creates venv, installs deps, verifies)
./setup.sh
```

**Setup script options:**

| Flag | Description |
|------|-------------|
| `--clean` | Remove existing venv before creating new |
| `--prod` | Install production dependencies only (no dev) |
| `--no-verify` | Skip verification step |
| `--help` | Show help message |

Examples:

```bash
./setup.sh                       # Dev install (pytest, ruff, mypy)
./setup.sh --clean               # Remove old venv, fresh install
./setup.sh --prod                # Production-only (no dev tools)
./setup.sh --clean --prod        # Clean production install
```

After setup, activate the environment:

```bash
source .venv/bin/activate
```

### Manual Install from Source

```bash
git clone https://github.com/jaggernaut007/Nexus-MCP.git
cd Nexus-MCP

# Option 1: Production only
pip install -e .

# Option 2: With dev dependencies
pip install -e ".[dev]"

# Option 4: With GPU (CUDA) support
pip install -e ".[gpu]"
```

---

## Verify Installation

```bash
# Check the module imports correctly
python3 -c "import nexus_mcp; print('OK')"

# Check the CLI is available
nexus-mcp --help

# Run the self-test demo (exercises the 8 tools and the Python API)
python self_test/demo_mcp.py
```

---

## AI Tool Integrations

### Claude Code (CLI)

### Tool names by client

Use one server name everywhere: `nexus-mcp`. The prefix that a client adds to each tool
name comes from that name, so it differs by route:

| Route | Tool name the agent sees |
|---|---|
| `claude mcp add nexus-mcp -- nexus-mcp-ci` | `mcp__nexus-mcp__search` |
| Claude Code plugin (`/plugin install nexus-mcp@nexus-mcp`) | `mcp__plugin_nexus-mcp_nexus-mcp__search` |
| Claude Desktop, Cursor, Cline (`"nexus-mcp"` key) | `search`, shown under the server name |
| Codex (`[mcp_servers.nexus-mcp]`) | `search`, called through the server `nexus-mcp` |

Project files and prompts should name tools without the prefix (`search`, `graph`).

### Claude Code plugin (recommended)

The plugin registers the server and adds a routing skill. Install the package, then run
`/plugin marketplace add jaggernaut007/Nexus-MCP` and `/plugin install nexus-mcp@nexus-mcp`.
Do not also run `claude mcp add`; that registers the server twice.

### Codex CLI

```bash
codex mcp add nexus-mcp -- nexus-mcp-ci
```

or in `~/.codex/config.toml` (or a trusted project's `.codex/config.toml`):

```toml
[mcp_servers.nexus-mcp]
command = "nexus-mcp-ci"
startup_timeout_sec = 60   # the first start loads the embedding model; the default is 10
```

Codex reads the server instructions on connect. Add the short block from the README to
your `AGENTS.md` if you want the rules in the project too.

**If installed via pip (recommended):**

```bash
# If nexus-mcp is on your PATH (pip install nexus-mcp-ci)
claude mcp add nexus-mcp -- nexus-mcp-ci

# With environment variables
claude mcp add nexus-mcp -e NEXUS_EMBEDDING_MODEL=bge-small-en -- nexus-mcp-ci
```

**If installed in a virtual environment:**

```bash
# Use the full venv path so the MCP client finds the right Python
claude mcp add nexus-mcp -- /path/to/Nexus-MCP/.venv/bin/nexus-mcp

# If updating an existing registration, remove first
claude mcp remove nexus-mcp
claude mcp add nexus-mcp -- /path/to/Nexus-MCP/.venv/bin/nexus-mcp
```

> **Why use the full venv path?** MCP clients spawn the server as a subprocess. If you just use `nexus-mcp`, it resolves to whatever Python is on your system PATH — which may not have the required dependencies installed. Using the venv path ensures the server runs with the correct, isolated environment.

**Manual config** — add to your settings (`~/.claude/settings.json`):

```json
{
  "mcpServers": {
    "nexus-mcp": {
      "command": "nexus-mcp-ci"
    }
  }
}
```

**After setup**, reload your VS Code window (Cmd+Shift+P → "Reload Window") or restart Claude Code for the MCP server to start.

**Usage in Claude Code:**
```
> index my codebase at ./my-project
> search for authentication logic
> find_symbol User
> explain Config
```

---

### Claude Desktop

Add to your Claude Desktop configuration:

| Platform | Config File Location |
|----------|---------------------|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |

```json
{
  "mcpServers": {
    "nexus-mcp": {
      "command": "nexus-mcp-ci",
      "args": []
    }
  }
}
```

Restart Claude Desktop after saving.

---

### Cursor

Cursor supports MCP servers through its extension system:

1. **Open Settings** → Extensions → MCP
2. **Add Server Configuration**:

```json
{
  "nexus-mcp": {
    "command": "nexus-mcp-ci",
    "transport": "stdio"
  }
}
```

Or add to `.cursor/mcp.json` in your project:

```json
{
  "servers": {
    "nexus-mcp": {
      "command": "nexus-mcp-ci"
    }
  }
}
```

---

### Windsurf (Codeium)

Windsurf supports MCP through Cascade:

1. Open **Cascade Settings**
2. Navigate to **MCP Servers**
3. Add configuration:

```json
{
  "nexus-mcp": {
    "command": "nexus-mcp-ci",
    "transport": "stdio"
  }
}
```

---

### Cline (VS Code)

Add to Cline's MCP settings in VS Code:

1. Open Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`)
2. Search "Cline: Open MCP Settings"
3. Add:

```json
{
  "mcpServers": {
    "nexus-mcp": {
      "command": "nexus-mcp-ci"
    }
  }
}
```

---

### Zed Editor

Zed supports MCP through its assistant panel. Add to settings:

```json
{
  "assistant": {
    "mcp_servers": {
      "nexus-mcp": {
        "command": "nexus-mcp-ci"
      }
    }
  }
}
```

---

### Continue (VS Code / JetBrains)

Add to your Continue configuration (`~/.continue/config.json`):

```json
{
  "mcpServers": [
    {
      "name": "nexus-mcp",
      "command": "nexus-mcp-ci"
    }
  ]
}
```

---

### Generic MCP Client

For any MCP-compatible client, use stdio transport:

```bash
# Command to run
nexus-mcp

# Transport
stdio (stdin/stdout)

# Protocol
Model Context Protocol (MCP)
```

---

## Configuration

All settings use the `NEXUS_` environment variable prefix:

| Variable | Default | Description |
|----------|---------|-------------|
| `NEXUS_STORAGE_DIR` | `.nexus` | Storage directory for indexes and graph DB |
| `NEXUS_EMBEDDING_MODEL` | `bge-small-en` | Embedding model: `bge-small-en` (`jina-code` is deprecated) |
| `NEXUS_EMBEDDING_DEVICE` | `auto` | Device: `auto` (CUDA > MPS > CPU), `cuda`, `mps`, `cpu` |
| `NEXUS_MAX_FILE_SIZE_MB` | `10` | Skip files larger than this |
| `NEXUS_CHUNK_MAX_CHARS` | `4000` | Max characters per code chunk |
| `NEXUS_MAX_MEMORY_MB` | `350` | Memory budget in MB |
| `NEXUS_SEARCH_MODE` | `hybrid` | Search mode: `hybrid`, `vector`, or `bm25` |
| `NEXUS_LOG_LEVEL` | `INFO` | Logging level |
| `NEXUS_LOG_FORMAT` | `text` | Log format: `text` or `json` |
| `NEXUS_PERMISSION_LEVEL` | `full` | Permission level: `full` or `read` |
| `NEXUS_AUDIT_ENABLED` | `true` | Enable audit logging |
| `NEXUS_RATE_LIMIT_ENABLED` | `false` | Enable per-tool rate limiting |
| `NEXUS_TRUST_REMOTE_CODE` | `true` | Allow `trust_remote_code` in models. Only the deprecated `jina-code` needs it; set `false` with `bge-small-en` |
| `NEXUS_AUTO_WATCH` | `true` | Auto-reindex on file change (debounced watcher started after `index`) |
| `NEXUS_STALENESS_CHECK_INTERVAL` | `15` | Seconds between `status()`/`search()` staleness checks |

Example:

```bash
NEXUS_LOG_LEVEL=DEBUG NEXUS_SEARCH_MODE=vector nexus-mcp
```

---

## Running Tests

```bash
# All tests (843)
pytest -v

# Skip slow performance benchmarks
pytest -m "not slow"

# Lint
ruff check .
```

---

## Troubleshooting

### ONNX Runtime / Optimum errors during indexing

If you see `Using the ONNX backend requires installing Optimum and ONNX Runtime`, install the required packages:

```bash
pip install "sentence-transformers[onnx]" "optimum[onnxruntime]>=1.19.0,<2.0"
```

**Version compatibility:** Ensure `optimum` and `transformers` versions are compatible. If you see `cannot import name 'FLAX_WEIGHTS_NAME'`, pin compatible versions:

```bash
pip install "optimum[onnxruntime]>=1.19.0,<2.0" "transformers>=4.46,<5.0"
```

**MCP server not picking up new packages:** If you installed packages but the MCP server still errors, the server process needs a restart. Reload your VS Code window, restart Claude Code, or restart Claude Desktop.

**Alternative: use a model that doesn't need ONNX:**

```bash
NEXUS_EMBEDDING_MODEL=bge-small-en nexus-mcp
```

### `ModuleNotFoundError: No module named 'nexus_mcp'`

Ensure you installed with `pip install -e .` from the project root and are using the correct Python version (3.10+). If using a venv, make sure it's activated: `source .venv/bin/activate`.

### `tree-sitter` FutureWarning

The warning `Language(path, name) is deprecated` is harmless and comes from the tree-sitter-languages compatibility layer. It does not affect functionality.

### High memory usage during indexing

The embedding model is loaded during indexing and unloaded after. Peak RSS may exceed the 350MB target briefly. Set `NEXUS_MAX_MEMORY_MB` to adjust the budget.

### `pip` resolves dependency conflicts

If you see dependency conflict warnings from other installed packages, these are unrelated to Nexus-MCP and can be safely ignored as long as `import nexus_mcp` succeeds.

### Demo fails at indexing step

Ensure `tree-sitter==0.21.3` and `tree-sitter-languages>=1.10.0` are installed. These are pinned for compatibility.

### Server not found after install

If `nexus-mcp` command is not found, ensure the install location is on your PATH. With a venv, activate it first. Without a venv, you may need `python3 -m nexus_mcp.server` as a fallback.

### MCP client can't connect

- Ensure `nexus-mcp` is on the PATH that the MCP client uses
- If installed in a venv, use the full path: `/path/to/Nexus-MCP/.venv/bin/nexus-mcp`
- Check the client's logs for connection errors

---

# Section 5: Usage Guide

# Usage Guide

## Getting Started

### Installation

```bash
# Install from PyPI
pip install nexus-mcp-ci

# Optional: with GPU (CUDA) support
pip install nexus-mcp-ci[gpu]
```

Or install from source for development:

```bash
git clone https://github.com/jaggernaut007/Nexus-MCP.git
cd Nexus-MCP
pip install -e ".[dev]"
```

### Running the Server

```bash
nexus-mcp-ci
```

The server starts on stdio (the default MCP transport). Configure your MCP client to connect to `nexus-mcp-ci`. The `nexus-mcp` command is an alias for the same entry point.

### MCP Client Configuration

**Claude Code:**

```bash
claude mcp add nexus-mcp -- nexus-mcp-ci
```

**Claude Desktop** (add to `~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "nexus-mcp": {
      "command": "nexus-mcp-ci",
      "args": []
    }
  }
}
```

**Other MCP clients** (Cursor, Windsurf, Cline, etc.):

```json
{
  "nexus-mcp": {
    "command": "nexus-mcp-ci",
    "transport": "stdio"
  }
}
```

See the full [Installation Guide](INSTALLATION.md) for client-specific instructions.

## Tool Reference

Nexus-MCP exposes 8 tools. The v2.0.0 release merged the old 15 tools; see the [CHANGELOG](../CHANGELOG.md) for the old-to-new mapping and [ADR-017](adr/ADR-017-tool-consolidation.md) for the reason.

### Discovery & Indexing

#### `index`
Index a codebase directory. Call it before any other tool except `status`.

```
# Single directory
index(path="/path/to/your/project")

# Multiple directories (comma-separated)
index(path="/path/to/project/src,/path/to/project/lib,/path/to/project/tests")

# Multiple directories (using paths parameter)
index(path="/path/to/project/src", paths="/path/to/project/lib,/path/to/project/tests")
```

Parameters:
- `path` — Absolute path to the codebase directory. Accepts comma-separated paths for multi-folder indexing.
- `paths` — (Optional) Additional comma-separated paths to index alongside `path`.

When you give multiple paths, the server indexes each folder in sequence (discover → parse → embed → store) and merges the results into shared engines. This keeps peak RAM low. The server removes duplicate files across overlapping paths.

Returns indexing statistics: file count, symbol count, chunk count, graph size and timing. Later calls run an incremental reindex (only changed files). The tool reports progress while it runs. When it finishes, a file watcher keeps the index fresh (`NEXUS_AUTO_WATCH`, default on).

#### `status`
Check whether a codebase is indexed, index size, engine availability and memory usage.

```
status()
```

Returns version, indexing state, chunk counts, graph stats, peak RSS memory and a `hint` field. If files changed since the last index, the result includes `stale` and `staleness_warning`, and the server starts a background reindex. The staleness check runs at most once every `NEXUS_STALENESS_CHECK_INTERVAL` seconds (default 15).

#### `map`
Project overview: files, languages, modules and structure. Use it before you list directories or open files one by one.

```
map(detail="summary")        # files, languages, symbol counts, quality, top modules
map(detail="architecture")   # layers, dependencies, classes, entry points, hub symbols
map(detail="full")           # both
```

`map` replaces the old `overview()` and `architecture()` tools.

### Search

#### `search`
Finds code by meaning or keyword: semantic + keyword + graph search, with code snippets.

```
search(query="authentication middleware", limit=10)
```

Parameters:
- `query` — Natural language or code query. Exact names (`parse_config`) bring their definitions first.
- `limit` — Max results (1-100, default 10)

The schema that the model sees holds only these two. The Python API (`nexus_mcp.core_api.search`) also takes `language`, `symbol_type` ("function", "class", "method", "variable" or "module"), `mode` ("hybrid", "vector" or "bm25"), `live_grep` and `detail` ("compact" or "full"). Agents used none of them in 49 calls, and each parameter costs prompt tokens on every turn.

Returns results with `filepath` (relative), `code_snippet`, `symbol_name`, `symbol_type`, `line_start`/`line_end`, and a `hint` field. A compact response has only `total`, `results`, `hint` and, when the index is stale, `warning`; `detail="full"` adds `query`, `search_mode`, `engines_used` and the `language` of each result. With `detail="compact"` each of the top 3 results carries its whole symbol, up to 2000 characters (cut at a line boundary and marked `(truncated)` when longer), so the agent can answer without a file read. The other results show the signature and the first docstring line (160 characters at most, with no `(truncated)` mark). The snippet is the code only: the stored chunk text also has a `# path:line` line, a `type: name` line, a copy of the signature and docstring, and `Imports:` and `Calls:` lines, and compact mode leaves those out. The fields that repeat the snippet (`signature`, `docstring`, `parent`), the internal fields (`id`, `score`, `rrf_score`, `_fusion_sources`, `absolute_path`) and empty values are also left out. Measured on a 460-file project, a compact result is about 7,000 characters, 40% of a full one.

Results in test files come after results in source files, unless the query itself asks for tests (a word such as "test", "fixture" or "mock", or a name such as `test_login`). No result is removed. On a 45,000-chunk index of django, where 70% of the chunks are tests, this raised hit@1 from 6 of 12 to 8 of 12 task queries. A query can hold exact names written as `snake_case` or `CamelCase` (`"parse_config RetryPolicy"`): their definitions come first, and a file that defines more of the names comes before the others. Hybrid mode fuses vector and keyword results; With `detail="full"` the snippet is up to 2000 characters and those fields are present. Raw embedding vectors are always stripped. The tool falls back to live grep on its own when hybrid results are sparse. If the index looks stale, the result carries a `warning` (in full mode the field is always there and is null when the index is fresh). A background reindex starts, and the results still return at once.

### Graph Analysis

#### `find_symbol`
Finds symbol definitions by name. Returns file path, line numbers, type, the `code` of the symbol, and its callers and callees.

```
find_symbol(symbol_name="UserService", exact=True)
find_symbol(symbol_name="should_keep, AUTO_APPLY_CATEGORIES, MailClassification")
```

Pass several names in one call (commas or spaces between them, 8 names at most): an agent pays for each call with one more read of its prompt. `code` is the whole symbol without its common indent, 6,000 characters at most for one symbol and 12,000 for one call; longer code is cut at a line and marked `(truncated)`, and `start_line` and `end_line` say where the rest is. A name with more than 3 definitions gets code for the first 3 (source files before test files). A name with no graph node, such as a constant, comes from the chunk table. Names with no match are in `not_found`.

A class longer than 2,000 characters gets `outline` in place of `code`. The outline holds the class line, the docstring and the attributes (25 lines or 1,200 characters at most). It then has one line for each method, with its line range (60 methods at most). The result also has a `hint`: ask for `Class.method` to get the code of a method. A short class, a class with no methods in the graph, and a function keep `code`. A `Class.method` name works with `exact=False` too. `more_matches` counts the matches past the first 10 of one name.

The code is read from the file at the path that the index stores, and only when that file is a regular file inside an indexed folder (symlinks are followed first), not larger than `NEXUS_MAX_FILE_SIZE_MB`; at most 1,000 lines are read for one symbol. The node of a whole file (the graph has one for each file, named like it) comes after functions and classes and has no code. When the file changed after it was indexed, so that the stored lines no longer hold the symbol, its code is left out and the result has a `warning`. A name has at most 10 entries (`more_matches` counts the rest). A symbol gets no code when less than 200 characters of the call budget are left.

Use `exact=False` for fuzzy substring matching. With `detail="compact"` (default) the callers and callees are given by name as `file -> ["name:line"]`, at most 20 each (`callers_total` or `callees_total` gives the true count when there are more). With `detail="full"` each symbol has every node field and the raw relationship records with node ids. A compact result is about a third of the size of a full one.

#### `graph`
Trace the call graph of a symbol. This tool replaces `find_callers`, `find_callees` and `impact`.

```
graph(symbol_name="authenticate", direction="callers")        # who calls this
graph(symbol_name="process_request", direction="callees")     # what this calls
graph(symbol_name="DatabaseConnection", direction="callers",
      transitive=True)                                         # change blast radius
```

Parameters:
- `symbol_name` — Name of the function or symbol
- `direction` — "callers" (default) or "callees"
- `transitive` — `True` returns the full transitive change impact. It works only with `direction="callers"`. Run it before you refactor a shared symbol.
- The Python API also takes `max_depth` and `detail` ("compact", or "full" with 25 symbols at most). Compact: callers or callees as `file -> ["name:line"]`, source files before test files. "full": one record for each symbol with docstring, complexity, signature types and the node id.

With `transitive=True` and `detail="compact"`, the result has `total_impacted` and `impacted_files` (`file -> [names]`, every impacted symbol). The `impacted_symbols` list of records is only in `detail="full"`. A compact result is about 10% of the size of a full one.

A callers result (immediate or transitive) also has `references`: every line where the name appears as a whole word, from a live text search of the project.

```
"references": {"total_files": 3, "total_lines": 6, "truncated": false,
               "files": {"src/agents/budget.py": ["12: def ensure_budget(n):"],
                         "src/agents/runtime.py": ["4: from budget import ensure_budget",
                                                   "88: ensure_budget(cost)"]},
               "test_files": {"tests/test_budget.py": 3}}
```

The caller list holds only the calls that static analysis resolved, so it is a lower bound. `references` is the complete text answer, so a separate grep is not needed. Source files come first, each line with its code (at most 8 lines per file, 60 lines in all, 110 characters per line). A source file past the budget is a line count in `more_files`. Test files are a line count each in `test_files`. `truncated` says when a cap cut the list, and the totals always count everything. In the caller lists a test file with more than 3 symbols shows 3 and `+N more`. A name with no graph node (a constant, or a language without graph support) returns its `references` with an empty caller list instead of an error.

> **Limit:** call edges are static and name-based. Dynamic dispatch, callbacks and reflection are invisible, and a name shared by several definitions gets no edge. Treat results as a lower bound and use `search` for other call sites. See [Known Limitations](../README.md#known-limitations).

#### `analyze`
Run code analysis on the indexed codebase: complexity metrics, dependency analysis, code smells and quality scores.

```
analyze(path="src/auth/")
```

The optional `path` parameter filters analysis to a subdirectory or file.

### Memory

#### `memory`
Persist and retrieve project context across sessions. This tool replaces `remember`, `recall` and `forget`.

```
# Store
memory(action="store",
       content="The auth service uses JWT tokens with 24h expiry",
       tags="auth,jwt")

# Search
memory(action="search", query="how does authentication work?", tags="auth")

# Delete
memory(action="delete", tags="temporary")
memory(action="delete", memory_id="abc-123")
```

Parameters:
- `action` — "store", "search" or "delete"
- `content` — Text to store (`action="store"`)
- `query` — Natural language query (`action="search"`)
- `tags` — Comma-separated; they set the tags when you store and filter when you search or delete
- `memory_id` — The note to delete (`action="delete"`)
- The Python API also takes `memory_type`, `ttl`, `project` and `limit`.

Memory types: note (default), decision, conversation, status, preference, doc.

With `NEXUS_PERMISSION_LEVEL=read`, `memory` allows `action="search"` only. `store` and `delete` need the `full` level.

## Configuration

Set via environment variables before starting the server:

```bash
# Embedding model selection
export NEXUS_EMBEDDING_MODEL=bge-small-en       # Default: bge-small-en (384d, lightweight)
                                                 # jina-code (768d) is deprecated
export NEXUS_EMBEDDING_DEVICE=auto               # auto (CUDA > MPS > CPU), cuda, mps, cpu

# Search tuning
export NEXUS_SEARCH_MODE=hybrid          # hybrid, vector, or bm25
export NEXUS_FUSION_WEIGHT_VECTOR=0.5    # Vector weight in RRF
export NEXUS_FUSION_WEIGHT_BM25=0.3      # BM25 weight in RRF
export NEXUS_WARM_START=true             # Load the index and the model while the server starts

# Resource limits
export NEXUS_MAX_FILE_SIZE_MB=10         # Skip files larger than this
export NEXUS_MAX_MEMORY_MB=350           # Memory budget target

# Logging
export NEXUS_LOG_LEVEL=INFO              # DEBUG, INFO, WARNING, ERROR
export NEXUS_LOG_FORMAT=json             # text or json (json for production)

# Storage
export NEXUS_STORAGE_DIR=.nexus          # Where indexes are stored

# Freshness
export NEXUS_AUTO_WATCH=true             # Debounced file watcher starts after index()
export NEXUS_STALENESS_CHECK_INTERVAL=15 # Seconds between status()/search() staleness checks

# Security and observability
export NEXUS_PERMISSION_LEVEL=full       # full or read (read allows query-only tools)
export NEXUS_AUDIT_ENABLED=true          # Audit log with correlation IDs
export NEXUS_RATE_LIMIT_ENABLED=false    # Per-tool token-bucket rate limiting
```

See the [README configuration table](../README.md#configuration) for every variable.

### Embedding Models

Nexus-MCP recommends one embedding model, `bge-small-en`. `jina-code` still loads, with a warning, so an existing index keeps working; it is deprecated. Only registered model names are accepted; custom model names raise a `ConfigurationError`.

| Model | HuggingFace ID | Dims | Code-specific? | Notes |
|-------|---------------|------|:-:|---|
| `bge-small-en` (default) | `BAAI/bge-small-en-v1.5` | 384 | No | Smallest download (~50MB), general text, PyTorch backend |
| `jina-code` (deprecated) | `jinaai/jina-embeddings-v2-base-code` | 768 | Yes | ONNX, needs `trust_remote_code` and a 612 MiB download; slow CPU index. Removal in a future major release |

GPU/MPS auto-detection (`NEXUS_EMBEDDING_DEVICE=auto`) tries CUDA first, then Apple MPS, then falls back to CPU. For explicit GPU support, install with `pip install -e ".[gpu]"`.

## Best Practices

### Indexing

1. **Index from the project root** — Point `index` at the top-level directory, not a subdirectory. This ensures .gitignore is respected and relative paths are meaningful.

2. **Use multi-folder indexing for monorepos** — For projects with multiple source roots (e.g., `src/`, `lib/`, `plugins/`), pass them as comma-separated paths in a single `index` call. Each folder is processed sequentially to keep RAM usage low.

3. **Let incremental reindex handle changes** — After the first full index, subsequent `index` calls only process changed files. No need to clear and re-index.

4. **Check status after indexing** — Use `status` to verify chunk counts and graph stats look reasonable.

### Searching

1. **Start with hybrid mode** — The default `hybrid` mode combines all three engines for the best results. Only switch to `vector` or `bm25` if you have a specific reason.

2. **Use filters to narrow results** — The `language` and `symbol_type` filters are applied before search, making results more relevant and queries faster.


### Memory

1. **Tag everything** — Tags make recall much more effective. Use consistent tag conventions (e.g., "auth", "api", "bug").

2. **Use TTL for ephemeral context** — Set `ttl="session"` or `ttl="day"` for temporary context that shouldn't persist.

3. **Use memory types semantically** — "decision" for architectural choices, "note" for observations, "status" for current state.

### Performance

1. **Monitor memory** — Check `status()` memory stats periodically. Peak RSS should stay under 350MB for typical codebases.

2. **Large codebases** — For codebases >10K files, consider increasing `NEXUS_MAX_WORKERS` for faster parallel parsing, or increasing `NEXUS_EMBEDDING_BATCH_SIZE` for faster embedding.

3. **Search latency** — If search is slow, try `mode="vector"` (skips BM25 and graph) or reduce the `limit`.

---

# Section 6: Architecture

# Architecture

Nexus-MCP is a unified Model Context Protocol (MCP) server that combines vector search, full-text search, code graph analysis, and semantic memory into a single process. It is designed to run locally with a small memory footprint (see docs/MEMORY.md for measured numbers).

Nexus-MCP consolidates two predecessor projects into a single server ([ADR-001](adr/ADR-001-single-mcp-consolidation.md)):
- **CodeGrok MCP** (by rdondeti / Ravitez Dondeti, MIT license) — Contributed the symbol extraction pipeline, embedding service, parallel indexing, core data models, and memory retrieval system.
- **code-graph-mcp** (by [entrepeneur4lyf](https://github.com/entrepeneur4lyf)) — Contributed the ast-grep structural parser, rustworkx graph engine, code complexity analysis, and relationship extraction.

## System Overview

8 tools: `index`, `status`, `search`, `find_symbol`, `graph`, `analyze`, `map`, `memory`. They were consolidated from an earlier 15 ([ADR-017](adr/ADR-017-tool-consolidation.md)). `explain` and `health` are no longer tools; their functions stay in `core_api.py`.

```mermaid
flowchart TB
    Client["MCP Client (Claude Code / IDE)"]

    subgraph Server["server.py — FastMCP"]
        Tools["8 tools: index, status, search,\nfind_symbol, graph, analyze, map, memory"]
        Guard["permission guard + rate limiter\n(security/permissions.py, security/rate_limiter.py)"]
        Audit["@_audited → middleware/audit.py"]
    end

    Core["core_api.py\ntransport-agnostic core logic — thin server.py\nwrappers call these same importable functions"]

    State["state.py\nglobal SessionState singleton, lazy-loaded engines"]

    subgraph Indexing["Indexing Pipeline — indexing/pipeline.py (8 steps)"]
        direction TB
        Parse["parsing/\ntree-sitter (symbols, parallel) +\nast-grep (relationships, sequential) +\nfile_watcher (NEXUS_AUTO_WATCH)"]
        Chunk["indexing/chunker.py\nsymbols → CodeChunks"]
        Embed["indexing/embedding_service.py\nbge-small-en (PyTorch)"]
        Parse --> Chunk --> Embed
    end

    LanceDB[("LanceDB\nchunks table")]

    subgraph Engines["engines/"]
        Vector["vector_engine.py\nvector search"]
        BM25["bm25_engine.py\nLanceDB FTS"]
        Graph["graph_engine.py\nrustworkx PyDiGraph"]
        LiveGrep["live_grep.py\nrg/grep fallback"]
        Fusion["fusion.py\nReciprocal Rank Fusion"]
    end

    Analysis["analysis/code_analyzer.py\ncomplexity, smells, deps"]
    Memory[("memory/memory_store.py\nLanceDB memories table, TTL")]
    Persistence[("persistence/store.py\nSQLite graph persistence")]
    Formatting["formatting/\ntoken_budget.py + response_builder.py"]

    Client -->|MCP stdio| Tools
    Tools --> Guard --> Audit --> Core
    Core --> State
    State --> Indexing
    State --> Engines
    State --> Analysis
    State --> Memory
    Embed --> LanceDB
    LanceDB --> Vector
    LanceDB --> BM25
    Vector --> Fusion
    BM25 --> Fusion
    Fusion --> LiveGrep
    Graph -.snapshot/restore.-> Persistence
    Core --> Formatting
    Formatting --> Tools
```

## Key Components

### Core API Layer (`core_api.py`)

Transport-agnostic core logic. Every MCP tool in `server.py` is a thin wrapper: it applies the permission guard, rate limiter, and audit decorator, then calls a plain Python function in `core_api.py`. Any other process — including an in-process agent harness — can import `core_api` directly and call the same functions, with no subprocess, stdio, or MCP protocol overhead. `core_api.py` owns the module-level `_pipeline` reference and its lock.

### Indexing Pipeline (`indexing/pipeline.py`)

The 8-step pipeline transforms source code into searchable indexes:

1. **Discover** — Walk directory tree, filter by extension/size/.gitignore
2. **Parse symbols** — tree-sitter extracts functions, classes, methods (parallel)
3. **Parse graph** — ast-grep extracts functions, classes and import relationships as `CONTAINS`/`IMPORTS` edges (sequential). It also records the callee names inside each function. After the last batch, `indexing/call_resolver.py` turns those names into `CALLS` edges. No parser creates `INHERITS` edges yet.
4. **Transfer graph** — Populate rustworkx graph from ast-grep results
5. **Chunk** — Convert symbols to CodeChunks with deterministic IDs
6. **Embed** — generates vectors (384-dim bge-small-en, PyTorch; the deprecated 768-dim jina-code still loads, on ONNX Runtime)
7. **Store** — Write chunks to LanceDB, rebuild FTS index
8. **Cleanup** — Unload embedding model, save metadata for incremental reindex

Incremental reindexing uses mtime-based change detection: only new/modified files are re-processed. Corrupt indexes are auto-detected and rebuilt.

### Search Engines

**Vector Engine** (`engines/vector_engine.py`) — LanceDB-backed semantic search. Embeds queries at search time and performs flat cosine similarity search. Filters via SQL-escaped WHERE clauses.

**BM25 Engine** (`engines/bm25_engine.py`) — LanceDB's native Tantivy full-text search. Reads from the same `chunks` table. Good for exact keyword matches.

**Graph Engine** (`engines/graph_engine.py`) — rustworkx (Rust-backed) directed graph. Stores nodes (functions, classes) and edges. The engine supports `CALLS` traversal for callers, callees and transitive impact analysis. `CALLS` edges come from `indexing/call_resolver.py`, which rebuilds them after every index or reindex.

**Fusion** (`engines/fusion.py`) — Reciprocal Rank Fusion combines results from the vector and BM25 engines with configurable weights (default: 0.5/0.3). Before fusion, results in test files move after results in source files unless the query asks for tests (`demote_test_files`).

**Live Grep** (`engines/live_grep.py`) — Fallback using ripgrep (`rg`), falling back to standard `grep` if `rg` isn't installed. Covers unindexed or newly created files, and is used automatically when hybrid results are sparse.

### Dual Parser Strategy

**tree-sitter** — Fast, incremental parser for 25+ languages. Extracts symbol definitions (functions, classes, methods) with metadata (line numbers, docstrings, signatures). Runs in parallel via ThreadPool.

**ast-grep** — Structural search tool that extracts the containment and import structure of each file (functions, classes, imports). It also records callee names per function (resolved later into `CALLS` edges). Inheritance extraction is planned. Runs sequentially to build a consistent graph.

tree-sitter also records the call names inside each symbol. The chunker writes them into the chunk text (`Calls: ...`), so they help search. They do not become graph edges yet.

### Memory Store (`memory/memory_store.py`)

LanceDB-backed semantic memory with 11-column PyArrow schema. Supports:
- TTL-based expiration (permanent, month, week, day, session)
- Tag-based filtering with LIKE wildcards
- Memory types: note, decision, conversation, status, preference, doc

### State Management

`state.py` holds a global singleton `SessionState` with references to all engines. Lazy-loaded: engines are `None` until `index` is called. Thread-safe shutdown with lock-protected `shutdown()` method that persists graph state.

### Persistence

- **LanceDB** — Disk-backed vectors and FTS via mmap (~20-50MB overhead)
- **SQLite** (`persistence/store.py`) — Graph persistence for warm-start recovery
- **JSON metadata** — File mtimes for incremental reindex detection

### Auto-Watch and Staleness ([ADR-015](adr/ADR-015-auto-watch-and-staleness-detection.md))

After `index()` finishes, `core_api._ensure_file_watcher` starts a debounced watchdog (`parsing/file_watcher.py`) for each indexed root (`NEXUS_AUTO_WATCH`, default on). A change triggers a background incremental reindex on a daemon thread. `status()` and `search()` also run a throttled mtime check (`NEXUS_STALENESS_CHECK_INTERVAL`, default 15 s). If files changed, the result carries `stale`/`staleness_warning` (or `warning`), the call still returns at once, and a background reindex starts.

### Security ([ADR-012](adr/ADR-012-tool-permission-model.md), [ADR-014](adr/ADR-014-rate-limiting.md))

- **Permissions** — `security/permissions.py` maps each tool to READ, MUTATE or WRITE. `NEXUS_PERMISSION_LEVEL=read` allows READ tools only. `memory` is action-aware: `search` is READ, `store` and `delete` are WRITE.
- **Rate limiting** — `security/rate_limiter.py` is a per-tool token bucket. It is off by default (`NEXUS_RATE_LIMIT_ENABLED`).
- **Audit** — `middleware/audit.py` logs each call with a correlation ID and redacts sensitive fields (`NEXUS_AUDIT_ENABLED`, default on).

## Data Flow

### Indexing
```
Source files → tree-sitter → Symbols → Chunker → CodeChunks
                                                      ↓
Source files → ast-grep → UniversalGraph → rustworkx   embed (PyTorch / ONNX)
                                                      ↓
                                              LanceDB (chunks table)
```

### Hybrid Search
```
Query → embed → Vector search (LanceDB)  ─┐
Query →       → BM25 search (Tantivy)     ┴→ RRF Fusion → exact names first → Live Grep (fallback) → Results
```

## Memory Budget

Measured: about 90MB idle and 460MB with the model loaded (docs/MEMORY.md). The footprint comes from:
- ONNX Runtime is used only by the deprecated jina-code; the bge-small-en default uses PyTorch
- LanceDB mmap (vectors stay on disk, ~20-50MB overhead)
- Lazy model loading — embedding model loaded during indexing, unloaded after
- GPU/MPS auto-detection (`NEXUS_EMBEDDING_DEVICE=auto`) for faster inference when available
- One recommended model: bge-small-en (384d). jina-code (768d) is deprecated

## Thread Safety

- Vector engine: RLock for write operations, concurrent reads allowed
- Graph engine: RLock for all mutations
- Pipeline: `core_api._pipeline_lock` is held for the whole of `index()`. A background reindex does a non-blocking acquire and skips when the pipeline is busy
- State shutdown: Lock-protected to prevent double-shutdown on signal + finally

## Configuration

All settings are in `config.py` with NEXUS_ env prefix. See README for the full table.

---

# Section 7: Developer Guide

# Developer Guide

## Development Setup

```bash
# Clone the repository
git clone https://github.com/jaggernaut007/Nexus-MCP.git
cd Nexus-MCP

# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate

# Install with dev dependencies
pip install -e ".[dev]"


# Verify installation
pytest -v
ruff check .
nexus-mcp-ci --help
```

## Project Structure

```
Nexus-MCP/
├── src/nexus_mcp/          # Source code (7,200+ lines)
│   ├── server.py           # FastMCP server: 10 thin tool wrappers, entry point
│   ├── core_api.py         # Tool logic + input validation, importable without MCP
│   ├── config.py           # Settings with NEXUS_ env prefix
│   ├── state.py            # Session state singleton
│   ├── core/               # Data models, interfaces, exceptions
│   ├── parsing/            # tree-sitter + ast-grep parsers
│   ├── engines/            # Vector, BM25, graph, fusion
│   ├── analysis/           # Code complexity and quality analysis
│   ├── memory/             # Semantic memory store
│   ├── indexing/           # Pipeline, embedding service, chunker
│   ├── formatting/         # Token budget, response builder
│   ├── persistence/        # SQLite graph persistence
│   ├── security/           # Permission categories, rate limiter
│   └── middleware/         # Audit logging
├── tests/                  # 843 tests
├── benchmarks/             # Token-efficiency benchmark harness (ADR-018)
├── self_test/              # End-to-end demo of all 10 tools
├── plugin/                 # Claude Code plugin: MCP registration + routing skill
├── docs/                   # Architecture, ADRs, research notes
│   ├── adr/                # 18 Architecture Decision Records
│   └── research/           # Research notes on libraries
├── pyproject.toml          # Build config, dependencies
├── AGENTS.md               # Source of truth for agent harnesses
├── CLAUDE.md               # Claude Code context
├── PROGRESS.md             # Phase tracking
└── LICENSE                 # MIT
```

## Running Tests

```bash
# Full suite (843 tests)
pytest -v

# Skip slow performance benchmarks (this is what CI runs)
pytest -v -m "not slow"

# Run specific test file
pytest tests/test_security.py -v

# Run with coverage (if pytest-cov installed)
pytest --cov=nexus_mcp --cov-report=term-missing
```

### Test Categories

| File | Tests | Purpose |
|------|-------|---------|
| `test_tools_basic.py` | 28 | Core tools (index, search, status), background reindex |
| `test_graph_tools.py` | 17 | `find_symbol` and `graph` (callers/callees direction) |
| `test_analyze_tool.py` | 6 | `analyze` tool |
| `test_impact_tool.py` | 6 | `graph(transitive=True)` change-impact analysis |
| `test_explain_tool.py` | 5 | `explain` tool with verbosity levels |
| `test_hybrid_search.py` | 8 | Hybrid search, fusion |
| `test_live_grep.py` | 4 | rg/grep fallback |
| `test_permissions.py` | 20 | READ/MUTATE/WRITE categories, action-aware `memory` |
| `test_rate_limiter.py` | 11 | Token-bucket rate limiter |
| `test_audit.py` | 10 | Audit logging and field redaction |
| `test_security.py` | 19 | Input validation, SQL injection, path traversal |
| `test_e2e.py` | 8 | End-to-end lifecycle, corrupt index recovery |
| `test_performance.py` | 4 | Performance benchmarks (marked slow) |
| `test_memory_usage.py` | 5 | RSS monitoring and memory stability |
| `test_memory_store.py` | 22 | Memory store CRUD, TTL, tags |
| `test_vector_engine.py` | 20 | LanceDB vector engine CRUD |
| `test_bm25_engine.py` | 19 | LanceDB full-text search |
| `test_pipeline.py` | 27 | Indexing pipeline, incremental reindex, staleness |
| `test_chunker.py` | 22 | Symbol-to-chunk conversion |
| `test_bench_*.py` | 128 | Benchmark harness (`benchmarks/`) |
| ... | ... | Run `pytest --collect-only -q` for the full list |

### Test Conventions

- **Naming:** `test_{function}_{scenario}`
- **Fixtures:** Shared setup in `tests/conftest.py` (`mini_codebase`, `mini_codebase_with_calls`, `_setup_indexed`, `_call_tool`)
- **No mocking of core models** (Symbol, ParsedFile, etc.)
- **Both happy path and error cases** required
- **Fast:** Each test <5s, full suite <30s
- **File system tests** use `tmp_path` fixture

## Linting

```bash
# Check for issues
ruff check .

# Auto-fix
ruff check --fix .

# Configuration in pyproject.toml: E, F, W, I rules, 100 char line length
```

## Adding a New MCP Tool

Each tool has two parts. The logic lives in `core_api.py`, so other processes can import it without MCP. The MCP wrapper in `server.py` adds the permission guard, rate limiter and audit log.

1. Add the logic as a plain function in `core_api.py`:
   ```python
   def my_tool(param: str) -> dict[str, Any]:
       """Tool description."""
       # Validate input
       err = validate_query(param)
       if err:
           return err

       # Require indexing if needed
       state, err = require_indexed()
       if err:
           return err

       # Tool logic here
       return {"result": "..."}
   ```

2. Add input validation if the tool accepts user input. Use `validate_path`, `validate_symbol_name` or `validate_query`, or add a new `validate_*` helper in `core_api.py`.

3. Add a thin wrapper inside `create_server()` in `server.py`:
   ```python
   @mcp.tool()
   @_audited
   def my_tool(
       param: Annotated[str, "What this parameter does"],
   ) -> dict[str, Any]:
       """Use when ... (one routing-oriented sentence for MCP clients)."""
       guard_err = _guard("my_tool")
       if guard_err:
           return guard_err
       return core_api.my_tool(param)
   ```

4. Register the tool in `TOOL_PERMISSIONS` in `security/permissions.py`.

5. Write tests in a new `tests/test_my_tool.py` or add to an existing file.

6. Update the tool table in `README.md`, the tool reference in `docs/USAGE_GUIDE.md`, `llms.txt` and `plugin/skills/nexus-mcp/SKILL.md`. Regenerate `llms-full.txt` with `python scripts/build_llms_full.py`.

7. Update the tool count in `CLAUDE.md` and `AGENTS.md`.

Keep the tool surface small. [ADR-017](adr/ADR-017-tool-consolidation.md) explains why the project merged 15 tools into 10. Prefer a new `action` or `detail` parameter on an existing tool to a new tool.

## Adding a New Engine

1. Create `engines/my_engine.py` implementing the `IEngine` interface (or a custom interface).

2. Wire it into `IndexingPipeline.__init__()` in `pipeline.py`.

3. Expose it via `SessionState` in `state.py` (add property + setter).

4. Wire it into `core_api.py` in the `index` function (store reference on state).

5. Write tests in `tests/test_my_engine.py`.

## Architecture Decision Records

All significant design decisions are documented in `docs/adr/`. When making a key decision:

1. Copy `docs/adr/ADR-000-template.md`
2. Number it sequentially (the next free number is ADR-020)
3. Document: Context, Decision, Alternatives Considered, Consequences
4. Add a reference to `CLAUDE.md` under "Key Decisions"

## Key Design Patterns

### Singleton State
`state.py` uses a module-level singleton. Always access via `get_state()`. Reset with `reset_state()` in tests.

### Lazy Loading
Engines are `None` until indexing runs. The embedding model is loaded during indexing and unloaded afterward to free RAM.

### Defensive Validation
All tool inputs are validated at entry (null bytes, length limits, path traversal). SQL filter values are escaped. This happens in `core_api.py` (the `validate_*` helpers) before any business logic.

### Graceful Degradation
If BM25 fails, hybrid search continues with remaining engines. Each engine failure is logged but doesn't crash the server.

## Debugging

### Enable Debug Logging
```bash
NEXUS_LOG_LEVEL=DEBUG nexus-mcp-ci
```

### JSON Logging (for structured log analysis)
```bash
NEXUS_LOG_FORMAT=json nexus-mcp-ci
```

### Check Index Health
Use the `status` tool to verify:
- `indexed: true` — Codebase has been indexed
- `vector_chunks > 0` — Vector store has data
- `bm25_fts_ready: true` — Full-text search index built
- `graph.total_nodes > 0` — Graph has structure
- `memory.peak_rss_mb < 350` — Within memory budget

## Code Provenance

Nexus-MCP was built by consolidating two earlier projects. Each source file that was ported retains a "Ported from" line in its module docstring documenting its origin.

- **CodeGrok MCP** — Original author: rdondeti (Ravitez Dondeti). Licensed under MIT.
- **code-graph-mcp** — Original author: [entrepeneur4lyf](https://github.com/entrepeneur4lyf).

| Component | Origin | Files |
|-----------|--------|-------|
| Core models (Symbol, ParsedFile, Memory) | CodeGrok MCP | `core/models.py`, `core/interfaces.py`, `core/exceptions.py` |
| tree-sitter parser | CodeGrok MCP | `parsing/treesitter_parser.py` |
| Embedding service (ONNX) | CodeGrok MCP | `indexing/embedding_service.py` |
| Parallel indexer | CodeGrok MCP | `indexing/parallel_indexer.py` |
| Graph models (UniversalNode) | code-graph-mcp | `core/graph_models.py` |
| ast-grep parser | code-graph-mcp | `parsing/astgrep_parser.py` |
| Graph engine (rustworkx) | code-graph-mcp | `engines/graph_engine.py` |
| Code analyzer | code-graph-mcp | `analysis/code_analyzer.py` |
| File watcher | code-graph-mcp | `parsing/file_watcher.py` |
| Language registry | Both (merged) | `parsing/language_registry.py` |

Everything else (vector engine, BM25, fusion, memory store, pipeline, formatting, persistence, server, all hardening) was written fresh for Nexus-MCP.

## Release Process

1. Update version in `pyproject.toml`
   (the CI workflow `.github/workflows/publish.yml` builds and publishes to PyPI)
2. Run full test suite: `pytest -v`
3. Run linter: `ruff check .`
4. Run Snyk scan
5. Build: `python -m build`
6. Test install: `pip install dist/nexus_mcp_ci-*.whl` in a clean venv
7. Verify: `nexus-mcp-ci` starts and `status` works
8. Update `CHANGELOG.md`, and regenerate `llms-full.txt` (`python scripts/build_llms_full.py`)
