# Comment la dérive de schéma MCP casse les agents de longue durée

Les agents qui fonctionnent longtemps rendent une mauvaise hypothèse particulièrement coûteuse : la description de l'outil chargée au début de la session resterait valable jusqu'à la fin de l'exécution. C'est souvent faux. Un serveur peut déployer une nouvelle version, activer une capacité propre à un compte, faire évoluer une API en amont ou corriger le contrat d'un résultat alors qu'un agent planifie encore ses actions à partir de la forme d'outil d'hier.

C'est cela, la dérive de schéma MCP. Il ne s'agit pas d'un cas limite exotique du protocole. C'est une modification de contrat entre un client qui a déjà établi un plan et un serveur qui a continué d'évoluer. Si vous testez seulement une nouvelle connexion après un déploiement, vous vérifiez le cas facile et laissez le cas dangereux de côté.

Le Model Context Protocol permet aux serveurs d'annoncer les changements de leur liste d'outils. Il ne transforme pas l'ancien cache d'un client en nouveau contrat, ne corrige pas les appels d'outils déjà proposés par un modèle et ne décide pas si une ancienne requête reste sûre. Ce sont des décisions d'ingénierie. Prenez-les volontairement, puis testez-les pendant qu'une session est active.

## La description d'un outil fait partie de l'état de la session

Un schéma d'outil est un contexte exécutable. Un agent utilise le nom, la description, le schéma d'entrée, les annotations et parfois le schéma de sortie pour décider de l'action à demander. De nombreux clients analysent aussi `tools/list` pour créer des structures locales, compiler des validateurs ou placer une description compacte de l'outil dans le contexte du modèle. Aucune de ces copies ne change simplement parce que le serveur déploie une autre version.

Cela reste vrai même lorsque le protocole réseau fonctionne exactement comme prévu. Supposons qu'un client ait commencé avec cet outil :

```json
{
  "name": "deploy_preview",
  "description": "Deploy the current branch to a preview environment.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "branch": { "type": "string" }
    },
    "required": ["branch"],
    "additionalProperties": false
  }
}
```

Une heure plus tard, le serveur modifie l'opération et exige un champ `region` explicite. Un client qui se connecte alors voit le nouveau schéma et peut le fournir. L'ancien client croit toujours que `{"branch":"fix-login"}` constitue une requête complète.

Il y a ici trois états distincts que les équipes confondent souvent :

1. Le **schéma annoncé** correspond à ce que le serveur renvoie maintenant avec `tools/list`.
2. L'**instantané du schéma côté client** correspond à ce qu'un client donné a conservé lors de sa dernière demande de liste d'outils.
3. Le **contrat d'exécution** correspond à ce que le serveur acceptera et fera lorsque `tools/call` arrivera.

Un serveur peut mettre à jour immédiatement le premier état. Il ne peut pas supposer que le deuxième a été actualisé. Il doit choisir comment gérer le troisième.

Parler simplement d'un problème d'invalidation de cache est incomplet. Cette expression évoque des données d'affichage obsolètes. Un schéma d'outil peut définir les comptes de destination, le périmètre d'écriture, les champs de confirmation et le sens des résultats. Lorsqu'un agent conserve l'ancienne version, le décalage peut provoquer un travail échoué, des tentatives répétées ou une requête qui signifie désormais davantage que ce que le modèle voulait demander.

La règle par défaut la plus sûre est simple : gardez le contrat d'entrée accepté par un outil déployé compatible avec les anciennes requêtes pendant une fenêtre de transition, sauf si l'acceptation d'une ancienne forme rendrait l'action dangereuse. Lorsque la sécurité et la compatibilité s'opposent, refusez clairement l'ancienne forme et exigez une nouvelle décision.

## `tools/list_changed` annonce un changement, mais ne synchronise rien

