AI

AgentOps : borner timeout, retries et circuit breaker d'un outil MCP avant la production

Un runbook de production pour définir le budget de latence, l'idempotence, les retries, le circuit breaker, les traces et le rollback d'un outil MCP appelé par un agent IA.

02 août 2026 aiagentopsagentsmcptoolsresilienceobservabilityidempotencyevaluationguardrailsautomationrunbookrollbackproduction

Un agent d’exploitation appelle un outil MCP pour lire l’état d’un déploiement, préparer une action puis l’exécuter après approbation. En test, chaque appel aboutit. En production, une dépendance ralentit : l’agent attend, le client expire, le runtime retente et l’opérateur relance la conversation. Une seule intention peut alors devenir plusieurs appels concurrents, avec une réponse tardive et un état final difficile à expliquer.

Le problème n’est pas seulement la latence du modèle. Il se trouve à la frontière entre agent, transport MCP, serveur d’outils et backend métier. Ce runbook sert à borner cette frontière avant l’ouverture en production : définir un budget de temps, décider quels appels sont retentables, couper les cascades avec un circuit breaker, observer un parcours complet, puis valider ou rollbacker la politique de résilience.

Figer un parcours et son résultat attendu

Commencez par un seul parcours représentatif. Le cas fil rouge utilise un agent interne qui vérifie un job de déploiement et peut demander sa relance. La lecture est sans effet de bord ; la relance est une écriture approuvée. Ces deux appels ne doivent pas partager aveuglément la même politique de timeout et de retry.

yaml mcp-tool-journey-contract.yml
journey:
name: inspect_then_restart_deployment_job
agent: ops-assistant-prod
user_deadline: 20s
tools:
  - name: get_deployment_job
    effect: read_only
    expected_result: current job state
  - name: restart_deployment_job
    effect: write
    approval: human_required
    expected_result: one accepted operation or no operation

evidence:
- one conversation and intent identifier
- one tool call identifier per attempt
- backend operation identifier when accepted
- timeout owner and elapsed time
- retry reason and idempotency key
- circuit state at call time
- final validation or rollback decision

Le délai utilisateur est un plafond pour le parcours, pas la valeur à recopier sur chaque couche. Si le backend peut consommer vingt secondes, le transport, l’agent et l’interface n’ont plus de marge pour signaler proprement l’échec ou proposer une reprise.

Classer les outils par effet, pas par protocole

MCP décrit une interface d’outil ; il ne rend pas l’opération idempotente. Classez chaque outil selon son effet réel et la capacité du backend à reconnaître une répétition.

text tool-retry-classes.txt
Lecture deterministe
Exemple : obtenir l'etat d'un job par identifiant
Retry possible sur erreur transitoire si le budget restant le permet
Limiter tout de meme le nombre de tentatives et la concurrence

Recherche ou calcul couteux
Exemple : interroger une large fenetre de logs
Retry seulement avec une requete bornee et un budget backend connu
Preferer pagination, cache ou reprise explicite a la duplication

Ecriture idempotente
Exemple : demander une operation avec une cle d'idempotence stable
Retry possible si le backend garantit un resultat unique pour cette cle
Toujours relire l'etat de l'operation apres une reponse ambigue

Ecriture non idempotente
Exemple : relancer un job sans identifiant d'operation stable
Aucun retry automatique
Apres timeout, verifier le backend puis reprendre manuellement ou rollbacker

Le point de contrôle doit vivre dans le catalogue d’outils ou la policy d’exécution, pas uniquement dans le prompt. Un modèle peut proposer un retry ; le runtime doit rester l’autorité qui l’autorise ou le bloque.

Construire un budget de timeout de l’extérieur vers l’intérieur

Un timeout utile indique quelle couche abandonne et laisse aux couches externes le temps de traiter ce résultat. Les valeurs suivantes illustrent un contrat, elles ne constituent pas des seuils universels.

yaml mcp-time-budget.example.yml
journey_budget_ms: 20000
layers:
user_interface:
  deadline_ms: 20000
