Samodzielnie hostowane serwery MCP się mnożą. Najpierw jest jeden do wewnętrznej dokumentacji, potem jeden do bazy danych, potem ktoś pisze mały do pipeline'u wdrożeń. Każdy dostaje własny port, własny URL i własny sekret, a każda osoba w zespole wkleja wszystkie trzy do konfiguracji swojego klienta. Zmiana tokena oznacza pisanie do wszystkich.
Brama zamienia to w jeden adres i jeden token. Klienci łączą się z https://mcp.example.com/docs/mcp, .../db/mcp i tak dalej. Brama sprawdza token, wysyła każdą ścieżkę do właściwego serwera na localhost, a same serwery nigdy nie są wystawione. Potrzeba niewiele: małego VPS z własnym IPv4 i Caddy.
Poniższą konfigurację zbudowaliśmy na serwerze testowym i sprawdziliśmy z prawdziwymi klientami, zanim ją opisaliśmy.
Czego potrzebujesz
- VPS z publicznym IPv4, żeby klienci dotarli do portu 443. Nano-IP wystarczy, chyba że Twoje serwery MCP same wykonują ciężką pracę.
- Domeny z rekordem A wskazującym na serwer, tu
mcp.example.com. - Ubuntu 24.04 albo Debiana 12/13. Caddy jest w standardowych repozytoriach obu.
1. Uruchom serwery MCP na localhost
Każdy serwer działa jako usługa systemd. Tu dwa razy używamy serwera referencyjnego server-everything, żeby przykład był kompletny; podstaw własne. Wymaga Node.js 20 lub nowszego:
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
Druga kopia używa portu 3102. Następnie:
systemctl daemon-reload
systemctl enable --now mcp-everything-1 mcp-everything-2
Coś, co zauważyliśmy podczas testów: ten serwer słucha na wszystkich interfejsach, nie tylko na 127.0.0.1, i wiele innych robi tak samo. Krok 3 to załatwia.
2. Caddy: najpierw sprawdź token, potem kieruj po ścieżce
Wygeneruj token i przekaż go Caddy przez plik środowiskowy:
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
Caddy sam pobierze certyfikat dla mcp.example.com.
Blok route nie jest ozdobą. Nasza pierwsza wersja miała sprawdzanie tokena i bloki handle_path obok siebie na najwyższym poziomie, a żądania bez tokena przechodziły z kodem 200. Caddy sortuje dyrektywy według własnej kolejności, a handle_path wykonuje się przed respond. Wewnątrz route wykonują się w zapisanej kolejności. Jeśli dostosowujesz tę konfigurację, najpierw przetestuj przypadek „bez tokena”.
handle_path usuwa prefiks, więc /everything/mcp trafia do serwera jako /mcp. flush_interval -1 przepuszcza zdarzenia strumienia od razu, gdy przychodzą.
3. Zamknij wszystko poza SSH i HTTPS
apt install -y ufw
ufw default deny incoming
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw enable
Po tym w naszym teście porty 3101 i 3102 były nieosiągalne spoza serwera, a 22 i 443 odpowiadały. Port 80 zostaje otwarty tylko po to, żeby Caddy mógł pobierać i odnawiać certyfikaty oraz przekierowywać na HTTPS.
4. Sprawdź
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"
Pierwsze żądanie powinno zwrócić 401, a drugie 200. W naszym teście błędny i ucięty token dostały 401, a nieznana ścieżka dostała 404 nawet z poprawnym tokenem.
Następnie z klienta, na przykład z Claude Code:
claude mcp add --transport http everything https://mcp.example.com/everything/mcp \
--header "Authorization: Bearer $TOKEN"
claude mcp list
Oba serwery pokazały ✔ Connected. Ten sam klient z błędnym tokenem pokazał ✘ Failed to connect. Więcej o konfiguracji klientów w Podłączenie EQVPS MCP do Claude Code i Codex.
Dodanie kolejnego serwera
Uruchom go na nowym porcie localhost, dodaj jeden blok handle_path, przeładuj Caddy. Niczyja konfiguracja się nie zmienia, poza nowym adresem URL pod tym samym hostem i z tym samym tokenem.
Ograniczenia, o których warto wiedzieć
- Jeden token dla wszystkich jest prosty, a jego zmiana to jeden plik, ale nie da się odebrać dostępu jednej osobie bez zmiany dla wszystkich. Dla dostępu per użytkownik postaw przed bramą proxy uwierzytelniające albo daj każdemu własną ścieżkę i token.
- Brama nie czyni serwera bezpiecznym. Serwer MCP, który wykonuje polecenia powłoki, nadal jest powłoką dla każdego, kto ma token. Trzymaj tokeny z dala od współdzielonych konfiguracji i od gita.
- Dobieraj rozmiar pod serwery, nie pod bramę. Sam Caddy zużywa kilkadziesiąt megabajtów. Jeśli któryś z serwerów uruchamia lokalny model albo indeksuje duże repozytorium, to on decyduje o planie, nie proxy.
Masz tylko jeden serwer MCP? VPS na serwery MCP opisuje ten prostszy przypadek. Chcesz, żeby agenci zarządzali też serwerem, na którym działają? Robi to serwer MCP EQVPS (albo REST API, jeśli wolisz skrypty), a plany z własnym IPv4 zaczynają się od $8 miesięcznie.
Komentarze
Brak komentarzy. Bądź pierwszy.