Build This Now
Build This Now
speedy_devvkoen_salo
Blog/Toolkit/Hooks/MCP Tool Hooks in Claude Code

MCP Tool Hooks in Claude Code

How to call MCP server tools directly from Claude Code hooks using type: mcp_tool — schema, substitution syntax, use cases, and production patterns.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

Published Apr 24, 20269 min readToolkit hubHooks index

Problem: Your hooks run shell scripts. Every time a hook needs to call an MCP server, it spawns a subprocess, wires up transport, handles auth, parses the response, and formats JSON output back to stdout. For a formatter or a security check that fires on every file write, that overhead adds up.

Quick Win: As of v2.1.118, hooks have a new type that calls MCP tools directly. Add this to .claude/settings.json to run a security scan after every file write, no subprocess required:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "semgrep",
            "tool": "scan_file",
            "input": { "path": "${tool_input.file_path}" }
          }
        ]
      }
    ]
  }
}

The MCP server is already running. The hook skips the shell entirely and calls into the server's RPC connection. The tool's text output goes through the same JSON decision parser as any command hook.

What type: "mcp_tool" Actually Is

Before v2.1.118, hooks had four handler types: command, http, prompt, and agent. Now there are five:

TypeWhat runs
commandShell subprocess (stdin/stdout)
httpPOST to a URL endpoint
mcp_toolDirect RPC call to a connected MCP server
promptSingle-turn LLM evaluation (Haiku default)
agentMulti-turn subagent with Read/Grep/Glob access

The mcp_tool type covers every hook event, the same as command and http. The only practical caveat: SessionStart and Setup fire while servers are still connecting, so those hooks may get a "server not connected" error on first run. Subsequent runs are fine.

The Full Schema

Three fields are specific to mcp_tool hooks. The rest are shared across all hook types:

{
  "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)"
}
FieldRequiredDescription
serverYESExact name of the MCP server as configured in settings
toolYESTool name on that server
inputnoArguments passed to the tool. Supports ${path} substitution
timeoutnoSeconds before the hook is canceled
statusMessagenoSpinner text shown while the hook runs
ifnoPermission-rule syntax filter. Hook only fires when the full call matches

Critical: server must exactly match the server name in your MCP configuration. A single character difference means the hook silently fails with a non-blocking error.

Input Substitution

String values in input support ${field.path} dot-notation into the hook's full event JSON. For a PostToolUse hook on a Write call, the event JSON looks like this:

{
  "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
}

So "${tool_input.file_path}" resolves to /your/project/src/api.ts. Any field in that object is reachable. The duration_ms field was added in v2.1.119, one release after mcp_tool shipped.

How the Output Is Processed

The MCP tool's text content is treated exactly like a command hook's stdout. If it parses as valid JSON, Claude Code acts on the decision fields. If not, the text becomes context for Claude.

The decision fields work the same as any hook:

{
  "decision": "block",
  "reason": "Security issue found in src/api.ts: SQL injection risk on line 42."
}

Return this from a PostToolUse MCP tool hook and Claude gets the message and fixes the file. The tool already ran, so this is advisory, not preventative. For blocking before a tool runs, use PreToolUse and return permissionDecision: "deny".

One field is exclusive to mcp_tool hooks on PostToolUse: updatedMCPToolOutput. It replaces what Claude sees as the tool's output before it enters the conversation. A running MCP server can post-process another tool's result before Claude reads it.

Why This Matters vs. Shell Command Hooks

There are two concrete differences, not just speed.

Stateful servers. A shell subprocess starts fresh every time. An MCP server is a live process with its own state: loaded configs, open connections, caches, accumulated session context. A linting MCP that pre-parsed your tsconfig.json on startup doesn't re-parse it on every file write. A command hook does.

No shell environment dependency. Command hooks fail silently when PATH is wrong, when jq isn't installed, when ~/.zshrc prints something to stdout on non-interactive shells. MCP tool hooks bypass all of that. The call goes straight from Claude Code to the server over the existing RPC connection.

The if Field: Scope Your Hooks

Without if, a hook fires on every event that matches the matcher. With if, the hook process only spawns when the full tool call (name and arguments) matches the permission rule syntax:

