← Лендинг Hermes Agent

Опубликовано

Тринадцать MCP-серверов, один порт: трей-гейтвей AOMG

Автор: Гусев Николай [портфолио]

Обновлено

Темы: mcp, agents, devops

Сложность: средняя

У каждого, кто гоняет AI-агентов с Model Context Protocol, рано или поздно наступает момент, когда конфиг агента превращается в зоопарк. У меня он наступил на третьем сервере. Долго я терпел, а потом сел и написал гейтвей - сейчас за одним портом у меня живёт тринадцать серверов, около 250 инструментов, и это единственный конфиг, который я не боюсь открывать.

Демо AOMG: панель, каталог, watchdog

Проблема

MCP-сервер - это процесс или эндпойнт, который даёт агенту инструменты. Локальные запускаются как stdio-процессы: npx что-то-mcp, uvx другой-mcp, python.exe ещё_какой-mcp. Удалённые живут по HTTP со своими заголовками и ключами.

Дальше начинается быт. Каждый MCP-клиент хранит свой конфиг со своими записями. Поменял команду запуска сервера - правь во всех клиентах. Хочешь посмотреть, жив ли сервер - запускай handshake руками. Сервер упал в фоне - агент узнает об этом в момент вызова инструмента, самым неприятным образом.

Итого три боли: дублирующиеся конфиги, нулевая наблюдаемость, ручной контроль процессов.

Идея

Классическое решение из мира серверной разработки: вынести все бэкенды за один прокси и посадить сверху супервизор. Только вместо nginx - трей-приложение на Windows, которое делает три вещи сразу:

AOMG.exe (трей)
 ├─ supervisor: поднимает и следит за локальными stdio-серверами
 ├─ gateway: HTTP на 127.0.0.1:9300, путь /<имя>/mcp
 ├─ watchdog: полный MCP-handshake каждые 30 секунд
 └─ admin: веб-панель управления на том же порту

Агенту теперь нужен один URL навсегда: http://localhost:9300/<имя>/mcp. Поменял внутри сервер weather с npx на питоновский - агенту в конфиге менять нечего.

Что внутри

Супервизор для stdio-детей использует mcp-proxy (версия 0.9.0 - про более новые ниже). Каждый локальный сервер превращается в HTTP-эндпойнт, gateway маршрутизирует по имени из пути. Проксирование важный нюанс: MCP-over-HTTP - это SSE-поток, соединение живёт долго, и обычный синхронный HTTP-клиент внутри async-обработчика вешает весь event loop. Об этом отдельно в граблях.

Watchdog - не пинг порта, а полный handshake: initialize, получить mcp-session-id, отправить notifications/initialized, спросить tools/list. Только так узнаёшь, что сервер реально отвечает, а не просто слушает порт. Для серверов за VPN проверка идёт через тот же egress - жёлтая точка в трее означает "VPN лежит", красная - "сервер умер". Разница важна, когда чинишь.

Веб-панель /admin: список серверов с цветными точками, добавление из каталога с поиском по официальному реестру MCP, форма ключей (секреты в конфиге только как ${VAR}-плейсхолдеры, через админ-API не возвращаются никогда). Реестр - отдельное приключение: он отвечает от полсекунды до 45 секунд, поэтому поиск идёт с минутным таймаутом и кэшируется на сутки - повторный запрос мгновенный, а при падении реестра отдаётся вчерашний результат. Трей показывает агрегатный статус и умеет "Перезапустить всё" - это единственная кнопка, которую нужно знать.

Отдельная приятность: ключи, которые живут в пользовательском окружении Windows (а многие инструменты прописывают свои токены именно туда), конфиг подхватывает сам - ${VAR} разворачивается и оттуда, не только из окружения процесса.

Грабли, на которые я потратил время

Многое сломалось, чиню описываю, чтобы вы сломали быстрее.

Пин mcp-proxy на 0.9.0. Версии 0.10+ сломаны для этого сценария. Не "работают хуже" - сломаны.

