Skip to main content
Glama
adborroto

semantic-search-mcp

by adborroto

semantic-search-mcp

CI Security npm node License: MIT

Миниатюрный, самодостаточный движок извлечения RAG-lite: он индексирует файлы на диске и отвечает на вопрос «что семантически релевантно этому запросу» — и ничего больше. Он не вызывает LLM и не генерирует ответы. Он возвращает наиболее релевантные текстовые фрагменты (файл, строка, оценка), чтобы тот, кто их потребляет — человек, скрипт или LLM через MCP — сам решал, что с ними делать.

Всё работает локально и офлайн после первого запуска:

  • Эмбеддинги: @huggingface/transformers с моделью Xenova/all-MiniLM-L6-v2 в int8-квантованных весах на CPU. Ни GPU, ни API-ключа, ни сетевых вызовов во время запроса.

  • Векторное хранилище: @lancedb/lancedb — встраиваемая, файловая векторная база данных. Ни серверного процесса, ни Docker.

  • Интерфейсы: CLI и stdio-сервер MCP, так что любой MCP-совместимый агент (Claude Code, Cursor, Zed, …) может искать по вашему корпусу напрямую.

Быстрый старт

npm install -g @adborroto/semantic-search-mcp

semantic-search add ~/code/my-project      # add a folder to the corpus
semantic-search index                      # embed it (incremental on later runs)
semantic-search search "how does the retry logic work"

Вот и вся настройка. Никакого конфигурационного файла, который нужно писать вручную — add создаёт и управляет им за вас. Чтобы попробовать без установки:

npx @adborroto/semantic-search-mcp add ~/code/my-project

Важно о размере установки: ~950 МБ зависимостей, плюс модель эмбеддингов (~25 МБ) загружается при первом использовании. Почти всё это — нативные бинарные файлы, которых на этом уровне не избежать — @lancedb/lancedb (~430 МБ, включая платформенный бинарник) и среда выполнения ONNX (~300 МБ, которая поставляет сборки для всех платформ в одном пакете). Оба кэшируются один раз; всё после первого запуска работает офлайн.

Related MCP server: rag-retriever-mcp

Требования

  • Node.js >= 22 (node:sqlite, используемый в резервном бэкенде, стабилен только с 22 версии).

  • ~950 МБ на диске для зависимостей и ~25 МБ для модели эмбеддингов, плюс примерно 1–3 КБ на каждый проиндексированный фрагмент.

  • Ни GPU, ни внешних сервисов, ни сервера баз данных.

Почему «RAG-lite»

Полный пайплайн RAG: извлечение фрагментов → передача их LLM → LLM пишет ответ. Этот проект останавливается на первом шаге. Это делает его простым, быстрым, дешёвым в работе и лёгким для понимания — и он чисто компонуется с любым LLM или фреймворком агентов, который вы уже используете, вместо того чтобы включать собственный слой генерации.

Управление корпусом

semantic-search add ~/code/api ~/notes     # add one or more folders
semantic-search list                       # show what's configured
semantic-search remove api                 # by folder name...
semantic-search remove ~/notes             # ...or by path
semantic-search config                     # where config + index actually live

add проверяет, что каждый путь является реальной директорией, преобразует его в абсолютный путь и пропускает дубликаты (включая одну и ту же директорию, достигнутую через симлинк). remove также удаляет фрагменты этой папки из индекса, так что её содержимое перестаёт появляться в результатах — передайте --keep-index, если хотите удалить её из корпуса, но оставить доступной для поиска.

Где хранятся данные

Конфигурация и индекс следуют спецификации базовых директорий XDG, поэтому они переживают обновления и используются всеми способами установки:

Что

Расположение

Конфигурация

~/.config/semantic-search/config.json

Индекс + кэш модели

~/.local/share/semantic-search/

Любой из этих путей можно переопределить с помощью SS_CONFIG_PATH, SS_INDEX_DIR, SS_MODEL_CACHE_DIR или стандартных XDG_CONFIG_HOME / XDG_DATA_HOME. SS_STORE_BACKEND=sqlite принудительно включает резервный бэкенд.

