# Code Search Mcp

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

- **Type:** MCP server
- **Install:** `agentstack add mcp-rutmir-code-search-mcp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [rutmir](https://agentstack.voostack.com/s/rutmir)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [rutmir](https://github.com/rutmir)
- **Source:** https://github.com/rutmir/code-search-mcp
- **Website:** https://roex.pro

## Install

```sh
agentstack add mcp-rutmir-code-search-mcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

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

```jsonc
// 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](https://qdrant.tech/)** — vector DB. Any recent version. Default port `6333`.
- **[llama.cpp server](https://github.com/ggml-org/llama.cpp)** running an embedding model. The reference stack uses [`jina-code-embeddings-0.5b`](https://huggingface.co/jinaai/jina-code-embeddings-0.5b) (896-dim, code-specialized). Default port `7788`.
- **[llama.cpp server](https://github.com/ggml-org/llama.cpp)** running a cross-encoder reranker. The recommended model is [`BAAI/bge-reranker-v2-m3`](https://huggingface.co/BAAI/bge-reranker-v2-m3) (8192 ctx, multilingual). Default port `7799`.

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

```yaml
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

```bash
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:

```toml
[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

```bash
./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

```bash
./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

```bash
./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:

```json
{
  "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:

```bash
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.

- **Author:** [rutmir](https://github.com/rutmir)
- **Source:** [rutmir/code-search-mcp](https://github.com/rutmir/code-search-mcp)
- **License:** Apache-2.0
- **Homepage:** https://roex.pro

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

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-rutmir-code-search-mcp
- Seller: https://agentstack.voostack.com/s/rutmir
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
