MCP без облака

от автора

Про MCP (Model Context Protocol) сейчас не писал разве что самый ленивый. Обычно его используют, чтобы собрать агента: дать модели доступ к почте, календарю, паре внешних API — чтобы она бронировала встречи или присылала мемчик каждое утро. А вот про то, что возможности MCP шире и применять его можно не только привычным способом, написано мало.

Например, у меня возникла такая задача: есть куча файлов, на которые хорошо бы натравить нейросеть, чтобы она искала ответы прямо по ним, а не выдумывала. Файлы разнородные, среди них тяжелые PDF. Закинуть их в модель целиком не выйдет: контекстное окно забьется раньше, чем она доберется до сути. Нужен способ скармливать модели не весь архив разом, а только нужный кусок.

Мне пришла идея применить MCP «с другой стороны» — не для агента, а для аккуратного доступа к большому архиву локальных документов. В этой статье расскажу, как это устроено: почему обычный RAG для такой задачи подходит не всегда, чем его заменить, как собрать маршрут от PDF до базы знаний и где грабли. А заодно покажу, что получается на выходе: доступ к локальным документам, который экономит контекстное окно и токены, не тащит данные в облако и сохраняет след для аудита.

А почему не построить классический RAG?

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

Но здесь важно не путать две вещи. RAG — это паттерн: извлечение нужной информации по близости использовали в графовых базах задолго до всякого ИИ (я когда-то контрибьютил в графовую БД, и там RAG-подобный подход спокойно жил рядом с алгоритмом Дейкстры).

MCP — это транспорт, протокол: он говорит модели, как и где подцепить источник. Что это за источник — RAG, граф, обычная папка с файлами, — протоколу все равно. Это не альтернативы друг другу, это разные слои.

Плюс есть несколько важных моментов. Во-первых, цена входа: RAG тянет за собой инфраструктуру: эмбеддинг-модель на 8–10 Гбайт, векторную БД, индексацию по расписанию и инженера, который все это поддерживает. Переиндексация по расписанию любит подкинуть сюрприз: попадешь в неудачное окно — получишь неконсистентные данные, потому что часть уже обновилась, а часть еще нет.

Во-вторых — аудит. Если модель ошиблась (сходила не туда, выбрала не тот фрагмент или вообще нагаллюцинировала), в чистом RAG ты не разберешься, почему так вышло. Вот тут и появляется альтернатива: подключить источники через MCP так, чтобы доступ остался управляемым и прозрачным.

Оглавление вместо архива

Модели не обязательно видеть весь архив. Достаточно дать ей компактную карту — индекс, который работает как оглавление. Сначала модель смотрит в него, понимает, куда идти, и только потом забирает конкретный кусок. Контекстное окно тратится не на всю базу знаний, а на один нужный фрагмент.

Идея индексного файла не нова — ее задала спецификация llms.txt (Джереми Ховард, сентябрь 2024). Это и есть тот самый индекс: ты складываешь информацию в хранилище (Git, файловая папка — что угодно), а модели отдаешь индекс. Захотела модель что-то найти — лезет в него, понимает, где это лежит, идет туда и берет. Без постоянного хранения всего архива в окне.

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

Подход рабочий и довольно хороший. Но у готовых реализаций есть ограничение: они хотят, чтобы информация уже была разложена по полочкам — структурирована, отформатирована, категоризирована. Чаще всего такие штуки я встречал в связке с URL: есть готовый API, structured-данные, ставь MCP-сервер и пользуйся. А что делать, если у тебя не аккуратный API, а свалка локальных PDF?

Главный index.md отдает модели общую карту полки: для коротких документов — сразу ссылку на файл, для длинных — ссылку на их субиндекс. Субиндекс — такое же оглавление, но уже внутри одного документа.

Корневой index.md:

# Network Equipment DocumentationБаза знаний по сетевому оборудованию для курса.Руководства, даташиты, спецификации протоколов.## Маршрутизаторы и коммутаторы- [MikroTik RouterOS 7 — Reference Manual](docs/routers/mikrotik-routeros/subindex.md)  Полное руководство, ~400 страниц, разбито на главы.- [MikroTik hAP ac3 — Datasheet](docs/routers/mikrotik-hap-ac3.md)- [Cisco Catalyst 9300 — Configuration Guide](docs/switches/cisco-catalyst-9300/subindex.md)## Сетевые протоколы- [BGP — RFC 4271 (краткое содержание)](docs/protocols/bgp-rfc4271.md)- [OSPF — операции и траблшутинг](docs/protocols/ospf-operations.md)## Беспроводные сети- [802.11 Wi-Fi — обзор стандартов](docs/wireless/wifi-standards.md)- [WPA3 — Security Whitepaper](docs/wireless/wpa3-security.md)Субиндекс длинного документа — docs/routers/mikrotik-routeros/subindex.md: # MikroTik RouterOS 7 — Reference ManualГлавы в порядке оригинала. Каждая секция — отдельный файл.## Часть II. Маршрутизация- [004 — Статическая маршрутизация](004-static-routing.md)- [005 — Протокол OSPF](005-ospf.md)- [006 — Протокол BGP](006-bgp.md)- [007 — Policy-based Routing и failover](007-policy-routing.md)## Часть III. Firewall и безопасность- [008 — Firewall: цепочки и правила](008-firewall-chains.md)- [009 — NAT и маскарадинг](009-nat.md)- [010 — Защита от DDoS](010-ddos-protection.md)

