SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-27

Héberger des serveurs MCP sur un VPS pour vos agents IA

Configurez des serveurs MCP stdio et HTTP sur un VPS avec systemd, nginx, TLS et authentification, en évitant les erreurs JSON-RPC et les endpoints exposés.

Ce que vous allez mettre en place

Deux configurations MCP fonctionnelles sur un seul VPS. D’abord, un serveur stdio : un outil de système de fichiers ou de base de données que Claude Code lance comme processus enfant et avec lequel il communique via un pipe. Ensuite, un serveur HTTP distant qui s’exécute comme service réseau permanent derrière systemd et un reverse proxy nginx avec TLS. Il est accessible depuis n’importe quel client MCP que vous configurez pour l’utiliser. L’installation de chacune de ces configurations est simple. L’essentiel de ce guide porte sur les deux points qui posent réellement problème : garder le flux JSON-RPC propre et ne jamais exposer un endpoint d’outil sans authentification sur Internet.

Ce qu’est réellement MCP

Le Model Context Protocol est un moyen standard pour qu’un client d’IA, Claude Code, Claude Desktop, le Gemini CLI sur un VPS ou votre propre script, appelle des outils externes et lise des ressources externes. Le modèle lui-même n’exécute rien. Il envoie une demande au client, le client échange en JSON-RPC 2.0 avec un serveur MCP, puis le serveur exécute l’outil et renvoie le résultat. C’est ce client que l’on désigne par agent harness : la boucle autour du modèle qui gère la liste des outils, les vérifications d’autorisation et l’état de la session. MCP sert simplement à étendre la partie dédiée aux outils. Un seul protocole suffit : un serveur que vous écrivez une fois fonctionne avec tous les clients compatibles avec MCP. Si cette séparation vous est nouvelle, et notamment si vous vous demandez comment un modèle décide d’utiliser un outil, un parcours progressif des fondamentaux des agents mérite une heure avant de fournir de véritables identifiants à l’un de ces serveurs.

Il existe deux transports, et toute la suite de ce guide s’articule autour d’eux :

  • stdio. Le client lance le serveur comme processus enfant et échange des messages JSON-RPC délimités par des retours à la ligne via son entrée et sa sortie standard. Il n’y a ni réseau, ni port, ni authentification : la limite de confiance est le processus lui-même. Presque tous les outils locaux sont distribués de cette manière.
  • Streamable HTTP (et son prédécesseur, HTTP+SSE). Le serveur est un service web exécuté en continu. Le client se connecte via HTTP, et le serveur peut diffuser les réponses sous forme de Server-Sent Events. C’est le mode à utiliser pour partager un serveur entre plusieurs clients ou exécuter un outil qui doit rester en permanence sur la machine.

Choisissez stdio lorsque l’outil est destiné à une seule machine et à un seul utilisateur. Choisissez HTTP lorsqu’il s’agit d’un service partagé.

Prérequis et pièges à connaître

Partez du principe que vous disposez d’un VPS KVM Ubuntu 24.04 fraîchement installé, avec un accès root ou sudo. Voici les autres éléments nécessaires :

  • Un runtime dans lequel le serveur est écrit. La plupart des serveurs de référence utilisent Node ou Python. Ubuntu 24.04 fournit Node 18, mais plusieurs packages MCP actuels exigent Node 20 ou une version ultérieure. Installez donc une version LTS récente avec NodeSource ou nvm, plutôt que de vous fier à apt. Python 3.12 est déjà présent.
  • Un domaine et un enregistrement DNS A, mais uniquement pour le serveur HTTP distant : TLS nécessite un nom qui résout vers ce VPS. L’exemple stdio ne nécessite aucun DNS.
  • 512 MB de RAM suffisent largement. Les serveurs MCP sont de petits processus JSON-RPC. La consommation mémoire dépend de ce que votre outil utilise, par exemple un pilote de base de données ou un cache de fichiers, et non du protocole.
  • La spécification est récente et évolue encore. La révision du 2025-03-26 a remplacé HTTP+SSE par Streamable HTTP et a marqué SSE comme obsolète. SSE fonctionne encore et de nombreux serveurs l’utilisent toujours. Considérez donc tout choix de transport comme un élément à revérifier dans les notes de version du serveur, et non comme une règle absolue.

