SSD Nodes Learn
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-07-24

Installare MCP server su VPS per AI agent

Configura server MCP via stdio o HTTP su VPS. Guida pratica su systemd, TLS e sicurezza per integrare tool esterni in Claude Code e altri agenti AI.

Cosa stai costruendo

Due configurazioni MCP funzionanti su un unico VPS. La prima è un server stdio — uno strumento per filesystem o database che Claude Code avvia come processo figlio e con cui comunica tramite una pipe. La seconda è un server remote HTTP che opera come servizio di rete persistente tramite systemd e un reverse proxy nginx con TLS, raggiungibile da qualsiasi client MCP configurato per puntare ad esso. L'installazione per entrambi è rapida. La maggior parte di questa guida riguarda i due aspetti critici: mantenere pulito lo stream JSON-RPC e non esporre mai endpoint di strumenti non autenticati su internet.

Cos'è effettivamente MCP

Il Model Context Protocol è uno standard che permette a un client AI — come Claude Code, Claude Desktop, il Gemini CLI su un VPS o uno script personalizzato — di chiamare tool esterni e leggere risorse esterne. Il modello non esegue alcun comando direttamente. Il modello interroga il client; il client comunica via JSON-RPC 2.0 a un server MCP; il server esegue il tool e restituisce il risultato. Un unico protocollo permette a un server scritto una sola volta di funzionare con ogni client compatibile con MCP.

Esistono due tipi di trasporto; il resto di questa guida è suddiviso in base ad essi:

  • stdio. Il client avvia il server come processo figlio e scambia messaggi JSON-RPC delimitati da nuova riga tramite standard input e standard output. Nessuna rete, nessuna porta, nessuna autenticazione: il perimetro di sicurezza è il processo stesso. Quasi tutti i tool locali utilizzano questo metodo.
  • Streamable HTTP (e la versione precedente, HTTP+SSE). Il server è un servizio web sempre attivo. Il client si connette via HTTP e il server può inviare risposte tramite Server-Sent Events. Questo metodo permette di condividere un server con più client o di eseguire un tool che deve risiedere permanentemente sul sistema.

Utilizza stdio quando il tool è limitato a una singola macchina e a un singolo utente. Utilizza HTTP quando si tratta di un servizio condiviso.

Prerequisiti e potenziali criticità

Si assume l'utilizzo di un VPS Ubuntu 24.04 KVM appena installato con privilegi root o sudo. Oltre a questo:

  • Un runtime per il server. La maggior parte dei server di riferimento utilizza Node o Python. Ubuntu 24.04 include Node 18, ma diversi pacchetti MCP attuali richiedono Node 20 o versioni successive; si consiglia di installare una versione LTS recente tramite NodeSource o nvm invece di affidarsi a apt. Python 3.12 è già presente.
  • Un dominio e un record DNS A, ma solo per il server HTTP remoto; il TLS richiede un nome che risolva verso questo VPS. L'esempio stdio non richiede alcun DNS.
  • 512 MB di RAM sono sufficienti. I server MCP sono processi JSON-RPC leggeri; il consumo di memoria dipende dagli strumenti utilizzati (un driver del database, una cache dei file) e non dal protocollo.
  • Lo spec è recente e in evoluzione. La revisione del 2025-03-26 ha sostituito HTTP+SSE con Streamable HTTP e ha marcato SSE come deprecato. SSE è ancora funzionante e molti server lo supportano ancora; pertanto, non considerare ogni vincolo di trasporto come assoluto, ma verifica sempre le note di rilascio del server.

Step 1: integra un server stdio in Claude Code

Inizia con il filesystem server: è ufficiale, viene mantenuto attivamente e richiede solo Node. Il comando seguente lo registra in Claude Code e lo limita al progetto corrente, salvando la configurazione in un file committabile:

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

Il separatore -- è fondamentale: tutto ciò che segue è il comando che Claude Code eseguirà, non un flag per Claude Code. Questa operazione scrive un .mcp.json nella root del progetto:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/home/matt/projects/api"
      ]
    }
  }
}