Индекс содержит дословный текст всего, что вы проиндексировали. Если вы укажете на приватный код, ~/.local/share/semantic-search/ будет хранить этот контент в открытом виде. Никогда не коммитьте его и не прикрепляйте к отчёту об ошибке.

Каждая опция документирована в src/config.js — размер фрагментов, шаблоны игнорирования, имя модели, top-k, конкурентность. Редактирование config.json напрямую по-прежнему работает для них; add/remove сохраняют любые ключи, которые им не принадлежат.

Использование

Индексация

semantic-search index                      # all configured folders
semantic-search index ~/code/one-project   # just this folder, ignoring config
semantic-search index --force              # reprocess everything

Индексация инкрементальная: неизменённые файлы пропускаются по времени модификации, файлы, чьё содержимое на самом деле не изменилось (только время доступа), пропускают повторное эмбеддинг, а файлы, удалённые с диска, удаляются из индекса. Обрабатывается только то, что действительно изменилось.

С несколькими настроенными папками index обходит их последовательно с заголовком для каждой папки и общим итогом:

[1/3] my-api  /home/me/code/my-api  ─────────────────────────────
  ↺ indexed   src/auth/middleware.js  (8 chunks)
  2 indexed  1,203 skipped  16 chunks  4.1s

[2/3] my-app  /home/me/code/my-app  ─────────────────────────────
  ...

──────────────────────────────────────────────────────────────
total  5 indexed  3,891 skipped  0 deleted  41 chunks  12.3s

Каждый вызов index <path> удаляет устаревшие записи только для файлов внутри этого пути, поэтому индексация папки B никогда не затрагивает записи папки A.

Полезные флаги: --max-files <n> останавливается после N новых файлов (ограничивает память для огромных корпусов), --concurrency <n> задаёт параллелизм, --verbose выводит каждый файл в stderr.

Поиск

semantic-search search "how does the retry logic work" -k 5

Выводит таблицу с путём к файлу, номером строки, оценкой и текстовым превью.

Поиск гибридный: запрос отправляется двум независимым веткам — векторному поиску по эмбеддингам и полнотекстовому поиску BM25 по тем же фрагментам — и два ранжирования объединяются с помощью Reciprocal Rank Fusion. Ветки терпят неудачу по-разному: векторная ветка пропускает точные идентификаторы, строки ошибок и ключи конфигурации, для которых у неё нет семантической привязки; лексическая ветка пропускает перефразировки. Запуск обеих — это исправление полноты, а слияние по рангу, а не по оценке, не позволяет неограниченной оценке BM25 заглушить косинусное сходство.

Установите "hybridSearch": false в config.json для поиска только по векторам и "rrfK" для настройки константы сглаживания ранга RRF (по умолчанию 60, из статьи).

Что индексируется

Укажите папку, и всё внутри неё индексируется рекурсивно. Нет белого списка «поддерживаемых» расширений файлов — .dart, .kt, .java, .tsx, .sql, .erb и всё остальное текстовое индексируется как есть, а .pdf и .docx сначала проходят через парсер.

Исключаются четыре вещи:

  1. Всё, что игнорирует git, если папка является git-репозиторием. .gitignore учитывается на любой глубине, а также .git/info/exclude, ваш глобальный файл исключений и шаблоны отрицания (!keep.this). Это делегируется git ls-files, а не перереализуется, поэтому точно соответствует git — это означает, что сгенерированные и вендорные выходные данные, которые ваш проект уже игнорирует, не попадают в индекс без необходимости вести второй список.

  2. Ваши правила .indexignore (см. ниже) для контента, который закоммичен, но не должен быть доступен для поиска — фикстуры, снимки, шаблон секретов.

  3. Бинарные файлы — по расширению (изображения, архивы, шрифты, скомпилированные объекты, веса моделей) и по содержимому — нулевой байт в первых 4 КБ означает бинарный файл, та же эвристика, что использует grep -I. Это мера безопасности, чтобы не допустить не текстовые байты в токенизатор, а не суждение о том, что стоит индексировать.

  4. Файлы размером более 500 000 байт (maxFileSizeBytes), что является основной защитой от того, чтобы сгенерированный однострочный мегабайтный файл не исчерпал память.