La spécification MCP définit `notifications/tools/list_changed` pour les serveurs dont la liste d'outils change. Le serveur envoie une notification, puis le client peut actualiser ses outils avec `tools/list`. La spécification prévoit aussi la prise en charge des notifications de changement de liste dans la capacité d'outils du serveur négociée pendant l'initialisation.

C'est utile, mais le mot « peut » fait beaucoup de travail. Une notification n'attend aucune réponse. À elle seule, elle ne permet pas au serveur de savoir si le client l'a reçue, s'il a actualisé sa liste, si son cache a été mis à jour ou si un modèle a déjà préparé un appel à partir de l'ancienne description.

Traitez la notification comme un signal d'invalidation, pas comme une barrière de synchronisation.

Un client qui gère correctement la dérive doit effectuer quatre opérations après réception de la notification :

- Demander à nouveau `tools/list` et remplacer de façon atomique les définitions locales correspondantes.
- Conserver suffisamment longtemps l'ancien instantané pour associer un appel déjà planifié au schéma qui l'a produit.
- Revalider tout appel en file d'attente avec le schéma actualisé avant de l'envoyer.
- Fournir au modèle une erreur réparable lorsqu'un appel construit à partir d'un ancien contexte ne peut plus être exécuté.

C'est souvent sur le troisième point que les clients prennent un raccourci. Ils actualisent la liste d'outils visible, mais laissent partir un appel déjà en file avec l'ancien objet d'arguments. Une course apparaît alors : l'interface utilisateur indique une chose, l'agent envoie autre chose et le serveur doit corriger le décalage.

Les serveurs doivent appliquer une discipline parallèle. Lorsqu'un outil change, envoyez la notification après que la nouvelle réponse de `tools/list` est prête. N'annoncez pas un nouveau contrat tout en laissant un ancien processus traiter les appels pendant une durée indéterminée. Si votre architecture de déploiement le permet, ajoutez une révision de contrat explicite dans la réponse du serveur et refusez les appels qui arrivent sur un processus dont le comportement est incompatible.

La notification ne résout pas non plus le cas des clients qui ne la prennent pas en charge, se déconnectent pendant l'événement ou passent par un intermédiaire disposant de son propre cache. La compatibilité au niveau de `tools/call` reste nécessaire. Si le serveur ne fonctionne que lorsque chaque client répond parfaitement à une notification, il ne fonctionnera pas en production.

## Les changements d'entrée entraînent des ruptures différentes

Ajouter un champ ne constitue pas une seule catégorie de changement. Le risque dépend de l'effet du champ sur la validation, le sens ou l'autorité de l'action.

Ajouter une préférence d'affichage facultative est généralement sûr. L'ancien appelant l'omet et le serveur choisit une valeur par défaut stable. Ajouter un filtre facultatif peut aussi être sûr si son omission renvoie le même résultat qu'auparavant.

Ajouter une entrée `region` obligatoire à `deploy_preview` est différent. L'ancien appelant ne satisfait plus le validateur. Vous pouvez refuser la requête ou fournir une valeur par défaut. La première option interrompt l'agent, mais énonce la réalité. La seconde n'est acceptable que si cette valeur a toujours été la région prévue pour ce dépôt et ne peut pas rediriger un déploiement vers un environnement plus sensible.

Modifier le sens d'un champ est encore plus grave qu'ajouter un champ obligatoire. Imaginez un outil qui accepte à l'origine `project` comme identifiant lisible d'un projet. Le serveur décide ensuite que `project` doit être un identifiant opaque d'organisation. Un ancien agent peut continuer à envoyer `payments`, et le serveur peut résoudre cette chaîne dans un espace de noms inattendu. La validation réussit, la requête aboutit et l'action est incorrecte. C'est une rupture sémantique, plus dangereuse qu'une erreur de validation nette.

La suppression d'un champ d'entrée demande la même prudence. `additionalProperties: false` dans JSON Schema rend la rupture visible. Un ancien client envoie un argument qui était auparavant valide et reçoit une erreur. Si le serveur ignore silencieusement le champ supprimé, l'appel peut réussir avec une interprétation différente de celle attendue par le modèle.