agent_runtime:
  deadline_ms: 15000
  reserve_for_final_response_ms: 3000
mcp_transport:
  timeout_ms: 10000
tool_handler:
  backend_timeout_ms: 7000

rules:
- inner_timeout_must_expire_before_outer_timeout
- no_retry_without_remaining_budget
- cancellation_must_propagate_to_backend_when_supported
- late_results_must_not_trigger_a_second_write
- timeout_events_must_name_the_layer_that_expired

Mesurez le p50, le p95 et la queue de distribution par outil, backend et résultat. Un seul timeout global masque la différence entre attente réseau, file d’exécution, throttling backend et traitement métier. Gardez aussi une réserve pour produire une réponse exploitable : l’agent doit pouvoir dire que l’état est ambigu et fournir l’identifiant de corrélation au lieu de disparaître à la dernière milliseconde.

Rendre le retry explicite et borné

Une politique de retry doit combiner classe d’erreur, budget restant, compteur de tentatives et idempotence. Un backoff exponentiel avec jitter réduit la synchronisation des clients, mais ne rend pas une écriture répétable.

json mcp-retry-policy.json
{
"tool": "get_deployment_job",
"max_attempts": 3,
"retry_on": ["transport_unavailable", "backend_429", "backend_503"],
"do_not_retry_on": ["invalid_arguments", "policy_denied", "approval_required", "backend_4xx_other"],
"backoff": {
  "strategy": "exponential_with_jitter",
  "initial_ms": 250,
  "max_ms": 1500
},
"constraints": {
  "remaining_budget_required": true,
  "same_intent_id": true,
  "new_tool_call_id_per_attempt": true
}
}

Pour une écriture, ajoutez une clé d’idempotence dérivée d’une intention stable, de la cible et de l’approbation, sans y placer de secret. Le backend doit persister la clé avec l’opération et renvoyer le même identifiant si la demande arrive de nouveau.

yaml write-idempotency-contract.yml
tool: restart_deployment_job
automatic_retry: false
idempotency:
key_components:
  - intent_id
  - target_job_id
  - environment
  - approval_id
backend_guarantee: one_operation_per_key
after_ambiguous_timeout:
- stop_agent_retry
- query_operation_by_idempotency_key
- compare_target_state
- resume_only_with_explicit_decision

Si le backend ne sait pas rechercher l’opération par clé, le retry automatique reste désactivé. Le correctif consiste d’abord à améliorer le contrat d’outil ou l’API cible, pas à demander au modèle d’être plus prudent.

Ouvrir le circuit avant la cascade

Le circuit breaker protège le serveur MCP et sa dépendance lorsqu’une série d’échecs indique que les retries ne peuvent plus aider. Il doit être défini par outil et, si nécessaire, par cible ou backend. Un circuit global peut rendre indisponibles des lectures saines à cause d’une seule opération dégradée.

yaml mcp-circuit-breaker.example.yml
breaker:
scope: tool_and_backend
tool: get_deployment_job
backend: deployment-api-prod
window: 60s
minimum_calls: 20
open_when:
  failure_ratio_gte: 0.50
  slow_call_ratio_gte: 0.70
open_duration: 30s
half_open:
  probe_calls: 3
  concurrent_probes: 1
fallback:
  mode: read_only_degraded_response
  include:
    - last_known_state_timestamp
    - incident_reference
    - no_write_action_performed

Les seuils doivent provenir de mesures et d’exercices contrôlés. En état ouvert, l’agent ne doit pas contourner le breaker en choisissant un outil équivalent ou en multipliant les conversations. La réponse dégradée doit distinguer une donnée fraîche, une donnée en cache et une absence de preuve.

Tracer la tentative, pas seulement la conversation

Un tableau de bord agrégé par conversation cache les retries. Émettez un événement par tentative avec les identifiants d’intention, d’appel et d’opération backend, ainsi que la couche de timeout et l’état du circuit.