Étape 1 : connecter un serveur stdio à Claude Code

Commencez par le serveur de système de fichiers. Il est officiel, activement maintenu et ne nécessite que Node. La commande ci-dessous l’enregistre auprès de Claude Code et le limite au projet courant, afin que la configuration soit stockée dans un fichier pouvant être versionné :

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

Le séparateur -- est important : tout ce qui le suit constitue la commande que Claude Code exécutera, et non une option de Claude Code. Cette commande crée un fichier .mcp.json à la racine du projet :

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

Rien ne s’exécute encore. Lorsque vous démarrerez Claude Code dans ce répertoire, l’agent lira .mcp.json, lancera npx -y @modelcontextprotocol/server-filesystem ... comme processus enfant, puis effectuera la négociation MCP via l’entrée et la sortie standard de ce processus. Vérifiez que la configuration a été prise en compte :

claude mcp list

Un serveur fonctionnel affiche sa commande et une coche verte, filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Dans la session, la commande slash /mcp liste les outils exposés par le serveur (read_file, write_file, list_directory). L’agent peut alors les appeler sur les chemins que vous avez autorisés. Un outil de base de données suit le même modèle : remplacez le paquet et transmettez une chaîne de connexion comme dernier argument. Consultez toutefois le dépôt du serveur pour connaître le nom actuel du paquet, car le serveur Postgres de référence a changé de mainteneur plusieurs fois.

C’est tout l’intérêt d’exécuter l’agent sur la machine elle-même : la session Claude Code s’exécute sur le VPS dans tmux, et ses serveurs stdio s’exécutent juste à côté avec un accès direct aux fichiers du projet et aux services locaux, sans aller-retour réseau. Une fois que l’agent dispose de write_file et de read_file, il est utile d’associer cet accès à une compétence qui l’oriente vers la modification minimale fonctionnelle, car un outil de système de fichiers rend une réécriture étendue exactement aussi peu coûteuse qu’une correction de deux lignes. Cette connexion ne se limite pas aux fichiers locaux : si vous exécutez déjà un moteur de recherche sur le VPS, vous pouvez fournir à l’agent votre propre instance SearXNG comme outil de recherche. Les requêtes restent ainsi sur votre machine, tandis que le texte non fiable des pages est injecté directement dans le contexte sur lequel l’agent agit ensuite.

Étape 2 : créer un serveur HTTP distant

Un serveur stdio s’arrête avec son processus parent et est lancé une fois par client. Si vous exécutez deux sessions Claude Code sur le serveur qui se transmettent du travail, chacune utilise sa propre copie privée de l’outil. Pour disposer d’un outil qui reste actif pour tous les clients, d’un outil d’exploitation partagé, d’une passerelle de base de données ou d’un service appelé à la fois par votre portable et par votre CI, vous devez utiliser le transport HTTP et un service réel. Voici un serveur Python minimal qui utilise le SDK officiel et expose un outil :

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

Notez host="127.0.0.1". Le serveur écoute uniquement sur localhost. Rien à l’extérieur du serveur ne peut y accéder directement, ce qui est exactement ce qu’il faut avant la mise en place de l’authentification. Installez-le dans son propre virtualenv afin que systemd dispose d’un chemin stable vers l’interpréteur :

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

Étape 3 : maintenir le service avec systemd

Un outil qui est arrêté lorsque l’agent en a besoin est pire que l’absence d’outil. Cela compte surtout lorsque le client est lui-même un processus de longue durée : un agent toujours actif qui conserve sa mémoire et ses planifications après les redémarrages appellera ces outils selon un calendrier, sans personne pour le surveiller. Le serveur doit donc également redémarrer de lui-même. Écrivez /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