Le conseil souvent répété « soyez tolérant sur ce que vous acceptez » convient mal aux outils qui déclenchent des actions. Il s'est imposé parce qu'il permet aux intégrations de continuer à fonctionner malgré des changements mal maîtrisés. Pour une préférence de formatage en lecture seule, cette tolérance peut être inoffensive. Pour une requête HTTP contenant des identifiants, une commande SSH, un déploiement, une suppression ou un paiement, une analyse permissive transforme une demande ambiguë en supposition côté serveur.

N'utilisez un adaptateur de compatibilité que si vous pouvez décrire précisément son comportement. Par exemple :

```ts
function normalizeDeployArgs(raw: unknown) {
  if (!isPlainObject(raw)) {
    throw executionError("Expected an object for deploy_preview.");
  }

  if (typeof raw.branch !== "string" || raw.branch.length === 0) {
    throw executionError("The branch field must be a non-empty string.");
  }

  if (raw.region === undefined) {
    return { branch: raw.branch, region: "us-east-preview", schemaRevision: 1 };
  }

  if (raw.region !== "us-east-preview" && raw.region !== "eu-preview") {
    throw executionError("region must be us-east-preview or eu-preview.");
  }

  return { branch: raw.branch, region: raw.region, schemaRevision: 2 };
}
```

Cet adaptateur a une propriété acceptable : l'ancienne requête produit la même destination de prévisualisation qu'auparavant. Il ne serait pas acceptable si `us-east-preview` n'était qu'une supposition pratique après un changement de propriété du compte.

Pour une modification incompatible, renvoyez une erreur que l'agent peut exploiter. Indiquez la révision d'outil attendue par le serveur, nommez le champ manquant ou obsolète et demandez au client d'actualiser ses outils. Ne renvoyez pas un message vague comme « entrée invalide ». Les modèles retentent les erreurs vagues avec de petites variations. Des erreurs claires donnent une chance de corriger la requête.

## Les changements de forme des résultats peuvent fausser la décision suivante

Les équipes prêtent attention à la validation des entrées, car une mauvaise requête s'arrête au serveur. Elles examinent moins les changements de résultats parce que l'action est déjà terminée. Pour les agents, c'est l'inverse qui devrait se produire. Le résultat sert souvent de preuve pour l'appel suivant.

Imaginez le résultat initial de `create_issue` :

```json
{
  "issue": {
    "id": "I-482",
    "url": "https://tracker.example/issues/I-482",
    "state": "open"
  }
}
```

Un agent peut extraire `issue.id`, le conserver dans sa mémoire de travail puis appeler `add_comment` avec cet identifiant. Si un serveur modifié renomme `id` en `issueId`, place le résultat sous `data` ou transforme `state` d'une chaîne en objet, l'action suivante de l'agent peut échouer très loin de l'appel initial. Pire encore, une solution de repli limitée au texte peut toujours contenir une phrase plausible, et le modèle peut improviser un identifiant à partir de cette prose.

Les résultats d'outils MCP peuvent contenir du contenu destiné au modèle et du contenu structuré destiné à un traitement programmatique. Si vous publiez un schéma de sortie, faites de la sortie structurée le contrat machine de référence. Gardez le texte concis et utile à une personne qui lit une transcription, mais ne comptez pas sur une extraction fiable par les clients.

Le travail de MCP autour de JSON Schema 2020-12 est pertinent ici. Les recommandations ultérieures du protocole rendent le dialecte explicite, et les schémas de sortie peuvent décrire davantage qu'un sous-ensemble de JSON limité aux objets. Cela améliore l'expressivité, mais ne donne pas le droit de remodeler à la légère un résultat utilisé par une session active. Un client peut valider les résultats avec une version précise, un type généré ou un décodeur qui ne tolère pas votre nouvelle forme d'union ou de tableau.

