SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-27

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

Inizia con il server filesystem: è ufficiale, viene mantenuto attivamente e richiede soltanto Node. Il comando seguente lo registra in Claude Code e ne limita l'ambito al progetto corrente, in modo che la configurazione venga salvata in un file che puoi includere 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 segue è il comando che Claude Code eseguirà, non un'opzione di Claude Code. Il comando crea un file .mcp.json nella directory principale 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'agent legge .mcp.json, avvia npx -y @modelcontextprotocol/server-filesystem ... come processo figlio ed esegue l'handshake MCP tramite stdin/stdout di quel processo. Verifica che la configurazione sia stata applicata:

claude mcp list

Un server funzionante mostra il 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) e l'agent può ora chiamarli sui percorsi autorizzati. Per uno strumento database la procedura è la stessa: sostituisci il pacchetto e passa una stringa di connessione come argomento finale. Controlla però il repository del server per verificare il nome aggiornato del pacchetto, perché il server Postgres di riferimento ha cambiato manutentore più di una volta.

Questo è il motivo principale per eseguire l'agent sul server: la sessione Claude Code resta sul VPS all'interno di tmux e i relativi 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'agent dispone sia di write_file sia di read_file, conviene associare queste capacità a una skill che lo indirizzi verso la modifica minima funzionante, perché uno strumento filesystem rende una riscrittura estesa esattamente facile quanto una correzione di due righe. Lo stesso collegamento può essere esteso oltre i file locali: se sul VPS esegui già un motore di ricerca, puoi fornire all'agent una tua istanza SearXNG come strumento di ricerca, mantenendo le query sul server ma inserendo direttamente nel contesto dell'agent il testo non attendibile delle pagine, su cui l'agent agirà poi.

Passaggio 2: creare un server HTTP remoto

Un server stdio termina insieme al processo padre e viene avviato una volta per ogni client. Di conseguenza, se esegui due sessioni di Claude Code sul server che si assegnano attività a vicenda, ciascuna usa una copia privata dello strumento. Quando ti serve uno strumento sempre attivo e condiviso tra tutti i client, ad esempio uno strumento operativo condiviso, un gateway per database o un servizio utilizzato sia dal laptop sia dalla CI, devi usare 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.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 soltanto su localhost; dall'esterno del server non è possibile raggiungerlo direttamente. È esattamente il comportamento desiderato prima di configurare l'autenticazione. Installalo 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.

Passaggio 5: la regola di sicurezza fondamentale per questo argomento

Non esporre mai un endpoint MCP senza autenticazione. Un server MCP non è un'API di sola lettura. Concede accesso agli strumenti per i tuoi file, il tuo database e, in alcuni casi, una shell. Un /mcp aperto su Internet è un estraneo 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. Il valore di un token sottratto dipende anche dal server che si trova dietro l'endpoint: il server MCP di sola lettura distribuito con il tracker di allenamento openGym può restituire soltanto dati sugli allenamenti, mentre uno strumento per il filesystem o la shell consente di prendere il controllo del sistema.

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. Inseriscilo in una rete privata. Associa l'indirizzo del tunnel a una VPN WireGuard self-hosted e consenti l'accesso soltanto ai peer della 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: è semplice da configurare e blocca completamente i tentativi casuali di accesso:
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 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 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.

Ogni difesa descritta sopra protegge 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

Avvia un'interfaccia su http://localhost:6274. Nelle versioni recenti stampa un URL con una stringa di query MCP_PROXY_AUTH_TOKEN; usa quel link esatto, altrimenti l'interfaccia rifiuta la connessione. Avvia anche un proxy sulla porta 6277. Fai clic su Connect, quindi su List Tools e infine su Call Tool, usando argomenti reali. Se funziona in Inspector ma non nell'agent, il problema è nella configurazione del client, non nel server. Per il server HTTP remoto, seleziona il trasporto Streamable HTTP, inserisci https://mcp.example.com/mcp, aggiungi l'header Authorization e connettiti. È il modo più rapido per verificare che l'autenticazione e il proxy siano configurati correttamente prima di coinvolgere qualsiasi agent.

Mantenere aggiornati i server

