−25%

на годовую оплату Windows, до 31.10. К тарифам

EQVPS
Начать

VPS для MCP-шлюза: несколько MCP-серверов за одним адресом и одним токеном

У команды три своих MCP-сервера, и у каждого человека три адреса и три секрета. Соберите их за одним HTTPS-адресом на небольшом VPS: Caddy проверяет bearer-токен, раскладывает запросы по путям и не выпускает сами серверы в интернет. Проверенная конфигурация.

Свои 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 в месяц.

Готовы развернуть? Оплата криптой, без KYC — сервер за минуту.

Развернуть →

Частые вопросы

Зачем вообще ставить MCP-серверы за шлюз?

Один адрес и один токен на команду вместо отдельных на каждый сервер, TLS в одном месте, а сами серверы слушают только localhost. Добавить четвёртый сервер — три строки в шлюзе, а не новый секрет для всех.

Какой тариф нужен?

Тариф со своим IPv4, потому что клиенты должны достучаться до портов 80 и 443. Nano-IP ($8/мес, 2 vCPU, 1 ГБ) хватит на шлюз и несколько лёгких MCP-серверов. Если какой-то сервер сам делает тяжёлую работу, подбирайте тариф под него.

Работает ли потоковая передача через Caddy?

Да. Streamable HTTP — это обычные POST-запросы плюс server-sent events. Поставьте flush_interval -1 в reverse_proxy, чтобы события уходили сразу, а не копились в буфере.

Одного общего токена достаточно?

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

Можно ли сделать это на NAT-тарифе?

Как публичный шлюз — нет. У NAT-тарифа один проброшенный порт для SSH и больше ничего входящего, поэтому до 443 клиенты не достучатся. Сработает, только если все клиенты живут на этом же сервере.

Комментарии

Пока нет комментариев. Будьте первым.

Оставить комментарий

Комментарии проходят модерацию перед публикацией.