mcp_tool hooks, the direct line to your MCP server
Claude Code v2.1.118 lets hooks call MCP tools directly. What changes, when it fits, when it doesn't.
The problem with bash-wrapper hooks
In lesson 4 you learned how hooks work. You write a bash hook that nudges the model: "now call nex_summarize before you stop". It works, but only because the model listens. Sometimes it ignores the nudge. Sometimes it calls the wrong tool. And you only notice days later when memory has gaps.
On April 23rd 2026, Claude Code v2.1.118 introduced a new hook type that fixes this deterministically: type: "mcp_tool".
What the mcp_tool hook is
Instead of a bash reminder, you write the tool call directly into ~/.claude/settings.json:
{
"type": "mcp_tool",
"server": "studiomeyer-memory",
"tool": "nex_summarize",
"input": { "session_id": "${session_id}" },
"timeout": 60,
"statusMessage": "Memory: auto-summary..."
}
Five fields. Claude Code fires the tool call itself, no model in the loop. If the server is up and the tool responds, done. If not, log and continue. Hooks are best-effort, never critical-path.
Substitution variables
Inside input you can interpolate runtime values:
${cwd}, current working directory${session_id}, UUID of the running session${tool_input.field}, on PostToolUse, any field from the tool input${tool_name}, tool name (Pre/PostToolUse)${duration_ms}, tool execution time (PostToolUse)${user_prompt}, the user prompt (UserPromptSubmit)
So you can fire nex_search with { "query": "${user_prompt}" } on every user input. Or crm_log_interaction with { "summary": "Edited ${tool_input.file_path}" } after every file edit. Reminder becomes action.
The five-rule check BEFORE you wire a hook
Hooks fire on lifecycle events without per-call user approval. Before you put a tool inside a hook, it MUST satisfy all five:
- Idempotent. N calls with the same input produce the same output without cumulative side effects.
nex_summarizeis idempotent.crm_create_companyis NOT (creates duplicates). - Fast. Default timeout is 60 seconds, recommended under 30 seconds in synchronous hooks (Stop, PreCompact). If your tool sometimes takes 90 seconds against a cold DB connection, the hook fails silently.
- Deterministic. Same input, same (or benign-equivalent) output. No
Math.random()without snapshotting. No "current time" output unless time is explicit input. - Side-effect-free without explicit user trigger. Read-tools on UserPromptSubmit must NOT persist. Write tools only when the user implicitly triggered (Edit/Write triggering a log is OK).
- GDPR-aware. The hook fires on every matching event. If the tool sends data to a third party (LLM API, logging endpoint), document it in the recipe README.
A tool that fails any of the five doesn't go in a hook. Period.
When mcp_tool, when bash
Not every workflow fits mcp_tool. Use a command (bash) hook when:
- You need multiple tool calls with logic in between
- You need to call something that isn't an MCP tool (CLI, curl, custom script)
- You need to inspect or modify the tool's response
Use mcp_tool when:
- You want exactly one MCP tool call with deterministic input
- The tool exists, is idempotent, and is fast
- You don't need to inspect or modify the result
Five concrete bundles for our SaaS MCPs
We've prepared 5 hook bundles, one per StudioMeyer SaaS MCP:
| MCP | What the hook does | Recipe |
|-----|--------------------|--------|
| Memory (studiomeyer-memory) | Stop -> nex_summarize + nex_session_end. PreCompact -> nex_summarize. UserPromptSubmit -> nex_search. SubagentStop -> nex_learn. | /recipes/16.2-memory-hook-bundle |
| CRM (studiomeyer-crm) | UserPromptSubmit -> crm_search on customer-name patterns. PostToolUse(Edit|Write) with if-filter -> crm_log_interaction on email drafts. | /recipes/16.3-crm-hook-bundle |
| GEO (studiomeyer-geo) | Stop with if=Edit(*.md) -> geo_check (auto-audit after content edit). | /recipes/16.4-geo-crew-hook-bundle |
| Crew (studiomeyer-crew) | SessionStart -> crew_activate (default persona based on cwd). Stop -> crew_feedback. | /recipes/16.4-geo-crew-hook-bundle |
| Academy (mcp-academy) | SessionStart -> academy_stats + academy_next_lesson. UserPromptSubmit on trigger phrase -> academy_concept_search. | /recipes/16.5-academy-hook-bundle |
Each bundle is a recipe in Phase 16 of the academy. Click through, copy the JSON block into your ~/.claude/settings.json, the hook is live.
Distribution: install the whole thing as a plugin
Phase 9 (Distribution) covers packaging your own MCP servers. Same principle applies to hook bundles: we publish the five SaaS hook bundles as plugins in studiomeyer-io/studiomeyer-marketplace. One claude plugin install studiomeyer-memory-hooks and you have all four Memory hooks active. Manual settings.json edits are then only for power users who want to customize.
Anti-pattern: automate everything
The temptation is big to fill every lifecycle event with a hook. Don't. Hooks are best-effort and fire silently. If 8 hooks parallel trigger memory operations and one of them is broken, you debug in the dark. Rule of thumb: one hook per clearly defined use case. Memory needs 4, CRM needs 2, GEO needs 1, Crew needs 2, Academy needs 2. That's 11 hooks total, more is overkill.
Next lesson
Lesson 11 (coming) is the follow-up: plugin distribution. How to turn your own hook bundles into an installable plugin. The pattern is identical to MCP server distribution from Phase 8.
Source
- Claude Code Hooks Reference (official, includes
mcp_toolschema since v2.1.118) - Recipe 16.1: mcp_tool hook intro
- Recipe 16.2: Memory hook bundle