AgentStack
MCP verified Apache-2.0 Self-run

Code Search Mcp

mcp-rutmir-code-search-mcp · by rutmir

Semantic code-search MCP server for AI coding agents: hybrid retrieval (embeddings + BM25 + reranking), tree-sitter AST chunking, 30-100x fewer tokens

No reviews yet
0 installs
4 views
0.0% view→install

Install

$ agentstack add mcp-rutmir-code-search-mcp

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

Are you the author of Code Search Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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_failure headers, not raw line ranges
  • Code-aware matching — the BM25 tokenizer splits snake_case / camelCase identifiers (buildSiPortfolio finds build_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):

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):

  1. Run claude interactively in the project root
  2. Accept the workspace trust prompt and approve the code-search server
  3. Verify with claude mcp list — your server should appear without the ⏸ Pending approval marker

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 first index/watch start
  • max_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 ranking
  • max_document_chars (default 8000) — per-doc truncation. Drop to ~4000 for ~2× faster reranks at some quality cost
  • timeout_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 cost
  • rrf_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.0 ranks 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.