Le chemin absolu vers Python dans l’environnement virtuel de ExecStart est obligatoire. Pointez-le vers /usr/bin/python3 afin que le processus démarre avec ModuleNotFoundError: No module named 'mcp', car l’interpréteur système n’a jamais vu votre pip install. Activez et vérifiez :

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 doit afficher active (running). curl renvoie HTTP/1.1 400 Bad Request avec une erreur JSON-RPC dans le corps de la réponse : la requête ne contenait aucune session ni charge utile JSON valide. C’est exactement le résultat attendu : il prouve que le port répond et utilise le protocole. Connection refused ou une réponse vide signifie que le processus n’est pas lié à l’adresse attendue. Consultez journalctl -u mcp-ops -n 50.

Étape 4 : placer TLS et un reverse proxy en frontal

Le serveur écoute sur localhost. Pour y accéder depuis n’importe où, terminez TLS sur nginx, puis faites suivre les requêtes vers le serveur. Installez nginx, obtenez un certificat avec Certbot et Let’s Encrypt sur nginx, puis écrivez le bloc location. Le point essentiel consiste à désactiver la mise en tampon, car le comportement par défaut de nginx conserve une réponse jusqu’à sa réception complète, ce qui bloque indéfiniment un flux 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;
    }
}

Rechargez la configuration avec sudo nginx -t && sudo systemctl reload nginx. Si vous exécutez déjà un ensemble de conteneurs, un reverse proxy Traefik avec TLS automatique effectue la même tâche : il délivre le certificat et achemine les requêtes selon le nom d’hôte. Il vous suffit d’ajouter des labels au conteneur MCP. Dans les deux cas, le reverse proxy est désormais le seul service exposé sur un port public, et il pointe vers un service que vous n’avez pas encore sécurisé. Corrigez ce problème avant d’enregistrer l’URL où que ce soit.

Étape 5 : la règle de sécurité qui domine ce sujet

N’exposez jamais un endpoint MCP sans authentification. Un serveur MCP n’est pas une API en lecture seule. Il donne accès à des outils, à vos fichiers, à votre base de données et parfois à un shell. Un /mcp ouvert sur Internet est accessible à n’importe qui avec les mêmes droits que votre agent IA : un tiers peut lister vos outils, puis les appeler. Traitez-le exactement comme un socket d’administration sans authentification, car c’est ce qu’il est. L’impact d’un token volé dépend aussi du serveur placé derrière : le serveur MCP en lecture seule fourni avec le suivi d’entraînement openGym ne peut renvoyer que des données d’entraînement, tandis qu’un outil d’accès au système de fichiers ou à un shell donne le contrôle de la machine.

Trois protections, par ordre de préférence :

  1. Ne le publiez pas. Laissez le serveur écouter sur 127.0.0.1 et accédez-y depuis votre ordinateur portable avec un tunnel SSH : ssh -L 8000:127.0.0.1:8000 matt@vps, puis configurez le client pour utiliser http://127.0.0.1:8000/mcp. Rien n’est exposé.
  2. Placez-le sur un réseau privé. Liez le tunnel à l’adresse d’un VPN WireGuard auto-hébergé et autorisez uniquement les pairs VPN à y accéder. Internet voit un port fermé.
  3. S’il doit être public, exigez un token. La solution appropriée est le flux OAuth de MCP, pris en charge nativement par le transport HTTP. Le minimum pragmatique consiste à utiliser un bearer token partagé, vérifié au niveau du proxy. C’est peu coûteux et cela bloque complètement les accès opportunistes :
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...
}

Générez le token avec openssl rand -hex 32, et ne liez jamais directement le serveur à 0.0.0.0 sans placer l’une de ces protections devant lui. Le client envoie ensuite le token dans un en-tête. Dans Claude Code :

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

