Recipe-Inhalt ist auf Englisch. Englisches Original lesen →
← Alle Recipes
Phase 16 · Claude Code Hooks·15 min·6 steps

The mcp_tool hook, direct MCP tool calls from Claude Code lifecycle events

Claude Code v2.1.118 added a new hook type: type: 'mcp_tool'. Skip the bash wrapper, call MCP server tools directly from Stop, PreCompact, UserPromptSubmit. The five-rule sanity check before you wire one.

6 steps0%
Du liest ohne Account. Mit Login speichern wir Step-Fortschritt + Notes.

The mcp_tool hook, direct MCP tool calls from Claude Code lifecycle events

Until Claude Code v2.1.118 (released 2026-04-23), every hook was a shell command. If you wanted a Stop hook to summarize the session into your memory MCP, you had to write a bash wrapper that called the tool indirectly. Now there is a type: "mcp_tool" hook type that calls a MCP server tool directly. No shell, no JSON munging, just a four-field config in ~/.claude/settings.json.

This recipe is the architectural intro. The next four recipes build the actual bundles for the five StudioMeyer SaaS MCPs (Memory, CRM, GEO, Crew, Academy).

Step 1: The new hook schema

Add this to ~/.claude/settings.json under hooks.Stop[0].hooks:

{
  "type": "mcp_tool",
  "server": "studiomeyer-memory",
  "tool": "nex_summarize",
  "input": { "session_id": "${session_id}" },
  "timeout": 60,
  "statusMessage": "Auto-summarize on stop..."
}

Five fields. type: "mcp_tool" switches on the new dispatcher. server is the MCP server name as it appears in claude mcp list. tool is the tool name without the mcp__server__ prefix. input is the JSON the tool expects. timeout is in seconds (default 60). statusMessage shows in the Claude Code statusline while the hook runs.

Step 2: Substitution variables you can use

Inside input you can interpolate runtime values:

  • ${cwd}, current working directory
  • ${tool_input.field}, any field from the tool input object (PostToolUse only)
  • ${tool_name}, the tool name being called (Pre/PostToolUse only)
  • ${session_id}, Claude Code session UUID
  • ${duration_ms}, execution time (PostToolUse / PostToolUseFailure only)

So nex_search triggered on UserPromptSubmit with { "query": "${user_prompt}" } works out of the box.

Step 3: The five-rule sanity check before you wire a hook

Hooks fire on lifecycle events without explicit user approval. That makes them powerful and dangerous. Before you add a mcp_tool hook, the tool you call MUST satisfy all five:

  1. Idempotent. N calls with the same input produce the same output without cumulative side effects. nex_summarize is idempotent (same session, same summary). crm_create_company is NOT idempotent (creates a duplicate every time).
  2. Fast. Default timeout is 60 seconds. Recommended ceiling: 30 seconds in synchronous hooks (Stop, PreCompact). If your tool sometimes hits a cold database connection and takes 90 seconds, the hook silently fails.
  3. Deterministic. Same input, same output. No Math.random() without snapshotting. No "current time" in the response unless time is explicit input.
  4. Side-effect-free without explicit user trigger. A Read-tool on UserPromptSubmit must NOT persist anything. Only persist when the user clearly asked for it.
  5. GDPR-aware. The hook fires automatically on every matching event. If the tool sends data to a third party (e.g. Anthropic, OpenAI, a logging endpoint), the user's consent must be documented in the recipe README.

A tool that fails any of these five doesn't go in a hook recipe. Period.

Step 4: Where hooks live and how they merge

There are three settings files Claude Code reads:

  • ~/.claude/settings.json, your personal user-scope config
  • .claude/settings.json, project-scope, committed to git
  • .claude/settings.local.json, project-scope, gitignored

Hooks merge across all three. If your user-level Stop hook calls nex_summarize and a project-level Stop hook calls geo_check, both fire. There is no override semantics. To disable plugin-supplied hooks you need disableAllHooks: true, which is a managed-only setting (i.e. requires IT admin).

Step 5: Verify your first hook fires

After editing ~/.claude/settings.json, run a fresh claude session and trigger the event:

claude
# Type something, then Ctrl-D to trigger Stop

Watch the statusline, statusMessage shows while the hook runs. If the hook fails (server not connected, tool errored, timeout), Claude Code logs a non-blocking error and continues. Hooks are best-effort, never critical-path.

To debug:

claude --debug

--debug prints hook execution details including the full request and response. Use this when a hook silently doesn't fire, usually it's a typo in server or tool name.

Step 6: When to use a Bash hook instead

Not every workflow fits mcp_tool. Use a command (bash) hook when:

  • You need to chain multiple tool calls with intermediate logic
  • You need to call something that isn't an MCP tool (a CLI, a curl, a script)
  • You need the result to actually return a JSON object that Claude Code uses (mcp_tool results are best-effort)

Use mcp_tool when:

  • You want to call exactly one MCP tool with deterministic input
  • The tool already exists and is idempotent + fast + GDPR-aware
  • You don't need to inspect or modify the tool's response

Done

You understand the new hook type, the five-rule check, and how to verify a hook fires. The next four recipes (16.2 Memory, 16.3 CRM, 16.4 GEO+Crew, 16.5 Academy) build the actual recipe bundles for our SaaS MCPs.

Source

Migration patterns, moving cusMemory hook bundle, auto-persi