kusto 01-mcp-tool-resilience-watch.kql
let StartTime = datetime(2026-08-02T08:00:00Z);
let EndTime = datetime(2026-08-02T10:00:00Z);
AgentToolCallEvents
| where TimeGenerated between (StartTime .. EndTime)
| where Protocol == "mcp"
| summarize
  calls=count(),
  intents=dcount(IntentId),
  retries=countif(Attempt > 1),
  timeouts=countif(Result == "timeout"),
  breakerRejects=countif(CircuitState == "open"),
  p50DurationMs=percentile(DurationMs, 50),
  p95DurationMs=percentile(DurationMs, 95),
  backendOperations=dcountif(BackendOperationId, isnotempty(BackendOperationId))
by bin(TimeGenerated, 5m), ToolName, Backend, Result
| extend CallsPerIntent = todouble(calls) / iif(intents == 0, 1.0, todouble(intents))
| order by TimeGenerated asc

Adaptez la table à votre pipeline. Le signal critique est le rapport entre intentions, tentatives et opérations backend. Une hausse de CallsPerIntent sans hausse de trafic utilisateur révèle une amplification interne. Pour une écriture idempotente, plusieurs tentatives peuvent exister, mais une seule opération backend doit être créée.

Tester les défaillances avant le trafic réel

La validation doit injecter lenteur, timeout, réponse tardive, indisponibilité et résultat ambigu. Elle couvre aussi le comportement de l’agent : explique-t-il l’état, s’arrête-t-il quand le budget est épuisé et respecte-t-il l’approbation ?

yaml mcp-resilience-evaluation.yml
cases:
- id: read_transient_503
  tool: get_deployment_job
  fault: backend_503_then_success
  expected:
    attempts: 2
    backend_operations: 0
    answer_contains_fresh_state: true

- id: write_response_lost_after_accept
  tool: restart_deployment_job
  fault: response_timeout_after_backend_accept
  expected:
    automatic_retry: false
    operation_lookup_by_idempotency_key: true
    duplicate_operations: 0

- id: sustained_dependency_failure
  tool: get_deployment_job
  fault: backend_503_for_90s
  expected:
    circuit_opens: true
    probe_concurrency: 1
    write_fallback: false

- id: budget_exhausted
  tool: get_deployment_job
  fault: slow_response
  expected:
    extra_retry: false
    timeout_layer_reported: true
    correlation_id_returned: true

Exécutez ces cas en environnement isolé, puis avec un canary de production sans effet métier. Vérifiez que l’arrêt de la requête annule réellement le traitement lorsque le backend le permet ; sinon, documentez qu’une réponse tardive reste possible et interdisez toute seconde écriture automatique.

Déployer par étapes et préparer le rollback

Commencez en shadow mode : calculez la décision de retry et de breaker sans l’appliquer. Comparez-la au comportement actuel, puis activez-la pour un seul outil read-only. Les outils d’écriture viennent ensuite, uniquement avec idempotence backend, approbation bornée et recherche d’état après timeout ambigu.

Le rollback porte sur la policy de résilience, pas sur les opérations métier déjà acceptées. Versionnez la configuration, gardez la précédente et prévoyez un kill switch qui bloque les écritures tout en conservant les diagnostics read-only. Si une opération a été créée, son annulation ou sa compensation suit le runbook du backend.

Conservez la nouvelle policy seulement si le canary confirme : un budget respecté, aucune amplification incontrôlée, un breaker observable, aucune opération dupliquée et une réponse agent exploitable en mode dégradé. Rollbackez si les appels légitimes sont coupés, si la latence se déplace vers une autre couche, si les retries augmentent la charge ou si l’état d’une écriture reste impossible à prouver.

Conclusion

La résilience d’un outil MCP ne se résume pas à ajouter trois retries. Elle relie le budget utilisateur aux timeouts internes, classe les effets de bord, exige l’idempotence des écritures, ouvre le circuit avant la cascade et conserve une trace par tentative.

La décision de production tient en une règle : activer la policy seulement si un test de panne prouve qu’une intention reste bornée dans le temps et ne crée qu’un effet métier explicable. Dans le cas contraire, revenez à la configuration précédente, bloquez les écritures concernées et corrigez d’abord le contrat d’outil ou l’observabilité.