Pour faire évoluer les résultats, suivez ces règles :

1. Ajoutez des champs avant de les renommer ou de les supprimer.
2. Gardez le sens des champs stable, en particulier pour les identifiants, les états et les horodatages.
3. Ajoutez un champ `schema_revision` ou `result_version` dans la sortie structurée lorsque plusieurs interprétations doivent coexister.
4. Renvoyez un objet d'erreur structuré complet en cas d'échec de l'exécution, au lieu de transformer un résultat de succès en excuse non structurée.
5. Ne supprimez l'ancienne forme qu'après avoir arrêté les anciennes sessions ou terminé une fenêtre de migration publiée.

Un champ de révision du résultat n'est pas décoratif. Il permet au client de distinguer « le serveur a renvoyé une ancienne réponse incomplète » de « le serveur a renvoyé une nouvelle réponse dont le champ facultatif est absent ». Cette distinction compte lorsqu'un agent doit décider s'il doit réessayer, interroger l'utilisateur ou poursuivre vers une action importante.

Ne transformez pas chaque sortie en enveloppe versionnée simplement parce que c'est possible. Placez un marqueur de révision lorsque plusieurs consommateurs déployés indépendamment en ont besoin. Pour un petit serveur privé et un client intégré unique, une forme stable et additive peut suffire. Pour un outil partagé entre plusieurs hôtes d'agents, processus et extensions, une information de révision explicite évite des jours de conjectures pendant un incident.

## Le test important oppose un ancien plan à un nouveau serveur

Un client neuf connecté à un serveur neuf vous indique que le nouveau schéma est valide. Il ne vous apprend rien sur la dérive. Le test nécessaire demande à un client de conserver un instantané, laisse le serveur changer, puis exécute des appels issus de cet instantané.

Construisez le test autour de deux configurations de serveur. La configuration A annonce l'ancienne définition de l'outil. La configuration B annonce la nouvelle définition et contrôle la manière dont les anciens arguments sont traités. Le client reste connecté pendant le changement. Si votre serveur ne peut pas modifier son comportement sans redémarrage, placez un commutateur de test déterministe dans le registre des outils au lieu d'essayer de reproduire le timing avec votre système de déploiement.

Voici la séquence minimale utile :

```text
1. Client initializes and receives tools.listChanged capability.
2. Client calls tools/list and stores deploy_preview revision 1.
3. Client prepares arguments: {"branch":"fix-login"}.
4. Server switches to revision 2, where region is required for new clients.
5. Server emits notifications/tools/list_changed.
6. Client sends the already prepared revision 1 call.
7. Client refreshes tools/list.
8. Client retries only if its repair policy permits it.
9. Client calls revision 2 with {"branch":"fix-login","region":"us-east-preview"}.
```

Le test doit examiner davantage que la réussite ou l'échec. Capturez les messages JSON-RPC réels, les arguments normalisés par le serveur, les appels simulés vers les services externes et le journal d'événements du client. Un serveur qui renvoie une erreur propre mais a déjà commencé un déploiement externe a échoué au test.

Utilisez un service aval fictif doté d'un journal de requêtes en ajout uniquement. Il doit enregistrer la méthode, le chemin, les en-têtes pertinents pour l'autorisation, le corps de la requête et un identifiant de corrélation de test. Vérifiez ensuite que l'appel obsolète n'a envoyé aucune requête en aval lorsqu'il devait être bloqué.

Un tableau concis facilite la revue du comportement attendu :