Définissez MCP_TOKEN dans votre shell afin que le secret ne soit jamais écrit en clair dans .mcp.json. Claude Code développe ${MCP_TOKEN} depuis l’environnement au moment de la lecture.

Toutes les protections ci-dessus sécurisent l’endpoint, et non l’agent qui détient déjà le token. C’est l’autre moitié du problème : si votre client est DeepSeek Harness, les plugins qui contrôlent les outils qu’un agent peut appeler et analysent la sortie des outils à la recherche d’instructions injectées couvrent cet aspect.

Étape 6 : diagnostiquer avec MCP Inspector

Lorsqu’un serveur se comporte de manière inattendue, ne cherchez pas la cause depuis l’agent. Pilotez-le directement avec Inspector, le client de test officiel accessible depuis le Web. Pour un serveur stdio, transmettez-lui la même commande que celle exécutée par l’agent :

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

L’interface démarre sur http://localhost:6274 (les versions récentes affichent une URL avec une chaîne de requête MCP_PROXY_AUTH_TOKEN ; utilisez ce lien exact, sinon l’interface vous refuse l’accès) et un proxy écoute sur 6277. Cliquez sur Connect, puis sur List Tools, et enfin sur Call Tool avec des arguments réels. Si cela fonctionne dans Inspector, mais échoue dans l’agent, le problème vient de la configuration de votre client, pas du serveur. Pour le serveur HTTP distant, sélectionnez le transport Streamable HTTP, saisissez https://mcp.example.com/mcp, ajoutez l’en-tête Authorization, puis connectez-vous. C’est le moyen le plus rapide de vérifier que l’authentification et le proxy sont correctement configurés avant toute intervention de l’agent.

Mettre les serveurs à jour

MCP évolue rapidement : appliquez les correctifs selon un calendrier. Les serveurs Node lancés avec npx -y récupèrent la dernière version à chaque création de processus. Cette approche est pratique, mais elle n’est pas reproductible. Épinglez la version exacte que vous avez testée. Lisez-la depuis npm view @modelcontextprotocol/server-filesystem version et ajoutez-la au nom du paquet dans .mcp.json (@modelcontextprotocol/server-filesystem@<version>) dès qu’un serveur devient important. Faites ensuite évoluer cette version volontairement. Pour les serveurs Python gérés par systemd, mettez à jour avec sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]", puis avec sudo systemctl restart mcp-ops. Vérifiez la révision de la spécification ciblée par votre SDK lors d’une mise à niveau. Un passage de SSE à Streamable HTTP peut modifier le transport que vos clients doivent demander.

Modes d’échec et messages affichés

L’agent indique que le serveur a échoué. claude mcp list affiche ✗ Failed to connect et la TUI signale MCP server 'filesystem' failed to start. Exécutez claude --debug. Vous verrez généralement Error: spawn npx ENOENT : la commande ne se trouve pas dans le PATH de l’agent. Le runtime est absent ou installé à un emplacement différent de celui recherché par l’agent : Node n’est pas installé, npx est absent ou un Python de virtualenv est référencé par un nom simple. Corrigez la commande en indiquant un chemin absolu ou installez le runtime, puis reconnectez-vous.

Un serveur stdio se connecte, puis se déconnecte immédiatement. Le client journalise une erreur d’analyse JSON, par exemple Unexpected token 'S', "Server sta"... is not valid JSON ou Failed to parse message. La cause est toujours la même : le serveur a écrit une ligne de journal sur stdout. Avec stdio, stdout est le canal JSON-RPC. Tout texte parasite corrompt donc le flux et fait échouer le handshake. Dans Node, console.log écrit sur stdout ; utilisez console.error. En Python, un print() simple écrit sur stdout. Écrivez les journaux avec logging configuré sur sys.stderr, ou transmettez file=sys.stderr. La règle est absolue : avec stdio, stdout ne doit contenir que du JSON-RPC ; tout ce qui est destiné aux utilisateurs doit aller sur stderr.

