SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor

Server MCP su VPS per agenti di coding AI

Configura server MCP stdio e HTTP remoto su un VPS con systemd, nginx, TLS e autenticazione. Evita errori JSON-RPC e endpoint esposti senza protezione.

Cosa stai realizzando

Due configurazioni MCP funzionanti su un unico VPS. Prima un server stdio, cioè uno strumento per il filesystem o il database che Claude Code avvia come processo figlio e con cui comunica tramite una pipe. Poi un server HTTP remoto che viene eseguito come servizio di rete persistente tramite systemd e nginx come reverse proxy con TLS, raggiungibile da qualsiasi client MCP configurato per usarlo. L'installazione di entrambe le configurazioni è semplice. La maggior parte di questa guida riguarda i due aspetti che causano realmente problemi: mantenere pulito il flusso JSON-RPC e non esporre mai su Internet un endpoint di strumenti senza autenticazione.

Che cos’è realmente MCP

Il Model Context Protocol è uno standard che consente a un client AI, come Claude Code, Claude Desktop, Gemini CLI su un VPS o un tuo script, di chiamare strumenti esterni e leggere risorse esterne. Il modello non esegue direttamente alcuna operazione. Invia una richiesta al client, il client usa JSON-RPC 2.0 per comunicare con un server MCP, il server esegue lo strumento e restituisce il risultato. Il client è il componente indicato quando si parla di agent harness: il ciclo che circonda il modello e gestisce l’elenco degli strumenti, i controlli delle autorizzazioni e lo stato della sessione. MCP è semplicemente il meccanismo con cui si estende la parte dedicata agli strumenti. Un solo protocollo permette quindi di usare con ogni client compatibile con MCP un server scritto una sola volta. Se questa separazione non ti è ancora chiara, in particolare il modo in cui un modello decide di usare uno strumento, vale la pena dedicare un’ora a un percorso graduale sui fondamenti degli agenti prima di fornire a uno di questi server credenziali reali.

Esistono due modalità di trasporto e il resto di questa guida segue questa distinzione:

  • stdio. Il client avvia il server come processo figlio e scambia messaggi JSON-RPC delimitati da newline tramite lo standard input e lo standard output. Non sono necessari rete, porte o autenticazione: il confine di attendibilità è il processo stesso. Quasi tutti gli strumenti locali vengono distribuiti in questo modo.
  • Streamable HTTP (e il suo predecessore, HTTP+SSE). Il server è un servizio web a esecuzione continua. Il client si connette tramite HTTP e il server può trasmettere le risposte come Server-Sent Events. Questo consente di condividere un solo server con più client oppure di eseguire uno strumento che deve rimanere permanentemente sul sistema.

Scegli stdio quando lo strumento appartiene a una sola macchina e a un solo utente. Scegli HTTP quando è un servizio condiviso.

Prerequisiti e aspetti da conoscere

Si presuppone un VPS KVM Ubuntu 24.04 appena installato, con accesso root o sudo. Oltre a questo:

  • Un runtime nel quale è scritto il server. La maggior parte dei server di riferimento usa Node o Python. Ubuntu 24.04 include Node 18, mentre diversi pacchetti MCP attuali richiedono Node 20 o versioni successive. Installare quindi una versione LTS attuale da NodeSource o tramite 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: TLS richiede un nome che risolva verso questo VPS. L'esempio stdio non richiede DNS.
  • 512 MB di RAM sono sufficienti. I server MCP sono processi JSON-RPC leggeri. Il consumo di memoria dipende da ciò che usa lo strumento, ad esempio un driver per database o una cache di file, non dal protocollo.
  • La specifica è recente e in evoluzione. La revisione 2025-03-26 ha sostituito HTTP+SSE con Streamable HTTP e ha contrassegnato SSE come deprecato. SSE continua a funzionare e molti server lo supportano ancora. Considerare quindi ogni vincolo sul trasporto come un elemento da verificare nuovamente nelle note di rilascio del server, non come un requisito immutabile.

Passaggio 1: collegare un server stdio a Claude Code