MCP evolve rapidamente, quindi applica le patch secondo una pianificazione. I server Node avviati con npx -y scaricano l'ultima versione a ogni avvio. Questo è pratico, ma non garantisce build riproducibili. Fissa la versione esatta che hai testato. Leggila da npm view @modelcontextprotocol/server-filesystem version e aggiungila al nome del pacchetto in .mcp.json (@modelcontextprotocol/server-filesystem@<version>) quando un server diventa importante. Aggiorna poi la versione in modo deliberato. I server Python gestiti da 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. Quando esegui un aggiornamento, controlla la revisione della specifica a cui è destinato il tuo SDK. Un passaggio dal confine SSE–Streamable HTTP può cambiare il trasporto che i client devono richiedere.

Modalità di errore e stringhe visualizzate

L'agente segnala che il server non è riuscito ad avviarsi. claude mcp list stampa ✗ Failed to connect e la TUI mostra MCP server 'filesystem' failed to start. Eseguendo claude --debug, nella maggior parte dei casi viene visualizzato Error: spawn npx ENOENT: il comando non si trova nel PATH dell'agente. Il runtime manca oppure non si trova nel percorso in cui l'agente lo cerca: Node non è installato, npx è assente oppure è stato indicato un interprete Python di virtualenv usando il solo nome. Correggere il comando specificando un percorso assoluto oppure installare il runtime, quindi riconnettersi.

Un server stdio si connette e si disconnette subito dopo. 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 stdio, stdout è il canale JSON-RPC. Qualsiasi testo aggiuntivo corrompe il flusso e interrompe l'handshake. In Node, console.log scrive su stdout; usare console.error. In Python, un print() non qualificato scrive su stdout; scrivere i log con logging configurato su sys.stderr oppure passare file=sys.stderr. La regola è assoluta: su stdio, stdout deve contenere soltanto JSON-RPC. Tutto il testo destinato alle persone deve essere scritto 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 rimane bloccato su Connect e non elenca gli strumenti. Dietro nginx, la causa è il buffering: il proxy trattiene il flusso SSE invece di inviarlo progressivamente, quindi il client attende una risposta che non arriva. Aggiungere proxy_buffering off;, insieme al resto del blocco del Passaggio 4, a location. Verificare 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. Il problema può essere l'assenza dell'header, un token errato oppure una variabile della shell vuota quando il client ha letto la configurazione. È un errore comune: ${MCP_TOKEN} restituisce una stringa vuota se la variabile non è impostata e nginx vede quindi Bearer senza alcun valore. Visualizzare il contenuto della variabile, aggiungere nuovamente l'header e verificare che i byte corrispondano esattamente al token nel if di nginx.

Il servizio non si avvia con systemd. journalctl -u mcp-ops mostra ModuleNotFoundError: No module named 'mcp' e ExecStart punta al Python di sistema invece che all'interprete del virtualenv. Oppure viene mostrato Address already in use: un altro processo occupa la porta 8000. Individuarlo 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: invia una richiesta al client, il client chiama il server MCP e il server esegue l'operazione 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, resta attivo soltanto insieme a un client sulla stessa macchina e non richiede rete né autenticazione. Un server HTTP è un servizio di rete a esecuzione continua che può essere raggiunto contemporaneamente da più client. Per questo richiede TLS e autenticazione. Usa stdio per strumenti locali destinati a un singolo utente. Usa HTTP (Streamable HTTP sui server attuali) per qualsiasi servizio condiviso o persistente.

Come si protegge un server MCP remoto?

Considera che possa concedere accesso tramite strumenti ai tuoi file, al database o alla shell e non esporlo mai senza autenticazione. La soluzione migliore consiste nel vincolarlo a 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 associare mai il server a 0.0.0.0 senza una di queste protezioni.

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

Controlla prima claude mcp list. ✗ Failed to connect con spawn ... ENOENT indica che manca il comando o il runtime. Correggi quindi il percorso oppure installa il componente mancante. Se il server si connette e poi si disconnette con un errore di analisi JSON, sta scrivendo i log su stdout e sta corrompendo il flusso JSON-RPC. Sposta tutti i log su stderr. Per qualsiasi altro problema, esegui il comando esatto tramite MCP Inspector. Questo avvia il server in isolamento e consente di distinguere un bug del server da un problema nella configurazione del client.