Hybrid search + code graph + semantic memory in a single local MCP server — under 350 MB RAM.
Nexus-MCP is a code intelligence server for the Model Context Protocol. It gives AI agents precise, token-efficient answers about your codebase without cloud dependencies: no API keys, no data egress, no subscriptions.
pip install nexus-mcp-ci
claude mcp add nexus-mcp-ci -- nexus-mcp-ci
AI coding agents are token-inefficient by default. An agent trying to understand verify_credentials() typically:
Glob("src/**/*.py")→ 120 files returned, agent reads the most likely 8 → ~12,000 tokensGrep("verify_credentials")→ 3 matches, agent reads surrounding context → ~4,000 tokensRead("auth/middleware.py")→ full 400-line file to understand callers → ~3,000 tokens
Total: ~19,000 tokens, 3+ tool calls, no graph relationships.
With Nexus-MCP:
explain("verify_credentials")→ symbol definition + all callers + all callees + complexity metrics → ~1,500 tokens, 1 tool call
Or for discovery:
search("credential verification flow")→ top-10 semantically relevant chunks across the codebase → ~2,000 tokens, 1 tool call
Estimated savings: 30–60% token reduction per coding session. The exact numbers depend on codebase size and task type — see the benchmarks table below.
# 1. Install
pip install nexus-mcp-ci
# 2. Register with Claude Code
claude mcp add nexus-mcp-ci -- nexus-mcp-ci
# 3. Verify (in any Claude Code session)
# Claude will automatically use nexus-mcp-ci tools when CLAUDE.md instructs itThen drop a CLAUDE.md in your project root:
## Code Navigation
Use nexus-mcp-ci tools before built-in file tools:
- Start sessions with `mcp__nexus-mcp__status`; run `index` if needed
- `search` before `Read/Grep`
- `explain` instead of reading a file to understand a symbol
- `impact` before any refactorThat's it. Claude will index your project on first use and use Nexus-MCP tools automatically.
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: call edges, import edges, inheritance edges
│ output: UniversalGraph(nodes=[], edges=[])
│
├─ 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 (default) or jina-code: 768-dim via ONNX
│ 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("how does auth work")
│
├─► vector_engine.search(query, n=30) ← cosine similarity on 768-dim 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, g_results)
│
│ Reciprocal Rank Fusion: score = Σ weight_i / (k + rank_i)
│ default weights: vector=0.5, bm25=0.3, graph=0.2
│
├─► reranker.rerank(top_20) ← FlashRank (optional, 4MB ONNX model, <10ms)
│
└─► token_budget.truncate() ← summary / detailed / full
│
└─► Top-N chunks, scored, formatted
| 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) |
| Embeddings | bge-small-en (default) or ONNX Runtime + jina-code | bge-small-en is lightweight (384-dim, no trust_remote_code). jina-code is code-specific (161M params, 8192 seq len) on ONNX (~50 MB vs PyTorch ~500 MB). Lazy-load/unload keeps RAM flat after indexing. (ADR-003) |
| Graph engine | rustworkx PyDiGraph | Rust-backed, O(1) node lookup, PageRank + centrality algorithms. Thread-safe with RLock. (ADR-006) |
| Symbol parser | tree-sitter 0.21.3 | 25+ languages, incremental parsing, AST-level symbol extraction with metadata. Parallel via ThreadPool. (ADR-005) |
| Graph parser | ast-grep | Structural pattern matching for call/import/inheritance edges. Sequential run for graph consistency. (ADR-005) |
| Chunking | Symbol-based | One chunk per function/class. Deterministic SHA256 IDs prevent duplicate inserts. (ADR-008) |
| Re-ranker | FlashRank (optional) | 4 MB ONNX cross-encoder, <10 ms on CPU for top-20. Graceful passthrough if not installed. |
| 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. |
Measured against equivalent agentic file-browsing workflows on a ~10,000-line Python codebase:
| Task | Without Nexus-MCP | With Nexus-MCP | Reduction |
|---|---|---|---|
| Find relevant code (agent reads 5–10 files) | 5,000–15,000 tokens | 500–2,000 tokens | 70–90% |
| Understand a symbol (grep + read + trace callers) | 3,000–8,000 tokens, 3–5 calls | 800–2,000 tokens, 1 call | 60–75% |
| Assess change impact (manual transitive trace) | 10,000–20,000 tokens | 1,000–3,000 tokens | 80–85% |
| Tool descriptions in context (2 MCP servers) | ~1,700 tokens (17 tools) | ~700 tokens (10 tools) | ~60% |
| Search precision (keyword-only needs retries) | 2–3 searches × 2,000 tokens | 1 hybrid search × 1,500 tokens | 60–75% |
Typical session savings: 15,000–40,000 tokens (30–60%) compared to file-browsing agents.
Every tool respects a verbosity parameter — agents request exactly the detail they need:
| Level | Token Budget | What's Included |
|---|---|---|
summary |
~500 tokens | Counts, scores, file:line pointers only |
detailed |
~2,000 tokens | Signatures, types, line ranges, docstrings |
full |
~8,000 tokens | Full code snippets, all relationships, metadata |
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 for the old→new mapping and
ADR-017 for why. Fewer, richer tools route
better under MCP Tool Search than many thin ones.
| 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. |
health() |
Liveness probe — uptime, which engines are ready. |
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. |
| Tool | Use When |
|---|---|
search(query, mode, language, type, n) |
Primary code discovery. mode: hybrid (default), vector, or bm25. Falls back to live grep if results are sparse. Returns a non-null warning if the index looked stale (a background reindex is triggered automatically; results still return immediately). |
| Tool | Use When |
|---|---|
find_symbol(name, exact) |
Look up a specific symbol. exact=False for fuzzy matching. |
graph(symbol, direction, transitive, max_depth) |
direction="callers" (who calls this, was find_callers) or "callees" (what this calls, was find_callees). transitive=True — MUST run before any refactor (was impact()): full transitive change blast radius across the graph. |
explain(symbol) |
Replaces Read for understanding code. Graph relationships + semantic context + quality metrics in one call. |
analyze(path) |
Code quality: cyclomatic complexity, cognitive complexity, code smells, dependency metrics. |
| Tool | Use When |
|---|---|
memory(action, ...) |
action="store" (was remember) to persist a decision/note across sessions (types: note, decision, conversation, status, preference, doc; TTL: permanent, month, week, day, session); "search" (was recall) for semantic retrieval; "delete" (was forget) to remove by ID, tag, or type. |
pip install nexus-mcp-ci
# GPU (CUDA) support — adds ONNX CUDA execution provider
pip install nexus-mcp-ci[gpu]
# FlashRank reranker — adds ~4MB cross-encoder for better search quality
pip install nexus-mcp-ci[reranker]
# Both
pip install nexus-mcp-ci[gpu,reranker]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.13 required. Optional: rg (ripgrep) for 100% search coverage fallback on unindexed files.
The optional
jina-codemodel requires ONNX Runtime. If you see ONNX/Optimum errors:pip install "sentence-transformers[onnx]" "optimum[onnxruntime]>=1.19.0"The default
bge-small-enmodel needs neither ONNX nortrust_remote_code.
# Minimal
claude mcp add nexus-mcp-ci -- nexus-mcp-ci
# With the code-specific embedding model (requires trust_remote_code)
claude mcp add nexus-mcp-ci -e NEXUS_EMBEDDING_MODEL=jina-code -- nexus-mcp-ci
# GPU embeddings
claude mcp add nexus-mcp-ci -e NEXUS_EMBEDDING_DEVICE=cuda -- nexus-mcp-ci
# Virtualenv install — pass the full binary path
claude mcp add nexus-mcp-ci -- /path/to/.venv/bin/nexus-mcp-ci~/Library/Application Support/Claude/claude_desktop_config.json (macOS):
{
"mcpServers": {
"nexus-mcp-ci": {
"command": "nexus-mcp-ci",
"args": [],
"env": {
"NEXUS_EMBEDDING_MODEL": "jina-code"
}
}
}
}{
"nexus-mcp-ci": {
"command": "nexus-mcp-ci",
"transport": "stdio"
}
}## Code Intelligence — nexus-mcp-ci
Every code task in this project MUST follow this workflow:
1. **Session start**: `mcp__nexus-mcp__status` → if not indexed, `mcp__nexus-mcp__index`
2. **Before any file read**: `mcp__nexus-mcp__search` to locate relevant code
3. **To understand a symbol**: `mcp__nexus-mcp__explain` (not Read)
4. **Before refactoring**: `mcp__nexus-mcp__impact` to assess blast radius
5. **For project orientation**: `mcp__nexus-mcp__overview` or `mcp__nexus-mcp__architecture`# Session start
status() → "indexed: True, 8,412 chunks, 1,203 symbols, 87 MB"
# Code discovery
search("JWT token validation", mode="hybrid", n=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
# Deep symbol understanding
explain("validate_token")
→ definition, docstring, params, complexity
→ callers: [require_auth, login_required, api_key_check]
→ callees: [decode_jwt, check_expiry, verify_signature]
→ quality: complexity=6, smells=[], maintainability=A
# Pre-refactor safety check
impact("validate_token")
→ direct callers: 3 symbols
→ transitive impact: 12 symbols across 4 files
→ high-risk: auth/middleware.py (5 dependents)
# 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")All settings via NEXUS_ environment variables:
| Variable | Default | Description |
|---|---|---|
NEXUS_EMBEDDING_MODEL |
bge-small-en |
bge-small-en (384-dim, lightweight) or jina-code (768-dim, code-optimized) |
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_STALENESS_CHECK_INTERVAL |
15 |
Seconds between status()/search() staleness checks (throttled, not per-call) |
NEXUS_MAX_FILE_SIZE_MB |
10 |
Skip files larger than this |
NEXUS_CHUNK_MAX_CHARS |
4000 |
Max chars per code chunk |
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_FUSION_WEIGHT_GRAPH |
0.2 |
Graph 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 |
Required for jina-code; set false with bge-small-en |
NEXUS_LOG_LEVEL |
INFO |
Logging level |
NEXUS_LOG_FORMAT |
text |
text or json |
| 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 | jina-code |
768 | 8,192 | ONNX | Yes |
After changing model, re-index. Embeddings from different models are incompatible.
| 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) | ✅ rustworkx | ✅ SCIP | ❌ | ❌ | ❌ |
| Re-ranking | ✅ FlashRank | ❌ | — | ❌ | ❌ |
| Semantic memory (persistent) | ✅ 6 types | ❌ | ❌ | ❌ | ❌ |
| Change impact analysis | ✅ | partial | ❌ | ❌ | ❌ |
| Token-budgeted responses | ✅ 3 levels | ❌ | ❌ | ❌ | ❌ |
| Languages | 25+ | 30+ | many | many | many |
| Cost | Free | $$$ | $40/mo | $10–39/mo | Free |
| API keys required | No | Yes | Yes | Yes | No |
| 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 | ✅ | unknown | unknown | ✅ SCIP | basic | ❌ |
| Semantic memory | ✅ persistent | ❌ | ❌ | ❌ | ❌ | ❌ |
| Token-budgeted output | ✅ | — | — | — | — | — |
| Open source | ✅ MIT | ❌ | ❌ | partial | ✅ | ✅ |
| Cost | Free | $20–40/mo | $10–39/mo | $0–49/mo | Free | Free |
git clone https://github.com/jaggernaut007/Nexus-MCP.git
cd Nexus-MCP
pip install -e ".[dev]"
pytest -v # 441 tests
pytest -m "not slow" # skip performance benchmarks
pytest tests/test_search.py # single module
ruff check . # lintsrc/nexus_mcp/
├── server.py # FastMCP entrypoint — 10 tools, input validation, graceful shutdown
├── 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
│ ├── fusion.py # Reciprocal Rank Fusion
│ └── reranker.py # FlashRank (optional, graceful degradation)
├── 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
├── 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
- Add the handler function to
server.pydecorated with@mcp.tool() - Add inline validation (
_validate_*helpers inserver.py) for any new input - Add permission category to
security/permissions.py - Write tests in
tests/ - Update
self_test/demo_mcp.pyto exercise the tool
- Add entry to
parsing/language_registry.pywith the tree-sitter grammar - Add structural patterns to
parsing/astgrep_parser.pyfor call/import extraction - Add test fixtures in
tests/fixtures/
Verify your installation exercises all 10 tools end-to-end:
python self_test/demo_mcp.py # built-in sample project
python self_test/demo_mcp.py /path/to/project # your own codebaseExpected output: all 10 tools exercised with pass/fail per tool and a summary.
- 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 lightweight model uses PyTorch instead of ONNX, so it doesn't benefit from the same ~50 MB footprint as jina-code.
- 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, but structural relationship extraction (callers/callees) is most accurate for Python, TypeScript, JavaScript, Go, and Rust. Other languages may have partial graph edges.
- Static call graph only:
find_callers/find_callees/impactare built from static parsing, not runtime tracing — dynamic dispatch, monkey-patching, and calls made through callbacks/closures/reflection won't show up as edges. Treatimpactas a lower bound on blast radius in highly dynamic code. - 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.
Key decisions are documented in docs/adr/:
| ADR | Decision |
|---|---|
| ADR-001 | Merge two MCP servers into one |
| ADR-002 | LanceDB over ChromaDB |
| ADR-003 | ONNX Runtime over PyTorch for embeddings |
| ADR-004 | bge-small-en as default embedding model |
| ADR-005 | Dual parser: tree-sitter + ast-grep |
| ADR-006 | rustworkx for graph algorithms |
| ADR-007 | 12-column PyArrow schema for LanceDB |
| ADR-008 | Symbol-based chunking with deterministic IDs |
| ADR-009 | 8-step indexing pipeline |
| ADR-010 | Graph tools API: serialization, ambiguity handling |
| ADR-011 | Graceful shutdown, corruption recovery, JSON logging |
| ADR-012 | READ/MUTATE/WRITE permission categories |
| Pydantic v2 I/O schemas — superseded by ADR-016 (never wired in, deleted) | |
| ADR-014 | Token-bucket rate limiting (off by default) |
| ADR-015 | Auto-watch + throttled staleness detection |
| ADR-016 | Removal of unused Pydantic schemas (supersedes ADR-013) |
| ADR-017 | Tool consolidation 15→10, action-aware permission categories |
- Installation Guide — Prerequisites, client-specific setup, troubleshooting
- Architecture — Data flow, component design, memory budget analysis
- Usage Guide — Full tool reference with examples
- Developer Guide — Contributing, adding tools/engines/languages
- Research Notes — Library evaluations and technology deep-dives
Nexus-MCP consolidates two earlier open-source projects:
- CodeGrok MCP by rdondeti (Ravitez Dondeti, MIT) — Contributed the symbol extraction pipeline, embedding service, parallel indexer, core data models, and memory retrieval system.
- code-graph-mcp by 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 for the consolidation rationale.
MIT — see LICENSE for details.