Свои MCP-серверы размножаются. Сначала один для внутренней документации, потом для базы, потом кто-то пишет маленький для пайплайна выкладки. У каждого свой порт, свой адрес и свой секрет, и каждый в команде вставляет все три в конфиг клиента. Сменить токен — значит написать всем.
Шлюз превращает это в один адрес и один токен. Клиенты подключаются к https://mcp.example.com/docs/mcp, .../db/mcp и так далее. Шлюз проверяет токен, отправляет каждый путь нужному серверу на localhost, а сами серверы наружу не торчат. Нужно для этого немного: небольшой VPS со своим IPv4 и Caddy.
Конфигурацию ниже мы собрали на тестовом сервере и проверили настоящими клиентами, прежде чем описывать.
Что понадобится
- VPS с публичным IPv4, чтобы клиенты достучались до 443. Nano-IP хватит, если ваши MCP-серверы сами не делают тяжёлую работу.
- Домен с A-записью на сервер, здесь —
mcp.example.com. - Ubuntu 24.04 или Debian 12/13. Caddy есть в стандартных репозиториях обоих.
1. MCP-серверы на localhost
Каждый сервер — отдельный сервис systemd. Чтобы пример был самодостаточным, мы дважды запускаем эталонный server-everything; подставьте свои. Нужен Node.js 20 или новее:
mkdir -p /opt/mcp && cd /opt/mcp
npm install @modelcontextprotocol/server-everything
# /etc/systemd/system/mcp-everything-1.service
[Unit]
Description=MCP server everything #1
After=network.target
[Service]
WorkingDirectory=/opt/mcp
Environment=PORT=3101
ExecStart=/opt/mcp/node_modules/.bin/mcp-server-everything streamableHttp
Restart=always
User=nobody
[Install]
WantedBy=multi-user.target
Вторая копия — на порту 3102. Затем:
systemctl daemon-reload
systemctl enable --now mcp-everything-1 mcp-everything-2
Что мы заметили на тесте: этот сервер слушает все интерфейсы, а не только 127.0.0.1, и многие другие ведут себя так же. С этим разбирается шаг 3.
2. Caddy: сначала токен, потом маршрут по пути
Сгенерируйте токен и передайте его Caddy через файл окружения:
apt install -y caddy
echo "MCP_GATEWAY_TOKEN=$(openssl rand -hex 32)" > /etc/caddy/gateway.env
chmod 600 /etc/caddy/gateway.env
mkdir -p /etc/systemd/system/caddy.service.d
printf '[Service]\nEnvironmentFile=/etc/caddy/gateway.env\n' > /etc/systemd/system/caddy.service.d/env.conf
systemctl daemon-reload
Caddyfile:
mcp.example.com {
route {
@unauthorized not header Authorization "Bearer {$MCP_GATEWAY_TOKEN}"
respond @unauthorized 401
handle_path /everything/* {
reverse_proxy 127.0.0.1:3101 {
flush_interval -1
}
}
handle_path /tools2/* {
reverse_proxy 127.0.0.1:3102 {
flush_interval -1
}
}
respond 404
}
}
caddy validate --config /etc/caddy/Caddyfile && systemctl restart caddy
Сертификат для mcp.example.com Caddy получит сам.
Блок route здесь не для красоты. В первой версии проверка токена и блоки handle_path стояли рядом на верхнем уровне, и запросы без токена проходили с ответом 200. Caddy сортирует директивы в своём порядке, и handle_path выполняется раньше respond. Внутри route они идут в том порядке, в каком написаны. Если будете переделывать конфиг под себя, первым делом проверьте случай «без токена».
handle_path срезает префикс, поэтому /everything/mcp приходит на сервер как /mcp. flush_interval -1 пропускает потоковые события сразу, как они приходят.
3. Закрыть всё, кроме SSH и HTTPS
apt install -y ufw
ufw default deny incoming
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw enable
После этого на нашем тесте порты 3101 и 3102 снаружи сервера стали недоступны, а 22 и 443 отвечали. Порт 80 остаётся открытым только для того, чтобы Caddy получал и продлевал сертификаты и перенаправлял на HTTPS.
4. Проверить
TOKEN=$(cut -d= -f2 /etc/caddy/gateway.env)
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"1"}}}'
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/everything/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT"
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/everything/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT"
Первый запрос должен вывести 401, второй — 200. У нас неверный и обрезанный токен получили 401, а неизвестный путь — 404 даже с правильным токеном.
Потом из клиента, например Claude Code:
claude mcp add --transport http everything https://mcp.example.com/everything/mcp \
--header "Authorization: Bearer $TOKEN"
claude mcp list
Оба сервера показали ✔ Connected. Тот же клиент с неверным токеном — ✘ Failed to connect. Подробнее о настройке клиентов — в статье Как подключить MCP EQVPS к Claude Code и Codex.
Следующий сервер
Запустить его на новом порту localhost, добавить один блок handle_path, перезагрузить Caddy. Ни у кого в команде не меняется ничего, кроме нового адреса на том же хосте с тем же токеном.
Ограничения, о которых стоит знать
- Один токен на всех — просто, и сменить его можно в одном файле, но отозвать доступ у одного человека, не поменяв токен остальным, нельзя. Для доступа по людям поставьте перед шлюзом прокси авторизации или дайте каждому свой маршрут и токен.
- Шлюз не делает сервер безопасным. MCP-сервер, который выполняет shell-команды, остаётся shell для любого, у кого есть токен. Не храните токены в общих конфигах и в git.
- Тариф выбирается под серверы, а не под шлюз. Самому Caddy хватает нескольких десятков мегабайт. Если один из серверов крутит локальную модель или индексирует большой репозиторий, тариф определяет он, а не прокси.
Сервер всего один? Этот случай попроще разобран в VPS для MCP-серверов. Хотите, чтобы агенты ещё и управляли сервером, на котором живут? Для этого есть MCP-сервер EQVPS (или REST API, если удобнее скриптом), а тарифы со своим IPv4 начинаются от $8 в месяц.
Комментарии
Пока нет комментариев. Будьте первым.