Referência MCP privada com Docker em VPS: guia prático

VPS como cofre técnico com três ferramentas protegidas por controles de acesso e logs

Navegue por tópicos

Esta referência mostra como montar um servidor MCP privado, de leitura, com Docker em uma VPS. Você precisa de VPS Linux administrada, SSH, Docker com Compose e um token aleatório. O custo depende da VPS e do domínio, portanto não é estimado aqui. O tempo também não foi medido. O exemplo não é produção nem integração pronta com Claude ou n8n.

Um servidor MCP conecta clientes de IA a dados, ferramentas e workflows externos. Ele não transforma um modelo em agente confiável. Em uma VPS, entram rede, autenticação, segredos, logs e recuperação. Para uma integração local simples, stdio costuma ter menos risco. Para uma tool persistente e remota, Docker e VPS podem fazer sentido se alguém assumir essa operação.12

A ideia deste guia é a Fronteira de Ação. Cada tool deve explicitar o que o agente pode fazer, com qual identidade, em quais dados e com qual registro. A VPS compra controle operacional. Ela não compra autonomia nem segurança automática.

VPS como cofre técnico com três ferramentas protegidas por controles de acesso e logs

MCP não é um agente: onde ele entra

O Model Context Protocol é um protocolo aberto para ligar aplicações de IA a fontes externas de dados, ferramentas e workflows. Ele usa mensagens JSON-RPC em UTF-8. O cliente descobre uma tool, entende seu schema e pede que o servidor a execute.111

A arquitetura mínima separa responsabilidades:

cliente compatível → proxy e HTTPS → autenticação → servidor MCP → allowlist e schema → API ou dado → log

Claude Code, ou outro cliente compatível, decide chamar uma ferramenta. O servidor oferece a ferramenta. A API, banco ou workflow executa apenas o trabalho permitido. Um proxy entrega HTTPS, mas não substitui escopo, validação ou auditoria.

stdio local ou HTTP remoto

A especificação MCP define stdio e Streamable HTTP como transportes padrão. stdio liga processos locais pela entrada e saída padrão. Streamable HTTP atende cliente e servidor remotos. Publicar HTTP muda a responsabilidade sobre rede e autorização.2

Critério stdio local Streamable HTTP em VPS
Alcance Cliente e processo na mesma máquina. Cliente e servidor podem ficar separados.
Exposição Não publica endpoint. Exige proxy, TLS, firewall e autenticação.
Operação Menos componentes para atualizar. Inclui domínio, containers, logs e restore.
Cenário Teste, uso individual e tool local. Automação persistente com responsável operacional.

Comece com stdio quando estiver validando uma tool, quando há um único usuário ou quando webhook e API existentes já resolvem a integração. HTTP remoto é justificável quando a tool precisa sobreviver ao notebook ou atender cliente remoto. A documentação do MCP diferencia stdio local de fluxos OAuth para servidores HTTP remotos.3

Referência MCP privada com Docker

O exemplo abaixo é didático. Ele expõe uma única tool de leitura, status_operacional, e fica acessível somente em 127.0.0.1 na VPS. Ele serve para revisar Docker, secret, autorização e logs antes de conectar uma API real. Não é servidor de produção, não inclui proxy e não configura Claude Code ou n8n.

1. Crie a pasta e o secret fora do Git

Crie uma pasta restrita. Guarde o token em cofre apropriado e não o cole em Git, logs ou capturas de tela.

mkdir -p ~/mcp-status/secrets
cd ~/mcp-status
umask 077
openssl rand -hex 32 > secrets/mcp_token.txt
printf "secrets/\n" > .gitignore

O Compose permite conceder secrets explicitamente aos serviços. Para aplicações que suportam essa leitura, a documentação do Docker recomenda secrets em vez de variável de ambiente para dados sensíveis.7

2. Salve o servidor de leitura

Crie server.py. O código não acessa banco, shell, ERP nem API. Essa limitação mantém a Fronteira de Ação pequena.