Iniziate con il server filesystem: è ufficiale, viene mantenuto attivamente e richiede soltanto Node. Il comando seguente lo registra in Claude Code e lo limita al progetto corrente, così la configurazione viene salvata in un file che può essere incluso nel repository:

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

Il separatore -- è importante: tutto ciò che lo segue è il comando che Claude Code eseguirà, non un flag di Claude Code. Il comando crea un file .mcp.json nella directory radice del progetto:

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

Non è ancora in esecuzione nulla. Al prossimo avvio di Claude Code in questa directory, l'agente legge .mcp.json, avvia npx -y @modelcontextprotocol/server-filesystem ... come processo figlio ed esegue l'handshake MCP tramite lo stdin e lo stdout di quel processo. Verificate che la configurazione sia stata applicata:

claude mcp list

Un server funzionante mostra 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_text_file, write_file, list_directory); l'agente può quindi richiamarli nei percorsi autorizzati. Uno strumento per database segue lo stesso schema: sostituite il pacchetto e passate una stringa di connessione come argomento finale. Controllate però il repository del server per verificare il nome attuale del pacchetto, perché il server Postgres di riferimento ha cambiato manutentore più di una volta.

Questo è il motivo principale per cui conviene eseguire l'agente sul server: la sessione di Claude Code risiede sul VPS all'interno di tmux e i server stdio vengono eseguiti accanto a essa, con accesso diretto ai file del progetto e ai servizi locali, senza round trip di rete. Quando l'agente dispone sia di write_file sia di read_text_file, conviene associare a questa capacità una skill che lo orienti verso la modifica minima funzionante, perché uno strumento filesystem rende una riscrittura estesa esattamente tanto semplice quanto una correzione di due righe. Lo stesso collegamento può essere esteso oltre i file locali: se sul VPS eseguite già un motore di ricerca, potete fornire all'agente la vostra istanza SearXNG come strumento di ricerca, mantenendo le query sul server ma inserendo direttamente nel contesto dell'agente il testo non attendibile delle pagine, su cui l'agente agirà poi.

Passaggio 2: creare un server HTTP remoto

Un server stdio termina insieme al processo padre e viene avviato una volta per client. Se esegui due sessioni di Claude Code sul server che si passano il lavoro, ciascuna usa una copia privata dello strumento. Quando vuoi uno strumento sempre attivo per ogni client, uno strumento operativo condiviso, un gateway per database o un servizio che vengano utilizzati sia dal laptop sia dalla pipeline CI, ti servono il trasporto HTTP e un servizio reale. Ecco un server Python minimale che usa l'SDK ufficiale ed espone uno strumento:

# /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". Il server è associato solo a localhost. Nessun client esterno al server può raggiungerlo direttamente, ed è esattamente il comportamento desiderato prima di introdurre l'autenticazione. Questa è la versione 2.x dell'SDK che pip install "mcp[cli]" installa attualmente (2.3.0 alla data del 2026-10-20). Ha rinominato FastMCP in MCPServer e ha spostato host e port dal costruttore a run(). L'importazione precedente mcp.server.fastmcp ora non riesce e restituisce ModuleNotFoundError. Installa il pacchetto in un virtualenv dedicato, in modo che systemd disponga di un percorso stabile per l'interprete:

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

Passaggio 3: mantenere il servizio attivo con systemd

Uno strumento non disponibile quando l'agent deve usarlo è peggio di uno strumento assente. Questo è particolarmente importante quando il client è a sua volta un processo di lunga durata: un agent sempre attivo che mantiene memoria e pianificazioni tra i riavvii chiamerà questi strumenti secondo una pianificazione, senza nessuno a controllare il sistema. Anche il server deve quindi riavviarsi autonomamente. 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 del Python del venv in ExecStart è obbligatorio. Impostalo su /usr/bin/python3, così il processo viene avviato con ModuleNotFoundError: No module named 'mcp', perché l'interprete di sistema non ha mai visto il tuo pip install. Abilita il servizio 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). curl restituisce HTTP/1.1 400 Bad Request con un errore JSON-RPC nel corpo: la richiesta non conteneva una sessione né un payload JSON valido. È esattamente il risultato desiderato: dimostra che la porta risponde e comunica tramite il protocollo. Connection refused o una risposta vuota indicano che il processo non è in ascolto dove previsto; leggi journalctl -u mcp-ops -n 50.

