MCP Tool Hooks no Claude Code
Como chamar ferramentas de servidores MCP diretamente dos hooks do Claude Code usando type: mcp_tool — schema, sintaxe de substituição, casos de uso e padrões para produção.
Pare de configurar. Comece a construir.
Templates SaaS com orquestração de IA.
O problema: os teus hooks executam shell scripts. Cada vez que um hook precisa chamar um servidor MCP, ele cria um subprocesso, configura o transporte, trata autenticação, faz o parse da resposta e formata a saída JSON de volta ao stdout. Para um formatter ou uma verificação de segurança que dispara a cada escrita de ficheiro, esse overhead vai acumulando.
A solução rápida: a partir da v2.1.118, os hooks têm um novo tipo que chama ferramentas MCP diretamente. Adiciona isto ao .claude/settings.json para executar um scan de segurança depois de cada escrita de ficheiro, sem subprocesso nenhum:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "semgrep",
"tool": "scan_file",
"input": { "path": "${tool_input.file_path}" }
}
]
}
]
}
}O servidor MCP já está a correr. O hook ignora completamente o shell e chama diretamente a conexão RPC do servidor. O output de texto da ferramenta passa pelo mesmo parser de decisões JSON que qualquer hook de comando.
O que é type: "mcp_tool" na prática
Antes da v2.1.118, os hooks tinham quatro tipos de handler: command, http, prompt e agent. Agora são cinco:
| Tipo | O que executa |
|---|---|
command | Subprocesso shell (stdin/stdout) |
http | POST para um endpoint URL |
mcp_tool | Chamada RPC direta a um servidor MCP conectado |
prompt | Avaliação LLM de turno único (Haiku por omissão) |
agent | Subagente multi-turno com acesso Read/Grep/Glob |
O tipo mcp_tool funciona com todos os eventos de hook, igual ao command e ao http. A única ressalva prática: SessionStart e Setup disparam enquanto os servidores ainda estão a conectar, por isso esses hooks podem receber um erro "servidor não conectado" na primeira execução. As seguintes correm bem.
O Schema Completo
Três campos são específicos dos hooks mcp_tool. Os restantes são partilhados entre todos os tipos de hook:
{
"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)"
}| Campo | Obrigatório | Descrição |
|---|---|---|
server | SIM | Nome exato do servidor MCP conforme configurado nas settings |
tool | SIM | Nome da ferramenta nesse servidor |
input | não | Argumentos passados à ferramenta. Suporta substituição ${path} |
timeout | não | Segundos antes de o hook ser cancelado |
statusMessage | não | Texto do spinner mostrado enquanto o hook corre |
if | não | Filtro de sintaxe de regras de permissão. O hook só dispara quando a chamada completa corresponde |
Atenção: server tem de corresponder exatamente ao nome do servidor na tua configuração MCP. Uma única diferença de caractere faz o hook falhar silenciosamente com um erro não bloqueante.
Substituição de Input
Os valores de string em input suportam notação de ponto ${field.path} para aceder ao JSON completo do evento do hook. Para um hook PostToolUse numa chamada Write, o JSON do evento é assim:
{
"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
}Então "${tool_input.file_path}" resolve para /your/project/src/api.ts. Qualquer campo desse objeto é acessível. O campo duration_ms foi adicionado na v2.1.119, uma release depois do mcp_tool ter chegado.
Como o Output é Processado
O conteúdo de texto da ferramenta MCP é tratado exatamente como o stdout de um hook de comando. Se fizer parse como JSON válido, o Claude Code age sobre os campos de decisão. Se não, o texto torna-se contexto para o Claude.
Os campos de decisão funcionam igual a qualquer hook:
{
"decision": "block",
"reason": "Security issue found in src/api.ts: SQL injection risk on line 42."
}Retorna isto num hook PostToolUse de ferramenta MCP e o Claude recebe a mensagem e corrige o ficheiro. A ferramenta já executou, por isso isto é consultivo, não preventivo. Para bloquear antes de uma ferramenta correr, usa PreToolUse e retorna permissionDecision: "deny".
Um campo é exclusivo dos hooks mcp_tool em PostToolUse: updatedMCPToolOutput. Substitui o que o Claude vê como output da ferramenta antes de entrar na conversa. Um servidor MCP a correr pode pós-processar o resultado de outra ferramenta antes de o Claude o ler.
Porquê Isto Importa vs. Hooks de Comando Shell
Há duas diferenças concretas, não só velocidade.
Servidores com estado. Um subprocesso shell começa do zero cada vez. Um servidor MCP é um processo vivo com o seu próprio estado: configurações carregadas, conexões abertas, caches, contexto de sessão acumulado. Um MCP de linting que fez o parse do teu tsconfig.json no arranque não o volta a fazer em cada escrita de ficheiro. Um hook de comando faz.
Sem dependência do ambiente shell. Os hooks de comando falham silenciosamente quando o PATH está errado, quando o jq não está instalado, quando o ~/.zshrc imprime algo no stdout em shells não-interativas. Os hooks de ferramenta MCP contornam tudo isso. A chamada vai diretamente do Claude Code para o servidor pela conexão RPC existente.
O Campo if: Restringe os Teus Hooks
Sem if, um hook dispara em cada evento que corresponde ao matcher. Com if, o processo do hook só é criado quando a chamada completa à ferramenta (nome e argumentos) corresponde à sintaxe de regras de permissão:
{
"type": "mcp_tool",
"server": "semgrep",
"tool": "scan_file",
"if": "Edit(*.py|*.ts|*.js)",
"input": { "path": "${tool_input.file_path}" }
}Este hook nunca corre em ficheiros .md ou .json. Num projeto com muitas edições de documentação, essa diferença de performance é real.
Padrão 1: Scan de Segurança em Cada Escrita
Um servidor MCP de segurança que aceita um caminho de ficheiro e retorna findings. Bloqueia o Claude se encontrar algo:
{
"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..."
}
]
}
]
}
}Se a ferramenta MCP retornar um finding, estrutura a resposta assim:
{
"decision": "block",
"reason": "Semgrep finding: [description of issue at line N]"
}O Claude recebe a mensagem de bloqueio e rework o ficheiro. O scan corre no ruleset em cache do servidor, não num parse de subprocesso a arrancar do zero.
Padrão 2: Hook Stop com Verificação Externa
Um hook Stop que chama um MCP do Linear ou Jira para verificar se o ticket relacionado está mesmo fechado antes de deixar o Claude declarar que terminou:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "linear",
"tool": "get_issue_status",
"input": { "issue_id": "${tool_input.issue_id}" }
}
]
}
]
}
}A ferramenta MCP retorna o estado do ticket. Se vier como In Progress, o JSON de resposta deve ter decision: "block" e uma razão. O Claude continua a trabalhar.
Verifica sempre stop_hook_active na lógica do teu hook Stop. O JSON do evento inclui este campo como "true" quando o Claude já está a continuar de um disparo anterior do hook Stop. Um servidor que não verifica isto cria um loop infinito. Incorpora a proteção na ferramenta MCP: se stop_hook_active for "true" no input, retorna output vazio e sai limpo.
Padrão 3: Verificação de Erros de Produção Antes de Parar
Depois de o Claude terminar uma funcionalidade, verifica se algo novo quebrou em staging antes de marcar a sessão como completa. Um MCP do Sentry trata a consulta:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "sentry",
"tool": "get_new_errors_since",
"input": { "minutes": "5", "skip_if_active": "${stop_hook_active}" }
}
]
}
]
}
}Se apareceram novos erros nos últimos cinco minutos, a ferramenta MCP retorna-os junto com decision: "block". O Claude lê os detalhes do erro e corrige a regressão antes de parar.
Padrão 4: Injeção Automática de Docs Antes de Cada Prompt
Um hook UserPromptSubmit com um MCP Context7 que vai buscar documentação atualizada de qualquer biblioteca mencionada no prompt, antes de o Claude o processar:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "context7",
"tool": "get_library_docs",
"input": { "prompt": "${prompt}" },
"timeout": 15
}
]
}
]
}
}Antes, isto exigia que o Claude chamasse explicitamente a ferramenta MCP. Agora acontece automaticamente em cada prompt. O Claude começa com docs atuais em vez de dados de treino.
Padrão 5: Aplicação de Políticas para Equipas de Agentes
Em workflows multi-agente, um servidor MCP de políticas partilhado pode controlar quais agentes escrevem em quais diretórios. A variável de ambiente CLAUDE_AGENT_NAME identifica o agente atual. Um hook PreToolUse chama o servidor de políticas:
{
"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}"
}
}
]
}
]
}
}O servidor de políticas tem o mapa de autorização completo. Atualiza o servidor uma vez e todos os agentes em todos os projetos herdam as novas regras, sem tocar num único settings.json.
Padrão 6: Hooks de Ferramenta MCP no Frontmatter de Agentes
Os hooks não têm de viver no settings.json. Podem ficar no frontmatter YAML de um agente, com scope para o ciclo de vida desse agente:
---
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."
---Cada agente especialista numa equipa orquestrada traz a sua própria lógica de validação. O agente de backend faz scan de segurança. O agente de frontend verifica acessibilidade. Nenhum precisa de um hook global que se aplique a toda a gente.
Controlo de Elicitation
O evento Elicitation dispara quando um servidor MCP pede input ao utilizador a meio de uma tarefa. Um hook mcp_tool pode responder automaticamente a prompts conhecidos chamando um gestor de segredos:
{
"hooks": {
"Elicitation": [
{
"matcher": "my-db-server",
"hooks": [
{
"type": "mcp_tool",
"server": "secrets-manager",
"tool": "get_credential",
"input": { "key": "${elicitation.field_name}" }
}
]
}
]
}
}Os prompts de credenciais previsíveis resolvem-se automaticamente. A tarefa corre sem interrupção.
Servidores MCP que Combinam Bem com Hooks
Nem todos os servidores MCP fazem sentido como alvo de hooks. Os que melhor se encaixam são ferramentas que precisam de disparar em eventos específicos sem intervenção do utilizador:
| Servidor | Evento | O que faz |
|---|---|---|
| Semgrep | PostToolUse: Write | Scan de segurança em cada escrita |
| Sentry | Stop | Verifica erros novos em staging antes de completar |
| Linear / Jira | Stop, TaskCompleted | Verifica estado do ticket, atualiza na conclusão |
| Context7 | UserPromptSubmit | Vai buscar docs atuais das bibliotecas mencionadas |
| ElevenLabs | Stop, Notification | Áudio TTS na conclusão de tarefas |
| Slack | Notification, Stop | Alertas de equipa sem boilerplate de curl |
| E2B | Stop | Executa scripts gerados numa sandbox antes de marcar como concluído |
| claude-mem | PostCompact, SessionStart | Restaura contexto de sessão depois de compaction |
| n8n | TaskCompleted | Dispara um workflow externo na conclusão |
Bug Conhecido: PostToolUse + Eventos MCP + additionalContext
Existe um bug aberto (GitHub issue #24788) onde additionalContext de hooks é silenciosamente descartado quando o evento que o desencadeou foi uma chamada de ferramenta MCP. Isto afeta hooks type: "command" que respondem a eventos de ferramentas MCP, não os hooks mcp_tool em si.
A distinção é importante: hooks que SÃO invocações MCP funcionam bem. Hooks que RESPONDEM A chamadas de ferramentas MCP e retornam additionalContext não. A solução é usar exit 2 mais stderr para mensagens críticas de hooks PostToolUse que visam chamadas de ferramentas MCP. O padrão de bloqueio funciona; a injeção consultiva não.
Os Hooks de Ferramenta MCP São a Última Peça em Falta
Antes, os hooks eram uma rede de segurança. Comandos shell que podiam bloquear coisas perigosas ou correr formatters. Sem estado, locais ao processo, desconectados de tudo o que os teus servidores MCP já sabem.
Depois: os hooks são uma camada de orquestração determinística. Qualquer evento, qualquer ferramenta MCP, controlo total de decisões, com estado que persiste entre chamadas e sem overhead de subprocessos.
O pipeline está agora completo. PreToolUse valida. PostToolUse formata e faz scan. PostToolBatch corre testes. Stop verifica com dados externos reais. Cada passo pode ser uma invocação de ferramenta MCP, e nenhum deles precisa de um shell script.
Pare de configurar. Comece a construir.
Templates SaaS com orquestração de IA.