# Requêtes MCP imbriquées : suivre chaque action externe en toute sécurité

Un agent peut appeler un outil qui en appelle un autre, lequel demande à un assistant de résoudre un environnement, avant d'envoyer finalement une requête HTTP ou d'exécuter une commande SSH. Si vos enregistrements s'arrêtent au premier nom d'outil, vous avez une histoire sur l'intention. Vous n'avez pas de compte rendu de ce qui s'est passé en dehors du processus.

Les requêtes MCP imbriquées nécessitent un traçage qui suit la chaîne de causalité jusqu'à chaque action externe utilisant des identifiants. Cela signifie qu'il faut enregistrer davantage qu'une transcription d'agent et davantage qu'un journal de requêtes classique. Il vous faut un graphe capable de répondre aux questions suivantes : quelle exécution a provoqué cet appel sortant, à quoi l'appel a-t-il abouti, qui l'a approuvé et a-t-il réellement été exécuté ?

J'ai vu des équipes prendre une trace d'outil bien présentée comme preuve que leurs contrôles fonctionnaient. Puis un incident leur demande quel appel a modifié une ressource de production, et la trace indique seulement `release_service`. Les noms conviviaux sont utiles aux opérateurs. Les preuves d'audit doivent décrire l'opération concrète.

## L'action externe est l'enregistrement qui compte

Le dernier appel d'une chaîne imbriquée porte souvent la conséquence. Il lui faut donc son propre enregistrement durable, même si chaque appel précédent possède déjà un span. Un outil qui vérifie un nom de branche peut être inoffensif. Un assistant qui utilise ensuite le résultat pour appeler un point de terminaison de déploiement est un événement différent.

Séparez trois éléments que les équipes confondent régulièrement :

- Une invocation d'outil est une demande d'exécuter une capacité nommée avec des arguments.
- Un span de trace décrit une unité de travail et sa place dans un graphe causal.
- Une action externe est une opération concrète qui franchit une frontière de confiance, par exemple une requête HTTP avec des identifiants injectés ou une commande SSH sur un hôte distant.

Se tromper sur ce point produit deux échecs opposés. Certaines équipes créent un enregistrement d'audit pour chaque appel de fonction interne. Leur journal se remplit de bruit et personne ne peut repérer les quelques appels qui ont modifié quoi que ce soit. D'autres équipes ne consignent qu'une ligne finale indiquant la réussite. Elles perdent la chaîne qui explique quelle décision de l'agent, quelle recherche et quelle approbation ont provoqué cet appel.

Attribuez à une action externe un `action_id` stable. Reliez-le au span qui l'a lancée, mais n'utilisez pas l'ID du span comme identité. Un span peut couvrir la préparation locale, une tentative HTTP, une redirection et l'analyse de la réponse. Ces détails sont utiles. L'enregistrement de l'action doit rester compréhensible même si l'implémentation évolue.

Un enregistrement d'action doit décrire la destination après résolution. Enregistrer `environment=production` ou `target=customer-api` ne suffit pas. Stockez l'hôte, le port, le protocole, la méthode de requête ou la commande SSH résolus, ainsi que la référence de l'identifiant sélectionné par l'exécuteur. Conservez les références des secrets, jamais leurs valeurs.

Cette distinction change aussi la manière dont vous évaluez la réussite. Un outil enveloppeur peut renvoyer `ok` parce qu'il a mis le travail en file d'attente. Une couche inférieure peut ensuite échouer avant d'ouvrir une connexion. Enregistrez séparément le résultat du span enveloppeur et celui de l'action externe. Ces informations peuvent être différentes sans qu'aucun des deux enregistrements soit faux.

## MCP ne fournit pas tout votre graphe d'appels

MCP fournit aux clients et aux serveurs un protocole pour découvrir et appeler des outils. Il n'oblige pas chaque implémentation à exposer un graphe d'appels interne. Un hôte peut orchestrer plusieurs serveurs. Un serveur peut appeler des assistants locaux. Un outil peut déclencher une tâche qui continue après la réponse. Votre conception d'audit doit prendre explicitement en compte ces différentes formes.

La spécification du Model Context Protocol décrit `tools/call` comme une demande du client au serveur pour un outil et des arguments nommés. C'est un contrat d'interface, pas un contrat de traçage. Le protocole ne transforme pas un appel de fonction local en événement enfant observable et ne définit pas de champ parent universel que chaque intermédiaire devrait préserver.