Passaggio 4: configura TLS e un reverse proxy davanti al servizio

Il server è in ascolto su localhost. Per raggiungerlo da qualsiasi rete, termina TLS su nginx e inoltra le richieste al servizio interno. Installa nginx, ottieni un certificato seguendo Certbot e Let's Encrypt su nginx, quindi scrivi il blocco location. Il punto fondamentale è disabilitare il buffering, perché il comportamento predefinito di nginx trattiene la risposta fino al suo completamento e blocca indefinitamente un flusso 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 esegui già una flotta di container, lo stesso compito viene svolto automaticamente da un reverse proxy Traefik con TLS automatico: emette il certificato e instrada le richieste in base al nome host, mentre tu devi soltanto aggiungere le label al container MCP. In entrambi i casi, il reverse proxy è ora l'unico componente esposto su una porta pubblica e inoltra le richieste a un servizio che non hai ancora protetto. Risolvi questo problema prima di registrare l'URL in qualsiasi posizione.

Passo 5: la regola di sicurezza che prevale su questo argomento

Non esporre mai un endpoint MCP senza autenticazione. Un server MCP non è un'API in sola lettura. Concede l'accesso agli strumenti, ai file, al database e talvolta a una shell. Un /mcp aperto su Internet è uno sconosciuto con lo stesso livello di accesso del tuo agente AI: può elencare gli strumenti e poi chiamarli. Trattalo esattamente come un socket amministrativo senza autenticazione, perché è questo che rappresenta. L'impatto di un token sottratto dipende anche dal server che si trova dietro l'endpoint: il server MCP in sola lettura incluso nel tracker di allenamento openGym può restituire soltanto dati sugli allenamenti, mentre uno strumento per il filesystem o la shell consente di accedere al server.

Tre difese, in ordine di preferenza:

  1. Non pubblicarlo. Mantieni il server su 127.0.0.1 e raggiungilo dal laptop tramite un tunnel SSH: ssh -L 8000:127.0.0.1:8000 matt@vps, quindi configura il client per usare http://127.0.0.1:8000/mcp. Nulla viene esposto.
  2. Collocalo su una rete privata. Associa l'indirizzo del tunnel a una VPN WireGuard self-hosted e consenti l'accesso soltanto ai peer VPN. Su Internet la porta risulta chiusa.
  3. Se deve essere pubblico, richiedi un token. La soluzione corretta è il flusso OAuth di MCP, supportato nativamente dal trasporto HTTP. Il minimo pratico è un token bearer condiviso, verificato dal proxy: è economico da configurare e blocca completamente gli accessi 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 associare mai direttamente il server a 0.0.0.0 senza una di queste protezioni davanti. Il client invia quindi il token in un 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 shell, in modo che il secret non venga mai scritto in chiaro in .mcp.json; Claude Code espande ${MCP_TOKEN} dall'ambiente al momento della lettura. Se la variabile non è impostata, Claude Code non sostituisce il valore con una stringa vuota: segnala la variabile mancante in claude mcp list e usa il testo letterale ${MCP_TOKEN}. Scegli anche un nome personalizzato, perché negli header di un server remoto Claude Code legge deliberatamente come vuote le proprie variabili e quelle delle credenziali del provider cloud, come ANTHROPIC_API_KEY, insieme alle altre credenziali presenti nell'ambiente, come NPM_TOKEN.

Tutte le difese precedenti proteggono l'endpoint, non l'agente che possiede già il token. Questa è l'altra metà del problema: se il client è DeepSeek Harness, i plugin che limitano gli strumenti che un agente può chiamare e analizzano l'output degli strumenti alla ricerca di istruzioni iniettate coprono questo aspetto.

Passaggio 6: eseguire il debug con MCP Inspector