Симлинки пропускаются, а не обрабатываются, поэтому ссылка, помещённая внутрь папки, не может привлечь внешний контент в индекс.

Для папок, которые не являются git-репозиториями, нет .gitignore, на который можно опереться, поэтому применяется небольшой встроенный список (node_modules/, .git/, dist/, build/, coverage/, vendor/, …).

Чтобы исключить больше, поместите .indexignore в стиле gitignore в одном из двух мест:

  • внутри папки, которую вы индексируете — шаблоны относительны этой папки;

  • рядом с вашей конфигурацией (~/.config/semantic-search/.indexignore) — применяется везде.

Смотрите .indexignore.example для начального набора, охватывающего артефакты сборки iOS, Android, Flutter, Ruby и JVM.

MCP-сервер

semantic-search mcp

Запускает stdio-сервер MCP, предоставляющий шесть инструментов.

search(query, k?) — семантический поиск, возвращает сырой JSON:

[{ filePath, text, score, offset, startLine }, ...]

gather(query, k?, contextLines?) — тот же поиск, возвращается в виде одного отформатированного markdown-блока, готового для вставки в контекстное окно:

### [1/5]  my-api  ·  src/auth/session.js  ·  line 42  ·  score 0.923
```
...chunk text...
```

contextLines (по умолчанию 0) читает N дополнительных строк вокруг каждого фрагмента из исходного файла — полезно, когда граница фрагмента обрезает необходимый контекст.

list_folders() — все настроенные папки с их именем и абсолютным путём. Хороший первый вызов, чтобы агент знал, какой корпус существует.

cat_file(filePath, startLine?, endLine?) — чтение файла по абсолютному пути, как возвращается search/gather. Ограничено настроенными папками (см. Безопасность).

grep(pattern, folder?, fileGlob?, caseSensitive?, maxResults?) — буквальный или regex-поиск по корпусу для случаев, когда нужны точные совпадения, а не сходство. Фильтруется по тому же списку файлов, который использовал бы индексатор, поэтому файлы, проигнорированные git и .indexignore, не могут просочиться через точный поиск.

