Выбор semantic code search MCP для агентов на примере Cursor

июл. 18, 2026 · 8 мин чтения
Fair MCP-бенчмарк: выбор semantic code search для агентов Cursor
blog AI coding

Агентам в Cursor мало одного Grep. Лексический поиск находит строки; он плохо вытаскивает нужный сервис или контроллер, когда запрос — смесь имён и намерения. Локальный semantic / hybrid MCP с search и find_related сокращает слепое чтение файлов, которое сжигает токены.

Выбор такого MCP — задача того же класса, что и раньше на реальных машинах: мерить под свои ограничения, не верить маркетинговым цифрам по RAM, потом делать cutover.

Сначала ограничения

Хост бенчмарка — не лабораторная станция:

ПараметрЗначение
CPUIntel Core i5-6300U @ 2.40 GHz, 4 cores
RAM15 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 / .sonarignoremigrations/, 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Роль в эксперименте
SemblePython / uvxБыл primary в Cursor; baseline
KenGoDrop-in кандидат
SonarRustDrop-in порт; .sonarignore
VelesRustDrop-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 (пути сверх того, что уже в .gitignorevendor/, 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).

МетрикаKenSembleSonarVeles
Cold Q1256.6 s71.5 s19.5 s15.6 s
Warm Q1 median43 ms448 ms38 ms7 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» RAMidle ~2.1 GB, peak ~2.9 GBНе подтверждено — тяжелее Semble
Warm search «дешёвый»Ken/Sonar десятки ms; Semble ~0.45 sWarm у 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-системы, веб-автоматизированную обработку заказов и симуляции для эксплуатации железнодорожного подвижного состава.