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.
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:
- Idempotent. N calls with the same input produce the same output without cumulative side effects.
nex_summarizeis idempotent (same session, same summary).crm_create_companyis NOT idempotent (creates a duplicate every time). - 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.
- Deterministic. Same input, same output. No
Math.random()without snapshotting. No "current time" in the response unless time is explicit input. - Side-effect-free without explicit user trigger. A Read-tool on
UserPromptSubmitmust NOT persist anything. Only persist when the user clearly asked for it. - 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_toolresults 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
- Claude Code Hooks Reference (official, includes
mcp_toolschema since v2.1.118) - Claude Code Settings Reference