Выбор semantic code search MCP для агентов на примере Cursor
Fair MCP-бенчмарк: выбор semantic code search для агентов CursorАгентам в Cursor мало одного Grep. Лексический поиск находит строки; он плохо вытаскивает нужный сервис или контроллер, когда запрос — смесь имён и намерения. Локальный semantic / hybrid MCP с search и find_related сокращает слепое чтение файлов, которое сжигает токены.
Выбор такого MCP — задача того же класса, что и раньше на реальных машинах: мерить под свои ограничения, не верить маркетинговым цифрам по RAM, потом делать cutover.
Сначала ограничения
Хост бенчмарка — не лабораторная станция:
| Параметр | Значение |
|---|---|
| CPU | Intel Core i5-6300U @ 2.40 GHz, 4 cores |
| RAM | 15 GiB |
| Клиент | Cursor MCP по stdio |
| Корпус | крупный ERP-класс monorepo для лизинговой / кредитной компании (авто, оборудование, недвижимость), multi-root со вторым API-репо |
Это отсекает тяжёлые transformer forward pass и всё, что тянет Ollama или cloud vector DB на той же машине. Рабочий класс — BM25 плюс статическое embedding в духе Model2Vec: семейство Semble, Ken и их Rust-портов.
Сам код — классический PHP / Yii monolith: одно толстое дерево приложения, а не рой мелких сервисов. Рядом с controllers, modules и services лежат годы migrations/, upload- и сгенерированные деревья под web/ (картинки, PDF, собранные ассеты) и крупные статические бандлы, которые не являются исходниками. .gitignore уже убирает vendor/, node_modules/, runtime/ и похожее из VCS — и из инструментов, которые смотрят только на gitignore. Остальной низкосигнальный объём он не отсекает. Для semantic search этот разрыв важен: каждый лишний путь — это файлы на диске для обхода, чанки для embedding, cold start и RSS. С дополнительным ignore в стиле Semble/Sonar fair-индекс на этом корпусе сошёлся примерно в ~4.3k files / ~18.6k chunks. Пути, которые отсекает только .sembleignore / .sonarignore — migrations/, web/image|assets|…, assets/reports|bonds и рядом — здесь около ~3.6k files / ~340 MiB на диске (из них ~1.1k файлов одних миграций). Без исключений шире gitignore индексатор спокойно жуёт эту массу поверх ~4.3k-файлового индекса приложения — почти удваивая обход — по деревьям, которые почти никогда не отвечают на запрос агента.
Semble я уже подключал к агентским сценариям (пост про Zoo Code). Вопрос был — оставлять ли его primary в Cursor.
Кандидаты
В scope: локальный (или self-hosted) MCP с semantic / hybrid retrieval и drop-in поверхностью search / find_related.
| Инструмент | Runtime | Роль в эксперименте |
|---|---|---|
| Semble | Python / uvx | Был primary в Cursor; baseline |
| Ken | Go | Drop-in кандидат |
| Sonar | Rust | Drop-in порт; .sonarignore |
| Veles | Rust | Drop-in + лишние symbol-tools; только gitignore |
После быстрой проверки отбросил: Bloop как search MCP (archived desktop, другой tool surface), ripgrep MCP как замену Semble (в Cursor уже есть Grep), Livegrep / Zoekt (лексический UI) и стеки с Ollama или удалённой vector DB на этом хосте.
Настройка
Ниже — достаточно, чтобы воспроизвести fair MCP-бенч у себя. Пути — плейсхолдеры (~/projects/lease-erp, ~/.local/bin). Бинарники — в PATH. После правок mcp.json перезапустите Cursor. В multi-root workspace агенты должны передавать абсолютный repo в search / find_related.
Semble (baseline)
Python / uvx. Был primary в Cursor до cutover; по-прежнему самый простой reference MCP с .sembleignore.
# нужен uv (https://docs.astral.sh/uv/)
uvx --from 'semble[mcp]' semble --help
{
"mcpServers": {
"semble": {
"command": "uvx",
"args": ["--from", "semble[mcp]", "semble", "--content", "code", "docs"],
"type": "stdio"
}
}
}
Корневой .sembleignore — синтаксис как у .gitignore (см. общий набросок ignore у Sonar).
Ken
Pure Go drop-in (townsendmerino/ken). Для бенча — hybrid: KEN_MCP_MODE=hybrid. Ignore — только gitignore (нет .kenignore).
# Go 1.26+ в PATH, либо binary с releases
go install github.com/townsendmerino/ken/cmd/ken@latest
go install github.com/townsendmerino/ken/cmd/ken-mcp@latest
# $(go env GOPATH)/bin в PATH, либо скопировать в ~/.local/bin
ken download-model # potion-code-16M (~60 MB); ken-mcp может скачать сам при первом запуске
{
"mcpServers": {
"ken": {
"command": "ken-mcp",
"env": { "KEN_MCP_MODE": "hybrid" },
"type": "stdio"
}
}
}
Warm у CLI обманчив: ken search переиндексирует на каждый вызов. Мерить нужно long-lived ken-mcp, как в Cursor.
Veles
Pure Rust (julymetodiev/Veles). Короткий путь — prebuilt installer; MCP — veles serve-mcp. Обход дерева — gitignore-only, аналога .sembleignore на этапе индекса нет.
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/julymetodiev/Veles/releases/latest/download/veles-cli-installer.sh | sh
# или: cargo install veles-cli
veles --version
# по желанию: symlink в ~/.local/bin, если установщик положил бинарник в ~/.cargo/bin
{
"mcpServers": {
"veles": {
"command": "veles",
"args": ["serve-mcp"],
"type": "stdio"
}
}
}
Индекс — в <repo>/.veles/. Для честного cold: veles clean ~/projects/lease-erp (и убрать остаток .veles/ при необходимости) перед первым MCP search.
Sonar
Pure Rust drop-in (ooboai/sonar). На момент cutover prebuilt не было — сборка через stable rustup. Есть .sonarignore (синтаксис gitignore).
# rustup stable → ~/.cargo/bin в PATH
git clone https://github.com/ooboai/sonar.git ~/src/sonar
cd ~/src/sonar && cargo build --release
install -m 755 target/release/sonar target/release/sonar-mcp ~/.local/bin/
sonar download-model # minishlab/potion-code-16M в кэш Sonar
which sonar-mcp
{
"mcpServers": {
"sonar": {
"command": "sonar-mcp",
"args": ["--content", "code", "docs"],
"type": "stdio"
}
}
}
Cursor-parity с baseline Semble — --content code docs (по умолчанию у Sonar MCP часто только code). Smoke: sonar search "SchedulePreviewService" -p ~/projects/lease-erp -k 5. Перед cold-бенчем очищайте ~/.cache/sonar.
Общая форма .sonarignore / .sembleignore для PHP/Yii ERP (пути сверх того, что уже в .gitignore — vendor/, node_modules/, runtime/ сюда не дублируем):
web/assets/
web/tmp/
web/upload/
web/pdf/
web/docs/
web/image/
assets/reports/
assets/bonds/
migrations/
tests/_output/
tests/_support/_generated/
Метод: MCP, не CLI
Я мерил долгоживущий MCP stdio — один процесс на сервер, cold первый search, затем warm-повторы. Метрики: cold Q1, warm Q1 median (5×), idle RSS / peak HWM, top-1 path на трёх фиксированных запросах и ignore-parity — возможность исключать пути шире .gitignore, как у .sembleignore. На PHP/Yii ERP-монолите это не «приятный бонус», а то, что держит индекс на коде приложения, а не на миграциях и массе под web/.
Одноразовый CLI здесь ловушка. CLI Ken переиндексирует на каждый вызов, поэтому «warm» CLI — не proxy для Cursor. Честное сравнение чистит кэши перед cold, а для Sonar использует .sonarignore — копию бывшего .sembleignore.
Cursor-parity у Sonar: --content code docs (как у Semble в mcp.json). Ранний прогон только с code был близко; каноническим считаю rebench с code docs.
Чего не мерил: многодневный churn индекса, глубину tree-sitter для PHP, и каждый non-drop-in MCP с другим контрактом tools.
Каркас бенчмарка
Fair-прогон — один долгоживущий MCP-процесс на сервер: очистить кэш индекса, cold search, затем warm-повторы, параллельно снимая RSS из /proc. Пути ниже — плейсхолдеры; --repo указывайте на своё дерево.
# Cursor-parity режимы content; бинарники в PATH
export BENCH_REPO="$HOME/projects/lease-erp"
uvx --with mcp --from mcp python bench_fair_mcp.py \
--repo "$BENCH_REPO" --order ken-first
uvx --with mcp --from mcp python bench_fair_rust_ports.py \
--repo "$BENCH_REPO" --servers sonar --sonar-content "code docs"
Ядро цикла замера (Python MCP client SDK):
REPO = os.environ.get("BENCH_REPO", str(Path.home() / "projects" / "lease-erp"))
QUERIES = [
"SchedulePreviewService preview",
"ReferenceCacheService",
"actionPreview schedule leasing",
]
TOP_K = 5
async def call_search(session, query: str) -> float:
t0 = time.perf_counter()
await session.call_tool(
"search",
arguments={"query": query, "repo": REPO, "top_k": TOP_K},
)
return time.perf_counter() - t0
async def measure(command: str, args: list[str]) -> dict:
# перед cold — сбросить disk cache этого сервера (зависит от инструмента)
params = StdioServerParameters(command=command, args=args)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
cold_s = await call_search(session, QUERIES[0])
warm = [await call_search(session, QUERIES[0]) for _ in range(5)]
return {
"cold_s": cold_s,
"warm_q1_median_s": statistics.median(warm),
}
RSS / peak HWM берутся из /proc/<pid>/status воркера (VmRSS, VmHWM), с исключением уже живущего Cursor MCP в IDE. В этом и отличие от one-shot CLI, который переиндексирует на каждый вызов.
Результаты
Корпус: тот же ERP-класс monorepo лизинга / кредитования. Ken и Semble — fair MCP от 2026-07-17; Sonar / Veles — 2026-07-18 (колонка Sonar = code docs).
| Метрика | Ken | Semble | Sonar | Veles |
|---|---|---|---|---|
| Cold Q1 | 256.6 s | 71.5 s | 19.5 s | 15.6 s |
| Warm Q1 median | 43 ms | 448 ms | 38 ms | 7 ms |
| Idle RSS | ~2.1 GB | ~0.63 GB | ~0.66 GB | ~0.52 GB |
| Peak HWM | ~2.9 GB | ~0.68 GB | ~0.84 GB | ~0.52 GB |
| Ignore parity | нет | да (.sembleignore) | да (.sonarignore) | gitignore-only |
Top-1 на трёх запросах: все четыре попали в ожидаемые PHP-сервисы на Q1/Q2. На NL-запросе Q3 (actionPreview schedule leasing) все четыре промахнулись в один и тот же неверный контроллер — паритет ошибки, не победа Sonar.
Smoke dual-MCP (тёплый Semble в Cursor плюс cold-старт Sonar) дал суммарный idle около ~1.1 GB. Dual на 15 GiB жизнеспособен; после cutover он не понадобился.
Заявки vs замер
| Утверждение (маркетинг / обзоры) | На этом корпусе | Вердикт |
|---|---|---|
| Semble RAM «~100–300 MB» | idle ~633 MB | Занижено для крупного репо; порядок суб-GB верен |
| Ken «extremely low» RAM | idle ~2.1 GB, peak ~2.9 GB | Не подтверждено — тяжелее Semble |
| Warm search «дешёвый» | Ken/Sonar десятки ms; Semble ~0.45 s | Warm у Ken/Sonar ок; UX ломает cold |
Решение
На хосте класса i5 / ≤16 GiB и крупном monorepo важнее cold start, RAM и контроль ignore, чем самый быстрый warm-миллисекунд.
18 июля 2026 я перевёл primary в Cursor на Sonar (sonar-mcp --content code docs).
- Sonar — cold примерно в 3.5× быстрее Semble, warm примерно в 10×, RAM в той же полосе, плюс
.sonarignoreдля ignore-parity. - Veles — лучшие сырые cold/warm/RSS в таблице, но только gitignore. На этом monorepo это не покрывает
migrations/, сгенерированные ассеты и прочие пути вне.gitignore. Не primary. - Ken — rejected: cold ~257 s, RSS ~2–3 GB, нет ignore-файла; маркетинговый RAM не совпал с замером.
- Semble — снят с primary в Cursor; остаётся полезной точкой отсчёта по контракту tools и модели ignore.
Non-drop-in semantic MCP (другой tool surface, knowledge graph, multi-repo index) — не primary, пока нет отдельной потребности.
Вывод
Для агентов Cursor на стеснённых ноутбуках не выбирайте code-search MCP по скриншотам warm-ms. Прогоните fair long-lived MCP-бенч на своём корпусе, проверьте ignore-parity и не путайте CLI-цифры с продуктом для Cursor.
На этом хосте процесс выбрал Sonar — не потому что он выиграл каждую ячейку, а потому что выиграл те, от которых зависит, останется ли агент usable после cold start.

Архитектор ПО, Архитектор данных
Опытный разработчик ПО с опытом работы в стартапах, банках и отраслях вроде космоса и железных дорог.
- Инженер Go, Python, C++, C с 2006 года.
- Последние 3 года: инженерия платформ, создание внутренних порталов разработчиков (IDP) и сдвиг организаций влево в DevOps.
- Проектировал и строил автономные и клиент-серверные приложения с базами Oracle DB, PostgreSQL и MySQL.
- Разрабатывал CRM-системы, веб-автоматизированную обработку заказов и симуляции для эксплуатации железнодорожного подвижного состава.