Nessun processo è ancora in esecuzione. Al prossimo avvio di Claude Code in questa directory, l'agent leggerà .mcp.json, avvierà npx -y @modelcontextprotocol/server-filesystem ... come processo child e eseguirà l'handshake MCP tramite lo stdin/stdout di tale processo. Verifica l'avvenuta configurazione:

claude mcp list

Un server funzionante stampa il proprio comando e un segno di spunta verde — filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. All'interno della sessione, il comando slash /mcp elenca gli strumenti esposti dal server (read_file, write_file, list_directory); l'agent può ora invocarli sui percorsi autorizzati. Un tool per database segue la stessa struttura — sostituisci il pacchetto e passa una stringa di connessione come ultimo argomento — ma verifica il nome del pacchetto nel repository ufficiale, poiché il server Postgres di riferimento è stato aggiornato più volte.

Questo è l'obiettivo principale di eseguire l'agent sulla macchina: la sessione di Claude Code risiede sul VPS all'interno di tmux, e i suoi server stdio vengono eseguiti accanto ad essa con accesso diretto ai file del progetto e ai servizi locali, senza latenza di rete.

Step 2: build a remote HTTP server

Un server stdio termina quando termina il processo padre. Se è necessario uno strumento che rimanga attivo per ogni client — come un tool di gestione condiviso, un gateway per database o un servizio chiamato sia dal laptop che dalla CI — è necessario utilizzare il trasporto HTTP e un servizio reale. Di seguito è riportato un server Python minimale che utilizza l'SDK ufficiale ed espone un singolo tool:

# /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")

Nota host="127.0.0.1". Il server è in ascolto solo su localhost — nessun dispositivo esterno può connettersi direttamente, che è il comportamento desiderato prima dell'implementazione dell'autenticazione. Installalo in un virtualenv dedicato per garantire a systemd un percorso dell'interprete stabile:

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

Step 3: mantieni il processo attivo con systemd

Uno strumento non funzionante quando l'agent tenta di interagirvi è peggiore di un'assenza di strumento. Scrivi /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

Il percorso assoluto al Python venv in ExecStart è obbligatorio — punta a /usr/bin/python3 e il processo partirà con ModuleNotFoundError: No module named 'mcp', poiché l'interprete di sistema non riconosce il tuo pip install. Abilita e verifica:

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 dovrebbe restituire active (running). Il curl ritorna con HTTP/1.1 400 Bad Request con un errore JSON-RPC nel corpo — la richiesta non conteneva una sessione né un payload JSON valido — ed è esattamente ciò che serve: dimostra che la porta risponde e utilizza il protocollo. Connection refused o una risposta vuota indicano che il processo non è in ascolto sull'indirizzo previsto; leggi journalctl -u mcp-ops -n 50.

Step 4: configurare TLS e un reverse proxy

Il server è in ascolto su localhost. Per renderlo accessibile dall'esterno, è necessario terminare la connessione TLS su nginx e instradare il traffico verso l'interno. Installa nginx, ottieni un certificato tramite Certbot e Let's Encrypt su nginx, quindi configura il blocco location. È fondamentale disabilitare il buffering. Il comportamento predefinito di nginx è mantenere la risposta in memoria fino al completamento; questo blocca definitivamente uno stream SSE:

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;
    }
}

Ricarica la configurazione con sudo nginx -t && sudo systemctl reload nginx. Se utilizzi già un cluster di container, puoi delegare questo compito a un reverse proxy Traefik con TLS automatico — questo emette il certificato e instrada il traffico tramite hostname; è sufficiente aggiungere le label al container MCP. In entrambi i casi, il reverse proxy è l'unico elemento esposto su una porta pubblica e punta verso un servizio non ancora protetto. Configura la sicurezza prima di registrare l'URL in qualsiasi servizio.

Step 5: la regola di sicurezza fondamentale per questo argomento

Non esporre mai un endpoint MCP non autenticato. Un server MCP non è una API in sola lettura. Esso concede l'accesso a tool — ai tuoi file, al tuo database, a volte a una shell. Un /mcp aperto su internet pubblico è un estraneo con lo stesso raggio d'azione del tuo agente AI: elencano i tuoi tool e poi li chiamano. Trattalo esattamente come un socket admin non autenticato, perché è proprio questo.

