Router-1c-mcp — один MCP-сервер на все базы 1С
Проблема
Несколько баз 1С в одном MCP-клиенте — это N одинаковых наборов инструментов. Расширение у всех одно, схемы у него те же; различаются инструменты только именем сервера. В настройках клиента это выглядит так:
{
"mcpServers": {
"ut": { "type": "http", "url": "http://1c-host/ut/hs/mcp/" },
"bp": { "type": "http", "url": "http://1c-host/bp/hs/mcp/" },
"ut-test": { "type": "http", "url": "http://1c-host/ut-test/hs/mcp/" }
}
}
В вызовах это mcp__ut__execute_query и mcp__bp__execute_query, а в контексте — полный комплект схем на каждую базу. Замер на моём наборе: 18 инструментов, их описания и схемы — 23 КБ JSON на одну базу. Тринадцать баз — 302 КБ в каждом запросе сессии.
Дальше начинаются неудобства, которые видны не сразу.
Модель выбирает не базу, а сервер. Различить два одинаковых инструмента по описанию нельзя — только по имени сервера. Ошибка «объект не найден» в такой схеме означает не то, что объекта нет, а то, что смотрели не в ту базу.
Недоступная база тянет за собой остальные. Клиент поднимает все серверы при старте и ждёт каждый. Неответившая публикация даёт ошибку подключения, и работа начинается не с задачи, а с выяснения, почему нет базы. Остальные при этом исправны и могли бы работать.
Нет общего выключателя. Инструмент, который не нужен или слишком дорог по контексту, приходится отключать в каждом сервере отдельно. Забыли один — он снова в списке.
N рукопожатий и N копий учётных данных. Логин и пароль к каждой публикации лежат в настройках клиента, а не в одном месте. Перевыпустили пароль — правите столько записей, сколько баз.
Решение
router-1c-mcp — один процесс на Rust, который знает все базы и пересылает вызовы в их HTTP-сервисы. База выбирается обязательным параметром base.
Список баз — один файл:
[
{
"name": "ut",
"url": "http://1c-host/ut",
"username": "mcp-user",
"password": "пароль",
"service_root": "mcp",
"max_concurrent": 4
},
{
"name": "bp",
"url": "http://1c-host/bp",
"username": "mcp-user",
"password": "пароль",
"service_root": "mcp",
"max_concurrent": 4
}
]
Клиент подключается одной записью:
{
"mcpServers": {
"1c": { "type": "http", "url": "http://127.0.0.1:8010/mcp" }
}
}
В каждом вызове обязателен base — имя базы из файла. Остальное как обычно: execute_query с текстом запроса, get_metadata_structure с именем объекта, validate_query для проверки синтаксиса.
Инструменты живут не в роутере: их реализует расширение MCP_Сервер Владимира Харина — https://github.com/vladimir-kharin/1c_mcp (MIT). Роутер только пересылает вызов на {url}/hs/{service_root}/rpc с Basic Auth. Без расширения на базе ему нечего отдавать — своих инструментов у него нет. У расширения есть свой Python-прокси, но он рассчитан на одну публикацию: транспорт и авторизацию решает, многобазовости не даёт.
Транспорт — Streamable HTTP без состояния: Mcp-Session-Id роутер игнорирует, после его перезапуска клиенту не нужно новое рукопожатие.
Как собирается список инструментов
При старте роутер опрашивает все базы сразу, каждую в своей задаче: initialize — заодно версия расширения, затем tools/list. Дожидается ответа всех; упавшая база не мешает остальным и не обрывает опрос.
Дальше инструменты объединяются по имени. В параметр base попадает не общий список баз, а только те, которые этот инструмент действительно отдают, — узкий enum. Остальные параметры и required сохраняются как есть:
{
"name": "execute_query",
"inputSchema": {
"properties": {
"base": { "enum": ["ut", "bp", "ut-test"], "type": "string" },
"query": { "type": "string" }
},
"required": ["base", "query"]
}
}
Так модель видит один execute_query и три допустимых базы вместо трёх одинаковых инструментов с разными именами.
Если одно имя пришло с нескольких баз с разными схемами, берётся определение с самой старой версией расширения: так вызов пройдёт на всех базах, где инструмент есть. Версии сравниваются по числовым компонентам, включая четвёртый; при равенстве — порядок баз в настройках. Расхождение пишется в журнал: имена баз, версии и выбранный источник.
Состав по базам пишется в tools.json рядом с файлом баз:
{
"_generated": "2026-10-28T07:06:53Z",
"common": ["execute_query", "db_tables"],
"separate": {
"ut": ["list_artifacts"],
"bp": ["export_artifact"]
}
}
Здесь common — инструменты, которые есть на всех ответивших базах, separate — «свои» инструменты конкретной базы вне общего набора. Файл перезаписывается при каждом старте и при восстановлении базы; секция tools_enabled, если она есть, переносится без изменений.
Что это экономит
На моей машине тринадцать баз: рабочие, копии, тестовые. Сравнение простое.
| Что подключено | Объём схем в контексте |
|---|---|
| Одна база отдельным сервером | 23 КБ |
| Тринадцать баз отдельными серверами | 302 КБ |
| Тринадцать баз через роутер | 31 КБ |
Считал байты JSON, как их отдаёт tools/list: восемнадцать инструментов в моём наборе, схемы одной базы без поля base — 23 216 байт. Тринадцать копий — 301 808 байт. Роутер отдаёт один набор на те же 18 инструментов; перечисление тринадцати баз в параметре base добавляет к нему около 8 КБ.
Разница — 9,7 раза, и она растёт с числом баз. Линейного роста больше нет: новая база добавляет несколько десятков байт в enum каждого инструмента, который она предоставляет, а не ещё одну копию всех описаний.
Клиенты считают токены по-своему, и токенизаторы у них разные, но доля одна и та же: в контексте лежит объём схем, а не число баз.
Отказ до сети
Белый список инструментов задаётся переменной MCP_TOOLS_ENABLED (имена через запятую) или секцией tools_enabled в tools.json; непустая переменная главнее файла. Список глобальный: он одинаково применяется к выдаче tools/list и к вызову инструмента, поэтому отключённый инструмент не вызвать, даже зная его имя.
Матрица «инструмент — базы» проверяется на каждом вызове, до пересылки. Порядок такой: белый список, матрица, пересылка.
Если у выбранной базы инструмента нет, клиент получает isError с текстом «Инструмент 'X' недоступен для базы 'Y'. Допустимые базы: [...]». Если имени нет в составе вообще — «Инструмент 'X' отсутствует в составе инструментов (допустимые: [...])».
Запрос в базу не уходит ни в одном из случаев. Это экономит не только время: ошибка видна сразу, а не после того, как 1С сходила в базу и вернула пустоту.
Упавшая база
Снимок успешной базы хранится и не удаляется: если база потом пропадёт, её инструменты останутся в списке, а ошибка возникнет при обычной пересылке вызова.
Базы без снимка опрашиваются повторно раз в две минуты. Попыток конечное число: по умолчанию пять, то есть десять минут, дальше цикл останавливается — бесконечно базы не опрашиваются. Число задаётся переменной MCP_RETRY_ATTEMPTS или полем retry_attempts в обёрнутом файле баз; ноль выключает повтор. Когда ответили все базы, повтор прекращается досрочно.
Восстановилась — состав пересобирается, файл перезаписывается, инструменты публикуются в работающий сервер без перезапуска процесса. Клиент увидит новый список после переподключения: listChanged роутер не заявляет и уведомлений не рассылает.
Что ещё в комплекте
GET /health— проверка без рукопожатия MCP: сколько баз ответило, версия службы.max_concurrentу базы — потолок одновременных запросов к ней, лишние ждут в очереди.
Что не прижилось
Снимок tools/list с первой живой базы. Первая версия брала состав инструментов у одной базы и строила по нему матрицу. Инструмент, который есть только у второй базы, при этом терялся. В 0.8.0 опрос идёт по всем базам сразу.
Свой ключ доступа X-MCP-Key. Заголовок и переменная с ключом убраны: открытое расширение их не использует, авторизация к базе — только Basic Auth. Если переменная осталась в окружении, она игнорируется.
Белый список в файле баз. Он переехал в файл состава: файл баз стал только про подключения. Старое поле tools_enabled в нём больше не читается — список надо перенести в tools.json или в переменную.
Ограничения
- Транспорт заточен под 1С: адрес
{url}/hs/{service_root}/rpc, только Basic Auth, ответы читаются как JSON (не SSE),Mcp-Session-Idне передаётся, пагинацияtools/listне обходится, версия протокола вinitializeзашита (2024-11-05). Для произвольных MCP-серверов нужен настраиваемый endpoint и заголовки. - Своих инструментов нет: без расширения Харина на базе роутеру нечего отдавать.
- Пароли к публикациям лежат в
bases.jsonоткрытым текстом — файл надо держать с ограниченными правами. - Автозапуска в комплекте нет: службу или systemd-юнит настраивают отдельно.
- Новый список инструментов клиент увидит только после переподключения — уведомлений об изменении роутер не рассылает.
Установка
Готовые архивы — в выпуске на GitHub: router-1c-mcp-windows-x64.zip и router-1c-mcp-linux-x64.tar.gz (Linux — glibc не новее 2.17). Внутри бинарник, README и инструкция.
Распакуйте в отдельный каталог и опишите базы в bases.json — имена, адреса публикаций, учётные данные, при необходимости service_root и max_concurrent.
Запуск — тремя переменными:
MCP_HOST=127.0.0.1
MCP_PORT=8010
MCP_ONEC_BASES_FILE=C:\Tools\router-1c-mcp\bases.json
router-1c-mcp.exe
Проверка:
curl http://127.0.0.1:8010/health
{"bases":2,"service":"router-1c-mcp","status":"ok","version":"0.8.0"}
Состав инструментов можно посмотреть без MCP-клиента:
curl -s -X POST http://127.0.0.1:8010/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Подключение в .mcp.json клиента:
{
"mcpServers": {
"1c": { "type": "http", "url": "http://127.0.0.1:8010/mcp" }
}
}
Исходники: https://github.com/Regsorm/router-1c-mcp (MIT).
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.