DocShelf: полка вместо свалки

Раз готовая инфраструктура с неба не падала, пришлось тряхнуть стариной и сделать ее самому.

Так появился DocShelf — инструмент, который превращает кучу локальных PDF в нормальную полку документов. С понятными разделами, индексами и маршрутом от вопроса к источнику. По сути, это MCP-сервер, который одной рукой держится за модель, а другой — за подготовленную структуру. С той разницей, что структуру он еще и готовит сам.

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

Поэтому DocShelf переводит PDF в Markdown. MD читается и моделью, и человеком, рендерится в нормальный вид с заголовками, а места занимает мало. И ретривим мы потом не из PDF, а из этих MD.

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

Пайплайн

Целиком маршрут выглядит так:
PDF → Markdown → секции → index.md / subindex.md → Git → DocShelf MCP → LLM

Сначала PDF переводится в Markdown и чистится. Дальше большие документы режутся на секции — иначе даже аккуратный MD останется неподъемным куском. Из секций собирается индекс: заголовки плюс ссылки на категории. Для совсем крупных документов одного индекса мало, поэтому появляется еще уровень вложенности — сабиндексы. Готовая структура кладется в Git (или другое хранилище), а DocShelf MCP подключает ее к модели.

Отдельная история — размер чанка. Чанк — это фрагмент, на который режется документ (те самые секции из схемы выше): модель забирает не файл целиком, а только эти куски. Слишком мелко нарежешь — модель начнет забрасывать базу десятком запросов на каждый вопрос. Слишком крупно — притащит в окно лишнее и снова его забьет.

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

Например, вопрос студента: как настроить failover между двумя WAN-каналами на MikroTik RouterOS 7?

Модель читает основной index.md (~3 КБ в контексте), видит запись про RouterOS 7 и запрашивает ее субиндекс: fetch(«docs/routers/mikrotik-routeros/subindex.md»), еще +1,5 КБ. В субиндексе находит главу 007 Policy-based Routing и failover и тянет уже саму секцию: fetch(«docs/routers/mikrotik-routeros/007-policy-routing.md»), +12 КБ. Внутри — нужный раздел про failover, по нему и собирается ответ со ссылкой на источник.

Теперь посчитаем контекст. Полка целиком — около 85 МБ. За весь запрос в окно попало ~16,5 КБ: индекс, субиндекс и одна секция. Это меньше 0,02% от объема полки. И без векторной базы, и без переиндексации.

Грабли

PDF парсится плохо чаще, чем хотелось бы. На сканах все совсем грустно. Кириллица при конвертации любит ломаться — это, пожалуй, самая частая боль. А еще конвертер легко выдает формально валидный, но нечитаемый Markdown — сплошную простыню текста через пробелы и точки с запятой, без заголовков и структуры. С такой кашей модель работает дорого, а человек не может проверить глазами — а значит, не поймет, где конвертация сломалась, и не отладит индекс.

Пара слов про сам конвертер. В DocShelf их два на выбор. По умолчанию работает pymupdf4llm. Быстрый, без GPU, его хватает примерно на 95% технических документов с нормальным текстовым слоем: спеки, даташиты, экспорт из Word или LaTeX. Для сложных случаев (таблицы, формулы, многоколоночная верстка) есть marker-pdf с флагом quality=»high»: он умнее разбирает структуру, но тянет PyTorch (~2 Гбайт) и работает 10–60 секунд на документ. Зависимость подгружается отложенно: не включаешь quality=»high» — тяжелый PyTorch не грузится. Для закрытого контура веса модели надо заранее положить в образ.

Перед нарезкой DocShelf прогоняет markdown через очистку: схлопывает лишние пустые строки и понижает мусорные H1, куски CLI-вывода или колонтитулы, которые конвертер принял за заголовок. Дальше режет по границам H2, секции получают имена вида NNN-slug.md. Операция идемпотентна: при повторном прогоне старая разбивка стирается и собирается заново.