Tre difese, in ordine di preferenza:

  1. Non pubblicarlo. Mantieni il server su 127.0.0.1 e connettiti dal tuo laptop tramite un tunnel SSH: ssh -L 8000:127.0.0.1:8000 matt@vps, quindi punta il client su http://127.0.0.1:8000/mcp. Nulla viene esposto.
  2. Inseriscilo in una rete privata. Vincola l'indirizzo del tunnel di un self-hosted WireGuard VPN e permetti l'accesso solo ai peer della VPN. Internet pubblico vedrà una porta chiusa.
  3. Se deve essere pubblico, richiede un token. La soluzione corretta è il flusso MCP OAuth supportato nativamente dal trasporto HTTP. Il minimo indispensabile è un bearer token condiviso controllato dal proxy — economico, e blocca completamente gli attacchi casuali:
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...
}

Genera il token con openssl rand -hex 32 e non vincolare mai il server stesso a 0.0.0.0 senza uno di questi sistemi davanti. Il client invierà quindi il token come header. In Claude Code:

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

Imposta MCP_TOKEN nella tua shell in modo che il segreto non venga salvato in .mcp.json in chiaro — Claude Code espande ${MCP_TOKEN} dall'ambiente al momento della lettura.

Step 6: debug con l'MCP Inspector

Quando un server non funziona correttamente, non tentare di indovinare dall'interno dell'agent. Utilizza l'Inspector, il client di test ufficiale basato su web. Per un server stdio, utilizza lo stesso comando eseguito dall'agent:

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

L'operazione avvia un'interfaccia UI su http://localhost:6274 (le versioni recenti stampano un URL con una query string MCP_PROXY_AUTH_TOKEN; utilizza esattamente quel link o l'UI rifiuterà la connessione) e un proxy sulla porta 6277. Clicca su Connect, poi su List Tools, e infine su Call Tool inserendo gli argomenti reali. Se l'operazione ha successo nell'Inspector ma fallisce nell'agent, il bug risiede nella configurazione del client e non nel server. Per i server HTTP remoti, seleziona il trasporto Streamable HTTP, inserisci https://mcp.example.com/mcp, aggiungi l'header Authorization e connettiti: questo è il metodo più rapido per verificare che l'autenticazione e il proxy siano corretti prima di coinvolgere l'agent.

Mantenere i server aggiornati

MCP evolve rapidamente, quindi applica le patch seguendo un programma prestabilito. I server Node avviati con npx -y scaricano l'ultima versione a ogni avvio; questo è comodo ma non garantisce la riproducibilità. Blocca la versione esatta che hai testato: leggi il valore da npm view @modelcontextprotocol/server-filesystem version e aggiungilo al nome del pacchetto in .mcp.json (@modelcontextprotocol/server-filesystem@<version>). Una volta che un server è in produzione, aggiornalo deliberatamente. I server Python sotto systemd si aggiornano con sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" seguito da sudo systemctl restart mcp-ops. Controlla la revisione dello spec target del tuo SDK durante l'aggiornamento: un passaggio oltre il confine SSE-to-Streamable-HTTP può cambiare il protocollo di trasporto richiesto dai client.

Modalità di errore e stringhe visualizzate

L'agent indica che il server è fallito. claude mcp list stampa ✗ Failed to connect e la TUI riporta MCP server 'filesystem' failed to start. Eseguendo claude --debug si visualizza solitamente Error: spawn npx ENOENT — il comando non è presente nel PATH dell'agent. Il runtime è assente o non si trova nel percorso cercato dall'agent: Node non è installato, npx è assente, oppure è stato referenziato un venv Python tramite nome semplice. Utilizzare il percorso assoluto per il comando o installare il runtime, quindi riconnettere.