my-api  ·  src/auth/session.js:42  export function createSession(user) {

index(root?, force?, maxFiles?, concurrency?) — запуск инкрементальной переиндексации, чтобы агент мог обновить корпус без вызова внешней команды.

Все инструменты поиска используют тот же код ранжирования и разрешения файлов, что и CLI; ни один не перереализует его.

Регистрация в MCP-клиенте

Claude Code:

claude mcp add --scope user semantic-search -- semantic-search mcp
claude mcp list   # should show "✔ Connected"

Любой клиент, принимающий JSON-определение сервера:

{
  "mcpServers": {
    "semantic-search": {
      "command": "semantic-search",
      "args": ["mcp"]
    }
  }
}

Предпочтительнее глобальная установка, а не npx: простой npx заново разрешает пакет при каждом запуске сервера, добавляя задержку при запуске и незаметно подхватывая обновления. Если вы используете npx, зафиксируйте версию — npx -y @adborroto/semantic-search-mcp@0.1.0 mcp.

Новые MCP-серверы обычно подхватываются только при запуске сессии, поэтому начните новую сессию после регистрации.

Безопасность

Это локальный, однопользовательский инструмент с простой моделью доверия: всё, что находится внутри настроенной папки, доступно для чтения любому MCP-клиенту, который может достичь сервера.

  • cat_file отказывает в доступе к путям за пределами настроенных папок, сначала разрешая симлинки, чтобы ссылка, помещённая внутрь папки, не могла быть использована для выхода за её пределы.

  • grep фильтруется по тому же списку файлов, который строит индексатор — правилам игнорирования git плюс ваш .indexignore — поэтому файлы, намеренно исключённые из индексации, не просачиваются через точный поиск.

  • Дочерние процессы запускаются с массивами argv (никогда не через оболочку), поэтому шаблоны не могут внедрять команды.

Учитывая это, не указывайте корпус, который вы бы не доверили своему LLM-провайдеру — фрагменты возвращаются тому клиенту, который их запросил. См. SECURITY.md.

Как это работает

Обнаружение файлов

Правило: «индексировать всё внутри папки», и единственная интересная часть — это то, что не нужно индексировать. Вместо того чтобы перереализовывать семантику игнорирования git — вложенные файлы .gitignore, отрицания, info/exclude, глобальный файл исключений — корень git перечисляется с помощью:

git ls-files -z --cached --others --exclude-standard

Отслеживаемые файлы плюс неотслеживаемые, но не игнорируемые, в пределах директории, в которой он запускается. Всё, что игнорирует git, отсутствует по построению. Папки, не являющиеся git-репозиториями, возвращаются к простому рекурсивному обходу со встроенным списком шаблонов.

Та же функция используется как в индексаторе, так и в инструменте MCP grep (src/ignoreRules.js). Это сделано намеренно: grep вызывает внешний grep -r, который с радостью сообщает о совпадениях внутри проигнорированных git результатов сборки, поэтому он фильтрует свои результаты по собственному списку файлов индексатора. Если бы они выводили свои правила отдельно, они бы расходились, и список игнорирования перестал бы быть границей.

Гибридный поиск

Запрос проходит через две ветки параллельно:

  • Векторная — эмбеддинг запроса, поиск ближайших соседей по косинусному расстоянию, затем переупорядочивание этого списка с небольшим лексическим повышением для фрагментов, содержащих буквальные термины запроса.

  • Лексическая — BM25 по тому же тексту фрагментов, через полнотекстовый индекс LanceDB (резервный sqlite вычисляет BM25 в JS, так как node:sqlite не гарантирует поставку FTS5).

Два ранжирования объединяются с помощью RRF: каждый список добавляет 1 / (60 + rank) ко каждому чанку, который он возвращает, и эти вклады суммируются. Слияние по рангу, а не по оценке, — в этом суть: косинус лежит в [-1, 1], тогда как BM25 не ограничен сверху, поэтому сложение или усреднение сырых оценок позволяет одному рычагу незаметно подавить другой в зависимости от размера корпуса.

Зачем вообще два рычага: лексический boost, применённый к выходу векторного рычага, может только переупорядочить то, что уже вернул векторный запрос. Чанк, единственным сигналом которого является точное совпадение термина — код ошибки, имя символа, ключ конфигурации без семантического окружения, — был недоступен, если он оказался за пределами векторного пула. Лексический рычаг извлекает его независимо. Это исправление полноты (recall), а не переранжирование, и именно поэтому оценки поиска теперь выглядят как 0.03, а не 0.9: это суммы RRF, а не косинусные сходства. Значим только их порядок.

Полнотекстовый индекс перестраивается в конце каждого цикла индексации, потому что FTS-индекс не охватывает строки, добавленные после его построения, — иначе чанки, только что записанные в ходе цикла, были бы невидимы для лексического рычага.

Разбиение на чанки

Текст разбивается на абзацы, затем жадно упаковывается в чанки примерно по 200 токенов с перекрытием ~35 токенов, подсчитанных с использованием реального токенизатора модели эмбеддингов, а не приближения по числу символов. Это не произвольно: all-MiniLM-L6-v2 имеет окно 256 токенов и молча обрезает всё, что длиннее, поэтому чанки подгоняются так, чтобы поместиться внутрь с запасом для токенов [CLS]/[SEP]. Перекрытие дополнительно ограничено так, чтобы перекрытие плюс следующий абзац никогда не могли превысить этот лимит — иначе хвост чанка был бы отброшен при создании эмбеддинга, но всё равно возвращался бы при поиске.

Один абзац, превышающий жёсткий лимит (минифицированный бандл, одна гигантская строка лога), переключается на упаковку по словам с той же логикой перекрытия, а любое отдельное «слово» длиннее 500 символов сначала разрезается, чтобы ничто слишком большое не передавалось токенизатору за один раз.

Количество токенов вычисляется один раз для каждого абзаца/слова и кэшируется для повторного использования при расчёте перекрытия. В более ранней версии токенизация выполнялась заново при каждом поиске перекрытия, что было приемлемо на небольших входных данных, но приводило к неконтролируемому росту загрузки CPU и памяти (гигабайты) на больших репозиториях. Если вы расширяете чанкер, сохраните это свойство.

Инкрементальная переиндексация

Отдельного манифеста нет — векторное хранилище и есть манифест. Каждый сохранённый чанк несёт mtimeMs исходного файла и хеш содержимого sha256. При каждом запуске:

  1. Если mtime файла на диске совпадает с сохранённым, пропустить его, не читая файл.

  2. Если mtime изменился, но хеш содержимого идентичен (команда touch), пропустить пересоздание эмбеддинга.

  3. В противном случае удалить старые чанки этого файла и вставить новые, только что созданные.

  4. После обхода все индексированные пути, которых больше нет на диске (и которые находятся в корневой директории индексации), удаляются.

Бэкенды хранения

По умолчанию используется LanceDB: встроенный, файловый, настоящий векторный поиск. Резервный вариант на node:sqlite + грубый косинус методом полного перебора (src/store/sqliteFallbackStore.js) реализует тот же интерфейс (src/store/vectorStore.js) для сред, где не загружается нативная привязка LanceDB — изолированные контейнеры, необычные архитектуры. Переключение через SS_STORE_BACKEND=sqlite.

Резервный вариант выполняет полное сканирование таблицы при каждом поиске: подходит для десятков тысяч чанков, но не для большего. Метрика по умолчанию в LanceDB — L2, а не косинус, поэтому в этом проекте явно задаётся .distanceType('cosine') для каждого запроса, так как эмбеддинги сравниваются как нормализованные векторы.

Структура проекта

src/
  config.js            Defaults + config file resolution (XDG) — the only source of tunables
  configFile.js        Read/modify/write the config file (backs add/remove/list)
  embeddings.js        transformers.js pipeline + tokenizer (lazy singletons)
  chunker.js           Token-aware paragraph packing with overlap
  ignoreRules.js       What is indexable: git ignore rules + .indexignore + binary filter,
                       shared by the indexer and grep so they can't drift apart
  safePath.js          Path confinement for the MCP file-reading tools
  version.js           Version read from package.json
  extractors/          text (anything not binary), pdf (pdf-parse), docx (mammoth)
  store/
    vectorStore.js        Storage interface + backend selector
    lancedbStore.js       LanceDB implementation (default)
    sqliteFallbackStore.js node:sqlite + manual cosine fallback
  indexer.js           List + extract + chunk + embed + incremental upsert/prune
  search.js            Hybrid retrieval: vector + BM25 arms fused with RRF — shared by CLI and MCP
  mcp-server.js        MCP stdio server: the six tools above
  index.js             CLI entrypoint (commander)
scripts/index-all.sh   Batched indexing for very large corpora on constrained hosts (Linux)

Разработка

git clone https://github.com/adborroto/semantic-search-mcp.git
cd semantic-search-mcp
npm install
npm test              # unit + end-to-end (node:test, no framework)
npm run test:unit     # skip the slow end-to-end test
npm run lint

Файл config.json в корне репозитория имеет приоритет над расположением XDG, так что вы можете разрабатывать на тестовом корпусе, не затрагивая свою реальную конфигурацию. Тесты всегда записывают во временные каталоги. См. CONTRIBUTING.md.

Вне рамок (по замыслу)

  • Генерация ответов. Этот проект возвращает чанки, а не ответы. Передайте их сами в LLM.

  • Переранжирование второй моделью. Гибридный поиск с RRF не требует дополнительных зависимостей и даёт почти тот же результат — но это не кросс-энкодерный переранжировщик.

  • Веб-интерфейс. Только CLI и MCP.

  • Корпуса массового масштаба. Создан для корпусов документации и кода размером с одного человека или команду — десятки тысяч чанков, а не миллионы. Оба бэкенда рассчитаны на такой масштаб.

Лицензия

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Local-first RAG indexing and semantic search MCP server. Enables document retrieval and context-aware queries using local embedding models.
    3
    13 npm
    MIT
  • F
    license
    A
    quality
    D
    maintenance
    A local-first document retrieval engine that mounts as an MCP tool for agents to index files, search for relevant passages, and let the agent's own LLM answer.
    4
    -
  • A
    license
    A
    quality
    B
    maintenance
    Indexes local Markdown/text files into a SQLite database with vector embeddings and provides MCP tools for semantic search without cloud dependencies.
    3
    AGPL 3.0