mcp server for rust code semantic hybrid search bm25 + embeddings with tree-sitter, gpu accelerated indexing and merkle tree updates
Overview
An MCP server for searching and analyzing Rust codebases. Combines hybrid BM25 + vector search with a HIR-driven workspace built on rust-analyzer, exposing 45+ tools for symbol navigation, call-graph traversal, structural audits, and semantic neighborhood queries. Website · Discord — come share how you're using it; we want to hear about people's workflows. See .docs/ARCHITECTURE.md for the per-module breakdown, TOOLS.md for the full MCP tool reference, .docs/configure-models-guide.md for embedding-model configuration, and THEORY.md for the principles each diagnostic maps to.
README
rust-code-mcp
An MCP server for searching and analyzing Rust codebases. Combines hybrid BM25 + vector search with a HIR-driven workspace hypergraph built on rust-analyzer, exposing 45+ tools for symbol navigation, call-graph traversal, structural audits, and semantic neighborhood queries.
Links: Website · Discord — come share how you’re using it; we want to hear about people’s workflows.
Architecture
See .docs/ARCHITECTURE.md for the per-module breakdown, TOOLS.md for the full MCP tool reference, .docs/configure-models-guide.md for embedding-model configuration, and THEORY.md for the principles each diagnostic maps to.
Features
- Hybrid search - BM25 keyword search + semantic vector similarity (RRF fusion)
- Pluggable embedding models - local GPU (Qwen3 via Candle/CUDA), local CPU (BGE via ONNX), or API-backed (OpenRouter); new API models are added through a config file with no recompile
- Symbol navigation - rust-analyzer–backed
find_definition/find_references/rename_symbol(rename returns a preview; no files are modified) - Persisted hypergraph - HIR-driven workspace snapshot (LMDB) with cross-crate imports, exports, re-exports, call edges, attributes, signatures, statics, and
unsafeblocks - Call-graph traversal -
who_calls/calls_from/call_graph/callers_in_crate/recursive_callers_count - Structural audits - dead public items, name collisions, module shadowing, forbidden cross-crate edges, Robert Martin instability/abstractness
- Safety audits -
unsafe_audit,mut_static_audit,recursion_check,channel_capacity_audit,fn_body_audit(unwrap/panic/lock-across-await detection) - Doc & API hygiene audits -
missing_docs_audit,derive_audit,pub_use_pub_type_audit,re_export_chain - Semantic neighbors -
similar_to_itemand workspace-widesemantic_overlapsclustering via cached embeddings - Codemap -
build_codemapproduces a task-conditioned subgraph (seeded by symbols, expanded over hypergraph edges) with Mermaid + outline rendering - Complexity metrics - LOC, cyclomatic complexity, function counts
- Incremental indexing - Merkle-tree change detection; background re-sync every 5 minutes
Tools
45+ MCP tools grouped by category. Full parameter reference in TOOLS.md.
| Category | Tools |
|---|---|
| Query | search, get_similar_code, read_file_content |
| Symbol analysis | find_definition, find_references, rename_symbol, get_dependencies, get_call_graph, analyze_complexity |
| Index lifecycle | index_codebase, health_check, clear_cache |
| Hypergraph build | build_hypergraph |
| Imports / exports | get_imports, get_exports, get_reexports, get_declared_reexports |
| Reverse lookup | who_imports, who_uses, who_uses_summary |
| Call graph | who_calls, calls_from, call_graph, callers_in_crate, recursive_callers_count |
| Workspace structure | dead_pub_in_crate, dead_pub_report, crate_edges, overlaps, module_tree, crate_types, crate_skeleton, workspace_stats |
| Architecture rules | forbidden_dependency_check, crate_dependency_metric |
| Signatures & attributes | function_signature, functions_with_filter, enum_variants, item_attributes, items_with_attribute |
| Safety & quality audits | unsafe_audit, mut_static_audit, recursion_check, channel_capacity_audit, fn_body_audit |
| Doc / API audits | missing_docs_audit, derive_audit, pub_use_pub_type_audit, re_export_chain |
| Semantic | similar_to_item, semantic_overlaps |
| Codemap | build_codemap |
Skills
The skills/ directory ships 26 Claude Code skills that compose these MCP tools into ready-made audit recipes. Each skill is a self-contained SKILL.md with prerequisites, step-by-step prompts, and hand-offs to related skills — invoke them in Claude Code as / once installed.
| Scope | Skill | Purpose |
|---|---|---|
| Workspace | rmc-workspace-overview |
First-look audit of a Rust workspace |
| Workspace | rmc-architecture-rules |
Enforce crate-edge rules |
| Workspace | rmc-dependency-metric |
Rank crates by Robert Martin instability / abstractness |
| Workspace | rmc-imports-exports |
Cross-crate imports and exports audit |
| Workspace | rmc-type-overlaps |
Name collisions and module shadows |
| Workspace | rmc-semantic-overlaps |
Duplicate logic detection by embedding similarity |
| Workspace | rmc-snapshot-diff |
Compare two workspace snapshots (e.g. across branches) |
| Crate | rmc-crate-audit |
Audit one crate end-to-end |
| Crate | rmc-api-surface |
Audit a crate’s public API |
| Crate | rmc-module-audit |
Audit a single module |
| Symbol | rmc-find-symbol |
Resolve a symbol’s qualified name |
| Symbol | rmc-symbol-forensics |
Deep dive on one symbol (refs, callers, attrs) |
| Symbol | rmc-method-api |
Audit a type’s methods |
| Symbol | rmc-trait-audit |
Audit a trait and its impls |
| Symbol | rmc-enum-variants |
Inspect an enum’s variants |
| Symbol | rmc-rename-symbol |
Preview a rename — exact reference set & refactor probe |
| Symbol | rmc-signature-search |
Find fns by signature shape |
| Quality | rmc-unsafe-audit |
Audit unsafe blocks |
| Quality | rmc-mut-static-audit |
Audit global mutable state |
| Quality | rmc-attribute-audit |
Audit attributes and doc-comments |
| Quality | rmc-complexity |
Complexity hotspots by blast radius |
| Quality | rmc-test-vs-prod |
Test vs production split |
| Quality | rmc-call-graph |
Fn-level call graphs |
| Quality | rmc-reexport-chain |
Trace pub use re-export chains |
| Planning | rmc-codemap |
Task-conditioned workspace subgraph (nodes / edges / hierarchy) |
| Planning | rmc-refactor-plan |
Plan a refactor with evidence from the hypergraph |
Install by symlinking (or copying) the directories into your Claude Code skills folder:
mkdir -p ~/.claude/skills
ln -s "$(pwd)/skills/"rmc-* ~/.claude/skills/
Then in Claude Code, type /rmc- to discover them, or /rmc-workspace-overview to kick off a tour of a new repo.
Installation
1. Build the binary
git clone https://github.com/molaco/rust-code-mcp.git
cd rust-code-mcp
cargo build --release
The binary is at target/release/rust-code-mcp.
Build prerequisite: the default embedding backend (Qwen3) runs on Candle with CUDA, so the build needs the CUDA toolkit (
nvcc) onPATH. The simplest way to get a correct toolchain is the Nix dev shell (see Nix) —nix developprovides nightly Rust plus the CUDA toolkit, cuDNN, and cuBLAS. See GPU & CUDA for details.
Optionally, copy it somewhere on your PATH:
cp target/release/rust-code-mcp ~/.local/bin/
2. Add to Claude Code
In your Rust project directory, create .mcp.json:
{
"mcpServers": {
"rust-code-mcp": {
"command": "/absolute/path/to/rust-code-mcp"
}
}
}
Or add it globally in ~/.claude.json so it’s available in all projects:
{
"mcpServers": {
"rust-code-mcp": {
"command": "/absolute/path/to/rust-code-mcp"
}
}
}
3. Index your codebase
Once Claude Code starts, the server is running. Use the index_codebase tool to index your project:
> index my codebase at /absolute/path/to/my-rust-project
Or call the tool directly with the directory parameter set to your project root. Pass an optional embedding_profile to choose the embedding model (see Embedding Models); the default is a local GPU model. Indexing is incremental — subsequent runs only process changed files (via Merkle tree change detection). A background sync also re-indexes every 5 minutes automatically, using the profile each index was built with.
4. Start using it
All tools accept a directory parameter pointing to your project root. Examples:
- Search code:
searchwith a query like “error handling in parser” - Find definitions:
find_definitionfor a symbol name - Find references:
find_referencesto see all usages of a symbol - Preview a rename:
rename_symbolreturns the full edit set without touching files - Call graph:
get_call_graphorwho_calls/calls_fromto trace function relationships - Skeleton facade:
crate_skeletonwrites a stripped mirrored source tree under.skeleton/ - Similar code:
get_similar_codefor semantic similarity search
For Rust-specific workspace analysis, first call build_hypergraph once (reuses a fingerprinted snapshot on subsequent calls), then run audits like unsafe_audit, dead_pub_report, overlaps, crate_dependency_metric, or semantic_overlaps. The codemap tool (build_codemap) produces a Mermaid-renderable subgraph seeded by symbols of interest. The skeleton tool (crate_skeleton) writes generated facade files to /.skeleton/, mirroring source-relative paths.
Index data is stored in ~/Library/Application Support/dev.rust-code-mcp.search/ (macOS) or ~/.local/share/search/ (Linux), keyed by a hash of the project path and the active embedding profile — so different profiles get independent indexes. It does not write index/cache data to your project directory; crate_skeleton is the explicit exception and writes generated files under .skeleton/. The persisted hypergraph lives alongside the index data (under graph//, in LMDB). clear_cache with include_hypergraph=true wipes both.
Shared daemon (one server per project)
The stdio transport is 1:1 with its client by construction: every editor window or agent session spawns its own server process — and with it another copy of the loaded rust-analyzer context (~2 GB per workspace) plus another ONNX/GPU context. Measured on one developer machine before this landed: six live servers, 8.9 GB, all analyzing the same repository.
The runtime state was already shareable (RuntimeState is a bundle of Arcs, semantic
contexts are cached per project path, and locking is per workspace), so the only missing
piece was a transport that accepts more than one client:
- running the binary with no arguments makes it a client of a per-project daemon,
spawning that daemon on first use — no change to
.mcp.jsonis needed; - the daemon serves every connection from one shared
RuntimeStateand exits after 30 minutes with no clients (--idle-secs/RMC_DAEMON_IDLE_SECS,0= never); - if the daemon cannot be reached or started, the client serves the session in-process, exactly as before — the daemon is a memory optimisation, not a new point of failure;
- a session whose daemon dies mid-flight ends with exit code 1 and a message on stderr, instead of continuing quietly in a second server: stdin is already partly consumed, so a server started afterwards would answer a truncated stream;
- a bad
RMC_EMBEDDING_PROFILEexits 2 in every mode that serves a session, the client included, while--helpand--print-socketanswer normally and exit 0, because they serve nothing and are how a host reads back the value it got wrong.
The socket key covers the workspace root — the walk upwards takes the nearest
Cargo.toml that declares a [workspace] and stops at a .git or .jj boundary, so two
sessions started in different subdirectories of one repository share a daemon while a stray
outer manifest cannot merge two repositories into one — the binary’s size and mtime, the
resolved index root (data_dir()), because it decides where the index lives and is itself
resolved from XDG_DATA_HOME and then HOME, LD_LIBRARY_PATH, because it decides which
CUDA and ONNX libraries the daemon links at start, and every RMC_* and RUST_CODE_MCP_*
variable plus OPENROUTER_API_KEY. The four transport knobs (RMC_DAEMON,
RMC_DAEMON_DIR, RMC_DAEMON_LOG_DIR, RMC_DAEMON_IDLE_SECS) are excluded, because they
choose which daemon a client talks to, or where it logs, rather than what it answers. A rebuilt binary, or a
differently configured one, therefore gets its own daemon instead of silently attaching to
one that answers differently.
The daemon runs in the project root it is keyed on, so every client of one daemon resolves
a relative directory argument the same way, whichever subdirectory its session started
in. A client whose own working directory is a subdirectory therefore sees "." mean the
project root through the daemon and that subdirectory with RMC_DAEMON=0, so prefer
absolute paths in tool arguments.
rust-code-mcp --print-socket # which socket this project resolves to
rust-code-mcp --in-process # previous behaviour (same as RMC_DAEMON=0)
rust-code-mcp --daemon # run the daemon in the foreground
Sockets and locks live in $XDG_RUNTIME_DIR/rust-code-mcp/ (override with
RMC_DAEMON_DIR). Daemon logs live on disk in $XDG_STATE_HOME/rust-code-mcp/logs/,
normally ~/.local/state/rust-code-mcp/logs/ (override with RMC_DAEMON_LOG_DIR), named
after the socket plus a short hash of its full path, with a .log extension, so two
user-named sockets that share a name do not share a log — never in the runtime directory,
which is a small tmpfs meant for sockets. The log is appended rather than replaced, so the crash of an earlier daemon stays readable
after a new one starts, and the daemon truncates it in place before any tracing event that
would take it past 32 MiB, so a long-lived daemon cannot fill a disk however verbose its
RUST_LOG. The bound is read from the file, not counted: panic output, which reaches the
same file through the daemon’s raw stderr, counts at the next event. A write the log
cannot take is dropped, never reported through stderr. Either
directory, when it already exists, is used as it is and never re-permissioned; only a
directory the daemon creates gets owner-only access. A symlink, a path that is not a
directory, and a directory owned by another user are refused. Unix only; on other
platforms the server stays in-process.
Embedding Models
Semantic search and the embedding-backed audits (get_similar_code, similar_to_item, semantic_overlaps) run on a configurable embedding profile. Built-in profiles:
| Profile | Model | Runtime | Dim |
|---|---|---|---|
local-cpu-small (default) |
BGE-small-en-v1.5 | local ONNX/CPU | 384 |
local-gpu-small |
Qwen3-Embedding-0.6B | local Candle/CUDA | 1024 |
local-qwen3-4b |
Qwen3-Embedding-4B | local Candle/CUDA | 2560 |
local-qwen3-8b |
Qwen3-Embedding-8B | local Candle/CUDA | 4096 |
openrouter-qwen3-8b |
Qwen3-Embedding-8B | OpenRouter API | 4096 |
The default when embedding_profile is omitted is local-cpu-small. Select another profile by passing embedding_profile to index_codebase and the search tools. Each profile gets its own independent index, and search must use the profile its index was built with.
Set the default with RMC_EMBEDDING_PROFILE — this environment variable names the profile used whenever a call omits embedding_profile, and changing it needs no rebuild:
RMC_EMBEDDING_PROFILE=local-gpu-small rust-code-mcp
Built-in profiles only: the canonical names above, their legacy aliases, and any letter case. A profile from embedding_profiles.toml needs the embedding_profile argument instead. Unset or blank keeps local-cpu-small, and whitespace is trimmed. The value is read once at startup, and an unusable name — including a local CUDA profile in a build without CUDA support — stops the server with one stderr line and exit code 2 rather than a silent fall back, because the profile is part of the index identity. Background sync is unchanged: it keeps syncing the indexes already on disk, and only a new local-CUDA index stays out of it.
API models (OpenRouter) require an API key in the environment — keys are never read from config files:
export OPENROUTER_API_KEY=sk-or-...
Adding an API model is a config change, no recompile — drop an embedding_profiles.toml in your project root:
[[profile]]
name = "openrouter-text-embedding-3-small"
model_id = "openai/text-embedding-3-small"
dim = 1536
max_len = 8191
Local models (Candle/ONNX) are code-bound and ship as built-ins. See .docs/configure-models-guide.md for the full TOML schema, OpenRouter tuning knobs, and the trade-offs between models.
Nix
A Nix flake is provided for easy setup:
# Enter dev shell with all dependencies
nix develop github:molaco/rust-code-mcp
# Build the binary
nix build github:molaco/rust-code-mcp
The dev shell includes nightly Rust, ONNX Runtime, and the full CUDA toolchain (toolkit, cuDNN, cuBLAS) needed when you opt into the GPU embedding path.
GPU & CUDA
The default embedding profile (local-cpu-small, BGE) runs on ONNX/CPU and needs no GPU. Local Qwen3 profiles (local-gpu-small, local-qwen3-4b, local-qwen3-8b) run on Candle with CUDA; OpenRouter profiles offload embedding to the API and need no local GPU.
Requirements
- NVIDIA GPU — ≥8 GB VRAM is comfortable for the default 0.6B model; the local 8B profile needs ~16 GB
- CUDA toolkit (
nvcc) at build time — Candle’s CUDA backend (cudarc) requires it during compilation - CUDA runtime + cuDNN + cuBLAS libraries at run time
Setup
The Nix dev shell provides the full CUDA build and runtime environment — this is the supported path:
nix develop # nightly Rust + CUDA toolkit + cuDNN/cuBLAS
cargo build --release --features cuda
When using a local CUDA profile and the MCP server is spawned by Claude Code (rather than launched from the Nix shell), its process still needs the CUDA runtime libraries on LD_LIBRARY_PATH. Set them in your MCP client config:
{
"mcpServers": {
"rust-code-mcp": {
"command": "/path/to/rust-code-mcp",
"env": {
"RUST_LOG": "info",
"CUDA_HOME": "/usr/local/cuda",
"LD_LIBRARY_PATH": "/usr/local/cuda/lib64:/usr/lib/x86_64-linux-gnu"
}
}
}
}
Adjust the paths to your CUDA installation.
Running without a GPU
A GPU is not required. The default build and default embedding profile are CPU-only:
cargo build --release
Index and search with the omitted/default profile or embedding_profile = "local-cpu-small" (BGE on ONNX/CPU). OpenRouter profiles also avoid local GPU use, but require OPENROUTER_API_KEY.
Performance
Indexing throughput measured on this repository (~2,280 chunks) with an RTX 3090:
| Profile | Indexing throughput | Notes |
|---|---|---|
local-gpu-small (Qwen3-0.6B) |
~55 chunks/sec | local GPU; private, deterministic |
openrouter-qwen3-8b (Qwen3-8B) |
~45–50 chunks/sec | API; per-request latency varies run-to-run |
openrouter text-embedding-3-small |
~220 chunks/sec | API; fastest, general-purpose (not code-tuned) |
Embedding is ~95%+ of indexing time. Larger / higher-quality models embed more slowly; text-embedding-3-small is fastest but not code-tuned. Vector search is exact brute-force KNN — fast at workspace scale regardless of dimension. Pick a profile for your quality / speed / privacy trade-off; see the config guide.
Stack
- tantivy - Full-text BM25 search
- fastembed - Embedding models: Qwen3 via Candle (GPU), BGE via ONNX (CPU)
- candle - GPU embedding runtime for Qwen3 (CUDA)
- reqwest - OpenRouter API embedding client
- lancedb - Embedded vector storage
- ra_ap_syntax - AST parsing
- ra_ap_ide - Semantic analysis (goto definition, find references, rename)
- ra_ap_hir - HIR-driven hypergraph extraction
- heed - LMDB-backed persisted hypergraph store
- sled - Embedded KV for indexing metadata cache
- rmcp - MCP protocol
Screenshots
License
MIT
Install
This server does not publish a one-line install command.
Open the repository installation guideConfiguration
{
"mcpServers": {
"rust-code-mcp": {
"command": "/absolute/path/to/rust-code-mcp"
}
}
}