Локальный семантический поиск по заметкам: свой MCP-сервер без облака и платных API
Когда в папке лежат конспекты созвонов и расшифровки встреч, обычный поиск по слову подводит: обсуждали найм, а в файле написано «собеседование» или «кандидат», и найти нужное не выходит. Отправлять заметки во внешний сервис ради эмбеддингов не хотелось, поэтому автор собрал маленький MCP-сервер, который ищет по смыслу прямо на его машине, и подключил его к Claude Code. Всё работает без платных API, а данные не покидают компьютер.
Решение уместилось в два файла на Python и папку с заметками. Движок search_index.py отвечает за нарезку текста, эмбеддинги, кэш и сам поиск, а server.py — это MCP-сервер с тремя инструментами поверх движка. Сам MCP-протокол позволяет программе вроде Claude Code запускать такой локальный сервер, спрашивать у него список доступных инструментов и затем вызывать их по мере необходимости через стандартный ввод и вывод, без открытия сетевых портов.

Пользователю доступны три команды: search_notes ищет по смыслу и возвращает фрагменты с именем файла и оценкой близости, list_notes показывает список заметок с размером и датой изменения, а read_note отдаёт файл целиком. Все инструменты только читают, что помечено в коде, чтобы клиент мог вызывать их без лишних уточнений. Для эмбеддингов используется модель paraphrase-multilingual-MiniLM-L12-v2.
Она весит около 120 мегабайт, поддерживает больше пятидесяти языков, включая русский и английский, и выдаёт векторы длиной 384. При первом запуске модель скачивается один раз, дальше работает офлайн. Вместе с sentence-transformers ставится PyTorch, который добавляет ещё один-два гигабайта на диск, но это разовая плата за автономность.
Первая грабля, на которую наткнулся автор, — молчаливое обрезание длинного входа моделью. У MiniLM жёсткий лимит в 128 токенов, и всё, что длиннее, она отбрасывает без предупреждения: ошибки нет, поиск работает, но хвост каждого длинного фрагмента в индексе не участвует. Поэтому главным ограничителем при нарезке стали токены, а символы оставили лишь ориентиром.
Сначала текст разбивается на «кирпичики» по строкам, длинные предложения режутся по знакам препинания или по символьному максимуму, затем каждый фрагмент проверяется на число токенов, и при необходимости его разрезают уже точно по токенам через encode и decode. Итоговый потолок на фрагмент — 124 токена: из 128 вычитается запас в четыре токена под служебные метки [CLS] и [SEP], которые модель добавляет сама. Второй проход жадно складывает кусочки во фрагменты, проверяя одновременно длину в символах и токенах.
Когда фрагмент заполняется, в начало следующего переносится хвост предыдущего на 80 символов, чтобы мысль, разрезанная посередине, не терялась. На трёх тестовых заметках получилось 12 фрагментов размером от 323 до 492 символов и от 88 до 123 токенов — на русском тексте токенный лимит срабатывает раньше символьного. Чтобы не считать эмбеддинги для всех заметок при каждом запросе, результаты кладутся в файл .index/cache.pkl.
Ключом служит относительный путь заметки плюс время её изменения, поскольку в разных подпапках могут лежать файлы с одинаковыми именами. Но у кэша есть вторая причина стать недействительным: смена модели или параметров нарезки. Для этого в кэш записан словарь параметров — версия структуры, имя модели, целевые размеры и токенный потолок.
При любом расхождении кэш пересобирается с нуля. Запись кэша идёт через временный файл с последующей атомарной заменой: если сбой случится посреди сохранения, на диске останется старый корректный файл, а не обрывок. При чтении повреждённый кэш не роняет сервер — исключение перехватывается, и движок продолжает работу, видимо, запуская повторную индексацию.
Это делает инструмент устойчивым к случайным сбоям и изменениям конфигурации. В итоге получился локальный семантический поиск, который понимает синонимы и не зависит от внешних API. Все заметки остаются на машине, модель работает офлайн, а кэш сам следит за актуальностью.
Такой подход можно повторить для собственных архивов переписок или рабочих заметок, если не хочется доверять их облачным сервисам.