{
  "type": "mcp_tool",
  "server": "semgrep",
  "tool": "scan_file",
  "if": "Edit(*.py|*.ts|*.js)",
  "input": { "path": "${tool_input.file_path}" }
}

This hook never runs on .md or .json files. On a project with heavy documentation edits, that's a real performance difference.

Pattern 1: Security Scanning on Every Write

A security MCP server that accepts a file path and returns findings. Block Claude if it finds something:

{
  "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..."
          }
        ]
      }
    ]
  }
}

If the MCP tool returns a finding, structure the response as:

{
  "decision": "block",
  "reason": "Semgrep finding: [description of issue at line N]"
}

Claude gets the block message and reworks the file. The scan runs on the server's cached ruleset, not a fresh subprocess parse.

Pattern 2: Stop Hook with External Verification

A Stop hook that calls a Linear or Jira MCP to check whether the related ticket is actually closed before allowing Claude to declare done:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "linear",
            "tool": "get_issue_status",
            "input": { "issue_id": "${tool_input.issue_id}" }
          }
        ]
      }
    ]
  }
}

The MCP tool returns the ticket status. If it comes back as In Progress, the response JSON should carry decision: "block" and a reason. Claude keeps working.

Always check stop_hook_active in your Stop hook logic. The event JSON includes this field as "true" when Claude is already continuing from a previous Stop hook firing. A server that doesn't check this creates an infinite loop. Build the guard into the MCP tool: if stop_hook_active is "true" in the input, return empty output and exit cleanly.

Pattern 3: Production Error Check Before Stopping

After Claude finishes a feature, check whether anything new broke in staging before marking the session complete. A Sentry MCP handles the lookup:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "sentry",
            "tool": "get_new_errors_since",
            "input": { "minutes": "5", "skip_if_active": "${stop_hook_active}" }
          }
        ]
      }
    ]
  }
}

If new errors appeared in the last five minutes, the MCP tool returns them along with decision: "block". Claude reads the error details and fixes the regression before stopping.

Pattern 4: Auto-Inject Docs Before Every Prompt

A UserPromptSubmit hook with a Context7 MCP fetches live documentation for any library mentioned in the prompt, before Claude processes it:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "context7",
            "tool": "get_library_docs",
            "input": { "prompt": "${prompt}" },
            "timeout": 15
          }
        ]
      }
    ]
  }
}

Previously this required Claude to explicitly call the MCP tool. Now it happens on every prompt automatically. Claude starts with current docs instead of training data.

Pattern 5: Policy Enforcement for Agent Teams

When running multi-agent workflows, a shared policy MCP server can enforce which agent writes to which directories. The CLAUDE_AGENT_NAME environment variable identifies the current agent. A PreToolUse hook calls the policy server:

{
  "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}"
            }
          }
        ]
      }
    ]
  }
}

The policy server holds the full authorization map. Update the server once and every agent in every project inherits the new rules, without touching a single settings.json.

Pattern 6: MCP Tool Hooks in Agent Frontmatter

Hooks don't have to live in settings.json. They can sit in an agent's YAML frontmatter, scoped to that agent's lifecycle:

---
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."
---

Each specialist agent in an orchestrated team carries its own validation logic. The backend agent scans for security issues. The frontend agent checks accessibility. Neither needs a global hook that applies to everyone.

Elicitation Control

The Elicitation event fires when an MCP server requests user input mid-task. An mcp_tool hook can auto-answer known prompts by calling a secrets manager:

{
  "hooks": {
    "Elicitation": [
      {
        "matcher": "my-db-server",
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "secrets-manager",
            "tool": "get_credential",
            "input": { "key": "${elicitation.field_name}" }
          }
        ]
      }
    ]
  }
}

Predictable credential prompts resolve automatically. The task runs without interruption.

MCP Servers Worth Pairing With Hooks

Not every MCP server makes sense as a hook target. The ones with the best fit are tools that need to fire on specific events without user intervention:

