Como rodar servidores MCP em uma VPS para agentes de IA
Configure servidores MCP em uma VPS com stdio e HTTP remoto, systemd, nginx, TLS e autenticação. Evite JSON-RPC corrompido e endpoints expostos sem proteção.
O que você vai construir
Duas configurações MCP funcionais no mesmo VPS. Primeiro, um servidor stdio, uma ferramenta de sistema de ficheiros ou base de dados que o Claude Code inicia como processo filho e com a qual comunica através de um pipe. Depois, um servidor HTTP remoto que funciona como um serviço de rede de longa duração, gerido pelo systemd e protegido por um reverse proxy nginx com TLS. Esse servidor fica acessível a qualquer cliente MCP que seja configurado para o utilizar. A instalação de cada opção é pequena. A maior parte deste guia trata dos dois problemas que realmente causam falhas: manter o fluxo JSON-RPC limpo e nunca expor um endpoint de ferramentas sem autenticação na Internet pública.
O que o MCP realmente é
O Model Context Protocol é uma forma padronizada de um cliente de IA, como Claude Code, Claude Desktop, o Gemini CLI numa VPS ou o seu próprio script, chamar ferramentas externas e ler recursos externos. O próprio modelo não executa nada. Ele faz um pedido ao cliente, o cliente comunica através de JSON-RPC 2.0 com um servidor MCP, o servidor executa a ferramenta e devolve o resultado. Esse cliente é o componente a que as pessoas se referem quando dizem agent harness: o ciclo em torno do modelo que gere a lista de ferramentas, as verificações de permissões e o estado da sessão. O MCP é simplesmente a forma de estender a parte das ferramentas. Existe um único protocolo, por isso um servidor escrito uma vez funciona com todos os clientes que falem MCP. Se esta separação for nova para si, especialmente a questão de como um modelo decide sequer usar uma ferramenta, vale a pena consultar um percurso faseado pelos fundamentos dos agentes durante uma hora antes de fornecer credenciais reais a um destes servidores.
Existem dois transportes, e o restante guia está dividido de acordo com eles:
- stdio. O cliente inicia o servidor como processo filho e troca mensagens JSON-RPC delimitadas por novas linhas através da entrada e da saída padrão do processo. Não há rede, porta ou autenticação. A própria fronteira de confiança é o processo. Quase todas as ferramentas locais são distribuídas desta forma.
- Streamable HTTP (e o seu antecessor, HTTP+SSE). O servidor é um serviço web de longa duração. O cliente liga-se através de HTTP, e o servidor pode transmitir as respostas como Server-Sent Events. É assim que partilha um servidor com vários clientes ou executa uma ferramenta que tem de permanecer permanentemente no servidor.
Escolha stdio quando a ferramenta pertence a uma máquina e a um utilizador. Escolha HTTP quando se trata de um serviço partilhado.
Pré-requisitos e limitações importantes
Considere um VPS KVM Ubuntu 24.04 recém-instalado, com root ou sudo. Além disso:
- Um runtime na linguagem em que o servidor foi desenvolvido. A maioria dos servidores de referência usa Node ou Python. O Ubuntu 24.04 inclui o Node 18, mas vários pacotes MCP atuais exigem o Node 20 ou mais recente. Por isso, instale uma versão LTS atual a partir do NodeSource ou use nvm, em vez de confiar no
apt. O Python 3.12 já está instalado. - Um domínio e um registo DNS A, mas apenas para o servidor HTTP remoto. O TLS requer um nome que resolva para este VPS. O exemplo stdio não requer DNS.
- 512 MB de RAM são suficientes. Os servidores MCP são processos JSON-RPC leves. O consumo de memória depende do que a ferramenta utiliza, como um driver de base de dados ou uma cache de ficheiros, e não do protocolo.
- A especificação é recente e continua a mudar. A revisão de 2025-03-26 substituiu HTTP+SSE por Streamable HTTP e marcou SSE como obsoleto. SSE continua a funcionar e muitos servidores ainda o utilizam. Por isso, trate qualquer definição fixa de transporte como algo que deve ser confirmado novamente nas notas de versão do servidor, e não como uma regra definitiva.
Etapa 1: ligar um servidor stdio ao Claude Code
Comece pelo servidor de sistema de ficheiros. É oficial, mantido ativamente e precisa apenas do Node. O comando abaixo regista-o no Claude Code e limita-o ao projeto atual, para que a configuração fique num ficheiro que pode ser versionado:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiO separador -- é importante: tudo o que vem depois dele é o comando que o Claude Code vai executar, não uma flag do Claude Code. Isso cria um .mcp.json na raiz do projeto:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Nada está em execução ainda. Quando iniciar o Claude Code novamente neste diretório, o agente lê .mcp.json, inicia npx -y @modelcontextprotocol/server-filesystem ... como processo filho e faz o handshake MCP através do stdin/stdout desse processo. Confirme que a configuração foi aplicada:
claude mcp listUm servidor saudável mostra o comando e um visto verde, filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✔ Connected. Dentro da sessão, o comando slash /mcp lista as ferramentas que o servidor expõe (read_text_file, write_file, list_directory), e o agente pode agora chamá-las nos caminhos que autorizou. Uma ferramenta de base de dados tem a mesma estrutura: troque o pacote e passe uma string de ligação como argumento final. No entanto, consulte o repositório do próprio servidor para confirmar o nome atual do pacote, porque o servidor Postgres de referência mudou de mantenedor mais de uma vez.
Este é o objetivo de executar o agente no próprio servidor: a sessão do Claude Code fica no VPS dentro do tmux, e os servidores stdio são executados ao lado dela, com acesso direto aos ficheiros do projeto e aos serviços locais, sem uma viagem de ida e volta pela rede. Quando o agente tiver write_file e read_text_file, vale a pena combinar esse alcance com uma skill que o oriente para a menor alteração que funcione, porque uma ferramenta de sistema de ficheiros torna uma reescrita extensa tão barata como uma correção de duas linhas. A mesma ligação pode ser estendida para além dos ficheiros locais: se já executar um motor de pesquisa no VPS, pode entregar ao agente a sua própria instância do SearXNG como ferramenta de pesquisa, mantendo as consultas no seu servidor, mas introduzindo diretamente no contexto o texto não confiável das páginas, sobre o qual o agente irá depois atuar.
Etapa 2: criar um servidor HTTP remoto
Um servidor stdio termina com o processo pai e é iniciado uma vez por cliente. Por isso, se executar duas sessões do Claude Code no servidor que encaminham trabalho entre si, cada uma terá a sua própria cópia privada da ferramenta. Quando precisa de uma ferramenta que permaneça ativa para todos os clientes, de uma ferramenta de operações partilhada, de um gateway de base de dados ou de algo que tanto o seu portátil como o seu CI possam chamar, precisa do transporte HTTP e de um serviço real. Este é um servidor Python mínimo que usa o SDK oficial e expõe uma ferramenta:
# /opt/mcp-ops/server.py
from mcp.server.mcpserver import MCPServer
import subprocess
mcp = MCPServer("ops-tools")
@mcp.tool()
def disk_free() -> str:
"""Return `df -h` for the server."""
out = subprocess.run(["df", "-h"], capture_output=True, text=True)
return out.stdout
if __name__ == "__main__":
# Serves Streamable HTTP at /mcp on 127.0.0.1:8000
mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)Nota host="127.0.0.1". O servidor faz bind apenas a localhost. Nada fora do servidor pode aceder-lhe diretamente, o que é exatamente o pretendido antes de existir autenticação. Este é o SDK 2.x que pip install "mcp[cli]" obtém atualmente (2.3.0 em 2026-10-20). A versão renomeou FastMCP para MCPServer e moveu host e port do construtor para run(). A importação antiga mcp.server.fastmcp agora falha com ModuleNotFoundError. Instale-o no seu próprio virtualenv para que o systemd tenha um caminho estável para o interpretador:
sudo useradd --system --home /opt/mcp-ops --shell /usr/sbin/nologin mcp
sudo install -d -o mcp -g mcp /opt/mcp-ops
sudo -H -u mcp python3 -m venv /opt/mcp-ops/.venv
sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install "mcp[cli]"Etapa 3: mantenha o serviço ativo com systemd
Uma ferramenta indisponível quando o agente precisa dela é pior do que não ter ferramenta nenhuma. Isto é especialmente importante quando o próprio cliente é um processo de longa duração: um agente sempre ativo que mantém a memória e os agendamentos entre reboots chamará estas ferramentas conforme o agendamento, sem ninguém a acompanhar, por isso o servidor também tem de voltar a iniciar sozinho. Escreva /etc/systemd/system/mcp-ops.service:
[Unit]
Description=MCP ops-tools server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-ops
ExecStart=/opt/mcp-ops/.venv/bin/python /opt/mcp-ops/server.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetO caminho absoluto para o Python do venv em ExecStart não é opcional. Aponte-o para /usr/bin/python3 e o processo será iniciado com ModuleNotFoundError: No module named 'mcp', porque o interpretador do sistema nunca viu o seu pip install. Ative e verifique:
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-ops
sudo systemctl status mcp-ops
curl -si -H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-X POST http://127.0.0.1:8000/mcpstatus deve apresentar active (running). curl devolve HTTP/1.1 400 Bad Request com um erro JSON-RPC no corpo, porque o pedido não tinha sessão nem um payload JSON válido. É exatamente isso que pretende: prova que a porta responde e fala o protocolo. Connection refused ou uma resposta vazia significa que o processo não está associado ao endereço esperado; consulte journalctl -u mcp-ops -n 50.
Passo 4: coloque o TLS e um reverse proxy na frente
O servidor escuta em localhost. Para o alcançar a partir de qualquer local, termine o TLS no nginx e faça proxy para o serviço interno. Instale o nginx, obtenha um certificado com Certbot e Let's Encrypt no nginx e, em seguida, escreva o bloco location. A parte crítica é desativar o buffering, porque o comportamento predefinido do nginx mantém uma resposta em espera até estar completa. Isso bloqueia um fluxo SSE indefinidamente:
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate lines managed by Certbot
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# The four lines that make SSE work through nginx:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
}
}Recarregue com sudo nginx -t && sudo systemctl reload nginx. Se já executar vários contentores, o mesmo trabalho é feito pelo reverse proxy Traefik com TLS automático. Ele emite o certificado e encaminha o tráfego com base no nome do host. Basta adicionar labels ao contentor MCP. Em qualquer dos casos, o reverse proxy passa a ser o único componente numa porta pública e encaminha o tráfego para um serviço que ainda não está protegido. Corrija isso antes de registar o URL em qualquer local.
Etapa 5: a regra de segurança que domina este tema
Nunca exponha um endpoint MCP sem autenticação. Um servidor MCP não é uma API apenas de leitura. Ele concede acesso a ferramentas, aos seus ficheiros, à sua base de dados e, por vezes, a uma shell. Um /mcp aberto na Internet pública permite que qualquer pessoa tenha o mesmo alcance que o seu agente de IA: essa pessoa pode listar as suas ferramentas e depois chamá-las. Trate-o exatamente como um socket administrativo sem autenticação, porque é isso que ele é. O que um token roubado permite fazer também depende do servidor que está por trás dele: o servidor MCP apenas de leitura distribuído com o rastreador de treinos openGym só pode devolver dados de treino, enquanto uma ferramenta de sistema de ficheiros ou de shell entrega o sistema.
Três defesas, por ordem de preferência:
- Não o publique. Mantenha o servidor em
127.0.0.1e aceda-lhe a partir do portátil através de um túnel SSH:ssh -L 8000:127.0.0.1:8000 matt@vps, apontando depois o cliente parahttp://127.0.0.1:8000/mcp. Nada fica exposto. - Coloque-o numa rede privada. Ligue o endereço do túnel a uma VPN WireGuard autoalojada e permita que apenas os pares da VPN lhe acedam. A Internet pública vê uma porta fechada.
- Se tiver de ser público, exija um token. A resposta adequada é o fluxo OAuth do MCP, que o transporte HTTP suporta nativamente. O mínimo pragmático é um token bearer partilhado, validado no proxy. É barato e impede completamente o acesso oportunista:
location /mcp {
if ($http_authorization != "Bearer REPLACE_WITH_LONG_RANDOM") {
return 401;
}
proxy_pass http://127.0.0.1:8000;
# ...buffering-off block from above...
}Gere o token com openssl rand -hex 32 e nunca associe o próprio servidor a 0.0.0.0 sem uma destas proteções à frente. O cliente envia depois o token num cabeçalho. No Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Defina MCP_TOKEN na shell para que o segredo nunca seja gravado em .mcp.json em texto simples. O Claude Code expande ${MCP_TOKEN} a partir do ambiente no momento da leitura. Se a variável não estiver definida, o Claude Code não substitui por uma cadeia vazia: assinala a variável em falta em claude mcp list e usa o texto literal ${MCP_TOKEN}. Escolha também um nome próprio, porque, nos cabeçalhos de um servidor remoto, o Claude Code lê deliberadamente como vazias as próprias variáveis e as variáveis de credenciais do seu fornecedor cloud, como ANTHROPIC_API_KEY, juntamente com outras credenciais transportadas pelo ambiente, como NPM_TOKEN.
Todas as defesas anteriores protegem o endpoint, e não o agente que já possui o token. Essa é a outra metade do problema: se o seu cliente for o DeepSeek Harness, os plugins que restringem as ferramentas que um agente pode chamar e analisam a saída das ferramentas em busca de instruções injetadas cobrem esse lado.
Etapa 6: depure com o MCP Inspector
Quando um servidor apresenta um comportamento incorreto, não tente adivinhar a partir do agente. Controle-o diretamente com o Inspector, o cliente de testes oficial baseado na Web. Para um servidor stdio, forneça o mesmo comando executado pelo agente:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpO Inspector disponibiliza a interface a partir de um servidor Web em http://localhost:6274 e imprime um URL com uma cadeia de consulta MCP_INSPECTOR_API_TOKEN. Abra essa ligação em vez de introduzir o endereço de memória. Desde a v2 (a 2.11.0 é a versão atual em 2026-10-20), o backend também injeta o token na página que disponibiliza em cada carregamento. Assim, uma recarga continua a funcionar. O token protege as rotas /api/*, que podem iniciar processos. O proxy v1 separado na porta 6277 foi removido, mas o servidor Web não é o único processo que fica à escuta. A v2 também inicia uma sandbox para MCP Apps em MCP_SANDBOX_PORT (6275 por predefinição) e um servidor dedicado à origem das aplicações em MCP_APP_ORIGIN_PORT (6278 por predefinição). Esse servidor disponibiliza qualquer MCP App cujo recurso de interface declare _meta.ui.domain. Se aceder ao Inspector através de um túnel SSH ou o executar num contentor e precisar do separador Apps, encaminhe as portas 6275 e 6278 juntamente com a 6274. Ligue-se, abra o separador Tools, selecione uma ferramenta, preencha argumentos reais e execute-a. Se funcionar no Inspector, mas falhar no agente, o problema está na configuração do cliente, não no servidor. Para o servidor HTTP remoto, inicie o Inspector sem um comando, adicione um servidor com o transporte Streamable HTTP e https://mcp.example.com/mcp, introduza o cabeçalho Authorization nas definições e ligue-se. Esta é a forma mais rápida de confirmar que a autenticação e o proxy estão corretos antes de envolver qualquer agente. O Inspector guarda os cabeçalhos no catálogo de servidores exatamente como foram escritos. Por isso, esse token também fica agora em ~/.mcp-inspector/mcp.json.
Atualizar os servidores
O MCP evolui rapidamente, por isso aplique patches segundo um calendário. Os servidores Node iniciados com um npx -y sem versão fixa executam a versão que o npx resolver: a instalada no projeto local, se existir, ou uma cópia que ele instala na cache do npm. O -y apenas ignora o pedido de instalação; não força uma atualização. Assim, não acompanha necessariamente a versão mais recente nem sabe com certeza qual versão está a executar. Depois de um servidor se tornar importante, fixe a versão exata que testou, consulte-a em npm view @modelcontextprotocol/server-filesystem version e acrescente-a ao nome do pacote em .mcp.json (@modelcontextprotocol/server-filesystem@<version>). Atualize-a de forma deliberada.
Os servidores Python executados pelo systemd são atualizados com sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]", seguido de sudo systemctl restart mcp-ops. Leia as notas da versão antes de fazer uma atualização principal: a passagem do SDK Python 1.x para o 2.x faz com que o código 1.x falhe durante a importação, e o guia de migração do próprio SDK recomenda manter um limite superior <2, como "mcp[cli]<2", até concluir a migração. Ao atualizar, acompanhe a revisão da especificação visada pelo SDK. Uma mudança que atravesse a fronteira entre SSE e Streamable-HTTP pode alterar o transporte que os clientes têm de solicitar.
Modos de falha e mensagens apresentadas
O agente indica que o servidor falhou. claude mcp list apresenta ✘ Failed to connect, e as versões atuais acrescentam os detalhes da falha a essa linha. Execute claude --debug. Normalmente verá Error: spawn npx ENOENT: o comando não está no PATH do agente. O runtime está em falta ou não está num local onde o agente o procure: Node não está instalado, npx está ausente ou foi referenciado um Python de virtualenv por um nome simples. Corrija o comando para usar um caminho absoluto ou instale o runtime. Em seguida, volte a ligar-se.
Um servidor stdio liga-se e desliga-se imediatamente. O cliente regista um erro de análise de JSON, semelhante a Unexpected token 'S', "Server sta"... is not valid JSON ou Failed to parse message. A causa é sempre a mesma: o servidor escreveu uma linha de log em stdout. Em stdio, stdout é o canal JSON-RPC. Qualquer texto adicional corrompe o fluxo e interrompe o handshake. Em Node, console.log escreve em stdout; use console.error. Em Python, um print() simples escreve em stdout. Escreva os logs com logging configurado para sys.stderr ou passe file=sys.stderr. A regra é absoluta: em stdio, stdout contém apenas JSON-RPC. Todo o texto destinado a pessoas deve ir para stderr.
Um servidor remoto excede o tempo limite ou fecha a ligação durante o handshake. O cliente falha com MCP error -32000: Connection closed, ou o Inspector fica bloqueado em Connect e nunca apresenta as ferramentas. Atrás de nginx, isto é causado pelo buffering: o proxy mantém o fluxo SSE em buffer em vez de o enviar imediatamente. O cliente fica à espera de uma resposta que nunca chega. Adicione proxy_buffering off; e o restante bloco do passo 4 ao location. Confirme com curl -N contra o URL público. Os dados dos eventos devem chegar progressivamente, não todos de uma vez no fim.
A autenticação é rejeitada. O cliente apresenta Error POSTing to endpoint (HTTP 401) ou simplesmente 401 Unauthorized. O cabeçalho pode estar ausente, o token pode estar errado ou a variável pode não ter sido expandida. Este é um erro comum. Se MCP_TOKEN não estiver definida quando o cliente lê a configuração, Claude Code avisa sobre a variável em falta em claude mcp list e envia o texto literal. Nesse caso, nginx vê Bearer ${MCP_TOKEN}. Uma variável com o nome de outra credencial do Claude Code, do fornecedor de cloud ou do ambiente pode ser lida como vazia. Nesse caso, nginx vê Bearer sem valor. Mostre o valor da variável, adicione novamente o cabeçalho e confirme que os bytes correspondem exatamente ao token no if do nginx.
O serviço não inicia no systemd. journalctl -u mcp-ops apresenta ModuleNotFoundError: No module named 'mcp', e ExecStart aponta para o Python do sistema em vez do interpretador do venv. Ou Address already in use: outro processo está a usar 8000. Encontre-o com sudo ss -ltnp | grep 8000.
FAQ
O que é exatamente um servidor MCP?
É um programa que expõe ferramentas e recursos a um cliente de IA através do Model Context Protocol, usando JSON-RPC 2.0. O modelo de IA nunca executa a ferramenta diretamente. Pede ao seu cliente, o cliente chama o servidor MCP e o servidor executa a operação e devolve um resultado. Como o protocolo é padrão, um servidor funciona com qualquer cliente compatível, seja Claude Code, Claude Desktop ou Gemini CLI.
Qual é a diferença entre transporte stdio e HTTP?
Um servidor stdio é iniciado pelo cliente como um processo filho e comunica através de stdin/stdout. Por isso, permanece ativo apenas enquanto um cliente numa máquina o utiliza e não precisa de rede nem de autenticação. Um servidor HTTP é um serviço de rede de execução contínua que pode ser acedido simultaneamente por vários clientes. Por isso, requer TLS e autenticação. Use stdio para ferramentas locais destinadas a um único utilizador. Use HTTP (Streamable HTTP nos servidores atuais) para qualquer serviço partilhado ou persistente.
Como proteger um servidor MCP remoto?
Parta do princípio de que ele permite aceder às suas ferramentas, ficheiros, base de dados ou shell. Nunca o exponha sem autenticação. A melhor opção é mantê-lo associado a localhost e aceder-lhe através de um túnel SSH ou de uma VPN privada. Se tiver de ser público, coloque-o atrás de um reverse proxy que imponha um bearer token ou o fluxo OAuth do MCP. Gere o token com openssl rand -hex 32 e nunca associe o servidor a 0.0.0.0 sem uma destas proteções à frente.
Como depurar um servidor que não inicia?
Primeiro, verifique claude mcp list e ✘ Failed to connect. O resultado com spawn ... ENOENT significa que o comando ou o runtime está em falta. Corrija o caminho ou instale-o. Se a ligação for estabelecida e depois cair com um erro de análise JSON, o servidor está a escrever logs em stdout e a corromper o fluxo JSON-RPC. Mova todos os logs para stderr. Para qualquer outro problema, execute o comando exato no MCP Inspector. Esta ferramenta executa o servidor de forma isolada, permitindo distinguir um erro do servidor de um erro na configuração do cliente.