L’outil fonctionnait très bien en test manuel. Envoyé depuis un client MCP de démonstration, il répondait correctement, avec les bonnes données. Mais dès qu’un agent de production l’appelait dans une conversation réelle, il ignorait purement et simplement le résultat, comme si l’outil n’avait rien renvoyé. Aucune erreur dans nos journaux côté serveur : la requête arrivait, la réponse partait, tout semblait normal de notre côté.
Ce symptôme — un outil « qui marche » mais dont l’agent ne tient jamais compte — nous a pris plus de temps à comprendre que n’importe quel bug plus bruyant. Voici comment nous l’avons diagnostiqué, et pourquoi le problème ne venait pas du réseau ni de l’agent, mais de notre propre schéma déclaré.
Le symptôme observé
Notre outil get_order_summary renvoyait le résumé d’une commande WooCommerce : numéro, statut, montant, articles. Dans le client de test, la réponse s’affichait telle quelle. Mais dans les échanges avec l’agent de production, celui-ci répondait systématiquement quelque chose comme « je ne parviens pas à récupérer cette information », alors que l’outil avait bien été appelé et avait bien renvoyé un objet JSON complet.
Notre premier réflexe a été de suspecter l’authentification ou un problème de timeout. Les deux pistes étaient fausses : la requête aboutissait en moins de 200 millisecondes, avec un code de statut correct.

Isoler le problème avec un client MCP de test
Nous avons rejoué exactement la même requête, avec les mêmes paramètres, depuis un client MCP minimal capable d’afficher la réponse brute sans l’interpréter. La réponse était bien un JSON valide syntaxiquement. Le vrai test consistait à la valider non pas comme du JSON générique, mais contre le schéma de sortie que notre propre outil avait déclaré au moment de son enregistrement.
{
"name": "get_order_summary",
"outputSchema": {
"type": "object",
"properties": {
"order_id": { "type": "integer" },
"status": { "type": "string" },
"total": { "type": "number" },
"currency": { "type": "string" },
"items": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["order_id", "status", "total", "currency", "items"]
}
}
En validant notre réponse réelle contre ce schéma avec un validateur JSON Schema en ligne de commande, le problème est apparu immédiatement : le champ currency était absent de la réponse pour les commandes passées avant une migration multi-devises effectuée quelques mois plus tôt. Ces commandes anciennes n’avaient tout simplement jamais eu ce champ enregistré.
Pourquoi le rejet était silencieux
Le client MCP utilisé par l’agent de production validait strictement chaque réponse d’outil contre le schéma déclaré, et écartait purement la réponse en cas de non-conformité, sans remonter d’erreur explicite à l’agent — celui-ci recevait simplement l’équivalent d’une absence de résultat. De notre côté, rien dans nos journaux ne signalait cet écart, puisque le serveur avait bien envoyé une réponse HTTP 200 avec un corps JSON syntaxiquement correct.
C’est la leçon la plus utile de cet incident : un schéma déclaré n’est pas une simple documentation, c’est un contrat vérifié à l’exécution. Toute divergence, même sur un seul champ optionnel devenu manquant, peut invalider silencieusement toute la réponse.
Le correctif appliqué
Deux changements ont réglé le problème durablement :
- Passage du champ
currencyen valeur par défaut garantie côté PHP, avec une valeur de repli explicite plutôt qu’une absence. - Ajout d’un test automatisé qui valide chaque réponse d’exemple de l’outil contre son propre schéma déclaré, exécuté à chaque déploiement.
function agence_order_summary( $order_id ) {
$order = wc_get_order( $order_id );
return array(
'order_id' => $order->get_id(),
'status' => $order->get_status(),
'total' => (float) $order->get_total(),
'currency' => $order->get_currency() ?: get_option( 'woocommerce_currency' ),
'items' => wp_list_pluck( $order->get_items(), 'name' ),
);
}
Généraliser la vérification
Depuis cet incident, chaque outil MCP que nous écrivons passe par une étape de validation automatisée avant d’être exposé : on génère plusieurs cas de test représentatifs, y compris des données anciennes ou incomplètes, et on vérifie que la réponse respecte le schéma déclaré dans chacun de ces cas, pas seulement dans le cas idéal.
Un schéma qui n’est testé que sur des données fraîches ne protège de rien : c’est justement la donnée ancienne, oubliée, qui casse le contrat en silence.
En résumé
Un outil MCP qui semble fonctionner en test manuel mais que l’agent ignore en production mérite une vérification systématique de son schéma de sortie contre des données réelles, y compris les cas limites hérités d’anciennes versions du site. Le rejet silencieux ne laisse aucune trace côté serveur : seul un client de test qui valide strictement la réponse permet de le repérer rapidement.