SSD Nodes Learn
Guias Matt ConnorPor Matt Connor · Atualizado 2026-07-24

Como rodar MCP servers em uma VPS

Aprenda a configurar MCP servers via stdio e HTTP remoto usando systemd e nginx com TLS. Garanta o transporte JSON-RPC seguro para agentes de IA via VPS.

O que você está construindo

Duas configurações de MCP funcionando em uma única VPS. Primeiro, um servidor stdio — uma ferramenta de filesystem ou banco de dados que o Claude Code inicia como um processo filho e comunica via pipe. Depois, um servidor remote HTTP que roda como um serviço de rede de longa duração via systemd e um proxy reverso nginx com TLS, acessível por qualquer cliente MCP configurado para ele. A instalação de ambos é simples. A maior parte deste guia foca nos dois pontos críticos: manter o stream JSON-RPC limpo e nunca expor um endpoint de ferramenta sem autenticação na internet pública.

O que é o MCP de fato

O Model Context Protocol é um padrão para que um cliente de IA — Claude Code, Claude Desktop, o Gemini CLI em um VPS ou seu próprio script — chame ferramentas externas e leia recursos externos. O modelo em si não executa nada. Ele solicita ao cliente, o cliente envia JSON-RPC 2.0 para um server MCP, e o server executa a ferramenta e retorna o resultado. Um único protocolo permite que um server escrito uma vez funcione com qualquer cliente que suporte MCP.

Existem dois transports, e o restante deste guia é dividido entre eles:

  • stdio. O cliente inicia o server como um processo filho e troca mensagens JSON-RPC delimitadas por nova linha via standard input e standard output. Sem rede, sem porta, sem autenticação — o limite de confiança é o próprio processo. Quase todas as ferramentas locais utilizam este método.
  • Streamable HTTP (e seu antecessor, HTTP+SSE). O server é um serviço web de longa duração. O cliente conecta via HTTP e o server pode transmitir respostas como Server-Sent Events. É assim que você compartilha um server com vários clientes ou executa uma ferramenta que deve residir permanentemente na máquina.

Escolha stdio quando a ferramenta pertencer a apenas uma máquina e um usuário. Escolha HTTP quando for um serviço compartilhado.

Pré-requisitos e detalhes importantes

Considere um VPS Ubuntu 24.04 KVM recém-instalado com acesso root ou sudo. Além disso:

  • Um runtime para o servidor. A maioria dos servidores de referência utiliza Node ou Python. O Ubuntu 24.04 vem com o Node 18, mas vários pacotes MCP atuais exigem o Node 20 ou superior; portanto, instale uma versão LTS atual via NodeSource ou nvm em vez de confiar no apt. O Python 3.12 já está instalado.
  • Um domínio e um registro DNS do tipo A, mas apenas para o servidor HTTP remoto — o TLS exige um nome que resolva para este VPS. O exemplo de stdio não requer DNS.
  • 512 MB de RAM são suficientes. Servidores MCP são processos JSON-RPC leves; o consumo de memória depende da sua ferramenta (um driver de banco de dados, um cache de arquivos), não do protocolo.
  • A especificação é recente e está em evolução. A revisão de 2025-03-26 substituiu o HTTP+SSE pelo Streamable HTTP e marcou o SSE como depreciado. O SSE ainda funciona e muitos servidores ainda o utilizam, portanto, trate qualquer exigência de transporte como algo a ser verificado nas notas de lançamento do servidor, e não como regra absoluta.

Passo 1: conecte um servidor stdio ao Claude Code

Comece com o servidor de filesystem — ele é oficial, mantido ativamente e requer apenas o Node. O comando abaixo registra o servidor no Claude Code e o limita ao projeto atual para que ele seja salvo em um arquivo passível de commit:

cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
  -- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/api

O separador -- é importante: tudo o que vem depois dele é o comando que o Claude Code executará, não um 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á rodando ainda. Quando você iniciar o Claude Code neste diretório, o agente lerá o .mcp.json, iniciará o npx -y @modelcontextprotocol/server-filesystem ... como um processo filho e realizará o handshake MCP via stdin/stdout desse processo. Confirme se funcionou:

claude mcp list

Um servidor funcional imprime seu comando e um check verde — filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Dentro da sessão, o comando de barra /mcp lista as ferramentas que o servidor expõe (read_file, write_file, list_directory), e o agente agora pode chamá-las nos caminhos que você permitiu. Uma ferramenta de banco de dados segue o mesmo formato — troque o pacote e passe uma string de conexão como último argumento — mas verifique o repositório do próprio servidor para o nome atual do pacote, pois o servidor Postgres de referência mudou de mantenedor mais de uma vez.

Este é o objetivo principal de rodar o agente na máquina: a sessão do Claude Code reside no VPS dentro do tmux, e seus servidores stdio rodam ao lado dele com acesso direto aos arquivos do projeto e serviços locais, sem latência de rede.

Passo 2: build a remote HTTP server