Un serveur distant expire ou ferme la connexion pendant le handshake. Le client échoue avec MCP error -32000: Connection closed, ou l’Inspector reste bloqué sur Connect et n’affiche jamais les outils. Avec nginx, il s’agit d’un problème de buffering : le proxy conserve le flux SSE au lieu de le transmettre progressivement. Le client attend donc une réponse qui n’arrive jamais. Ajoutez proxy_buffering off;, ainsi que le reste du bloc de l’étape 4, dans location. Vérifiez avec curl -N sur l’URL publique. Les données d’événement doivent arriver progressivement, et non toutes en même temps à la fin.

L’authentification est refusée. Le client signale Error POSTing to endpoint (HTTP 401) ou simplement 401 Unauthorized. L’en-tête peut être absent, le token incorrect ou la variable du shell vide au moment où le client a lu la configuration. C’est un piège fréquent : ${MCP_TOKEN} ne produit rien si la variable n’est pas définie, et nginx reçoit alors Bearer sans valeur. Affichez la variable, ajoutez de nouveau l’en-tête et vérifiez que les octets correspondent exactement au token présent dans if de nginx.

Le service ne démarre pas sous systemd. journalctl -u mcp-ops affiche ModuleNotFoundError: No module named 'mcp' et ExecStart pointe vers le Python du système au lieu de l’interpréteur du venv. Il est également possible que Address already in use indique qu’un autre processus utilise le port 8000 ; trouvez-le avec sudo ss -ltnp | grep 8000.

FAQ

Qu’est-ce qu’un serveur MCP, exactement ?

C’est un programme qui expose des outils et des ressources à un client IA via le Model Context Protocol, en utilisant JSON-RPC 2.0. Le modèle d’IA n’exécute jamais lui-même l’outil. Il le demande à son client, le client appelle le serveur MCP, puis le serveur l’exécute et renvoie un résultat. Comme le protocole est standard, un même serveur fonctionne avec tout client compatible, qu’il s’agisse de Claude Code, de Claude Desktop ou de Gemini CLI.

Quelle est la différence entre les transports stdio et HTTP ?

Un serveur stdio est lancé par le client comme processus enfant et communique via stdin/stdout. Il reste donc actif tant qu’un client donné fonctionne sur une machine donnée, et ne nécessite ni réseau ni authentification. Un serveur HTTP est un service réseau qui s’exécute en continu et que plusieurs clients peuvent joindre simultanément. C’est pourquoi il nécessite TLS et une authentification. Utilisez stdio pour les outils locaux destinés à un seul utilisateur. Utilisez HTTP (Streamable HTTP sur les serveurs actuels) pour tout service partagé ou persistant.

Comment sécuriser un serveur MCP distant ?

Considérez qu’il donne accès à vos fichiers, à votre base de données ou à votre shell, et ne l’exposez jamais sans authentification. La meilleure solution consiste à le lier à localhost et à y accéder via un tunnel SSH ou un VPN privé. S’il doit être public, placez-le derrière un reverse proxy qui impose un bearer token ou le flux OAuth de MCP. Générez le token avec openssl rand -hex 32 et ne liez jamais le serveur à 0.0.0.0 sans l’un de ces mécanismes en amont.

Comment déboguer un serveur qui ne démarre pas ?

Commencez par vérifier claude mcp list. ✗ Failed to connect avec spawn ... ENOENT signifie que la commande ou le runtime est absent. Corrigez donc le chemin ou installez-le. S’il se connecte puis se déconnecte avec une erreur d’analyse JSON, le serveur écrit ses journaux sur stdout et corrompt le flux JSON-RPC. Redirigez tous les journaux vers stderr. Dans les autres cas, exécutez la commande exacte avec MCP Inspector. Celui-ci pilote le serveur de manière isolée et permet de distinguer un bug du serveur d’un problème de configuration du client.