Model Context Protocol (MCP)

Construire des clients MCP résilients : Maîtriser les disjoncteurs et la logique de nouvelle tentative

Le protocole de contexte de modèle (MCP) représente un changement de paradigme dans la façon dont les agents IA interagissent avec les outils et sources de données externes. À mesure que nous déployons ces agents dans des systèmes distribués, la fiabilité des connexions aux serveurs MCP distants devient cruciale. Un simple dysfonctionnement réseau transitoire ou une surcharge temporaire du serveur peut entraîner une défaillance complète de l'agent s'il n'est pas géré correctement. Pour construire des applications MCP de qualité production, nous devons aller au-delà des simples blocs « try-catch » et implémenter des motifs de résilience sophistiqués tels que les disjoncteurs et une logique de nouvelle tentative intelligente.

Pourquoi la résilience est importante dans MCP

Dans une architecture MCP standard, le client (souvent l'agent LLM) communique avec un serveur (le fournisseur d'outils) via JSON-RPC sur des flux. Contrairement aux appels de fonctions locaux, les serveurs MCP distants peuvent souffrir de latence réseau, de dépassements de délai et d'épuisement des ressources. Sans mécanismes de protection, un serveur défaillant peut consommer l'intégralité du budget de délai d'attente du client, entraînant des retards visibles par l'utilisateur ou des blocages. Les motifs de résilience garantissent que le client reste réactif et peut dégrader gracieusement les fonctionnalités lorsqu'un outil spécifique devient indisponible.

Implémentation de la logique de nouvelle tentative avec un retour exponentiel

La première ligne de défense est la logique de nouvelle tentative. Cependant, de nouvelles tentatives immédiates et naïves peuvent submerger un serveur déjà en difficulté. Au lieu de cela, nous devrions utiliser un retour exponentiel avec un facteur d'aléa (jitter). Cette stratégie augmente le temps d'attente entre les tentatives de manière exponentielle, réduisant la charge sur le serveur tout en permettant aux problèmes transitoires de se résoudre.


import { Client } from "@modelcontextprotocol/sdk/client";

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  baseDelayMs: number = 100
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      // Vérifier si l'erreur est réessayable (par ex., délai d'attente réseau)
      if (!isRetryableError(error)) {
        throw error;
      }

      const delay = baseDelayMs * Math.pow(2, attempt);
      const jitter = Math.random() * 100;
      await new Promise(resolve => setTimeout(resolve, delay + jitter));
    }
  }
  throw new Error("Nombre maximal de tentatives dépassé") from lastError;
}

Il est crucial de distinguer les erreurs réessayables (dépassements de délai réseau, codes de statut 503) des erreurs non réessayables (400 Bad Request, erreurs de syntaxe). Réessayer des requêtes invalides ne fait que gaspiller des ressources.

Le motif du disjoncteur

Les nouvelles tentatives seules sont insuffisantes si le serveur est hors ligne. Si le serveur est inaccessible, les nouvelles tentatives ne feront que retarder la défaillance. C'est là que le motif du disjoncteur brille. Il surveille l'état de santé de la connexion et « déclenche » le disjoncteur pour l'ouvrir, empêchant d'autres requêtes pendant une période spécifique. Cela donne au serveur le temps de se rétablir et empêche le client d'être submergé par des requêtes en attente.

Un disjoncteur typique a trois états :

  1. Fermé : Fonctionnement normal. Les requêtes passent. Les échecs sont comptés.
  2. Ouvert : Les requêtes sont immédiatement rejetées. Cet état dure pendant une période de « délai d'attente ».
  3. À moitié ouvert : Après le délai d'attente, un nombre limité de requêtes de test est autorisé. Si elles réussissent, le circuit se ferme ; sinon, il s'ouvre à nouveau.

class CircuitBreaker {
  private state: "closed" | "open" | "half-open" = "closed";
  private failureCount = 0;
  private lastFailureTime = 0;

  constructor(
    private failureThreshold: number = 5,
    private resetTimeoutMs: number = 10000
  ) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === "open") {
      if (Date.now() - this.lastFailureTime > this.resetTimeoutMs) {
        this.state = "half-open";
      } else {
        throw new Error("Le disjoncteur est ouvert");
      }
    }

    try {
      const result = await fn();
      this.handleSuccess();
      return result;
    } catch (error) {
      this.handleFailure();
      throw error;
    }
  }

  private handleSuccess() {
    this.failureCount = 0;
    this.state = "closed";
  }

  private handleFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.failureCount >= this.failureThreshold) {
      this.state = "open";
    }
  }
}

Combinaison des stratégies pour une résilience maximale

Les clients MCP les plus robustes combinent les deux motifs. Le disjoncteur enveloppe la connexion, et la logique de nouvelle tentative enveloppe l'exécution individuelle de la requête dans les états fermé ou à moitié ouvert. Cela garantit que nous tentons de nous rétablir des défaillances transitoires sans submerger une infrastructure défaillante.


const breaker = new CircuitBreaker();
const safeCall = async (request: any) => {
  return breaker.execute(() => retryWithBackoff(() => client.send(request)));
};

Conclusion

À mesure que l'adoption de MCP augmente, la complexité des systèmes d'IA distribués augmentera. L'implémentation de disjoncteurs et de logique de nouvelle tentative n'est pas seulement une bonne pratique ; c'est une nécessité pour construire des agents IA fiables et de qualité professionnelle. En gérant proactivement les états de défaillance et la consommation des ressources, vous vous assurez que vos clients MCP restent stables, réactifs et résilients face aux perturbations réseau et de service inévitables.

Share: