Mon serveur MCP s'arrêtait tout simplement de fonctionner. Pas de dump de crash. Pas de trace de pile (stack trace) dans les logs. Les clients se connectaient sans se plaindre, puis après quelques heures, tout devenait muet. Les requêtes disparaissaient et l'agent IA à l'autre bout ne recevait que du vide.

C'est une histoire frustrante mais courante dans l'écosystème du Model Context Protocol (MCP). Le protocole définit comment les agents IA découvrent et appellent des outils externes, mais la spécification part du principe que vous gérerez les erreurs vous-même. La plupart des tutoriels et des implémentations de départ passent rapidement sur cette partie. Ils se concentrent sur le "happy path" (le chemin idéal) : annoter une fonction, l'exposer via le serveur et renvoyer un résultat propre. Ils montrent rarement ce qui se passe lorsqu'une micro-coupure réseau affecte votre API externe, ou lorsque le modèle hallucine un nom de paramètre et envoie des données erronées. Le résultat est un serveur fragile qui semble en bonne santé mais qui est en réalité mort depuis des heures.

Pourquoi les réponses vides sont pires que les plantages

Lorsqu'une exception non gérée s'infiltre dans un gestionnaire d'outils MCP, la couche de transport l'avale souvent. Le processus du serveur reste actif, le socket reste ouvert, mais le client reçoit une réponse vide. C'est plus dangereux qu'un plantage bruyant car votre monitoring pourrait ne rien remarquer. Le processus tourne toujours. Le port écoute toujours. Pourtant, chaque appel d'outil ne renvoie rien.

Le modèle d'IA n'interprète pas le silence comme un échec. Il l'interprète comme un appel réussi qui n'a produit aucune donnée. Cette réponse vide entraîne le modèle à improviser. Il commence à halluciner des faits pour combler le vide, ou il entre dans une boucle de tentatives répétées du même appel défectueux. De petits problèmes, comme un timeout réseau passager ou un argument d'outil invalide, ne devraient jamais être autorisés à provoquer ce genre de comportement.

Le pattern Wrapper : trois lignes de défense

J'ai résolu ce problème en enveloppant chaque gestionnaire d'outils dans une fine couche de récupération d'erreurs. Le wrapper ne cherche pas à prédire chaque défaillance possible. Il les catégorise et répond en conséquence.

ConnectionError et TimeoutError
Ces erreurs surviennent lorsque votre serveur communique avec une API externe et que le réseau vacille. La solution instinctive est de redémarrer l'intégralité du processus du serveur MCP. Ne faites pas cela. Un redémarrage interrompt les connexions clients actives, efface tout état en mémoire et force une réinitialisation complète. Au lieu de cela, capturez l'échec de la connexion et ne reconnectez que la couche de transport ou le client HTTP utilisé par votre outil. Le serveur reste opérationnel et prêt pour la requête suivante immédiatement.

ValueError
C'est ce que vous voyez lorsque le client IA envoie des arguments malformés. Peut-être que le modèle a inventé un paramètre, a passé une chaîne de caractères là où un entier était requis, ou a oublié un champ obligatoire. Si vous laissez cette erreur remonter sans la gérer, le client recevra soit un plantage, soit une réponse vide. Capturez-la à l'intérieur du wrapper, puis construisez un message clair et spécifique qui indique au modèle exactement ce qui n'a pas fonctionné. Expliquez quel paramètre a échoué et ce qui était attendu. La plupart des modèles d'IA modernes liront ce message et s'auto-corrigeront dès le tour suivant. Une erreur vague gaspille un cycle de raisonnement. Une erreur précise résout le problème immédiatement.

Exceptions générales
Gardez un filet de sécurité. Si une erreur ne rentre pas dans les catégories ci-dessus, enregistrez les détails dans vos logs et renvoyez une réponse d'échec propre et générique au client. Cela empêche un cas particulier étrange de tuer la session pour tout le monde. Le serveur survit, le client reçoit le signal qu'un échec est survenu, et vous conservez suffisamment de contexte dans vos logs pour déboguer plus tard.

Le flag isError est non négociable