Trailing slash. Маршрут должен быть /mcp/ со слэшем на конце, иначе часть клиентов получает загадочные 4xx. Потратил вечер на диагностику, решается одной строкой в конфиге маршрутизации.

Дефис в спавне. Когда супервизор передаёт mcp-proxy команду ребёнка с флагами (npx -y пакет), флаг -y без разделителя -- парсится как флаг самого mcp-proxy. Ребёнок мгновенно умирает, супервизор честно его перезапускает, и так по кругу. Лечится одним "--" в argv, но находится только под лупой.

PyInstaller и relative import. При сборке exe mcp_proxy/__main__.py становится top-level модулем, и относительные импорты падают с ImportError: attempted relative import with no known parent package. Лечится wrapper-точкой входа: runpy.run_module("mcp_proxy.__main__", run_name="__main__"). Плюс frozen-сборка требует пары exe: основной и отдельный mcp-proxy.exe рядом.

Самая коварная история. Подключаю агента к гейтвею, делаю тест соединения - и весь гейтвей замирает намертво: все маршруты таймаутят, соединения висят в CLOSE_WAIT, помогает только kill. Причина: код синхронизации конфига использовал синхронный httpx.Client внутри async-обработчика FastAPI. На быстрых curl-проверках баг не воспроизводился, а на долгом SSE-соединении MCP-клиента блокировка event loop убивала весь сервис. Фикс - httpx.AsyncClient с пулом. Мораль: тестируй интеграцию реальным клиентом, а не curl-смоком, если у тебя стриминг.

Прокси-наследование. Дети, которым egress не нужен, не должны наследовать прокси-переменные хоста: половина HTTP-клиентов игнорирует NO_PROXY="*" и честно пытается идти в корпоративную сеть через твой локальный socks. Симптом выглядит как "сервер сломался", причина - окружение.

Каталог-поиск. Официальный реестр MCP отвечает от полсекунды до 45 секунд - и это не преувеличение, я замерял. Таймаут в 20 секунд давал ровно то, что вы думаете: "поиск иногда работает". Теперь таймаут минута плюс ретрай, плюс индикатор "ищу в реестре, бывает до минуты" вместо молчаливого ожидания.

Добавление из панели. Самое унизительное: запись в конфиг писалась, а сервер в списке не появлялся - супервизор смотрел в старый рантайм-конфиг и молча пропускал спавн. Работало только после полного рестарта приложения, что убивает смысл панели. Урок: после любой записи в конфиг синхронизируй рантайм ДО того, как скажешь пользователю "готово".

Что вышло

Тринадцать боевых серверов за одним портом 9300 - трекеры, документация, почта, календарь, чат, поиск по коду, локальные PII-шлюзы и пара самописных:

issue-tracker    ok  16 tools
docs-wiki        ok  22 tools
code-host        ok  33 tools
team-chat        ok  53 tools
graph-search     ok  67 tools (SSE)
docs-lookup      ok   2 tools
mail             ok  10 tools
calendar         ok  12 tools
...и ещё пять, включая локальные pii-guard и свою обвязку

Около 250 инструментов, один процесс в трее, один URL у агента. Watchdog раз в 30 секунд, живой вызов инструмента через гейтвей идёт в обычные миллисекунды. E2e-проверка реальным агентом: вызовы инструментов через гейтвей, гейтвей жив.

Из того, что осознал в процессе: маленький локальный сервис "на вечер" неизбежно обрастает тем же, что и большой - супервизором, watchdog'ом, панелью, публикационными граблями. Разница только в масштабе боли. Так что если у тебя два MCP-сервера и мысль "да ну, потяну руками" - она верная. Три и больше, с ключами и VPN - уже нет.

Код на GitHub: https://github.com/NikolayGusev-astra/aomg (зеркало на GitFlic: https://gitflic.ru/manve-sulimo2/aomg). Python/MVP-фаза, в планах порт на Rust - Tauri вместо трея на pystray, остальное то же.