Опубликовано
Тринадцать MCP-серверов, один порт: трей-гейтвей AOMG
Автор: Гусев Николай [портфолио]
Обновлено
Сложность: средняя
У каждого, кто гоняет AI-агентов с Model Context Protocol, рано или поздно наступает момент, когда конфиг агента превращается в зоопарк. У меня он наступил на третьем сервере. Долго я терпел, а потом сел и написал гейтвей - сейчас за одним портом у меня живёт тринадцать серверов, около 250 инструментов, и это единственный конфиг, который я не боюсь открывать.

Проблема
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, остальное то же.