import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

TOKEN_PATH = "/run/secrets/mcp_token"
with open(TOKEN_PATH, encoding="utf-8") as secret_file:
    TOKEN = secret_file.read().strip()

TOOLS = [{
    "name": "status_operacional",
    "description": "Retorna o estado não sensível da referência MCP.",
    "inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
}]

def response(request_id, result=None, error=None):
    body = {"jsonrpc": "2.0", "id": request_id}
    body["error" if error else "result"] = error if error else result
    return body

class MCPHandler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        print("mcp_http", self.client_address[0], fmt % args, flush=True)

    def do_POST(self):
        if self.path != "/mcp":
            self.send_error(404)
            return
        if self.headers.get("Authorization") != f"Bearer {TOKEN}":
            self._send(401, {"error": "unauthorized"})
            return
        try:
            payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
            request_id = payload.get("id")
            method = payload["method"]
        except (KeyError, ValueError, json.JSONDecodeError):
            self._send(400, response(None, error={"code": -32600, "message": "Invalid Request"}))
            return
        if method == "initialize":
            result = {"protocolVersion": payload.get("params", {}).get("protocolVersion", "2025-11-25"), "capabilities": {"tools": {}}, "serverInfo": {"name": "mcp-status", "version": "0.1.0"}}
            self._send(200, response(request_id, result))
        elif method == "tools/list":
            self._send(200, response(request_id, {"tools": TOOLS}))
        elif method == "tools/call" and payload.get("params", {}).get("name") == "status_operacional":
            self._send(200, response(request_id, {"content": [{"type": "text", "text": "status=ok; escopo=leitura"}]}))
        else:
            self._send(200, response(request_id, error={"code": -32601, "message": "Method not found"}))

    def _send(self, status, body):
        raw = json.dumps(body).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(raw)))
        self.end_headers()
        self.wfile.write(raw)

ThreadingHTTPServer(("0.0.0.0", 8080), MCPHandler).serve_forever()

Crie Dockerfile no mesmo diretório:

FROM python:3.12-alpine
WORKDIR /app
COPY server.py /app/server.py
USER 65534
CMD ["python", "/app/server.py"]

3. Use Compose com bind local

Salve como compose.yaml. O bind 127.0.0.1:8080:8080 impede acesso público direto durante a referência.

services:
  mcp-server:
    build: .
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    ports:
      - "127.0.0.1:8080:8080"
    secrets:
      - source: mcp_token
        target: mcp_token
    security_opt:
      - no-new-privileges:true

secrets:
  mcp_token:
    file: ./secrets/mcp_token.txt

Suba o container e veja os logs:

docker compose up -d --build
docker compose ps
docker compose logs --tail=50 mcp-server

Container em execução só confirma que o processo iniciou. Não prova backup, TLS ou autorização.8

Comparação visual entre stdio local protegido e HTTP remoto passando por proxy e firewall

4. Valide recusa e chamada permitida

Leia o token apenas para montar o cabeçalho no shell atual. A primeira chamada é um POST JSON sem Authorization; pelo código, ela deve devolver 401. As seguintes usam o token lido em $TOKEN. Elas devem devolver JSON-RPC com status_operacional e status=ok; escopo=leitura.

TOKEN="$(<secrets/mcp_token.txt)"

curl -i -X POST http://127.0.0.1:8080/mcp \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

curl -sS -X POST http://127.0.0.1:8080/mcp \
  -H "Authorization: Bearer ${TOKEN}" \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

curl -sS -X POST http://127.0.0.1:8080/mcp \
  -H "Authorization: Bearer ${TOKEN}" \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"status_operacional","arguments":{}}}'

Esses resultados são comportamentos esperados do exemplo, não alegações de teste do Runzos. Se a primeira resposta não for 401, pare e revise o controle de acesso. Se as chamadas autorizadas não retornarem os valores descritos, confira docker compose logs mcp-server.

