Les MCP Tool Hooks dans Claude Code
Comment appeler les outils d'un serveur MCP directement depuis les hooks de Claude Code avec type: mcp_tool — schéma, syntaxe de substitution, cas d'usage et patterns de production.
Arrêtez de configurer. Commencez à construire.
Templates SaaS avec orchestration IA.
Le problème : tes hooks exécutent des scripts shell. Chaque fois qu'un hook doit appeler un serveur MCP, il lance un sous-processus, câble le transport, gère l'auth, parse la réponse et formate la sortie JSON vers stdout. Pour un formateur ou un scan de sécurité qui se déclenche à chaque écriture de fichier, ça finit par peser.
La solution rapide : depuis la v2.1.118, les hooks ont un nouveau type qui appelle les outils MCP directement. Ajoute ça dans .claude/settings.json pour lancer un scan de sécurité après chaque écriture de fichier, sans sous-processus :
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "semgrep",
"tool": "scan_file",
"input": { "path": "${tool_input.file_path}" }
}
]
}
]
}
}Le serveur MCP tourne déjà. Le hook court-circuite le shell et appelle directement la connexion RPC du serveur. La sortie texte de l'outil passe par le même parseur de décision JSON que n'importe quel hook de commande.
Ce qu'est vraiment type: "mcp_tool"
Avant la v2.1.118, les hooks avaient quatre types de handlers : command, http, prompt et agent. Maintenant il y en a cinq :
| Type | Ce qui s'exécute |
|---|---|
command | Sous-processus shell (stdin/stdout) |
http | POST vers une URL |
mcp_tool | Appel RPC direct vers un serveur MCP connecté |
prompt | Évaluation LLM en un seul tour (Haiku par défaut) |
agent | Sous-agent multi-tour avec accès Read/Grep/Glob |
Le type mcp_tool couvre tous les événements de hook, comme command et http. La seule contrainte pratique : SessionStart et Setup se déclenchent pendant que les serveurs se connectent encore, donc ces hooks peuvent renvoyer une erreur "server not connected" au premier lancement. Les suivants se passent bien.
Le schéma complet
Trois champs sont spécifiques aux hooks mcp_tool. Le reste est partagé avec tous les types de hooks :
{
"type": "mcp_tool",
"server": "my-mcp-server",
"tool": "tool_name",
"input": {
"arg1": "${tool_input.file_path}",
"arg2": "${session_id}"
},
"timeout": 30,
"statusMessage": "Checking...",
"if": "Edit(*.ts|*.tsx)"
}| Champ | Requis | Description |
|---|---|---|
server | OUI | Nom exact du serveur MCP tel que configuré dans settings |
tool | OUI | Nom de l'outil sur ce serveur |
input | non | Arguments passés à l'outil. Supporte la substitution ${path} |
timeout | non | Secondes avant annulation du hook |
statusMessage | non | Texte du spinner affiché pendant l'exécution |
if | non | Filtre en syntaxe permission-rule. Le hook ne se déclenche que si l'appel complet correspond |
Important : server doit correspondre exactement au nom du serveur dans ta configuration MCP. Un seul caractère de différence et le hook échoue silencieusement avec une erreur non bloquante.
La substitution dans input
Les valeurs string dans input supportent la notation ${field.path} pour naviguer dans le JSON complet de l'événement. Pour un hook PostToolUse sur un appel Write, l'événement ressemble à ça :
{
"session_id": "abc123",
"cwd": "/your/project",
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_use_id": "toolu_01...",
"tool_input": {
"file_path": "/your/project/src/api.ts",
"content": "..."
},
"tool_response": { "filePath": "/your/project/src/api.ts", "success": true },
"duration_ms": 142
}Donc "${tool_input.file_path}" résout vers /your/project/src/api.ts. N'importe quel champ de cet objet est accessible. Le champ duration_ms a été ajouté en v2.1.119, une release après le lancement de mcp_tool.
Comment la sortie est traitée
Le contenu texte de l'outil MCP est traité exactement comme le stdout d'un hook de commande. Si c'est du JSON valide, Claude Code applique les champs de décision. Sinon, le texte devient du contexte pour Claude.
Les champs de décision fonctionnent comme sur n'importe quel hook :
{
"decision": "block",
"reason": "Security issue found in src/api.ts: SQL injection risk on line 42."
}Retourne ça depuis un hook MCP PostToolUse et Claude reçoit le message et corrige le fichier. L'outil a déjà tourné, donc c'est consultatif, pas préventif. Pour bloquer avant l'exécution d'un outil, utilise PreToolUse et retourne permissionDecision: "deny".
Un champ est exclusif aux hooks mcp_tool sur PostToolUse : updatedMCPToolOutput. Il remplace ce que Claude voit comme sortie de l'outil avant que ça entre dans la conversation. Un serveur MCP actif peut post-traiter le résultat d'un autre outil avant que Claude le lise.
Pourquoi c'est mieux que les hooks shell
Deux différences concrètes, pas juste la vitesse.
Serveurs avec état. Un sous-processus shell repart de zéro à chaque fois. Un serveur MCP est un processus vivant avec son propre état : configs chargées, connexions ouvertes, caches, contexte de session accumulé. Un MCP de lint qui a pré-parsé ton tsconfig.json au démarrage ne le re-parse pas à chaque écriture de fichier. Un hook shell, si.
Pas de dépendance à l'environnement shell. Les hooks de commande échouent silencieusement quand PATH est mauvais, quand jq n'est pas installé, quand ~/.zshrc écrit quelque chose sur stdout sur des shells non-interactifs. Les hooks MCP contournent tout ça. L'appel va directement de Claude Code au serveur via la connexion RPC existante.
Le champ if : cible tes hooks
Sans if, un hook se déclenche sur chaque événement correspondant au matcher. Avec if, le hook ne s'active que quand l'appel complet (nom et arguments) correspond à la syntaxe permission-rule :
{
"type": "mcp_tool",
"server": "semgrep",
"tool": "scan_file",
"if": "Edit(*.py|*.ts|*.js)",
"input": { "path": "${tool_input.file_path}" }
}Ce hook ne tourne jamais sur les fichiers .md ou .json. Sur un projet avec beaucoup d'éditions de documentation, la différence de perfo est réelle.
Pattern 1 : Scan de sécurité à chaque écriture
Un serveur MCP de sécurité qui accepte un chemin de fichier et retourne les résultats. Bloque Claude s'il trouve quelque chose :
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "semgrep",
"tool": "scan_file",
"if": "Write(*.ts|*.py|*.js|*.go)",
"input": { "path": "${tool_input.file_path}" },
"statusMessage": "Scanning..."
}
]
}
]
}
}Si l'outil MCP retourne un résultat, structure la réponse comme ça :
{
"decision": "block",
"reason": "Semgrep finding: [description of issue at line N]"
}Claude reçoit le message de blocage et retravaille le fichier. Le scan tourne sur le ruleset mis en cache du serveur, pas un parse de sous-processus à froid.
Pattern 2 : Hook Stop avec vérification externe
Un hook Stop qui appelle un MCP Linear ou Jira pour vérifier si le ticket associé est vraiment fermé avant de laisser Claude déclarer que c'est terminé :
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "linear",
"tool": "get_issue_status",
"input": { "issue_id": "${tool_input.issue_id}" }
}
]
}
]
}
}L'outil MCP retourne le statut du ticket. S'il revient en In Progress, le JSON de réponse doit porter decision: "block" avec une raison. Claude continue à travailler.
Pense toujours à vérifier stop_hook_active dans ta logique de hook Stop. L'événement JSON inclut ce champ avec la valeur "true" quand Claude continue déjà depuis un hook Stop précédent. Un serveur qui ne vérifie pas ça crée une boucle infinie. Intègre la protection directement dans l'outil MCP : si stop_hook_active vaut "true" dans l'input, retourne une sortie vide et sors proprement.
Pattern 3 : Vérification d'erreurs de prod avant de stopper
Après qu'une feature soit terminée par Claude, vérifie si quelque chose de nouveau a cassé en staging avant de marquer la session comme complète. Un MCP Sentry gère la vérification :
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "sentry",
"tool": "get_new_errors_since",
"input": { "minutes": "5", "skip_if_active": "${stop_hook_active}" }
}
]
}
]
}
}Si de nouvelles erreurs sont apparues dans les cinq dernières minutes, l'outil MCP les retourne avec decision: "block". Claude lit les détails d'erreur et corrige la régression avant de s'arrêter.
Pattern 4 : Injection automatique de docs avant chaque prompt
Un hook UserPromptSubmit avec un MCP Context7 qui récupère la documentation à jour pour n'importe quelle lib mentionnée dans le prompt, avant que Claude le traite :
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "context7",
"tool": "get_library_docs",
"input": { "prompt": "${prompt}" },
"timeout": 15
}
]
}
]
}
}Avant, ça nécessitait que Claude appelle explicitement l'outil MCP. Maintenant ça se passe automatiquement à chaque prompt. Claude démarre avec la doc à jour plutôt que ses données d'entraînement.
Pattern 5 : Application de politiques pour les équipes d'agents
Dans les workflows multi-agents, un serveur MCP de politique partagée peut contrôler quel agent écrit dans quels répertoires. La variable d'environnement CLAUDE_AGENT_NAME identifie l'agent courant. Un hook PreToolUse appelle le serveur de politique :
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "policy-server",
"tool": "check_write_permission",
"input": {
"agent": "${agent_name}",
"path": "${tool_input.file_path}"
}
}
]
}
]
}
}Le serveur de politique détient la carte d'autorisation complète. Mets-le à jour une fois et chaque agent dans chaque projet hérite des nouvelles règles, sans toucher un seul settings.json.
Pattern 6 : MCP Tool Hooks dans le frontmatter des agents
Les hooks ne doivent pas forcément vivre dans settings.json. Ils peuvent se trouver dans le frontmatter YAML d'un agent, scopés au cycle de vie de cet agent :
---
name: backend-developer
description: Builds API endpoints and database logic
hooks:
PostToolUse:
- matcher: "Write"
hooks:
- type: mcp_tool
server: semgrep
tool: scan_file
input: { "path": "${tool_input.file_path}" }
Stop:
- hooks:
- type: agent
prompt: "Verify all API endpoints have corresponding tests. Block if any are missing."
---Chaque agent spécialisé dans une équipe orchestrée porte sa propre logique de validation. L'agent backend scanne les problèmes de sécurité. L'agent frontend vérifie l'accessibilité. Aucun n'a besoin d'un hook global qui s'applique à tout le monde.
Contrôle de l'élicitation
L'événement Elicitation se déclenche quand un serveur MCP demande une entrée utilisateur en cours de tâche. Un hook mcp_tool peut répondre automatiquement aux prompts connus en appelant un gestionnaire de secrets :
{
"hooks": {
"Elicitation": [
{
"matcher": "my-db-server",
"hooks": [
{
"type": "mcp_tool",
"server": "secrets-manager",
"tool": "get_credential",
"input": { "key": "${elicitation.field_name}" }
}
]
}
]
}
}Les demandes de credentials prévisibles se résolvent automatiquement. La tâche tourne sans interruption.
Les serveurs MCP à coupler avec des hooks
Tous les serveurs MCP n'ont pas vocation à être des cibles de hooks. Ceux qui s'y prêtent le mieux sont les outils qui doivent se déclencher sur des événements spécifiques sans intervention utilisateur :
| Serveur | Événement | Ce qu'il fait |
|---|---|---|
| Semgrep | PostToolUse: Write | Scan de sécurité à chaque écriture |
| Sentry | Stop | Vérifie les nouvelles erreurs de staging avant de terminer |
| Linear / Jira | Stop, TaskCompleted | Vérifie le statut du ticket, met à jour à la complétion |
| Context7 | UserPromptSubmit | Récupère la doc à jour des libs mentionnées |
| ElevenLabs | Stop, Notification | Audio TTS à la complétion de tâche |
| Slack | Notification, Stop | Alertes d'équipe sans boilerplate curl |
| E2B | Stop | Lance les scripts générés dans un sandbox avant de marquer terminé |
| claude-mem | PostCompact, SessionStart | Restaure le contexte de session après compaction |
| n8n | TaskCompleted | Déclenche un workflow externe à la complétion |
Bug connu : PostToolUse + événements MCP + additionalContext
Il existe un bug ouvert (issue GitHub #24788) où additionalContext des hooks est silencieusement ignoré quand l'événement déclencheur est un appel d'outil MCP. Ça affecte les hooks de type "command" qui répondent aux événements d'outils MCP, pas les hooks mcp_tool eux-mêmes.
La distinction est importante : les hooks qui SONT des invocations MCP fonctionnent bien. Les hooks qui RÉPONDENT aux appels d'outils MCP et retournent additionalContext, non. Le contournement consiste à utiliser exit 2 plus stderr pour les messages critiques depuis les hooks PostToolUse ciblant des appels d'outils MCP. Le pattern de blocage fonctionne, l'injection consultative non.
Les MCP Tool Hooks complètent le système de hooks
Avant, les hooks étaient un filet de sécurité. Des commandes shell qui pouvaient bloquer des choses dangereuses ou lancer des formateurs. Stateless, locaux au processus, déconnectés de tout ce que tes serveurs MCP savaient déjà.
Maintenant : les hooks sont une couche d'orchestration déterministe. N'importe quel événement, n'importe quel outil MCP, contrôle total des décisions, avec un état qui persiste entre les appels et zéro overhead de sous-processus.
Le pipeline est complet. PreToolUse valide. PostToolUse formate et scanne. PostToolBatch lance les tests. Stop vérifie avec de vraies données externes. Chaque étape peut être une invocation d'outil MCP, et aucune ne nécessite un script shell.
Arrêtez de configurer. Commencez à construire.
Templates SaaS avec orchestration IA.