Install
$ agentstack add mcp-rutmir-code-search-mcp ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
code-search-mcp
A token-saving, quality-first replacement for Claude Code's built-in Explore / Grep+Read iterative search. Exposed to Claude Code as an MCP server, it returns a ranked list of code chunks for any natural-language query in a single tool call.
[](https://github.com/rutmir/code-search-mcp/actions/workflows/ci.yml) [](#license)
Why
When Claude Code explores a codebase to answer a question, it currently does it the hard way: many grep / read iterations, each one feeding back into the model. A real exploration query consumes tens of thousands of tokens (and a lot of wall time) before the model converges on an answer.
code-search-mcp returns the final ranked list (typically ~1500 tokens of structured file:line + symbol + preview) from a single tool call. The hit rate matches or exceeds the iterative approach because:
- Hybrid retrieval — dense semantic embeddings + sparse BM25 + cross-encoder reranking together outperform any single signal
- AST-aware chunking — chunks aligned to function / class / method boundaries via tree-sitter for 9 languages; the LLM sees
fn AdaptiveBatcher::note_failureheaders, not raw line ranges - Code-aware matching — the BM25 tokenizer splits snake_case / camelCase identifiers (
buildSiPortfoliofindsbuild_si_portfolio), and a query that literally names a symbol boosts that chunk to the top — exact-symbol lookups don't need grep - Quality-first defaults — recall is the lever, latency is not. A 2-minute search that finds the right answer beats a 10-second search that misses
Typical savings: 30–100× on tokens for code exploration, with equal or better recall.
What it gives Claude Code
Once wired up, your Claude Code session gets a single tool:
// tools/call → code_search
{
"query": "AIMD adaptive batching halving on failure",
"limit": 5, // optional, default 10
"lang": "rust", // optional, restrict to one language
"path": "crates/" // optional, only hits whose path contains this substring
}
Output (each hit carries file:start-end, language, syntactic kind+name, score, preview):
#1 crates/bot/src/execution/cross_pressure.rs:69-91 [rust] struct CrossPressureDetector score=0.0655
pub struct CrossPressureDetector { /// Diagnostic only ... future_feed: Arc, ...
#2 crates/bot/src/execution/cross_pressure.rs:390-399 [rust] fn observe_tick_returns_verdicts score=0.0512
fn observe_tick_returns_verdicts() { let fut = L2Feed::new(); ...
(The CLI search command additionally prints per-component dense= / bm25= / rerank= scores for debugging; the MCP response keeps them out to save tokens.)
How it works
┌──────────────────────────────────────────────────────────────────────┐
│ Claude Code (MCP client) │
└─────────────────────────┬────────────────────────────────────────────┘
│ JSON-RPC over stdio
┌─────────────────────────▼────────────────────────────────────────────┐
│ code-search-mcp serve (one process per project) │
│ ┌──────────────────┐ ┌──────────────────────────────────────────┐ │
│ │ search loop │ │ background watcher (incremental reindex)│ │
│ └────┬─────────────┘ └────────┬─────────────────────────────────┘ │
│ │ │ │
└───────┼─────────────────────────┼────────────────────────────────────┘
│ │
▼ ▼
embed query walk + chunk + embed (changed files)
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Qdrant │ │ tantivy (local) │
│ dense vectors │ │ BM25 sparse index │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
└────────────┬─────────────┘
▼
┌──────────────────────┐
│ RRF merge │
│ + top-N rerank │
│ (bge-reranker-v2-m3)│
│ fused as rank-vote │
└──────────────────────┘
│
▼
ranked results
The indexer is incremental (sha-cached), the watcher tracks live edits, and the search is two-stage (RRF over a 30+30 candidate pool, cross-encoder over the top 30) so reranker cost is bounded regardless of corpus size. The cross-encoder's opinion is fused into the dense+sparse RRF as a weighted rank-vote rather than replacing the score — one reranker miss can't sink a candidate both retrieval modalities agree on (see [search].rerank_weight).
Requirements
You'll need three running services. The recommended setup is docker-compose on a single LAN host (or localhost on a workstation):
- Qdrant — vector DB. Any recent version. Default port
6333. - llama.cpp server running an embedding model. The reference stack uses
jina-code-embeddings-0.5b(896-dim, code-specialized). Default port7788. - llama.cpp server running a cross-encoder reranker. The recommended model is
BAAI/bge-reranker-v2-m3(8192 ctx, multilingual). Default port7799.
A Rust toolchain is needed to build the binary. MSRV is 1.88 (declared in Cargo.toml; driven by the dependency tree).
Reference docker-compose
services:
qdrant:
image: qdrant/qdrant:latest
ports: ["6333:6333"]
volumes: ["./qdrant-storage:/qdrant/storage"]
restart: always
embedding:
image: ghcr.io/ggml-org/llama.cpp:server
ports: ["7788:7788"]
volumes: ["./llm-models:/models"]
command:
["-m", "/models/jina/jina-code-embeddings-0.5-Q8_0.gguf",
"--embedding", "--pooling", "last",
"--port", "7788", "--host", "0.0.0.0",
"-c", "8192", "-ub", "2048", "--parallel", "1",
"--no-webui"]
restart: always
reranking:
image: ghcr.io/ggml-org/llama.cpp:server
ports: ["7799:7799"]
volumes: ["./llm-models:/models"]
command:
["-m", "/models/bge/bge-reranker-v2-m3-q8_0.gguf",
"--reranking",
"--port", "7799", "--host", "0.0.0.0",
"-c", "8192", "-b", "8192", "-ub", "4096",
"--parallel", "1", "--threads", "4",
"--no-webui"]
restart: always
mem_limit: 6g
The --pooling last on the embedding server is non-optional for jina-code-embeddings-0.5b (it's a Qwen3-decoder model; cls/mean pooling produces semantically broken vectors). --parallel 1 on the reranker is the right choice for memory-bandwidth-bound CPUs.
On the reranker, a cross-encoder processes each query+document pair in one pass, so the physical batch (-ub) must fit your longest truncated document — and llama.cpp silently clamps -ub to -b, so set both. -b 8192 -ub 4096 comfortably fits the default max_document_chars = 8000 (≈2000–4000 tokens depending on language). If a document still overflows, the client halves its truncation and retries automatically, at some quality cost.
Install
git clone code-search-mcp
cd code-search-mcp
cargo build --release
# binary lands at ./target/release/code-search-mcp
Configure
Create .claude/code-search.toml at the root of the project you want to make searchable. The minimal config:
[project]
id = "myproject"
root = "."
[index]
# `languages` is OPTIONAL. Omit it to index everything that isn't a known
# binary or >2 MB (zero-config, nothing silently dropped). Set it only to
# NARROW a noisy repo to a whitelist — any of: rust, python, cpp, dart,
# typescript, javascript, go, csharp, java, xml, gradle, properties, shell,
# systemd, env, text, yaml, json, markdown, toml.
respect_gitignore = true
exclude = ["target/**", "build/**", "**/*.lock"]
[embedding]
provider = "openai-compatible"
url = "http://localhost:7788/v1/embeddings"
model = "jina-code-embeddings-0.5b"
dimensions = 896
[vector_store]
provider = "qdrant"
url = "http://localhost:6333"
# `collection` intentionally unset → auto-derives a collision-safe name.
[bm25]
provider = "tantivy"
index_path = "~/.local/state/code-search-mcp/myproject/tantivy"
[reranker]
provider = "openai-compatible"
url = "http://localhost:7799/v1/rerank"
model = "bge-reranker-v2-m3"
enabled = true
[chunking]
strategy = "lines"
max_chunk_lines = 60
overlap_lines = 10
max_chunk_chars = 20000
[chunking.per_language.rust]
strategy = "tree-sitter"
max_chunk_chars = 8000
[chunking.per_language.markdown]
strategy = "headings"
max_chunk_chars = 15000
[chunking.per_language.python]
strategy = "tree-sitter"
max_chunk_chars = 8000
[watcher]
enabled = true # serve auto-spawns the watcher
debounce_ms = 500
A complete annotated example with every knob explained lives at examples/full-config.toml.
Languages
| Language | Strategy | Method qualifier | Notable | |---|---|---|---| | rust | tree-sitter | Type::method | impl_method for impl blocks | | python | tree-sitter | Class.method | @decorator preserved; imports grouped | | cpp | tree-sitter | Type::method | inline + outline methods; templates unwrapped | | dart | tree-sitter | Class.method | class methods reliable; top-level fns fall back | | typescript | tree-sitter | Class.method | export keyword preserved, interface/type/enum | | javascript | tree-sitter | Class.method | subset of TS emitter | | go | tree-sitter | Receiver.method | pointer & value receivers normalized | | csharp | tree-sitter | Class.method | namespaces descend without standalone emission | | java | tree-sitter | Class.method | classes / interfaces / enums / records / annotations | | markdown | headings | n/a | H1/H2 cuts, fenced-block aware | | toml, yaml, json | lines (default) | n/a | structured config | | shell, systemd, env, text | lines | n/a | catch-all for *.sh, *.service, *.env, *.ini, *.cfg, *.conf, *.example, *.local |
Pick the ones you actually have and add them to [index].languages.
Verify the stack
./target/release/code-search-mcp --config .claude/code-search.toml check
This pings the embedding endpoint, verifies the model's dimensions match the config, pings Qdrant, (if there's an existing index) verifies the project-identity marker, and probes the reranker with a contrastive document pair — a healthy cross-encoder must score an on-topic document above an off-topic one, which catches the "server is up but the model outputs garbage scores" failure mode.
Expected output:
INFO embedding endpoint OK model=jina-code-embeddings-0.5b dimensions=896
INFO qdrant collection does not exist yet — will be created on first `index`
INFO reranker endpoint OK model=bge-reranker-v2-m3 on_topic=2.31 off_topic=-8.7
INFO all checks passed
Build the first index
./target/release/code-search-mcp --config .claude/code-search.toml index
The first run does a full scan + embed. Subsequent runs only process changed files (via sha-cache). On modest CPU hardware expect ~5–30 minutes for the first run of a typical mid-sized repo, then seconds for incremental updates.
Try a search from the CLI
./target/release/code-search-mcp --config .claude/code-search.toml \
search "adaptive batching halving on failure" -n 5
query: adaptive batching halving on failure
results: 5 (18207 ms)
#1 src/adaptive_batcher.rs:42-89 [rust] fn AdaptiveBatcher::note_failure score=0.0648 dense=0.531 bm25=14.207 rerank=1.582
pub fn note_failure(&mut self) { self.consecutive_ok = 0; ...
#2 src/indexer.rs:445-510 [rust] fn embed_with_adaptive_batching score=0.0489 dense=0.502 rerank=0.611
...
Each hit carries its component scores: dense= / bm25= / rerank=. If rerank= is missing on every hit, the reranker failed and search fell back to RRF-only ranking — check the reranker server.
Wire to Claude Code
Copy examples/mcp.json to your project's .mcp.json at the project root (NOT inside .claude/ — Claude Code looks for it at the repo root). Edit the absolute path to the binary inside:
{
"mcpServers": {
"code-search": {
"command": "/abs/path/to/target/release/code-search-mcp",
"args": ["--config", ".claude/code-search.toml", "serve"]
}
}
}
Project-scoped MCP servers require explicit approval the first time (security against malicious repo-supplied configs):
- Run
claudeinteractively in the project root - Accept the workspace trust prompt and approve the
code-searchserver - Verify with
claude mcp list— your server should appear without the⏸ Pending approvalmarker
After approval the code_search tool appears in Claude Code's tool list. The MCP server also runs the file watcher in the background (if [watcher].enabled = true), so the index stays live as you edit.
Make Claude Code prefer code_search over Grep/Read
Tool descriptions alone don't always override Claude Code's habitual patterns (or your own auto-memory hints like "start by reading docs/INDEX.md"). To make code_search the default for any project-content query, drop a project-level CLAUDE.md at the project root — instructions there sit above MCP tool descriptions in Claude Code's instruction hierarchy:
cp examples/CLAUDE.md /path/to/your/project/CLAUDE.md
# edit the "Project-specific notes" section at the bottom to point at your own canonical files
The example tells Claude Code to use code_search BEFORE Grep / Glob / Read / Bash for any question about project content (code, docs, ROADMAPs, configs), and lists the genuine fall-back cases (known file paths, exact-bytes ops, non-indexed paths). After restart, Claude Code will reach for code_search first for exploration queries — visible as tools/call code_search entries in the MCP server's stderr logs at ~/.cache/claude-cli-nodejs//mcp-logs-code-search/.
Configuration reference
The minimal config above is enough for most setups. Optional knobs you may want to touch:
[embedding]
startup_wait_secs(default 300) — how long to wait for the server to finish loading the model at firstindex/watchstartmax_input_chars— warm-start hint for the adaptive batcher's initial budget. Defaults to 10 000 if unset; the batcher self-tunes from there
[vector_store]
collection— leave unset (auto-derive). Only set if you intentionally want to share a collection across multiple installs
[reranker]
enabled(default true) — set false if your environment doesn't have a reranker available; search falls back to RRF-only rankingmax_document_chars(default 8000) — per-doc truncation. Drop to ~4000 for ~2× faster reranks at some quality costtimeout_secs(default 120) — outer reqwest cap; the throughput-aware dynamic timeout almost always fires first
[search] (all fields optional)
dense_k,sparse_k,rerank_top_n(all default 30) — top-K from each modality + rerank ceiling. Drop to 15–20 for ~30–40% faster searches at recall costrrf_k(default 60) — Reciprocal Rank Fusion constant per Cormack et al.rerank_weight(default 2.0) — the reranker's rank-vote weight in the final fusion, relative to one retrieval modality. The cross-encoder's opinion is fused into the dense+sparse RRF as a third rank-vote rather than replacing the score outright, so a candidate both retrieval sides agree on survives a single reranker miss.0.0ranks purely by RRF (rerank scores still reported per-hit)symbol_boost(default 1.0) — extra #1 rank-vote for chunks whose AST symbol is literally named in the query (`AdaptiveBatc
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: rutmir
- Source: rutmir/code-search-mcp
- License: Apache-2.0
- Homepage: https://roex.pro
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.