Um servidor stdio encerra junto com o processo pai. Quando você precisa de uma ferramenta que permaneça ativa para todos os clientes — uma ferramenta de operações compartilhada, um gateway de banco de dados ou algo que seu laptop e seu CI chamem — você precisa do transporte HTTP e de um serviço real. Aqui está um servidor Python minimalista usando o SDK oficial, expondo uma ferramenta:

# /opt/mcp-ops/server.py
from mcp.server.fastmcp import FastMCP
import subprocess

mcp = FastMCP("ops-tools", host="127.0.0.1", port=8000)

@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")

Observe host="127.0.0.1". O servidor faz o bind apenas em localhost — nada fora da máquina pode acessá-lo diretamente, que é o comportamento desejado antes da implementação de autenticação. Instale-o em seu próprio virtualenv para que o systemd tenha um path de interpretador estável:

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]"

Passo 3: mantenha-o ativo com o systemd

Uma ferramenta indisponível quando o agent tenta acessá-la é pior do que não ter ferramenta nenhuma. 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.target

O caminho absoluto para o Python do venv em ExecStart não é opcional — aponte para /usr/bin/python3 e o processo iniciará com ModuleNotFoundError: No module named 'mcp', pois o interpretador do sistema não reconhece 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/mcp

status deve retornar active (running). O curl retorna HTTP/1.1 400 Bad Request com um erro JSON-RPC no corpo — a requisição não continha sessão nem um payload JSON válido — e é exatamente isso que você deseja: isso prova que a porta responde e utiliza o protocolo. Connection refused ou uma resposta vazia significa que o processo não está vinculado onde você imagina; leia journalctl -u mcp-ops -n 50.

Passo 4: configurar TLS e um reverse proxy

O servidor escuta em localhost. Para acessá-lo externamente, você deve encerrar o TLS no nginx e fazer o proxy para o serviço interno. Instale o nginx, obtenha um certificado usando Certbot e Let's Encrypt no nginx e configure o bloco location. O ponto crítico é desativar o buffering; o comportamento padrão do nginx retém a resposta até que ela esteja completa, o que trava um stream SSE permanentemente:

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 você já utiliza um cluster de containers, o reverse proxy Traefik com TLS automático realiza essa tarefa: ele emite o certificado e roteia pelo hostname, bastando adicionar labels ao container MCP. Em ambos os casos, o reverse proxy é o único elemento exposto em uma porta pública, apontando para um serviço que ainda não está protegido. Corrija isso antes de registrar a URL em qualquer lugar.

Passo 5: a regra de segurança fundamental deste tópico

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 arquivos, seu banco de dados e, às vezes, um shell. Uma /mcp aberta na internet pública é um estranho com o mesmo alcance do seu agente de IA: eles listam suas ferramentas e depois as executam. Trate isso exatamente como um socket de administrador sem autenticação, pois é isso que ele é.

Três defesas, em ordem de preferência:

  1. Não o publique. Mantenha o servidor em 127.0.0.1 e acesse-o do seu laptop via túnel SSH: ssh -L 8000:127.0.0.1:8000 matt@vps, então aponte o cliente para http://127.0.0.1:8000/mcp. Nada fica exposto.
  2. Coloque-o em uma rede privada. Faça o bind do endereço do túnel de uma VPN WireGuard self-hosted e permita que apenas peers da VPN o acessem. A internet pública verá uma porta fechada.
  3. Se precisar ser público, exija um token. A solução ideal é o fluxo MCP OAuth suportado nativamente pelo transporte HTTP. O mínimo pragmático é um bearer token compartilhado validado no proxy — é simples e impede ataques automatizados:
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 faça o bind do servidor diretamente em 0.0.0.0 sem uma dessas proteções à frente. O cliente então envia o token como um header. No Claude Code:

claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
  --header 'Authorization: Bearer ${MCP_TOKEN}'

Configure MCP_TOKEN no seu shell para que o segredo nunca fique em texto puro no .mcp.json — o Claude Code expande ${MCP_TOKEN} do ambiente no momento da leitura.

Passo 6: debug com o MCP Inspector

Quando um servidor apresentar comportamento inesperado, não tente adivinhar o erro pelo agente — utilize o Inspector, o cliente de teste oficial baseado em web. Para um servidor stdio, utilize o mesmo comando que o agente executa:

npx @modelcontextprotocol/inspector \
  npx -y @modelcontextprotocol/server-filesystem /tmp

O processo inicia uma interface UI em http://localhost:6274 (versões recentes exibem uma URL com uma query string MCP_PROXY_AUTH_TOKEN — utilize este link exato para evitar erros de conexão) e um proxy na porta 6277. Clique em Connect, depois em List Tools e, em seguida, em Call Tool com argumentos reais. Se o comando funcionar no Inspector mas falhar no agente, o erro está na configuração do seu cliente, não no servidor. Para servidores HTTP remotos, selecione o transporte Streamable HTTP, insira https://mcp.example.com/mcp, adicione o header Authorization e conecte — este é o método mais rápido para validar a autenticação e o proxy antes de testar com o agente.

