Héberger des serveurs MCP sur un VPS pour agents IA
Déployez vos serveurs MCP en mode stdio ou HTTP distant sur un VPS. Ce guide détaille la configuration systemd, Nginx, TLS et la sécurisation indispensable des endpoints.
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 filesystem. 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 enregistrée dans un fichier pouvant être commité :
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiLe séparateur -- est important : tout ce qui le suit est 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. Au prochain démarrage de Claude Code dans ce répertoire, l’agent lit .mcp.json, lance npx -y @modelcontextprotocol/server-filesystem ... comme processus enfant et effectue le handshake MCP via l’entrée et la sortie standard de ce processus. Vérifiez que la configuration a bien été prise en compte :
claude mcp listUn serveur fonctionnel affiche sa commande et une coche verte, filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✔ Connected. Dans la session, la commande slash /mcp répertorie les outils exposés par le serveur (read_text_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 package 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 package, 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 le serveur : 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_text_file, il est pertinent d’associer cet accès à une compétence qui l’oriente vers la plus petite modification fonctionnelle, car un outil filesystem rend une réécriture complète exactement aussi facile qu’une correction de deux lignes. Cette configuration s’étend au-delà des fichiers locaux : si vous exécutez déjà un moteur de recherche sur le VPS, vous pouvez fournir votre propre instance SearXNG à l’agent comme outil de recherche. Les requêtes restent ainsi sur votre serveur, mais le texte non fiable des pages est directement injecté dans le contexte sur lequel l’agent agit ensuite.
Étape 2 : construire un serveur HTTP distant
Un serveur stdio s’arrête avec son processus parent et est lancé une fois par client. Ainsi, si vous exécutez deux sessions Claude Code sur le serveur qui se transmettent du travail, chacune dispose de sa propre copie privée de l’outil. Pour utiliser un outil qui reste actif pour tous les clients, un outil d’exploitation partagé, une passerelle de base de données ou un service appelé à la fois par votre ordinateur portable et votre CI, vous avez besoin du transport HTTP et d’un véritable service. Voici un serveur Python minimal utilisant le SDK officiel et exposant un outil :
# /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)Remarque host="127.0.0.1". Le serveur écoute uniquement sur localhost. Rien en dehors du serveur ne peut y accéder directement, ce qui est exactement le comportement souhaité avant la mise en place de l’authentification. Il s’agit du SDK 2.x récupéré actuellement par pip install "mcp[cli]" (2.3.0 au 2026-10-12). Il a renommé FastMCP en MCPServer et déplacé host et port du constructeur vers run(). L’ancienne importation mcp.server.fastmcp échoue désormais avec ModuleNotFoundError. 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.targetLe 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/mcpstatus 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 prime dans ce domaine
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 qui peuvent manipuler 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 d’IA : un tiers peut lister vos outils, puis les appeler. Traitez-le exactement comme un socket d’administration sans authentification, car c’est précisément ce qu’il est. L’impact d’un token volé dépend également du serveur situé derrière celui-ci : 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 de système de fichiers ou de shell donne accès à la machine.
Trois protections, par ordre de préférence :
- Ne le publiez pas. Laissez le serveur sur
127.0.0.1et 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 utiliserhttp://127.0.0.1:8000/mcp. Rien n’est exposé. - Placez-le sur un réseau privé. Liez l’adresse du tunnel à un VPN WireGuard auto-hébergé et autorisez uniquement les pairs VPN à y accéder. Internet voit un port fermé.
- 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 à vérifier un bearer token partagé au niveau du proxy. Cette solution est simple et 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 header. 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. Si la variable n’est pas définie, Claude Code ne la remplace pas par une chaîne vide : il signale la variable manquante dans claude mcp list et utilise le texte littéral ${MCP_TOKEN}. Choisissez également un nom qui vous est propre, car dans les headers d’un serveur distant, Claude Code lit volontairement ses propres variables d’identifiants ainsi que celles de votre fournisseur cloud, comme ANTHROPIC_API_KEY, comme des valeurs vides.
Toutes les protections précédentes sécurisent l’endpoint, mais pas l’agent qui détient déjà le token. C’est l’autre moitié du problème : si votre client est DeepSeek Harness, des plugins qui limitent les outils qu’un agent peut appeler et analysent la sortie des outils à la recherche d’instructions injectées couvrent cet aspect.
Étape 6 : déboguer avec MCP Inspector
Lorsqu’un serveur se comporte mal, ne cherchez pas la cause depuis l’agent. Pilotez-le directement avec Inspector, le client de test officiel accessible depuis un navigateur. 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 /tmpIl démarre un serveur web sur http://localhost:6274 et affiche une URL contenant une chaîne de requête MCP_INSPECTOR_API_TOKEN ; ouvrez ce lien au lieu de saisir l’adresse de mémoire. Depuis la v2 (2.10.1 est la version actuelle au 2026-10-12), le backend injecte également le token dans la page qu’il sert. Un rechargement ou un favori continue donc de fonctionner. Le token protège les routes /api/* qui peuvent lancer des processus. Le proxy v1 distinct sur 6277 a été supprimé. Le seul listener supplémentaire est un sandbox pour MCP Apps, sur 6275 par défaut. Connectez-vous, ouvrez l’onglet Tools, sélectionnez un outil, renseignez de vrais arguments et appelez-le. S’il 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, lancez Inspector sans commande, ajoutez un serveur avec le transport Streamable HTTP et https://mcp.example.com/mcp, ajoutez l’en-tête Authorization dans ses paramètres, puis connectez-vous. C’est le moyen le plus rapide de vérifier que l’authentification et le proxy sont correctement configurés avant d’impliquer un agent. Inspector enregistre les en-têtes dans son catalogue de serveurs exactement tels qu’ils sont écrits. Ce token se trouve donc également dans ~/.mcp-inspector/mcp.json.
Mettre les serveurs à jour
MCP évolue rapidement : appliquez donc les correctifs selon un calendrier. Les serveurs Node lancés avec un npx -y sans version figée utilisent la version résolue par npx : celle installée dans le projet local, s’il y en a une, ou une copie installée dans le cache npm. -y ignore uniquement l’invite d’installation ; il ne force pas une mise à jour. Vous ne suivez donc pas nécessairement la dernière version et vous ne savez pas avec certitude laquelle est utilisée. Une fois qu’un serveur devient important, figez la version exacte que vous avez testée. Lisez-la dans npm view @modelcontextprotocol/server-filesystem version et ajoutez-la au nom du paquet dans .mcp.json (@modelcontextprotocol/server-filesystem@<version>), puis mettez-la à niveau volontairement. Les serveurs Python exécutés sous systemd se mettent à jour avec sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]", puis avec sudo systemctl restart mcp-ops. Consultez les notes de version avant une mise à niveau majeure : le passage du SDK Python 1.x au SDK 2.x rend le code 1.x incompatible dès l’import. Le guide de migration du SDK recommande de conserver une borne supérieure <2, par exemple "mcp[cli]<2", jusqu’à la fin de la migration. Lors d’une mise à niveau, vérifiez la révision de la spécification ciblée par votre SDK. Un changement qui franchit la transition de SSE vers Streamable HTTP peut modifier le transport que vos clients doivent demander.
Modes de panne et messages affichés
L’agent indique que le serveur n’a pas démarré. claude mcp list affiche ✘ Failed to connect, et les versions actuelles ajoutent le détail de l’échec sur cette ligne. 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 situé à 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 son nom court. Corrigez la commande pour utiliser un chemin absolu ou installez le runtime, puis reconnectez-vous.
Un serveur stdio se connecte, puis se déconnecte immédiatement. Les journaux du client indiquent 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 la négociation. 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 le texte destiné aux utilisateurs doit aller sur stderr.
Un serveur distant expire ou ferme la connexion pendant la négociation. Le client échoue avec MCP error -32000: Connection closed, ou l’Inspector reste bloqué sur Connect et n’affiche jamais les outils. Derrière nginx, il s’agit d’un problème de buffering : le proxy conserve le flux SSE au lieu de le transmettre immédiatement. 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, à location. Vérifiez avec curl -N sur l’URL publique. Les données d’événement doivent arriver progressivement, et non toutes à la fin.
L’authentification est refusée. Le client signale Error POSTing to endpoint (HTTP 401) ou simplement 401 Unauthorized. L’en-tête est absent, le token est incorrect ou la variable n’a pas été développée. Ce dernier cas est fréquent. Si MCP_TOKEN n’est pas définie lorsque le client lit la configuration, Claude Code signale la variable manquante dans claude mcp list et envoie le texte littéral. nginx voit alors Bearer ${MCP_TOKEN}. Une variable portant le nom de l’un des identifiants de Claude Code ou de votre fournisseur cloud est au contraire lue comme une chaîne vide. nginx voit alors Bearer sans valeur. Affichez la variable, ajoutez de nouveau l’en-tête et vérifiez que les octets correspondent exactement au token indiqué dans if de nginx.
Le service ne démarre pas avec systemd. journalctl -u mcp-ops affiche ModuleNotFoundError: No module named 'mcp', tandis que ExecStart pointe vers le Python système au lieu de l’interpréteur du venv. Autre possibilité : Address already in use, 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 exécute l’outil 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, 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 fonctionne donc avec un seul client sur une seule machine et ne nécessite ni réseau ni authentification. Un serveur HTTP est un service réseau permanent 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, ou Streamable HTTP sur les serveurs actuels, pour tout ce qui est 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. Le mieux est de le laisser écouter sur localhost et d’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 faites jamais écouter le serveur sur 0.0.0.0 sans l’une de ces protections 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 le chemin ou installez-le. Si la connexion s’établit puis se ferme avec une erreur d’analyse JSON, le serveur écrit ses journaux sur stdout et corrompt le flux JSON-RPC. Déplacez tous les journaux vers stderr. Pour tout autre problème, exécutez la commande exacte avec MCP Inspector. Il exécute le serveur de manière isolée, ce qui permet de distinguer un bug du serveur d’un problème de configuration du client.