| Événement de dérive | Comportement de l'ancien client | Comportement du serveur | Effet en aval |
| --- | --- | --- | --- |
| Ajout de `label` facultatif | Appel sans `label` | Applique l'ancienne valeur par défaut | Une requête attendue |
| Ajout de `region` obligatoire avec une valeur historique sûre | Appel sans `region` | Normalise vers une valeur par défaut stable | Une requête attendue |
| Ajout d'une autorisation obligatoire | Appel sans autorisation | Renvoie une erreur d'exécution réparable | Aucune requête |
| Modification du sens de `project` | Appel avec l'ancien `project` | Refuse la requête comme incompatible | Aucune requête |
| Ajout d'un champ de sortie | Analyse les champs précédents | Renvoie les anciens champs et le nouveau | Aucune action supplémentaire |
| Suppression d'un identifiant de sortie | Tente l'appel dépendant suivant | Le client s'arrête et signale une erreur de contrat | Aucune requête dépendante |

Ne testez pas uniquement la nouvelle tentative réussie. Les agents savent bien réessayer, et c'est précisément pourquoi ils peuvent amplifier une mauvaise migration. Testez les appels obsolètes répétés, une notification qui arrive après l'entrée d'un appel dans la file, un échec de l'actualisation et une reconnexion du client au milieu de la transition.

Le cas délicat est celui d'un appel d'outil en cours. Un serveur doit exécuter chaque appel selon une seule révision de contrat cohérente. Ne commencez pas la validation avec la révision 1, ne rechargez pas la configuration puis ne construisez pas la requête aval avec la révision 2. Capturez la configuration du gestionnaire au moment de l'admission de l'appel. Si l'opération peut durer suffisamment longtemps pour que le contrat cible change lui-même, exposez un travail durable ou refusez l'appel avant la phase irréversible. Ne mélangez pas deux révisions dans une seule action.

## La logique de correction du client doit encadrer les tentatives

Lorsqu'un serveur refuse une requête obsolète, le client dispose de plusieurs options : actualiser, demander au modèle de corriger les arguments, réessayer avec une requête convertie ou s'arrêter pour demander une intervention. Le bon choix dépend de la question suivante : le changement concerne-t-il uniquement la syntaxe ou modifie-t-il aussi l'autorité de l'action ?

Actualisez automatiquement puis réessayez uniquement si toutes les conditions suivantes sont réunies :

- Le serveur identifie explicitement une révision de schéma obsolète ou un champ manquant.
- La définition actualisée de l'outil fournit une valeur par défaut non sensible et sans ambiguïté, ou une conversion déterministe.
- L'action d'origine reste dans les mêmes limites de cible et d'autorisation.
- La première tentative n'a produit aucun effet externe.

Dans tous les autres cas, une nouvelle décision est nécessaire. Si une révision ajoute `environment`, `account_id`, `repository`, `host`, `user` ou un texte de confirmation, une nouvelle tentative automatique peut élargir ou rediriger l'action. Même si le modèle peut deviner une réponse probable, il doit obtenir un contexte actualisé ou une approbation humaine.

Séparez l'identité de la requête de l'identité de la tentative. Si un appel d'outil peut atteindre un système externe avant que le client ne reçoive sa réponse, le client ne doit pas le renvoyer aveuglément après une actualisation du schéma. Utilisez un jeton d'idempotence lorsque l'API aval le permet. Pour SSH, lorsqu'aucun mécanisme générique d'idempotence n'est disponible, concevez les commandes de manière à ce qu'une exécution répétée soit sûre ou détectable. Une migration de schéma est le pire moment pour découvrir qu'un délai d'attente crée un travail en double.

Une bonne erreur côté client donne au modèle les informations nécessaires sans lui transmettre une instruction trompeuse. Par exemple :

```json
{
  "isError": true,
  "content": [
    {
      "type": "text",
      "text": "deploy_preview rejected this request because its input contract changed. Refresh tools before retrying. The current schema requires branch and region. No deployment was started."
    }
  ],
  "structuredContent": {
    "error_code": "STALE_TOOL_SCHEMA",
    "tool": "deploy_preview",
    "required_action": "refresh_tools",
    "side_effect_started": false,
    "current_revision": 2
  }
}
```