5. Conecte sem abrir a porta pública

Mantenha o serviço privado e abra um túnel SSH a partir da máquina cliente:

ssh -N -L 8080:127.0.0.1:8080 usuario@IP_DA_SUA_VPS

O endpoint local passa a ser http://127.0.0.1:8080/mcp. Use-o apenas em cliente compatível com Streamable HTTP e com a autenticação escolhida. A documentação do Claude Code descreve a conexão a servidores MCP, mas a sintaxe depende da versão instalada. Confirme a documentação atual antes de registrar comandos.10

No n8n, os papéis são diferentes: MCP Client Tool consome servidor externo; MCP Server Trigger expõe um workflow; recursos MCP da instância atendem outro cenário.6 Esta referência não entrega a configuração pronta de nenhum deles.

Segurança e problemas comuns

Prompt injection é LLM01:2025 para a OWASP. Docker, TLS e OAuth resolvem partes distintas do problema. Para ações de maior impacto, use menor privilégio e aprovação humana.9

  • Tool com allowlist e schema de entrada estrito.
  • Credencial por serviço, com o menor escopo possível.
  • Token fora do Git, Compose e logs.
  • Proxy HTTPS e firewall antes de qualquer exposição pública.
  • Timeout em chamadas externas e logs sem headers de autorização.
  • Aprovação humana para ação destrutiva, financeira ou irreversível.
  • Backup de configuração e restore testado antes de chamar o processo de recuperação.
Sintoma Verificação segura Correção inicial
Container reinicia docker compose logs mcp-server Confirme o arquivo do secret e a leitura pelo Docker.
401 com token Compare o token sem imprimi-lo. Recrie o secret, suba o serviço e reteste no host.
Cliente não lista tools Faça tools/list pelo túnel. Corrija rede e autenticação antes de culpar o cliente.
Porta pública Confira firewall e bind. Remova a regra pública e preserve 127.0.0.1.

Quando MCP e VPS não são a resposta

MCP padroniza como clientes de IA descobrem e chamam capacidades. Não substitui API, webhook ou n8n. Use API ou webhook quando a integração for determinística. Use n8n quando o workflow visual for o centro da operação. Use MCP quando a interoperabilidade entre cliente e tools for o problema real.

Evite VPS quando o caso ainda for teste local, uma integração de uma etapa já estiver resolvida, a ação for perigosa sem aprovação ou ninguém assumir atualização e restore. Se a operação remota fizer sentido, compare rotas de VPS para automações com custo e requisitos atuais e uma alternativa como a Servla VPS para n8n. Preço, região, SLA e recursos exigem consulta na página vigente.

FAQ

O que é um servidor MCP?

É um servidor que oferece contexto, recursos ou tools para cliente compatível com MCP. O protocolo define como capacidades são descobertas e chamadas.1

Posso rodar MCP em VPS?

Sim, quando há acesso remoto ou automação persistente e alguém assume autenticação, proxy, TLS, firewall, secrets, logs, atualizações e restore.2

MCP substitui API ou webhook?

Não. MCP atende interoperabilidade entre cliente de IA e tools. APIs e webhooks continuam adequados para contratos e eventos determinísticos.

Como proteger MCP remoto?

Reduza a Fronteira de Ação com tools específicas, schema estrito, credenciais limitadas, secrets fora do Git, logs sem segredo e aprovação humana para alto impacto.39

Conclusão

A referência privada é uma forma de revisar os limites antes de conectar sistemas reais. Comece com tool de leitura, mantenha a porta local e amplie o catálogo somente depois de validar escopo, autorização e recuperação. Se decidir operar uma VPS, veja a rota de infraestrutura da Turbo Cloud e escolha conforme a stack, não por promessa de autonomia.

Foto de Maicon Ramos

Maicon Ramos

Infoprodutor e especialista em automações de Marketing, fundador do Automação sem Limites, uma comunidade para ajudar empreendedores e startup.