Claude Code v2.1.118 adds direct MCP tool hooks
Claude Code v2.1.118 introduces a new `mcp_tool` hook type that calls MCP servers directly over RPC, eliminating the subprocess overhead of traditional shell-based hooks.
Score breakdown
Teams using Claude Code hooks for security scanning, linting, or CI checks can now route those hooks through stateful MCP servers — eliminating subprocess overhead, shell environment fragility, and cold-start re-parsing on every file write.
- 01Claude Code v2.1.118 adds a fifth hook handler type: `mcp_tool`, which calls MCP servers directly over RPC with no subprocess.
- 02The three fields specific to `mcp_tool` hooks are `server`, `tool`, and `input`; `server` must exactly match the MCP config name or the hook silently fails.
- 03Input values support `${field.path}` dot-notation into the full hook event JSON (e.g., `tool_input.file_path`, `session_id`, `cwd`, `duration_ms`).
The post by speedy_devv describes a new `mcp_tool` hook type introduced in Claude Code `v2.1.118` that allows hooks to call MCP tools directly over an existing RPC connection, rather than spawning a shell subprocess for each invocation. The configuration lives in `.claude/settings.json` and requires three fields specific to this hook type: `server` (which must exactly match the server name in MCP config — one character off causes a silent non-blocking failure), `tool`, and `input`. Input values support `${field.path}` dot-notation into the full hook event JSON, giving access to fields like `tool_input.file_path`, `tool_input.content`, `session_id`, `cwd`, and `duration_ms`. The `if` field uses permission-rule syntax (e.g., `"Edit(*.py|*.ts|*.js)"`) to conditionally fire hooks only on matching file extensions, which the post notes is a meaningful performance difference on docs-heavy projects.
Second, no shell environment dependency: `command` hooks can fail silently when `PATH` is misconfigured or when `~/.zshrc` emits unexpected stdout; `mcp_tool` hooks bypass all of that.
Two concrete advantages over `command` hooks are highlighted. First, stateful servers: an MCP server is a live process that retains loaded configs, caches, and session context between calls — a linting server that pre-parsed `tsconfig.json` on startup does not re-parse it on every file write, whereas a command hook does. Second, no shell environment dependency: `command` hooks can fail silently when `PATH` is misconfigured or when `~/.zshrc` emits unexpected stdout; `mcp_tool` hooks bypass all of that.
The post also covers output handling: an MCP tool's text content is treated identically to a `command` hook's stdout — valid JSON is parsed for decision fields like `decision: "block"` or `permissionDecision: "deny"`, while non-JSON text becomes context for Claude. A field exclusive to `mcp_tool` hooks on `PostToolUse` is `updatedMCPToolOutput`, which lets a running MCP server replace what Claude sees as another tool's output before it enters the conversation. For `Stop` hooks, the post warns that the event JSON includes a `stop_hook_active` field set to `"true"` when Claude is already continuing from a previous Stop hook — MCP tools must check this field and return empty output to avoid infinite loops.
Key facts
- 01Claude Code v2.1.118 adds a fifth hook handler type: `mcp_tool`, which calls MCP servers directly over RPC with no subprocess.
- 02The three fields specific to `mcp_tool` hooks are `server`, `tool`, and `input`; `server` must exactly match the MCP config name or the hook silently fails.
- 03Input values support `${field.path}` dot-notation into the full hook event JSON (e.g., `tool_input.file_path`, `session_id`, `cwd`, `duration_ms`).