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.json 的 mcpServers |
用户级 |
<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 会:
- 在
PreToolUse阶段被触发 - 匹配到危险模式
- 退出码
2+ stderr 写入警告 - Claude Code 把 stderr 回填成 tool_result 的错误,告知模型"这个命令被守护脚本阻断了"
- 模型看到错误,要么换更安全的命令,要么请求用户确认
三种扩展机制的分工
| 机制 | 谁发起 | 谁执行 | 模型是否感知 | 适合做什么 |
|---|---|---|---|---|
| 工具 | 模型 | Claude Code | 是 | 模型决定是否执行的操作(读文件、调 API) |
| Skill | 模型或用户 | Claude Code + 模型 | 是 | 固化一段复杂思考流程 |
| Hook | Claude Code | Shell | 通过 stderr 反馈时才感知 | 自动化、无需模型思考的副作用 |
不应互相替代:
- 想"每次保存都格式化" → Hook(不需要模型参与)
- 想"让模型有能力格式化代码" → 工具(模型主动决定)
- 想"固化一套代码审查流程" → Skill(模型需要思考,但流程固定)