跳到主要内容

扩展点 — 进程内工具、权限回调、钩子

本章讲 SDK 给你的三个"插手"口子。它们都建立在第 02 章那条控制协议上:CLI 发反向请求,SDK 调你的 Python 代码,把结果送回去。

1. 三个口子先分清

口子你提供什么什么时候触发目的
@tool + create_sdk_mcp_server一个 async 函数模型决定调这个工具时给模型加能力
can_use_tool一个权限回调CLI 权限规则判成"要问"时决定工具准不准用
hooks一批钩子函数生命周期节点(工具前/后、提交、停止…)观察/干预流程

关键区别(权限 vs 钩子),来自 types.py:1803-1826 的 docstring:

  • can_use_tool 在 CLI 权限规则评估为"ask"时触发。已被 allowed_toolspermission_mode、settings 里 allow 规则放行的调用,压根不会问它。
  • 想观察/拦截每一个工具调用(不管权限规则),用 PreToolUse 钩子。

2. 进程内工具:@tool

2.1 它要解决的小问题

"我想让模型能调用我 Python 里的一个函数(比如查我自己的数据库),而不用另起一个 MCP 服务器进程。"

2.2 直觉:进程内 vs 外部 MCP

传统 MCP 服务器是独立进程,靠 IPC 通信。SDK MCP 服务器跑在你的 Python 进程里(__init__.py:313 docstring),好处:没 IPC 开销、单进程好部署、能直接访问你应用的状态。

2.3 原理演示

# 示意,非源码:定义一个进程内工具服务器
from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions

@tool("add", "Add two numbers", {"a": float, "b": float})
async def add(args): # 函数必须是 async
return {"content": [{"type": "text", "text": f"{args['a'] + args['b']}"}]}

calc = create_sdk_mcp_server(name="calc", tools=[add])
options = ClaudeAgentOptions(mcp_servers={"calc": calc}, allowed_tools=["add"])

真实可运行版见 examples/mcp_calculator.py

2.4 真实实现:两步

第一步,tool 装饰器(__init__.py:169)只是把元数据打包成一个 SdkMcpTool dataclass(__init__.py:158):name、description、input_schema、handler。不做别的。

第二步,create_sdk_mcp_server(__init__.py:310)才是重头:

  • 建一个真正的 mcp.server.Server 实例。
  • 把每个工具的输入 schema 预计算成 JSON Schema(_build_schema,__init__.py:402),创建时算一次、缓存。
  • 注册 list_toolscall_tool 两个 handler;call_tool 里把你返回的 {"content":[...]} 翻译成 MCP 的 CallToolResult__init__.py:454-520)。
  • 返回一个 McpSdkServerConfig(type="sdk",带 instance)。

2.5 精妙处:Python 类型 → JSON Schema

_python_type_to_json_schema(__init__.py:238)让你能用 {"a": float} 甚至 TypedDict 当 schema,而不必手写 JSON Schema。它递归处理 Annotated(取描述)、Optional/Union(单个非 None 就解包)、list/dict、嵌套 TypedDict(_typeddict_to_json_schema,__init__.py:292)。这是纯粹的"开发体验"打磨。

2.6 实例怎么留在本进程

回顾第 02 章 2.3:传给 CLI 的 --mcp-config 里,SDK 服务器被剥掉 instance 字段(subprocess_cli.py:312-317),只告诉 CLI"有这么个 sdk 服务器"。真正的 Server 实例留在 Query.sdk_mcp_servers,当 CLI 发 mcp_message 反向请求时,由 _handle_sdk_mcp_request 就地调用(query.py:548)。这就是"进程内"的实现真相。

3. 权限回调:can_use_tool

3.1 权限模式先看

permission_mode(types.py:1684)是粗粒度总开关:

模式行为
default危险操作要问
acceptEdits自动接受文件编辑
bypassPermissions全部放行(慎用)
plan只规划、不执行工具
dontAsk不问;没预批准的就拒
auto模型分类器逐个批/拒(见 query.py:61 docstring)

3.2 求值顺序

工具调用来了
├─ 在 allowed_tools 里? ──── 是 ─▶ 直接放行
├─ permission_mode 定了? ── acceptEdits/bypass ─▶ 放行
├─ settings 的 allow 规则命中? ─▶ 放行
└─ 都没 → 规则评估为 "ask" ─▶ 调 can_use_tool 回调

依据:README 的 permissions 说明 + types.py:1803-1813can_use_tool docstring。

3.3 回调签名与返回

CanUseTool 类型(types.py:253):(tool_name, input, context) -> PermissionResult。返回二选一:

  • PermissionResultAllow(types.py:234):可选 updated_input(改写入参)、updated_permissions(顺带更新权限规则)。
  • PermissionResultDeny(types.py:243):带 message,可选 interrupt=True(顺便打断)。

_handle_control_requestcan_use_tool 分支(query.py:384-436)负责把这两种结果翻译成控制协议的 dict。ToolPermissionContext(types.py:198)给回调带了很多上下文:tool_use_idblocked_pathtitle(现成的权限提示句)、suggestions(CLI 建议的权限更新)等。

3.4 一个硬约束

can_use_tool 要求 streaming 模式:若 prompt 是 str 就抛错(_internal/client.py:100-106client.py:160-166)。而且它和 permission_prompt_tool_name 互斥;设了回调时,SDK 自动把 permission_prompt_tool_name 设成 "stdio" 走控制协议(client.py:169-176)。

4. 钩子:hooks

4.1 能挂哪些事件

HookEvent(types.py:259)列了十种:PreToolUsePostToolUsePostToolUseFailureUserPromptSubmitStopSubagentStopPreCompactNotificationSubagentStartPermissionRequest

4.2 怎么配

HookMatcher(types.py:585):matcher 是工具名匹配模式(如 "Bash",或 None 匹配全部),hooks 是回调列表。真实例子(examples/hooks.py:162):

# 示意,非源码:拦截含 foo.sh 的 bash 命令
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[check_bash_command])]},
)

钩子函数返回一个 HookJSONOutput,里面 hookSpecificOutput 可带 permissionDecision: "deny" 来拦(examples/hooks.py:62-68)。

4.3 两个坑

  • 并发派发:同一事件注册的多个 matcher 是 CLI 并行触发的,不保证顺序(types.py:1821-1826)。每个钩子要设计成独立的。
  • 字段名转换:Python 侧用 async_ / continue_(避开关键字),写回 CLI 前 _convert_hook_output_for_cli(query.py:42)把它们转回 async / continue

4.4 initialize 时的注册

钩子不走命令行,而是在 initialize 里为每个回调分配 hook_{id}、建立 id→函数表(query.py:186-200)。之后 CLI 触发时发 hook_callback 控制请求带 callback_id,SDK 靠这张表找到函数(query.py:438-452)。

5. 代码地图

主题文件路径符号名
工具装饰器src/claude_agent_sdk/__init__.pytool SdkMcpTool
建服务器同上create_sdk_mcp_server _build_schema
类型转 schema同上_python_type_to_json_schema _typeddict_to_json_schema
权限类型src/claude_agent_sdk/types.pyCanUseTool PermissionResultAllow PermissionResultDeny ToolPermissionContext
权限模式同上ClaudeAgentOptions.permission_mode
钩子类型同上HookEvent HookMatcher
反向请求处理src/claude_agent_sdk/_internal/query.py_handle_control_request _handle_sdk_mcp_request
钩子字段转换同上_convert_hook_output_for_cli