Quando un server presenta anomalie, non fare supposizioni dall'interno dell'agent. Usalo direttamente tramite Inspector, il client di test ufficiale basato sul web. Per un server stdio, passagli lo stesso comando eseguito dall'agent:

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

Inspector pubblica la propria interfaccia tramite un web server sulla porta http://localhost:6274 e stampa un URL con una stringa di query MCP_INSPECTOR_API_TOKEN; apri quel collegamento invece di digitare l'indirizzo a memoria. Dalla versione v2 (la versione corrente è la 2.11.0 al 2026-10-20), il backend inserisce inoltre il token nella pagina a ogni caricamento. In questo modo un reload continua a funzionare e il token protegge le route /api/*, che possono avviare processi. Il proxy v1 separato sulla porta 6277 non esiste più, ma il web server non è l'unico listener: v2 avvia anche un sandbox per MCP Apps sulla porta MCP_SANDBOX_PORT (6275 per impostazione predefinita) e un server dedicato per l'origine delle app sulla porta MCP_APP_ORIGIN_PORT (6278 per impostazione predefinita). Quest'ultimo pubblica qualsiasi MCP App la cui risorsa UI dichiari _meta.ui.domain. Se accedi a Inspector tramite un tunnel SSH oppure lo esegui in un container e ti serve la scheda Apps, inoltra le porte 6275 e 6278 insieme alla 6274. Connettiti, apri la scheda Tools, seleziona uno strumento, inserisci argomenti reali ed eseguilo. Se funziona in Inspector ma non nell'agent, il problema è nella configurazione del client, non nel server. Per il server HTTP remoto, avvia Inspector senza un comando, aggiungi un server con il trasporto Streamable HTTP e https://mcp.example.com/mcp, inserisci l'header Authorization nelle relative impostazioni e connettiti. Questo è il modo più rapido per verificare che l'autenticazione e il proxy siano configurati correttamente prima di coinvolgere qualsiasi agent. Inspector salva gli header nel catalogo dei server esattamente come sono stati scritti; il token si trova quindi anche in ~/.mcp-inspector/mcp.json.

Mantenere aggiornati i server

MCP evolve rapidamente, quindi applicate le patch secondo una pianificazione. I server Node avviati con un npx -y non vincolato eseguono la versione risolta da npx: quella installata nel progetto locale, se presente, oppure una copia che npx installa nella cache di npm. -y salta soltanto la richiesta di installazione; non forza un aggiornamento. Di conseguenza, non seguite necessariamente l'ultima release e non sapete con certezza quale versione state eseguendo. Quando un server diventa importante, fissate la versione esatta che avete testato, leggetela da npm view @modelcontextprotocol/server-filesystem version e aggiungetela al nome del pacchetto in .mcp.json (@modelcontextprotocol/server-filesystem@<version>). Aggiornatela poi deliberatamente. Per i server Python gestiti da systemd, eseguite sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" seguito da sudo systemctl restart mcp-ops. Prima di un aggiornamento principale, leggete le note di rilascio: il passaggio dall'SDK Python 1.x al 2.x interrompe il codice 1.x già durante l'importazione e la guida alla migrazione dell'SDK raccomanda di mantenere un limite superiore <2, ad esempio "mcp[cli]<2", fino al completamento della migrazione. Durante l'aggiornamento, controllate la revisione della specifica supportata dall'SDK. Un passaggio attraverso la transizione da SSE a Streamable HTTP può cambiare il trasporto richiesto dai client.

Modalità di errore e messaggi visualizzati

L'agente segnala che il server non è riuscito ad avviarsi. claude mcp list stampa ✘ Failed to connect e le versioni attuali aggiungono a quella riga i dettagli dell'errore. Esegui claude --debug. Di solito vedrai Error: spawn npx ENOENT: il comando non è presente nel PATH dell'agente. Il runtime manca oppure non si trova nel percorso in cui l'agente lo cerca: Node non è installato, npx non esiste oppure è stato indicato un interprete Python di virtualenv usando solo il nome. Correggi il comando specificando un percorso assoluto oppure installa il runtime, quindi riconnetti l'agente.

