Власні 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. Докладніше про налаштування клієнтів — у статті Підключити EQVPS MCP до Claude Code і Codex.
Додати наступний сервер
Запустіть його на новому порту localhost, додайте один блок handle_path, перезавантажте Caddy. Ні в кого конфіг не змінюється, крім нової адреси під тим самим хостом і з тим самим токеном.
Обмеження, про які варто знати
- Один токен для всіх — це просто, і змінюється він одним файлом, але відкликати доступ в однієї людини, не змінивши його для всіх, не вийде. Для доступу кожного окремо поставте перед шлюзом auth-проксі або дайте кожній людині власний маршрут і токен.
- Шлюз не робить сервер безпечним. MCP-сервер, що виконує shell-команди, лишається shell'ом для кожного, у кого є токен. Не тримайте токени в спільних конфігах і в git.
- Розмір обирайте під сервери, а не під шлюз. Сам Caddy займає кілька десятків мегабайт. Якщо один із серверів запускає локальну модель чи індексує великий репозиторій, тариф визначає він, а не проксі.
Лише один MCP-сервер? Простіший випадок розібрано в VPS для MCP-серверів. Хочете, щоб агенти ще й керували сервером, на якому живуть? Це вміє MCP-сервер EQVPS (або REST API, якщо зручніше скриптами), а тарифи з власним IPv4 — від $8 на місяць.
Коментарі
Поки немає коментарів. Будьте першим.