Servidores MCP auto-hospedados se multiplicam. Primeiro há um para a documentação interna, depois um para o banco de dados, depois alguém escreve um pequeno para o pipeline de deploy. Cada um ganha sua porta, sua URL e seu segredo, e cada pessoa da equipe cola os três na configuração do cliente. Trocar um token significa avisar todo mundo.
Um gateway transforma isso em um endereço e um token. Os clientes se conectam a https://mcp.example.com/docs/mcp, .../db/mcp e assim por diante. O gateway confere o token, envia cada caminho ao servidor certo em localhost, e os servidores nunca ficam expostos. Precisa de muito pouco: um VPS pequeno com IPv4 própria e o Caddy.
Montamos a configuração abaixo num servidor de testes e a conferimos com clientes reais antes de escrever.
O que você precisa
- Um VPS com IPv4 pública, para os clientes chegarem à 443. O Nano-IP basta, a menos que seus servidores MCP façam trabalho pesado.
- Um domínio com registro A apontando para o servidor, aqui
mcp.example.com. - Ubuntu 24.04 ou Debian 12/13. O Caddy está nos repositórios padrão de ambos.
1. Rodar os servidores MCP em localhost
Cada servidor roda como serviço do systemd. Aqui usamos duas vezes o servidor de referência server-everything para o exemplo ficar completo; troque pelos seus. Ele precisa do Node.js 20 ou mais novo:
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
Uma segunda cópia usa a porta 3102. Depois:
systemctl daemon-reload
systemctl enable --now mcp-everything-1 mcp-everything-2
Uma coisa que notamos nos testes: esse servidor escuta em todas as interfaces, não só em 127.0.0.1, e muitos outros fazem o mesmo. O passo 3 resolve isso.
2. Caddy: conferir o token e depois rotear por caminho
Gere um token e passe ao Caddy por um arquivo de ambiente:
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
O 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
O Caddy obtém sozinho o certificado de mcp.example.com.
O bloco route não é enfeite. Nossa primeira versão tinha a checagem do token e os blocos handle_path lado a lado no nível superior, e requisições sem token passavam direto com 200. O Caddy ordena as diretivas do jeito dele, e handle_path roda antes de respond. Dentro de route, elas rodam na ordem escrita. Se adaptar esta configuração, teste primeiro o caso "sem token".
handle_path remove o prefixo, então /everything/mcp chega ao servidor como /mcp. flush_interval -1 repassa os eventos em streaming assim que chegam.
3. Fechar tudo menos SSH e HTTPS
apt install -y ufw
ufw default deny incoming
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw enable
Depois disso, no nosso teste, as portas 3101 e 3102 ficaram inacessíveis de fora do servidor, enquanto 22 e 443 respondiam. A porta 80 fica aberta só para o Caddy obter e renovar certificados e redirecionar para HTTPS.
4. Conferir
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"
A primeira requisição deve imprimir 401 e a segunda 200. Na nossa execução, um token errado e um token cortado receberam 401, e um caminho desconhecido recebeu 404 mesmo com o token certo.
Depois, a partir de um cliente, por exemplo o Claude Code:
claude mcp add --transport http everything https://mcp.example.com/everything/mcp \
--header "Authorization: Bearer $TOKEN"
claude mcp list
Os dois servidores mostraram ✔ Connected. O mesmo cliente com um token errado mostrou ✘ Failed to connect. Mais sobre configurar clientes em Conectar o EQVPS MCP ao Claude Code e ao Codex.
Adicionar o próximo servidor
Suba-o numa nova porta de localhost, adicione um bloco handle_path e recarregue o Caddy. A configuração de ninguém muda, exceto por uma URL nova sob o mesmo host e o mesmo token.
Limites que vale conhecer
- Um token para todos é simples e trocá-lo é um arquivo, mas não dá para revogar uma pessoa sem mudar para todas. Para acesso por usuário, coloque um proxy de autenticação na frente ou dê a cada pessoa sua própria rota e token.
- O gateway não deixa um servidor seguro. Um servidor MCP que executa comandos de shell continua sendo um shell para quem tiver o token. Mantenha os tokens fora de configurações compartilhadas e do git.
- Dimensione pelos servidores, não pelo gateway. O próprio Caddy usa algumas dezenas de megabytes. Se um dos seus servidores roda um modelo local ou indexa um repositório grande, é ele quem decide o plano, não o proxy.
Tem só um servidor MCP? VPS para servidores MCP cobre esse caso mais simples. Quer que seus agentes também cuidem do servidor onde vivem? O servidor MCP da EQVPS faz isso (ou a API REST, se preferir scripts), e os planos com IPv4 própria começam em $8 por mês.
Comentários
Nenhum comentário ainda. Seja o primeiro.