Cirurgic Search MCP
README.md
# π¬ Cirurgic Search MCP
[](https://www.npmjs.com/package/cirurgic-search-mcp)
[](https://www.npmjs.com/package/cirurgic-search-mcp)
[](https://opensource.org/licenses/MIT)
A token-bounded, read-only code search engine exposed as a
[Model Context Protocol](https://modelcontextprotocol.io) server.
It combines **exact lexical recall** (ripgrep) with **relational AST symbols**
(Tree-Sitter, with a memory-safe Python `ast` path) and fuses both rankings with
**Reciprocal Rank Fusion (RRF)**. The result is a small, high-signal payload:
whole functions and classes with line ranges, call metadata and the exact
literal matches that led there β never a raw directory dump.
```bash
# Run universally via NPX (Node/Bun):
npx -y cirurgic-search-mcp
# Or via UVX / Pip (Python):
uvx cirurgic-search-mcp
pip install cirurgic-search-mcp
```
---
## The problem: context dumping
An agent asked to "find where billing is calculated" usually reaches for
`cat`, `grep -r`, or a full-file reader. That approach breaks down quickly:
- **Context flooding.** A single `grep -r` over a monorepo can return tens of
thousands of lines, exhausting the context window before reasoning starts.
- **Lost structure.** A matching line without its enclosing function, class,
decorators and call graph is hard to act on safely.
- **Truncation roulette.** When the payload is naively truncated, the useful
block is often the one that gets cut.
- **Stale indexes.** Semantic indexes are expensive to build and drift as soon
as a file changes; agents need answers about the *current* working tree.
- **No budget.** Most tools cannot promise "use at most N tokens".
Surgical search flips the default: instead of maximizing **recall per call**,
it maximizes **useful signal per token**.
---
## Why not plain ripgrep or BM25?
| Capability | ripgrep alone | BM25 / chunk index | Cirurgic Search |
|---|---|---|---|
| Exact literal recall | Yes, excellent | Tokenizer-dependent | Yes (ripgrep lane) |
| Whole symbol boundaries | No | No (fixed chunks) | Yes (`ast` / Tree-Sitter) |
| Call graph / base classes | No | No | Yes (`calls:`, `bases:`) |
| Works on uncommitted edits | Yes | Only after reindex | Yes (live filesystem walk) |
| Hard token ceiling | No | Usually no | Yes (strict guard) |
| Cold start on a large repo | Fast | Index build required | Bounded walk + candidate ceiling |
| Background daemon | No | Often | No |
Pure lexical search has perfect precision on literals but no notion of a
function. Pure semantic search handles paraphrases but loses exact identifiers,
line numbers and freshness. Fusing both covers the failure mode of each.
---
## Architecture
```
βββββββββββββββββββββββββββββββ
query βββββββββββΆβ HybridSearchEngine β
ββββββββββββββββ¬βββββββββββββββ
β
ββββββββββββββββββββββ΄βββββββββββββββββββββ
β β
ββββββββββββΌβββββββββββ βββββββββββββΌββββββββββββ
β Lexical lane β β Symbol lane β
β ripgrep / grep β β Python ast β
β literal unions β β Tree-Sitter grammars β
β match counts β β relation extraction β
ββββββββββββ¬βββββββββββ βββββββββββββ¬ββββββββββββ
β file ranking β file ranking
ββββββββββββββββββ¬βββββββββββββββββββββββββ
βΌ
Reciprocal Rank Fusion (RRF)
β
βΌ
Macro/micro packaging + Token Guard
β
βΌ
snippets + line ranges + relations + exact matches
```
### 1. Lexical lane
`ripgrep` runs with literal-union patterns (the full query plus its significant
words), skips vendor/build directories and groups matches by file. A POSIX
`grep` fallback keeps the server usable when `rg` is not installed. This lane is
what catches config files, lockfiles and strings that no parser understands.
### 2. Symbol lane
Python files are parsed with the standard-library `ast` module, which is
memory-safe and gives exact ranges **including decorators**. Other languages are
parsed with Tree-Sitter when a grammar bundle is available:
`JavaScript`, `TypeScript`, `TSX`, `Go`, `Rust`, `Dart`, `Java`, `Kotlin`,
`C#`, `Ruby`, `PHP`, `Swift`, `C`, `C++` and `Bash`.
Every extracted block carries relational metadata:
```
type:function | name:calculate_total
calls:parse_amount,apply_tax
type:class | name:BillingService | bases:BaseService
```
The extractor degrades gracefully: Tree-Sitter β regex (JS/TS) β lexical
chunks. A missing grammar never aborts a search.
### 3. Reciprocal Rank Fusion
Each lane ranks files independently. Fusion uses the standard RRF formula:
```
score(file) = Ξ£ weight_lane / (k + rank_lane(file))
```
with `k = 60` by default, `weight_lexical = 1.0` and `weight_symbol = 1.2`.
RRF needs no score calibration between lanes: it only consumes ranks, so a
strong signal from either side promotes the file.
### 4. Macro/micro packaging and the Token Guard
After fusion, the engine iterates files in RRF order. For each file it emits the
highest-scoring AST blocks (micro) and attaches any exact lexical matches that
fall inside the block's line range (macro). If a file has no parsed symbol, a
small literal preview is returned instead.
Before anything is appended, its estimated size is checked against the remaining
budget. The budget is a **hard ceiling**, not a suggestion: the first oversized
snippet is trimmed to fit and subsequent snippets that do not fit are dropped.
Token counts use a conservative `chars / 3.8` estimator.
---
## Installation
Requirements: Python 3.10+ on Linux or macOS. `ripgrep` is recommended for the
fast lane; without it the server falls back to POSIX `grep`.
```bash
# Ephemeral execution, no install step
uvx cirurgic-search-mcp
# Regular installation
pip install cirurgic-search-mcp
# Recommended: maintained grammar bundle with wider language coverage
pip install "cirurgic-search-mcp[language-pack]"
```
The base install uses `tree-sitter-languages` on Python < 3.13 and
`tree-sitter-language-pack` on newer interpreters. Installing the
`language-pack` extra explicitly is recommended on every version.
---
## MCP client configuration
With `uvx`:
```json
{
"mcpServers": {
"cirurgic-search": {
"command": "uvx",
"args": ["cirurgic-search-mcp"]
}
}
}
```
With a regular installation:
```json
{
"mcpServers": {
"cirurgic-search": {
"command": "cirurgic-search-mcp"
}
}
}
```
The server speaks stdio by default. Other transports are available for local
experimentation:
```bash
cirurgic-search-mcp --transport sse
cirurgic-search-mcp --transport streamable-http
```
---
## Tool reference
### `cirurgic_search`
| Argument | Type | Default | Description |
|---|---|---|---|
| `query` | string | required | Identifier, technical term or natural-language question. |
| `workspace` | string | current cwd | Repository root. Must be an existing directory. |
| `scope` | string | `""` | Comma-separated directories, packages or files to restrict the walk. |
| `max_tokens` | integer | `1200` | Hard ceiling for the returned payload. |
| `depth` | integer | `2` | Relational depth reserved for symbol expansion. |
Example response:
```json
{
"status": "success",
"workspace": "/path/to/repo",
"latency_ms": 18.4,
"total_tokens": 742,
"token_savings_pct": 97.8,
"snippets_count": 2,
"files_found": 4,
"candidates_scanned": 312,
"candidates_truncated": false,
"snippets": [
{
"file": "billing/service.py",
"lines": "42-67",
"rrf_score": 0.0284,
"relations": [
"type:function | name:calculate_total",
"calls:parse_amount,apply_tax"
],
"exact_matches": [
{"line": 51, "text": "# rule: daily rate for ICU"}
],
"content": "def calculate_total(...):\n ...",
"source": "hybrid"
}
]
}
```
`source` is `hybrid` when the file was found by both lanes, `ast` when it came
from the symbol lane only, and `lexical` for literal-only files.
### `qmd_search`
Backwards-compatible alias of `cirurgic_search`. It is registered with the exact
same coroutine object and schema, so the two names cannot drift. New clients
should call `cirurgic_search`; existing clients keep working unchanged.
---
## Environment variables
| Variable | Default | Purpose |
|---|---|---|
| `CIRURGIC_RG_PATH` | auto-detected | Explicit path to a ripgrep binary. |
| `CIRURGIC_GREP_TIMEOUT` | `5.0` | Subprocess timeout in seconds for the lexical lane. |
| `CIRURGIC_MAX_CANDIDATE_FILES` | `800` | Upper bound on files walked per search. Narrow `scope` when hit. |
| `CIRURGIC_TS_EXTRACT` | `1` | Set to `0` to disable Tree-Sitter and use regex/lexical fallbacks only. |
---
## Design principles
- **Read-only.** The server never writes to the workspace, spawns no daemon and
makes no network calls.
- **Fresh by construction.** Every search walks the current working tree, so
uncommitted edits are visible immediately.
- **Fail-soft.** Missing binaries, missing grammars, unreadable files and parse
errors degrade to the next lane instead of failing the request.
- **Memory-safe Python path.** Python always uses `ast`, never a native binding.
- **Bounded work.** Candidate files, AST nodes and returned tokens are all
capped, protecting latency and memory on pathological repositories.
- **No hidden state.** Results are deterministic for a given workspace, query
and budget.
---
## Development
```bash
python -m venv .venv
. .venv/bin/activate
pip install -e ".[dev]"
python -m pytest
```
The deterministic suite uses real temporary directories and real `rg`/`grep`
subprocesses. Tests that require an optional grammar skip cleanly when that
grammar is not installed.
---
## Limitations
- Ranking is lexical/AST-based, not embedding-based. It favors exact
identifiers, symbol names and structural proximity over paraphrase.
- Very large repositories should be searched with a narrow `scope`; the
candidate ceiling exists to protect latency, not to index the world.
- Tree-Sitter coverage depends on the installed grammar bundle. Python works
everywhere; other languages fall back to regex or lexical chunks.
- Symbolic links are not followed and binary files are ignored.
- `depth` is reserved for relational expansion; the current release ranks
symbols by name, relations and content and does not traverse the full call
graph transitively.
---
## License
MIT. See [LICENSE](LICENSE).
This server cannot be deployed