La forme exacte de l'enveloppe d'erreur relève de votre conception, mais certains faits sont indispensables. Indiquez si un effet a commencé. Indiquez si une actualisation peut aider. Indiquez la révision actuelle si votre serveur en expose une. Un modèle peut exploiter ces faits. Une erreur de transport générique ne le peut pas.

Ne présentez pas un problème de validation du schéma comme une panne de transport. La spécification MCP distingue les échecs au niveau du protocole des échecs d'exécution d'un outil, et les recommandations actuelles privilégient les erreurs d'exécution pour une entrée d'outil invalide afin de laisser au modèle une chance de se corriger. Utilisez cette distinction. Une méthode JSON-RPC inconnue n'est pas le même événement qu'un outil connu qui refuse des arguments obsolètes.

## La revue de sécurité doit porter sur le sens, pas uniquement sur les secrets

La dérive de schéma devient un problème de sécurité lorsqu'une ancienne description autorise implicitement une action plus récente. Cela se produit avec les valeurs par défaut, les champs renommés, les autorisations ajoutées et les adaptateurs de serveur trop accommodants.

Imaginez un outil initialement nommé `run_report` qui accepte `{\"team\":\"sales\"}`. Le serveur est modifié pour accepter une chaîne `target` pouvant désigner une équipe, un rapport enregistré ou une requête brute. Un ancien agent continue d'envoyer `team`. Si l'adaptateur transforme cela en `target: \"sales\"`, qu'a-t-il autorisé ? Une équipe ? Un rapport nommé sales ? Un alias de requête ? Le serveur a créé une ambiguïté à la frontière d'une action. Refusez cette conversion et publiez un outil distinct ou un parcours de migration explicite.

Les identifiants augmentent les enjeux. Un agent qui détient directement des clés API peut mélanger la correction de schéma et le traitement des secrets dans son propre processus et ses journaux. Un plan obsolète dispose alors de davantage de possibilités pour causer un dommage. Sallyport conserve les identifiants HTTP et SSH dans son coffre-fort chiffré et exécute l'action au lieu de remettre l'identifiant à l'agent. Cette séparation ne rend pas à elle seule un contrat modifié sûr, mais elle facilite l'inspection de la requête réelle, du point d'autorisation et de la trace d'audit.

L'approbation doit s'attacher à l'action concrète qui va se produire maintenant, et non à une description d'outil mémorisée. Si un outil passe d'un hôte unique à un sélecteur d'hôte flexible, une approbation par session fondée sur une ancienne identité de code ne suffit pas pour la nouvelle cible. Exigez une nouvelle approbation par appel pour l'action sensible, ou utilisez un nouveau nom d'outil qui rende visible l'autorité accrue.

C'est aussi là que les traces d'audit deviennent indispensables. Enregistrez le nom de l'outil, la révision déclarée du schéma lorsqu'elle est disponible, les arguments bruts reçus, les arguments normalisés utilisés, la version du serveur ou du gestionnaire, la décision d'approbation et la cible en aval. Ne remplacez pas les arguments bruts par les arguments normalisés. Pendant un incident, vous devez pouvoir vérifier si le client a envoyé une ancienne forme, si l'adaptateur l'a modifiée et si la requête externe correspondait à la promesse de l'adaptateur.

Le journal Activity et le journal Sessions de Sallyport illustrent bien la séparation entre une exécution d'agent et chaque appel externe. Pour les tests de dérive, vous voulez les deux vues : un enregistrement indiquant que l'exécution a été autorisée et un autre pour chaque action HTTP ou SSH ayant quitté ou non la machine.

## Utilisez des fenêtres de compatibilité, puis supprimez-les volontairement

La compatibilité ascendante doit avoir une date de fin, même si cette date est liée à un cycle de publication ou à la durée de vie d'une session plutôt qu'au calendrier. Sinon, chaque convertisseur d'ancienne entrée reste en place pour toujours et le serveur devient un musée d'hypothèses que personne n'ose modifier.