ServerEventWhat it does
SemgrepPostToolUse: WriteSecurity scan on every write
SentryStopCheck for new staging errors before completing
Linear / JiraStop, TaskCompletedVerify ticket status, update on completion
Context7UserPromptSubmitAuto-fetch live docs for mentioned libraries
ElevenLabsStop, NotificationTTS audio on task completion
SlackNotification, StopTeam alerts without curl boilerplate
E2BStopRun generated scripts in a sandbox before marking done
claude-memPostCompact, SessionStartRestore session context after compaction
n8nTaskCompletedTrigger an external workflow on completion

Known Issue: PostToolUse + MCP Events + additionalContext

There is an open bug (GitHub issue #24788) where additionalContext from hooks gets silently dropped when the triggering event was an MCP tool call. This affects type: "command" hooks that respond to MCP tool events, not mcp_tool hooks themselves.

The distinction matters: hooks that ARE MCP invocations work fine. Hooks that RESPOND TO MCP tool calls and return additionalContext do not. The workaround is using exit 2 plus stderr for critical messages from PostToolUse hooks targeting MCP tool calls. The blocking pattern works; advisory injection does not.

MCP Tool Hooks Are the Hook System's Last Missing Piece

Before this, hooks were a safety net. Shell commands that could block dangerous things or run formatters. Stateless, process-local, disconnected from everything your MCP servers already know.

After: hooks are a deterministic orchestration layer. Any event, any MCP tool, full decision control, with state that persists across calls and no subprocess overhead.

The pipeline is now complete. PreToolUse validates. PostToolUse formats and scans. PostToolBatch runs tests. Stop verifies with real external data. Every step can be an MCP tool invocation, and none of them require a shell script.

Continue in Hooks

  • Claude Code Setup Hooks
    Braid scripts, agents, and docs into Claude Code setup hooks. One command runs a deterministic script, hands output to a diagnosing agent, logs living docs.
  • Context Backup Hooks for Claude Code
    A StatusLine-driven Claude Code context backup hook. Writes structured snapshots every 10K tokens so auto-compaction never eats errors, signatures, decisions.
  • Cross-Platform Hooks for Claude Code
    Cross-platform Claude Code hooks: skip .cmd, .sh, and .ps1 wrappers and invoke node directly so one .mjs file runs on macOS, Linux, and Windows across the team.
  • Hooks Guide
    Claude Code hooks from first principles: exit codes, JSON output, async commands, HTTP endpoints, PreToolUse and PostToolUse matchers, production patterns.
  • Claude Code Permission Hook
    Install a three-tier Claude Code permission hook: instant allow for safe calls, instant deny for dangerous ones, LLM check for the gray area. No skip flag.
  • Self-Validating Claude Code Agents
    Self-validating Claude Code agents: wire PostToolUse lint hooks, Stop hooks, and read-only reviewer sub-agents into agent definitions so bad output never ships.

More from Toolkit

  • Keyboard Shortcuts
    Configure Claude Code keybindings.json: 17 contexts, keystroke syntax, chord sequences, modifier combinations, and how to unbind any default shortcut instantly.
  • Status Line Guide
    Set up a Claude Code status line for model name, git branch, session cost, and context usage. settings.json config, JSON input, bash, Python, Node.js scripts.
  • AI SEO and GEO Optimization
    A rundown of Generative Engine Optimization: how to get content cited inside ChatGPT, Claude, and Perplexity responses instead of just ranked on Google.
  • Claude Code vs Cursor in 2026
    A side-by-side look at Claude Code and Cursor in 2026: agent models, context windows, pricing tiers, and how each tool fits different developer workflows.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

On this page

What type: "mcp_tool" Actually Is
The Full Schema
Input Substitution
How the Output Is Processed
Why This Matters vs. Shell Command Hooks
The if Field: Scope Your Hooks
Pattern 1: Security Scanning on Every Write
Pattern 2: Stop Hook with External Verification
Pattern 3: Production Error Check Before Stopping
Pattern 4: Auto-Inject Docs Before Every Prompt
Pattern 5: Policy Enforcement for Agent Teams
Pattern 6: MCP Tool Hooks in Agent Frontmatter
Elicitation Control
MCP Servers Worth Pairing With Hooks
Known Issue: PostToolUse + MCP Events + additionalContext
MCP Tool Hooks Are the Hook System's Last Missing Piece

Stop configuring. Start building.

SaaS builder templates with AI orchestration.