Un server stdio si connette e si disconnette istantaneamente. Il client registra un errore di parsing JSON — simile a Unexpected token 'S', "Server sta"... is not valid JSON o Failed to parse message. La causa è sempre la stessa: il server ha scritto una riga di log su stdout. In modalità stdio, stdout è il canale JSON-RPC; qualsiasi testo estraneo corrompe lo stream e interrompe l'handshake. In Node, console.log viene inviato su stdout — utilizzare console.error. In Python, un semplice print() viene inviato su stdout — scrivere i log tramite logging configurato su sys.stderr, oppure passare file=sys.stderr. La regola è assoluta: in modalità stdio, solo JSON-RPC su stdout, tutto il testo leggibile da umani su stderr.

Un server remoto va in timeout o chiude durante l'handshake. Il client fallisce con MCP error -32000: Connection closed, oppure l'Inspector si blocca su Connect e non elenca gli strumenti. Se si utilizza nginx, il problema è il buffering: il proxy trattiene lo stream SSE invece di inviarlo immediatamente, quindi il client attende una risposta che non arriva mai. Aggiungere proxy_buffering off; (e il resto del blocco nello Step 4) alla location. Verificare con curl -N puntando all'URL pubblico — i dati degli eventi devono arrivare incrementalmente e non tutti insieme alla fine.

L'autenticazione viene rifiutata. Il client riporta Error POSTing to endpoint (HTTP 401) o semplicemente 401 Unauthorized. L'header è mancante, il token è errato, oppure la variabile shell era vuota quando il client ha letto la configurazione — un errore comune, poiché ${MCP_TOKEN} non espande nulla se la variabile non è impostata e nginx vede quindi Bearer senza valore. Verificare la variabile con echo, riaggiungere l'header e confermare che i byte corrispondano esattamente al token nel if di nginx.

Il servizio non si avvia sotto systemd. journalctl -u mcp-ops mostra ModuleNotFoundError: No module named 'mcp'ExecStart punta al Python di sistema invece dell'interprete del venv. Oppure Address already in use — un altro processo occupa la porta 8000; individuarlo con sudo ss -ltnp | grep 8000.

FAQ

Cos'è esattamente un server MCP?

È un programma che espone tool e risorse a un client AI tramite il Model Context Protocol, utilizzando JSON-RPC 2.0. Il modello AI non esegue mai direttamente il tool: richiede l'esecuzione al client, il client chiama il server MCP, il server esegue il comando e restituisce il risultato. Poiché il protocollo è standard, un singolo server è compatibile con qualsiasi client conforme, come Claude Code, Claude Desktop o Gemini CLI.

Qual è la differenza tra il trasporto stdio e HTTP?

Un server stdio viene avviato dal client come processo figlio e comunica tramite stdin/stdout; pertanto, il server esiste solo finché è attivo il client sulla stessa macchina e non richiede rete o autenticazione. Un server HTTP è un servizio di rete persistente a cui possono connettersi più client contemporaneamente; per questo motivo richiede TLS e autenticazione. Utilizzare stdio per tool locali per un singolo utente; utilizzare HTTP (Streamable HTTP sugli attuali server) per qualsiasi servizio condiviso o persistente.

Come posso mettere in sicurezza un server MCP remoto?

Considera che il server concede l'accesso ai tool per i tuoi file, database o shell; non esporlo mai senza autenticazione. La soluzione migliore è limitare il server a localhost e connettersi tramite un tunnel SSH o una VPN privata; se deve essere pubblico, posizionalo dietro un reverse proxy che imponga un bearer token o il flusso MCP OAuth. Genera il token con openssl rand -hex 32 e non vincolare mai il server a 0.0.0.0 senza uno di questi sistemi di protezione.

Come posso eseguire il debug di un server che non si avvia?

Controlla prima claude mcp list✗ Failed to connect con spawn ... ENOENT indica che il comando o il runtime sono mancanti; correggi il path o installa il componente. Se il server si connette ma poi si interrompe con un errore di parsing JSON, il server sta scrivendo i log su stdout corrompendo lo stream JSON-RPC; sposta tutti i log su stderr. Per qualsiasi altro problema, esegui il comando esatto tramite l'MCP Inspector, che avvia il server in isolamento per distinguere un bug del server da un errore di configurazione del client.