claude-code-learn_
06MCP & HOOKS

06 - MCP 与 Hooks

MCP 和 Hooks 是 Claude Code 的两条扩展机制:

  • MCP 扩展"模型可以调用什么"
  • Hooks 扩展"在某些事件上自动执行什么"

一个给模型新工具,另一个给客户端新触发点。两者不应相互替代。

读完你能回答

问题
MCP 和内建工具在模型侧有区别吗?
一个 MCP server 能暴露哪三类对象?
Hook 的退出码 2 有什么特殊含义?
"格式化所有改动的文件"该用工具、Skill 还是 Hook?

MCP (Model Context Protocol)

MCP 是一种开放协议,用于把外部进程暴露为模型可调用的资源。Claude Code 作为 MCP 客户端连接到任意数量的 MCP server。

Server 类型

传输方式 连接方式 适用场景
stdio 子进程 + stdin/stdout 本地工具,由 Claude Code 启动
SSE HTTP 长连接 远程托管服务
HTTP streamable 流式 HTTP 远程服务,更通用

Server 能暴露什么

对象 在 Claude Code 中的呈现
Tool 作为额外工具出现在 tools[],命名带前缀如 mcp__myserver__do_thing
Resource 可被引用的外部内容(文件、API 返回)
Prompt 作为预设 prompt 可由用户调用

模型层面看到的 MCP 工具和内建工具没有区别,只是命名前缀不同。执行路径跨进程,但模型无从感知。

连接生命周期

阶段 关键动作
Configured 读取 MCP 配置条目
Connecting 启动子进程或发起 HTTP 连接
Handshake 协议版本、能力声明交换
Registered Server 的 tools 合并到 Claude Code 的 tools[]
Disconnected 失联时从 tools[] 剔除,后续请求里这些工具就不出现

配置位置

MCP server 可以在多处声明,优先级从低到高:

位置 作用范围
~/.claude.json~/.claude/settings.jsonmcpServers 用户级
<repo>/.mcp.json 项目级,可被 git 追踪
claude mcp add 命令动态添加 会话内

同名 server 以项目级为准

展开一个真实的 MCP 配置
{
  "mcpServers": {
    "filesystem": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
    },
    "supabase": {
      "type": "http",
      "url": "https://mcp.supabase.co",
      "headers": { "Authorization": "Bearer ${SUPABASE_MCP_TOKEN}" }
    }
  }
}

注册完成后,模型会在 tools[] 里看到:

mcp__filesystem__read_file
mcp__filesystem__list_directory
mcp__supabase__query
mcp__supabase__insert
...
  • 前缀 mcp__<server>__ 是自动加的,避免不同 server 的工具重名
  • ${ENV_VAR} 会被展开为实际环境变量值
  • type: stdio 的 server 由 Claude Code fork 启动并管理生命周期
  • type: http 的 server 由 Claude Code 作为 HTTP 客户端连接

Hooks

Hook 是客户端在特定事件发生时自动执行的 Shell 命令。它与工具调用完全独立:

  • 工具是模型主动发起
  • Hook 是客户端主动触发

事件类型

事件 触发时机
UserPromptSubmit 用户提交输入后,模型调用前
SessionStart 会话开始时
PreToolUse 工具即将执行前
PostToolUse 工具执行完成后
Stop 模型决定结束本轮
Notification 需要通知用户时

配置

Hook 在 settings.json 中以事件名为键配置。

展开一个真实的 hooks 配置
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write $CLAUDE_FILE_PATHS"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "scripts/guard-destructive.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude 停下了\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

几个可观察点:

  • matcher 用正则筛选事件子集(只对 Edit/Write 生效,只对 Bash 检查)
  • 环境变量 $CLAUDE_FILE_PATHS$CLAUDE_TOOL_NAME 等由 Claude Code 注入
  • 一个事件可以挂多个 hook,顺序执行
  • Hook 命令的 stderr 会被捕获并在退出码 2 时作为反馈传回模型

阻断与放行

Hook 的退出码决定流程:

退出码 含义
0 放行,继续正常流程
2 阻断本次操作,stderr 作为反馈传给模型
其他非零 视为 Hook 错误,按配置决定是否阻断

这是实现"强制格式化""禁止改某目录""审阅危险命令"等约束的核心机制。

展开一个"守护危险命令"的真实脚本

scripts/guard-destructive.sh

#!/usr/bin/env bash
set -eu

cmd="${CLAUDE_TOOL_INPUT_command:-}"

case "$cmd" in
  *"rm -rf"*|*"DROP TABLE"*|*"git push --force"*|*"git reset --hard"*)
    echo "⚠ Detected destructive command: $cmd" >&2
    echo "Please confirm with the user before proceeding." >&2
    exit 2
    ;;
  *)
    exit 0
    ;;
esac

当模型企图执行 rm -rf /tmp/cache,这个 hook 会:

  1. PreToolUse 阶段被触发
  2. 匹配到危险模式
  3. 退出码 2 + stderr 写入警告
  4. Claude Code 把 stderr 回填成 tool_result 的错误,告知模型"这个命令被守护脚本阻断了"
  5. 模型看到错误,要么换更安全的命令,要么请求用户确认

三种扩展机制的分工

机制 谁发起 谁执行 模型是否感知 适合做什么
工具 模型 Claude Code 模型决定是否执行的操作(读文件、调 API)
Skill 模型或用户 Claude Code + 模型 固化一段复杂思考流程
Hook Claude Code Shell 通过 stderr 反馈时才感知 自动化、无需模型思考的副作用

不应互相替代

  • 想"每次保存都格式化" → Hook(不需要模型参与)
  • 想"让模型有能力格式化代码" → 工具(模型主动决定)
  • 想"固化一套代码审查流程" → Skill(模型需要思考,但流程固定)