Cela compte lorsqu'une équipe affirme qu'un outil « a appelé un autre outil MCP ». Cette phrase peut désigner un véritable second appel de protocole. Elle peut aussi signifier qu'un serveur a appelé une fonction de bibliothèque portant un nom similaire. Ou qu'un hôte d'agent a reçu un résultat, l'a analysé, puis effectué un nouvel appel. Le graphe obtenu peut sembler similaire, mais ses frontières de confiance sont différentes.

Traitez ces relations comme des types d'arêtes distincts dans vos enregistrements :

- `protocol_call` relie une demande d'un client MCP à l'invocation d'un outil sur un serveur.
- `local_call` relie du code au sein d'un même processus de confiance.
- `delegated_job` relie une demande à un travail exécuté plus tard par un autre processus de travail.
- `external_action` relie un span à une opération HTTP ou SSH.

N'inférez pas le type d'arête à partir du nom de l'outil. Enregistrez-le au moment où le transfert a lieu. C'est à ce moment que vous savez si l'identité, les identifiants et les règles d'annulation sont passés dans un autre processus.

Une tâche différée demande une attention particulière. Si un outil met une tâche en file d'attente puis renvoie une réponse, conservez l'ID de trace d'origine et l'ID de l'exécution racine dans les données de la tâche. Lorsque le processus de travail s'exécute plus tard, créez un nouveau span pour cette exécution et reliez-le au plan d'action d'origine. Ne prétendez pas que le processus de travail est resté dans la requête initiale. Son temps d'exécution, son identité et son état d'autorisation peuvent avoir changé.

## Créez des identifiants à chaque frontière de confiance

Une trace n'est utile que si chaque participant peut rattacher son travail à la même chaîne de causalité sans accepter comme faits des éléments d'historique falsifiés. Générez vos propres identifiants lorsqu'une requête entre dans un composant que vous contrôlez. Conservez le contexte amont comme donnée de diagnostic non fiable, sauf si elle provient d'un pair authentifié.

La recommandation W3C Trace Context définit l'en-tête `traceparent` avec une version, un ID de trace de 32 caractères hexadécimaux, un ID parent de 16 caractères hexadécimaux et des indicateurs. OpenTelemetry utilise largement ce format. Employez-le lorsque HTTP ou un autre transport peut transmettre des en-têtes, car les outils de traçage existants le comprennent. Ne confondez pas compatibilité et modèle d'audit.

Pour MCP sur stdio, il peut ne pas y avoir d'en-tête HTTP. Placez un contexte équivalent dans votre enveloppe applicative ou gérez le contexte dans le processus qui distribue l'appel. Le mécanisme compte moins que deux propriétés : chaque opération enfant doit connaître son parent immédiat, et le composant qui reçoit le contexte doit enregistrer l'identité de celui qui l'a transmis.

Une structure d'événement minimale peut ressembler à ceci :

```json
{
  "event_id": "evt_01J8...",
  "time": "2025-03-08T14:32:11.214Z",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "parent_span_id": "b7ad6b7169203331",
  "root_run_id": "run_8d43",
  "edge_type": "external_action",
  "actor": {
    "kind": "agent_process",
    "identity": "signed-process-identity"
  },
  "action": {
    "action_id": "act_5f17",
    "channel": "http",
    "method": "POST",
    "host": "deploy.internal.example",
    "path_template": "/v1/releases/{name}",
    "credential_ref": "ops-deploy"
  },
  "outcome": {
    "state": "sent",
    "http_status": 202
  }
}
```

L'horodatage ci-dessus illustre une structure, il ne constitue pas une recommandation de format de conservation. Dans un système réel, utilisez un format temporel que votre outil de vérification des journaux peut analyser de manière uniforme. Gardez les points de terminaison et les arguments bruts hors des exports de télémétrie étendus lorsqu'ils contiennent des noms de locataires, des chemins de dépôt ou des données personnelles. Un modèle accompagné d'un enregistrement d'investigation protégé suffit souvent aux opérateurs, sans recopier les données sensibles dans chaque tableau de bord.

Un service qui reçoit un contexte ne doit pas faire aveuglément confiance à `parent_span_id` parce qu'un agent le lui a fourni. Créez un nouveau span local, enregistrez la valeur reçue et ajoutez un champ tel que `upstream_context_source=authenticated_mcp_client` ou `upstream_context_source=unverified_input`. Cette petite distinction empêche un appelant malveillant de rattacher après coup son action à une exécution sans danger.