Отдельно про кириллицу. В теле документа DocShelf ее не «лечит» — markdown сохраняется как есть. На практике быстрый конвертер с кириллицей обычно справляется, а где спотыкается на сложной верстке, помогает переключение на marker-pdf. Unicode-aware обработка нужна в другом месте — в именах файлов: заголовок на кириллице нормализуется и транслитерируется в ASCII-подобный slug, чтобы ссылки оставались стабильными. Если документ все же пришел кашей — это видно глазами, и его прогоняем через quality=»high», либо оставляем целым (split=False).

Если скан без текстового слоя, требуется OCR (Tesseract, PaddleOCR) еще до попадания на «полку». Это сознательно вынесено за рамки DocShelf.

Кроме проблем с конвертацией были и другие: то модель тащит лишнее, то бегает за данными слишком часто, то не находит нужный раздел. Собрал самые частые из них и лечение в одну таблицу:

Симптом

Причина

Решение

Модель приносит много лишнего

Секции слишком крупные

Уменьшить размер чанка

Модель делает слишком много запросов

Секции слишком мелкие

Укрупнить фрагменты

Ответы странно выглядят на русском

Сломалась кириллица при конвертации

Проверить Markdown вручную

Нужный раздел не находится

Индекс плохо описывает документы

Переписать заголовки и описания

Git (и не только Git)

По умолчанию DocShelf работает с Git-репозиторием — это самый удобный формат. Git есть почти в любой инженерной среде, его можно поднять в закрытом контуре, и какой-нибудь GitLab есть почти у всех. Плюсы: версионирование, pull request, review, ограничения доступа, полный трек изменений. Видно, кто и когда поменял документ, что нужно не только для удобства, но и для расследования: можно понять, на какой версии документа модель строила ответ.

Добавить документ — git push. Не надо ничего перезаливать в контекстное окно или в хранилище проекта: обновил индексный файл, остальное приехало через обычный git-флоу.

Но Git — не догма. DocShelf, по-хорошему, все равно, откуда читать: ему важно, чтобы в индексе были правильные ссылки на категории, а сама информация была достижима. Поэтому «полка» спокойно ложится на объектные хранилища (S3, MinIO), на nginx, на обычную файловую систему с папками. Если у архитектора в контуре регламентный запрет на Git за периметром, можно использовать другое хранилище.

Вот как выглядит репозиторий «полки»:

network-shelf/├── .docshelf.json├── index.md└── docs/    ├── routers/    │   ├── mikrotik-routeros/    │   │   ├── subindex.md    │   │   ├── 001-introduction.md    │   │   ├── 004-static-routing.md    │   │   ├── 007-policy-routing.md    │   │   └── ...    │   ├── mikrotik-hap-ac3.md    │   └── mikrotik-hap-ac3.meta.json    ├── protocols/    │   └── bgp-rfc4271.md    └── wireless/        └── wifi-standards.md 

Конфиг .docshelf.json:

{  "name": "Network Equipment Documentation",  "base_url": "https://gitea.internal.local/training/network-shelf/raw/branch/main",  "default_language": "ru"}

Закрытый контур

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

Ключевой момент — независимость от вендора. Подход не привязан к модели: работает и с ChatGPT, и с Claude, и с DeepSeek, и с GigaChat. Причем с теми их версиями, которые умеют жить on-premise, в закрытом контуре. Это важно, потому что многие чужие MCP-серверы требуют авторизации под конкретным логином и работают только с GPT.

Облачные конкуренты вроде Context7, GitMCP, DeepWiki физически не работают за периметром: это SaaS, им нужен доступ в интернет. То же касается обвязок, которым нужен выход в облако. DocShelf не требует ничего из этого.

Мы почти всегда работаем в закрытых контурах — наши заказчики не выносят свои данные наружу. И обучение у нас такое же: курсы с закрытыми материалами, уникальные оцифрованные учебники, методички, роадмапы — то, чем мы делиться не можем или не хотим. Здесь DocShelf и оказывается на своем месте: модель остается внутри периметра и работает строго по нашей базе знаний.

Когда DocShelf не нужен

Если документов мало, то хватит обычного контекстного окна или проекта, заводить индекс незачем. Если у вас уже есть хороший RAG, который устраивает по качеству, скорости, аудиту и поддержке, — не меняйте его только потому, что хочется MCP. Если нужен полноценный семантический поиск по хаотичному корпусу — эмбеддинги, скорее всего, справятся лучше. А если индекс некому поддерживать, то подход начнет деградировать в тот же день.

И отдельно: MCP-сервер сам по себе — это риск. Небрежно выставленный наружу сервер — это дыра, и таких в интернете светится в избытке. Поднимаете MCP в контуре — обносите его забором так же тщательно, как и все остальное.

ссылка на оригинал статьи https://habr.com/ru/articles/1066908/