Mantendo os servidores atualizados

O MCP evolui rápido, então aplique patches seguindo um cronograma. Servidores Node iniciados com npx -y buscam a versão mais recente em cada spawn; isso é conveniente, mas não é reprodutível. Fixe a versão exata que você testou — leia a versão em npm view @modelcontextprotocol/server-filesystem version e adicione-a ao nome do pacote em .mcp.json (@modelcontextprotocol/server-filesystem@<version>) — quando a estabilidade do servidor for importante, e atualize deliberadamente. Servidores Python sob systemd atualizam com sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" seguido de sudo systemctl restart mcp-ops. Monitore a revisão da especificação que seu SDK utiliza ao fazer o upgrade — uma mudança entre SSE e Streamable-HTTP pode alterar o transporte que seus clientes devem solicitar.

Failure modes, with the strings you will see

The agent shows the server failed. claude mcp list prints ✗ Failed to connect, and the TUI reports MCP server 'filesystem' failed to start. Run claude --debug and you will usually see Error: spawn npx ENOENT — the command is not on the agent's PATH. The runtime is missing or not where the agent looks: Node not installed, npx absent, or a virtualenv Python referenced by bare name. Fix the command to an absolute path or install the runtime, then reconnect.

A stdio server connects, then instantly drops. The client logs a JSON parse error — something like Unexpected token 'S', "Server sta"... is not valid JSON or Failed to parse message. The cause is always the same: the server wrote a log line to stdout. On stdio, stdout is the JSON-RPC channel, so any stray text corrupts the stream and the handshake dies. In Node, console.log goes to stdout — use console.error. In Python, a bare print() goes to stdout — write logs with logging configured to sys.stderr, or pass file=sys.stderr. The rule is absolute: on stdio, only JSON-RPC on stdout, everything human on stderr.

A remote server times out or closes mid-handshake. The client fails with MCP error -32000: Connection closed, or the Inspector hangs on Connect and never lists tools. Behind nginx this is buffering: the proxy holds the SSE stream instead of flushing it, so the client waits for a response that never arrives. Add proxy_buffering off; (and the rest of the block in Step 4) to the location. Confirm with curl -N against the public URL — you should see event data arrive incrementally, not all at once at the end.

Auth is rejected. The client reports Error POSTing to endpoint (HTTP 401) or plainly 401 Unauthorized. Either the header is missing, the token is wrong, or the shell variable was empty when the client read the config — a common trap, since ${MCP_TOKEN} expands to nothing if the variable is unset and nginx then sees Bearer with no value. Echo the variable, re-add the header, and verify the exact bytes match the token in the nginx if.

The service will not start under systemd. journalctl -u mcp-ops shows ModuleNotFoundError: No module named 'mcp'ExecStart points at the system Python instead of the venv interpreter. Or Address already in use — another process holds 8000; find it with sudo ss -ltnp | grep 8000.

FAQ

O que é exatamente um servidor MCP?

É um programa que expõe ferramentas e recursos para um cliente de IA via Model Context Protocol, utilizando JSON-RPC 2.0. O modelo de IA nunca executa a ferramenta diretamente — ele solicita ao cliente, o cliente chama o servidor MCP, e o servidor executa e retorna o resultado. Como o protocolo é padronizado, um único servidor funciona com qualquer cliente compatível, como Claude Code, Claude Desktop ou Gemini CLI.

Qual é a diferença entre o transporte stdio e HTTP?

Um servidor stdio é iniciado pelo cliente como um processo filho e se comunica via stdin/stdout; portanto, ele vive e morre junto com um único cliente em uma única máquina, sem necessidade de rede ou autenticação. Um servidor HTTP é um serviço de rede de longa duração que múltiplos clientes podem acessar simultaneamente, por isso exige TLS e autenticação. Use stdio para ferramentas locais de usuário único; use HTTP (Streamable HTTP em servidores atuais) para qualquer recurso compartilhado ou persistente.

Como eu protejo um servidor MCP remoto?

Considere que ele concede acesso a ferramentas em seus arquivos, banco de dados ou shell, e nunca o exponha sem autenticação. A melhor prática é mantê-lo vinculado ao localhost e acessá-lo via túnel SSH ou VPN privada; se ele precisar ser público, coloque-o atrás de um reverse proxy que exija um bearer token ou o fluxo MCP OAuth. Gere o token com openssl rand -hex 32 e nunca vincule o servidor ao 0.0.0.0 sem um desses mecanismos à frente.

Como eu depuro um servidor que não inicia?

Primeiro, verifique claude mcp list✗ Failed to connect com spawn ... ENOENT indica que o comando ou o runtime está ausente; corrija o path ou instale-o. Se ele conectar e depois cair com um erro de parse de JSON, o servidor está enviando logs para o stdout e corrompendo o stream JSON-RPC; mova todos os logs para o stderr. Para qualquer outro problema, execute o comando exato no MCP Inspector, que executa o servidor de forma isolada para que você possa distinguir um bug do servidor de um bug de configuração do cliente.