## Enregistrez la résolution, pas seulement les arguments de l'outil

L'action demandée par un agent et celle exécutée par l'exécuteur peuvent différer après l'application de modèles, de valeurs par défaut, d'alias, de redirections, d'une recherche d'environnement et d'une sélection d'identifiant. Vos enregistrements doivent conserver les deux versions, car c'est souvent dans cet écart que se cache une conception dangereuse.

Considérez un appel d'agent avec les arguments suivants :

```json
{
  "tool": "publish_release",
  "arguments": {
    "environment": "prod",
    "release": "2025.03.08-rc2"
  }
}
```

Un outil peut traduire `prod` en URL de base, sélectionner un identifiant, transformer la chaîne de version en chemin de requête et ajouter des en-têtes. L'appel initial ne prouve pas la destination. Une chaîne complète devrait exposer une transition comme celle-ci :

1. L'agent appelle `publish_release` dans l'exécution `run_8d43`.
2. L'outil résout `prod` vers un point de terminaison autorisé précis et sélectionne la référence d'identifiant `ops-deploy`.
3. L'exécuteur enregistre `act_5f17` juste avant d'envoyer la requête.
4. L'exécuteur enregistre la réponse ou l'erreur de transport pour cette action.
5. L'enveloppeur renvoie un résultat qui cite `act_5f17` sans exposer les identifiants.

Cette séquence permet à l'enquêteur de suivre le chemin entre la demande de l'agent et l'opération réseau. Elle donne aussi à la personne chargée de l'approbation un élément concret à examiner avant tout envoi.

Ne consignez pas des en-têtes d'autorisation complets, des cookies, des commandes privées ou des corps de requête arbitraires pour remplacer une bonne modélisation. Sous pression, on le fait parce qu'une copie brute résout le problème de débogage du jour. Elle crée la fuite d'identifiants de demain. Capturez plutôt une référence d'identifiant, le nom de l'en-tête, une empreinte du corps, les champs non sensibles sélectionnés et un état explicite de masquage.

SSH demande la même rigueur. Un enregistrement indiquant `ssh deploy` est trop vague si l'assistant développe ensuite un alias d'hôte, sélectionne une identité et construit une commande distante. Consignez l'hôte et le port résolus, le nom du compte, la référence de l'identité, un modèle de commande ou son empreinte, ainsi que le code de sortie. Si la commande contient des données sensibles, conservez une copie d'investigation protégée uniquement si vous avez une raison claire et une règle de conservation définie.

## Une nouvelle tentative est une autre tentative, pas une note de bas de page

Les nouvelles tentatives et l'exécution en parallèle transforment un arbre simple en graphe. Si vous les forcez à entrer dans un seul span avec un champ final `success`, vous effacez les informations qui expliquent les effets de bord en double et les échecs partiels.

Utilisez trois ID lorsqu'une opération peut être retentée : un ID de trace pour l'exécution globale, un ID d'action pour l'opération logique voulue et un ID de tentative pour chaque envoi réel. Chaque tentative reçoit son propre span. L'enregistrement de l'action pointe ensuite vers toutes les tentatives.

Supposons qu'un outil envoie une requête de version, expire après que le service distant l'a acceptée, puis réessaie. La deuxième requête peut créer deux fois la même version si le point de terminaison distant ne gère pas l'idempotence. Un `200` final vous apprend peu de choses. Votre journal doit montrer que la première tentative a atteint le réseau, s'est terminée localement par un délai d'attente et que la deuxième a reçu une réponse.

Utilisez un jeton d'idempotence lorsque la destination le prend en charge. Dérivez-le de l'ID d'action logique, pas d'un ID de span temporaire. Le service distant peut ainsi reconnaître un doublon même si votre exécuteur redémarre ou si votre bibliothèque de traçage génère de nouveaux spans.

```text
trace_id=4bf92f... action_id=act_5f17 attempt=1 state=timeout bytes_sent=418
trace_id=4bf92f... action_id=act_5f17 attempt=2 state=completed http_status=200
```