Commencez par classer le changement.

Un changement additif garde l'ancien appel valide et en préserve le sens. Conservez le même nom d'outil, annoncez le changement de liste et acceptez les deux formes pendant que les sessions actives se terminent.

Une migration limitée modifie la syntaxe, mais dispose d'une conversion déterministe sûre. Conservez le nom de l'outil uniquement si vous pouvez tester complètement le convertisseur et enregistrer chaque utilisation. Présentez le schéma actuel aux nouveaux clients et n'acceptez l'ancienne entrée que pendant une courte période.

Une modification sémantique ou d'autorité exige un nouveau nom d'outil. `deploy_preview` et `deploy_environment` peuvent partager une implémentation, mais ils ne doivent pas partager un contrat si l'un sélectionne une destination de prévisualisation connue tandis que l'autre peut sélectionner la production. Cela peut sembler verbeux dans une liste d'outils. Le coût reste inférieur à celui d'un agent qui croit avoir appelé l'opération la plus limitée.

Une suppression doit échouer clairement. Renvoyez une erreur d'exécution qui nomme l'outil de remplacement ou indique que la capacité n'existe plus. Ne conservez pas un nom d'outil qui ne fait rien. Une réussite silencieuse est toxique pour le travail automatisé, car l'agent enregistre la tâche comme terminée alors que l'effet attendu n'a jamais eu lieu.

Utilisez la télémétrie pour décider du retrait d'un adaptateur, mais ne recueillez pas uniquement des taux de réussite globaux. Comptez les appels normalisés depuis chaque ancienne révision, les appels obsolètes refusés, les corrections automatiques effectuées par les clients et les appels ayant nécessité l'intervention d'une personne. Un faible volume d'anciennes entrées peut rester important si elles proviennent des tâches d'agents les plus longues ou les plus privilégiées.

Avant le retrait, exécutez le test de dérive dans l'autre sens : démarrez un client dans l'ancienne version, mettez à jour le serveur au-delà de la fenêtre de compatibilité et vérifiez que l'échec est clair, sans effet secondaire et récupérable par une reconnexion ou une actualisation des outils. Une rupture nette vaut mieux qu'une réinterprétation silencieuse.

## Une étape de publication qui détecte la dérive avant les utilisateurs

Ajoutez la dérive de schéma à l'étape de validation de chaque serveur MCP capable d'effectuer des actions. Il n'est pas nécessaire de commencer par une matrice gigantesque. Il faut une configuration de test rigoureuse pour chaque catégorie de modification de contrat.

Pour chaque outil modifié, répondez aux questions suivantes dans la demande de modification ou la revue de publication :

1. Une session existante peut-elle encore envoyer les arguments auparavant valides ?
2. Si oui, ces arguments conservent-ils exactement le même sens d'action ?
3. Si non, le refus intervient-il avant tout effet externe ?
4. Le serveur émet-il `notifications/tools/list_changed` uniquement après que la liste de remplacement est disponible ?
5. Le client peut-il expliquer le chemin de correction sans inventer une autorité manquante ?

Rendez ensuite les réponses exécutables. Stockez les configurations anciennes et nouvelles de `tools/list` à côté du test. Exécutez la séquence avec un instantané provenant d'un ancien client. Validez les requêtes en aval, pas seulement les réponses MCP. Conservez une configuration de régression après la migration, car la prochaine refactorisation pourrait supprimer une branche de compatibilité sans que personne ne se souvienne de sa raison d'être.

Le premier test à ajouter est volontairement minuscule : listez un outil, modifiez un paramètre obligatoire, envoyez l'ancien appel et prouvez que le serveur conserve l'ancien comportement sûr ou n'effectue rien. Ce test impose la question que la plupart des déploiements évitent : que signifie exactement une autorisation détenue par un agent déjà en cours d'exécution après la modification de l'outil sous-jacent ?