Un server stdio si connette e poi si disconnette immediatamente. Il client registra un errore di analisi JSON, ad esempio 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 il flusso e interrompe l'handshake. In Node, console.log scrive su stdout; usa console.error. In Python, un print() non qualificato scrive su stdout. Scrivi i log con logging configurato su sys.stderr oppure passa file=sys.stderr. La regola è assoluta: in modalità stdio, su stdout deve passare solo JSON-RPC; tutto il testo destinato alle persone deve passare su stderr.

Un server remoto va in timeout o chiude la connessione durante l'handshake. Il client restituisce MCP error -32000: Connection closed oppure Inspector resta bloccato su Connect e non elenca mai gli strumenti. Con nginx la causa è il buffering: il proxy trattiene il flusso SSE invece di inviarlo subito, quindi il client attende una risposta che non arriva. Aggiungi proxy_buffering off; (insieme al resto del blocco del Passaggio 4) a location. Verifica con curl -N sull'URL pubblico. I dati degli eventi devono arrivare progressivamente, non tutti insieme al termine.

L'autenticazione viene rifiutata. Il client segnala Error POSTing to endpoint (HTTP 401) oppure semplicemente 401 Unauthorized. L'header può mancare, il token può essere errato oppure la variabile potrebbe non essere stata espansa: è un errore comune. Se MCP_TOKEN non è impostata quando il client legge la configurazione, Claude Code avvisa della variabile mancante in claude mcp list e invia il testo letterale. nginx vede quindi Bearer ${MCP_TOKEN}. Se il nome della variabile coincide con quello di un'altra credenziale di Claude Code, del provider cloud o dell'ambiente, la variabile viene invece letta come vuota e nginx vede Bearer senza alcun valore. Stampa la variabile, aggiungi di nuovo l'header e verifica che i byte corrispondano esattamente al token nella configurazione if di nginx.

Il servizio non si avvia con systemd. journalctl -u mcp-ops mostra ModuleNotFoundError: No module named 'mcp'; ExecStart punta al Python di sistema invece che all'interprete del virtualenv. In alternativa, Address already in use indica che un altro processo occupa la porta 8000. Individua il processo con sudo ss -ltnp | grep 8000.

FAQ

Che cos'è esattamente un server MCP?

È un programma che espone strumenti e risorse a un client AI tramite il Model Context Protocol, usando JSON-RPC 2.0. Il modello AI non esegue direttamente lo strumento: lo richiede al client, il client chiama il server MCP, quindi il server lo esegue e restituisce un risultato. Poiché il protocollo è standard, un server funziona con qualsiasi client compatibile, ad esempio 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. Di conseguenza, rimane attivo finché è attivo un client su una macchina e non richiede rete né autenticazione. Un server HTTP è un servizio di rete a esecuzione prolungata, raggiungibile contemporaneamente da più client. Per questo richiede TLS e autenticazione. Usa stdio per gli strumenti locali destinati a un singolo utente. Usa HTTP (Streamable HTTP nei server attuali) per qualsiasi servizio condiviso o persistente.

Come si protegge un server MCP remoto?

Considera che concede accesso tramite strumenti ai tuoi file, al database o alla shell e non esporlo mai senza autenticazione. La soluzione migliore consiste nel mantenerlo in ascolto su localhost e raggiungerlo 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 OAuth di MCP. Genera il token con openssl rand -hex 32 e non mettere mai il server in ascolto su 0.0.0.0 senza una di queste protezioni.

Come si esegue il troubleshooting di un server che non si avvia?

Per prima cosa controlla claude mcp list, ✘ Failed to connect con spawn ... ENOENT indica che manca il comando o il runtime. Correggi quindi il percorso o installa il componente mancante. Se il server si connette e poi interrompe la connessione con un errore di analisi JSON, significa che scrive i log su stdout e corrompe il flusso JSON-RPC. Sposta tutti i log su stderr. Per qualsiasi altro problema, esegui il comando esatto tramite MCP Inspector. Questo strumento esegue il server in isolamento e consente di distinguere un problema del server da un errore nella configurazione del client.