`bytes_sent` aide à distinguer une défaillance de connexion avant la transmission de la requête d'un délai d'attente survenu après l'écriture des données par le client, mais il ne prouve pas ce que le service distant a validé. Indiquez cette incertitude dans l'enregistrement. Ne qualifiez pas la première tentative d'échec d'une manière qui laisserait penser que le système distant n'a rien fait.

Le travail en parallèle nécessite des spans frères, pas un champ de trace mutable partagé que les processus de travail écrasent. Si un outil de planification appelle quatre vérifications d'environnement, créez quatre spans enfants et quatre résultats distincts. Si deux branches conduisent à des actions externes, attribuez deux ID d'action. Un opérateur doit pouvoir révoquer ou examiner une branche sans la confondre avec sa branche sœur.

## L'approbation doit être liée à l'opération résolue

Une approbation humaine n'est utile que si la personne qui examine la demande peut voir l'opération qui aura lieu après résolution. Approuver une étiquette générale telle que `deploy` laisse presque tout à deviner, surtout lorsque des outils imbriqués sélectionnent ensuite l'hôte et l'identifiant réels.

Construisez les informations présentées pour approbation à partir de l'enregistrement de l'action externe en attente : canal, destination résolue, forme de l'opération, référence de l'identifiant, identité du processus et description concise de l'impact. Conservez les ID de trace et d'action dans la décision d'approbation. Lorsque l'exécuteur envoie la requête, il doit prouver qu'il a utilisé la même action approuvée, et non une approbation provenant simplement de la même exécution d'agent.

N'approuvez pas toute une chaîne parce que son premier outil semblait inoffensif. Une chaîne peut commencer par `find_release` et se terminer par une commande SSH qui modifie un hôte. Si la conception permet à une résolution ultérieure d'élargir ce que la chaîne peut faire, exigez une nouvelle décision à la frontière sortante.

Cela ne signifie pas qu'il faut demander à une personne de valider chaque concaténation de chaînes dans un outil. Il faut placer la décision au moment où une capacité sort du chemin d'exécution de confiance. Cette frontière fournit une demande compréhensible et crée dans le journal un lien durable entre le consentement et l'effet produit.

Sallyport adopte cette approche pour ses canaux HTTP et SSH pris en charge : les identifiants restent dans son coffre, le produit exécute lui-même l'action et peut exiger une approbation pour chaque utilisation d'un identifiant sélectionné. Le point d'intégration utile est l'action externe obtenue, pas l'affirmation d'un agent sur ce que son assistant imbriqué avait l'intention de faire.

Les enregistrements d'approbation ont aussi besoin de règles d'expiration et de liaison. Liez une décision à l'ID d'action, à la cible résolue, à la référence de l'identifiant et à l'empreinte des arguments. Si l'un de ces éléments change entre la demande et l'exécution, supprimez la décision et demandez une nouvelle approbation. Un jeton d'approbation réutilisable qui suit le processus de l'agent jusque dans des appels sans rapport finira par autoriser quelque chose que personne n'a lu.

## Un journal d'audit doit être ordonné et vérifiable

Les journaux applicatifs classiques aident à diagnostiquer une panne, mais ils indiquent rarement si quelqu'un a supprimé la ligne gênante. Pour les actions réalisées par des agents autonomes, préservez l'ordre des événements et rendez toute modification ultérieure visible.

Une chaîne de hachage enregistre les octets sérialisés de manière canonique de chaque événement, le hachage de l'événement précédent et le nouveau hachage. Un vérificateur commence au premier enregistrement conservé et recalcule chaque lien. Si un attaquant modifie, insère ou supprime un enregistrement au milieu, la vérification échoue à l'endroit concerné.

La sérialisation canonique est importante. Si un processus trie les champs JSON et qu'un autre ne le fait pas, des enregistrements équivalents produisent des hachages différents. Définissez l'ordre des champs, la normalisation Unicode, la précision des horodatages, le traitement des champs absents et l'encodage des octets. Testez ces règles dans chaque langage qui écrit des événements. La plupart des chaînes d'audit défaillantes échouent ici, pas dans la fonction de hachage.

Un vérificateur doit produire des résultats exploitables par un opérateur :

```text
$ audit verify journal.events
records_checked: 1842
first_sequence: 91001
last_sequence: 92842
chain: valid
signature: valid
```

Lorsqu'il détecte un dommage, il doit indiquer la première séquence incorrecte ainsi que le hachage précédent attendu et celui qui a été observé. Il ne doit pas tenter de réparer silencieusement le fichier. Une réparation détruit les preuves de ce qui s'est passé.