Voici le détail qui détermine réellement si votre correctif fonctionne. Les réponses MCP incluent un champ booléen isError. Si une exception se produit et que vous renvoyez un message d'erreur sans définir isError sur true, le client traitera ce texte d'erreur comme le résultat réussi d'un outil.

Imaginez que votre API externe atteigne une limite de débit (rate limit). Vous capturez l'exception et renvoyez la chaîne "API rate limit exceeded" mais laissez isError à false. Le client transmet cette chaîne dans la fenêtre de contexte du modèle comme s'il s'agissait d'un véritable résultat d'outil. Le modèle tente alors de raisonner sur ce texte comme s'il s'agissait de données. Il pourrait citer l'erreur dans un résumé ou, pire encore, il pourrait halluciner des relations entre ce texte d'erreur et d'autres faits. Vous avez transformé un simple incident d'infrastructure temporaire en une source de désinformation.

Définissez toujours isError sur true lorsque vous renvoyez une charge utile d'erreur. Cela donne au client un signal clair que l'appel de l'outil a échoué, ce qui permet au modèle de décider s'il doit réessayer, demander des précisions ou essayer un tout autre outil.

Sachez ce qu'il faut intercepter et ce qu'il faut arrêter

N'enveloppez pas l'intégralité de votre serveur dans un bloc try-catch aveugle qui avale tout. Certaines erreurs signifient que le serveur doit s'arrêter immédiatement. Si une variable d'environnement requise est manquante au démarrage, ou si votre fichier de configuration est corrompu, aucune interception au niveau de la requête ne servira à rien. Créez une classe d'exception spécifique pour ces erreurs fatales et laissez-les faire planter le processus.

La règle est simple. Si l'erreur est temporaire ou isolée à une seule requête, interceptez-la et récupérez la situation. Si l'erreur signifie que chaque requête ultérieure est vouée à l'échec, laissez le serveur s'arrêter brutalement. Un échec rapide au démarrage est infiniment préférable à un serveur qui traîne la patte pendant des jours dans un état défectueux.

Ajoutez de l'observabilité avant d'en avoir besoin

Une fois que vous avez mis en place le wrapper, associez-le à une journalisation structurée. Journalisez chaque appel d'outil et son résultat au format JSON. Incluez le nom de l'outil, les arguments bruts, la latence, et si l'appel a réussi, échoué ou a fait l'objet d'une nouvelle tentative.

Cette discipline porte ses fruits rapidement. Lorsque vous remarquez un pic d'erreurs, vous pouvez filtrer par outil et repérer des schémas en quelques minutes. Peut-être qu'une API externe spécifique commence à générer des timeouts à la même heure chaque jour, signalant une fenêtre de maintenance planifiée dont vous n'aviez pas connaissance. Peut-être qu'un outil reçoit systématiquement des arguments malformés, révélant un défaut d'ingénierie de prompt en amont. Des journaux en texte brut enfouis dans des traces de pile rendent ce travail de détective pénible. Le JSON structuré le rend trivial.

Le résultat en production

J'ai utilisé ce modèle de wrapper sur deux serveurs MCP en production au cours des trois dernières semaines. Durant cette période, je n'ai constaté aucun échec silencieux. Avant d'ajouter le wrapper, je comptais en moyenne environ un échec inexpliqué par jour. Le modèle n'est pas complexe, mais son impact est considérable car il sépare le bruit gérable des problèmes réels.

Les échecs silencieux coûtent plus cher que les plantages. Un plantage déclenche votre système d'alerte. Le silence, lui, ne fait qu'éroder la confiance. Un jour, votre agent IA renvoie des données d'outils utiles, et le lendemain, il commence à inventer des choses parce que le serveur a cessé de répondre il y a des heures. Le modèle de wrapper comble cette lacune. Il permet à votre serveur de continuer à fonctionner malgré des turbulences mineures, donne au modèle suffisamment de contexte pour corriger ses propres erreurs, et garantit que lorsqu'un problème véritablement fatal survient, vous en soyez informé immédiatement.

Si vous construisez des outils MCP aujourd'hui, commencez par le wrapper et le flag isError. Tout le reste n'est que du nettoyage.