Les chaînes de hachage ne résolvent pas toutes les menaces. Quelqu'un qui contrôle le matériel de signature et le stockage peut réécrire une histoire alternative complète. Des points de contrôle signés périodiquement et stockés en dehors du contrôle habituel de l'écrivain réduisent ce risque. Séparez également les chemins d'accès qui écrivent les événements de ceux qui les lisent. Décrivez précisément la protection dont vous disposez au lieu de qualifier n'importe quel journal d'immuable.

Conservez l'événement d'action près du point d'exécution. Un collecteur en arrière-plan qui reçoit des lots après coup peut perdre le seul enregistrement prouvant qu'un appel sortant a eu lieu. L'exécuteur doit ajouter un enregistrement « planifié » avant la transmission, puis un enregistrement d'achèvement immédiatement après avoir reçu un résultat. S'il tombe en panne entre les deux, la paire incomplète indique que l'action a peut-être quitté le système.

## Testez la trace avec les défaillances réellement provoquées

Une conception de traçage n'est crédible qu'après avoir résisté à un contexte mal formé, à des processus de travail perdus, à des envois en double et à une révocation par un opérateur. Les démonstrations du parcours normal masquent précisément les cas limites qui comptent lorsqu'un agent se comporte de manière inattendue.

Exécutez une petite suite de tests sur un point de terminaison ou un hôte jetable. Chaque test doit vérifier le graphe d'audit, pas seulement la réponse de l'outil :

- Envoyez un appel imbriqué avec un ID parent falsifié et vérifiez que le récepteur identifie ce contexte comme non vérifié.
- Provoquez un délai d'attente après le départ des octets de la requête du client, puis réessayez et vérifiez que les deux tentatives partagent un ID d'action.
- Mettez le travail en file d'attente, redémarrez le processus de travail et vérifiez que le span repris est relié à l'exécution d'origine sans prétendre avoir fonctionné sans interruption.
- Révoquez l'autorisation après la planification mais avant l'exécution et vérifiez qu'aucun enregistrement d'action externe n'atteint l'état `sent`.
- Effectuez deux appels frères en parallèle et vérifiez qu'aucune branche n'adopte le span parent ou le résultat de l'autre.

Le troisième test révèle un mensonge fréquent dans les journaux. Les systèmes signalent souvent une seule requête ininterrompue alors qu'un processus de travail a redémarré des heures plus tard avec une autre identité de processus. Cela masque l'identité réelle de l'auteur de l'action. Enregistrez l'identité et l'heure de démarrage du processus de travail comme des faits du nouveau span.

Le quatrième test détecte une autre défaillance connue. Un outil obtient une autorisation avant de résoudre sa cible finale, puis utilise cette décision obsolète pour l'appel résolu. Votre test doit modifier la cible ou la référence d'identifiant après l'approbation et vérifier que l'exécuteur la rejette.

Ne vous contentez pas de captures d'écran d'un outil de visualisation des traces. Exportez les enregistrements bruts, vérifiez leur chaîne et écrivez des assertions sur les ID parents, les ID d'action, les destinations, les liaisons d'autorisation et les résultats. La visualisation est un outil pratique. Le flux d'événements est la preuve.

## Construisez d'abord la frontière, puis complétez le graphe

Commencez par le code qui effectue les opérations HTTP et SSH. Faites-lui accepter explicitement le contexte de trace, créer un enregistrement d'action avant l'envoi, créer un enregistrement de résultat après celui-ci et refuser l'exécution si l'autorisation ne correspond pas à l'opération résolue.

Une fois cette frontière fiable, instrumentez les appels d'outils imbriqués situés au-dessus. Vous saurez quelles informations chaque parent doit transmettre, car l'exécuteur les exigera. Travailler dans l'autre sens produit souvent de beaux arbres dont les feuilles ne sont pas fiables.

Gardez les noms d'outils, les notes de planification et le raisonnement du modèle séparés du journal des actions externes. Ils peuvent aider à comprendre pourquoi une exécution a eu lieu. Ils ne remplacent pas un enregistrement indiquant quel système distant a reçu une requête, avec quelle référence d'identifiant, après quelle approbation et avec quel résultat. C'est cette chaîne dont vous aurez besoin lorsque la réponse de l'agent semblera plausible, mais que le système distant dira le contraire.
