工具是模型执行任务时可以调用的能力。Qoder Agent SDK 支持两类工具:
使用内置工具时,你不需要实现工具本身,只需要在 options 中控制本次会话能看到哪些工具、哪些工具预授权、哪些工具禁止。
常用内置工具包括
当你希望模型调用自己的业务能力时,可以自定义工具。例如查询订单、搜索内部知识库、调用审批系统、访问只读数据库等。
自定义工具通常分三步:
先看一个完整的最小示例,然后按三步拆开说明每一步可以配置什么:
这一步负责定义工具本身,包括工具名、描述、输入参数、执行逻辑和工具元信息。
TypeScript 的
Python 工具 handler 必须是 async 函数,通常接收一个
两个 SDK 的输入 schema 写法不同,最终都会被规范化为 MCP 协议的 JSON Schema。
TypeScript:传 Zod raw shape,也就是字段对象,不是
Python:支持三类写法。
写法一:简单 dict,适合几个简单参数的场景。key 是参数名,value 是 Python 类型;这种写法下所有 key 都是 required。可以用
写法二:
写法三:完整 JSON Schema dict,需要 enum、数值范围、字符串格式约束或嵌套对象时使用:
annotations 用来传 MCP 工具注解。SDK 会把它放到 MCP tool 定义里,CLI 可据此做调度、权限或状态展示。
常用字段:
注意:这些字段不会替代权限配置。是否允许调用工具仍由
Python 的
把 server 放进 MCP servers 配置后,CLI 会发现其中的工具,并在模型需要时通过 SDK 调回你的 handler。
自定义工具的完整名称格式是:
例如 server 名是
当模型调用工具时,SDK 提供多层权限控制。你可以决定:
这些方式可以组合使用。常见做法是:先用
当同一个工具同时匹配允许和禁止规则时,禁止规则优先。
权限模式用一行配置设置整次会话的默认权限行为。
权限回调(
常见返回值:
在子 Agent 中使用自定义工具时,也使用完整工具名:
如果你已经使用 hooks 体系,可以通过
工具 handler 有几类错误路径。
可预期的业务失败推荐返回错误标记(TypeScript 为
适合使用错误标记的场景:
如果 handler 抛出异常,MCP 层会把异常转换成错误结果,agent loop 不会因为普通工具异常直接崩掉。但模型通常只能看到异常消息,格式和内容不如显式返回错误标记可控。
建议:业务上可预期的失败用显式错误标记;真正意外的异常再抛出。
Python SDK 会在运行时兜底检查 handler 返回值:
工具 handler 返回 MCP 的
也可以返回结构化 JSON 字符串,方便模型理解和继续处理:
常见内容块:
Python 版还有两个需要注意的结果差异:
- 内置工具:由 Qoder CLI 提供,例如读文件、搜索、执行命令、调用子 Agent。
- 自定义工具:由 SDK 使用者定义并以进程内 MCP server 的方式暴露给模型调用——TypeScript 使用
tool()+createSdkMcpServer(),Python 使用@tool()装饰器 +create_sdk_mcp_server()。
内置工具
使用内置工具时,你不需要实现工具本身,只需要在 options 中控制本次会话能看到哪些工具、哪些工具预授权、哪些工具禁止。
Read、Edit、Write、Bash、Glob、Grep、WebFetch、WebSearch、Agent 等。工具名称由底层 Qoder CLI 决定;在权限配置里应使用 CLI 暴露给模型的工具名。完整清单、名称和输入输出结构见 SDK References。
自定义工具
当你希望模型调用自己的业务能力时,可以自定义工具。例如查询订单、搜索内部知识库、调用审批系统、访问只读数据库等。
自定义工具通常分三步:
- 创建工具:TypeScript 用
tool(),Python 用@tool()装饰一个async defhandler。 - 注册到进程内 MCP server:
createSdkMcpServer()/create_sdk_mcp_server()。 - 在 options 中通过 MCP servers 配置接入,并用权限配置控制调用。
自定义工具接入步骤
先看一个完整的最小示例,然后按三步拆开说明每一步可以配置什么:
第一步:创建工具
这一步负责定义工具本身,包括工具名、描述、输入参数、执行逻辑和工具元信息。
入参
TypeScript 的 tool() 有 5 个入参,Python 的 @tool() 装饰器有 4 个入参:
| 入参 | 是否必填 | 语义 |
|---|---|---|
name | 是 | 工具在当前 MCP server 内的唯一标识 |
description | 是 | 给模型看的工具说明,描述工具何时使用、做什么、返回什么 |
inputSchema / input_schema | 是 | 定义工具输入参数:TypeScript 传 Zod raw shape;Python 支持简单 dict、TypedDict 和完整 JSON Schema dict |
handler(TypeScript) | 是 | 接收解析后的参数并返回 CallToolResult 的异步执行函数;Python 中被装饰的 async def 即 handler |
extras.annotations(TypeScript)/ annotations(Python) | 否 | MCP 工具注解,例如 readOnlyHint、destructiveHint、openWorldHint |
args dict。如果 handler 声明第二个位置参数,SDK 会传入 ToolInvocationContext,其中 signal 是一个 asyncio.Event,当 CLI 取消正在执行的工具调用时会被设置,适合长任务主动停止:
配置输入参数
两个 SDK 的输入 schema 写法不同,最终都会被规范化为 MCP 协议的 JSON Schema。
TypeScript:传 Zod raw shape,也就是字段对象,不是 z.object(...):
| 需求 | TypeScript 写法 |
|---|---|
| 必填字符串 | z.string().describe('...') |
| 可选参数 | z.string().optional().describe('...') |
| 默认值 | z.number().default(5) |
| 枚举值 | z.enum(['docs', 'tickets']) |
| 数字范围 | z.number().min(1).max(10) |
typing.Annotated 给字段增加描述:
| Python 写法 | JSON Schema 语义 |
|---|---|
str | {"type": "string"} |
int | {"type": "integer"} |
float | {"type": "number"} |
bool | {"type": "boolean"} |
list[str] | 字符串数组 |
dict | 对象 |
Annotated[T, "..."] | 在 T 的 schema 上增加 description |
TypedDict,适合字段较多、需要可选字段或希望复用类型定义的场景。可选字段用 NotRequired(Python 3.11+ 可直接从 typing 导入;3.10 需要 typing_extensions):
配置工具元信息
annotations 用来传 MCP 工具注解。SDK 会把它放到 MCP tool 定义里,CLI 可据此做调度、权限或状态展示。
| 字段 | 类型 | 语义 |
|---|---|---|
title | string | 工具的人类可读标题 |
readOnlyHint | boolean | 标记工具只读,不修改任何状态 |
destructiveHint | boolean | 标记工具可能修改或删除数据 |
openWorldHint | boolean | 标记工具会访问外部系统或网络 |
maxResultSizeChars(Python) | int | Python SDK 会通过 _meta["anthropic/maxResultSizeChars"] 传给 CLI,用于放宽工具返回长度限制 |
tools、allow/deny 规则、权限模式、权限回调和 hooks 决定。MCP status 中回显的 annotations 字段名也可能是 CLI 投影后的 readOnly、destructive、openWorld,而不是 MCP 原始的 *Hint 名称;如果宿主 UI 需要展示这些信息,请在自己的工具定义侧保留映射。
第二步:注册到 MCP server
createSdkMcpServer() / create_sdk_mcp_server() 把一个或多个工具注册为同进程 MCP server。server 名会进入完整工具名,因此建议短、稳定。
| 字段 | 怎么填 | 说明 |
|---|---|---|
name | 如 kb、orders | server 名,会组成完整工具名 mcp__{name}__{tool} |
version | 如 '1.0.0' | 信息性版本号,可省略 |
tools | [searchDocs, lookupOrder] | 注册到这个 server 的工具列表 |
create_sdk_mcp_server() 会做同步校验:server 名 / tool 名 / tool 描述必须是非空字符串,同一个 server 内不能有重复 tool 名。
第三步:接入 query()
把 server 放进 MCP servers 配置后,CLI 会发现其中的工具,并在模型需要时通过 SDK 调回你的 handler。
orders,tool 名是 lookup_order,完整工具名就是 mcp__orders__lookup_order。这个完整名称会用于工具白名单 / 黑名单、权限回调、hooks matcher 和子 Agent 的 tools 配置。
Python 的 QoderSDKClient 多轮会话也使用同样的 mcp_servers 配置:
控制 tool 权限
当模型调用工具时,SDK 提供多层权限控制。你可以决定:
- 本次会话提供哪些工具。
- 哪些工具可以默认放行。
- 哪些工具明确禁止。
- 每次工具调用前是否交给宿主应用动态判断。
权限控制方式总览
| 方式(TypeScript / Python) | 作用 | 粒度 | 适用场景 |
|---|---|---|---|
tools | 限制本次会话可见工具集合 | 会话级 | 想从源头收窄模型能看到的工具 |
allowedTools / allowed_tools、disallowedTools / disallowed_tools | 预授权或禁止指定工具 | 工具级 | 明确知道要允许或禁止哪些工具 |
permissionMode / permission_mode | 设置整次会话的默认权限策略 | 全局 | 快速切换计划模式、自动接受编辑、跳过权限等 |
canUseTool / can_use_tool | 每次调用前执行自定义判断逻辑 | 调用级 | 需要根据参数内容动态决策 |
hooks.PreToolUse | 通过 hooks 生命周期拦截工具调用 | 调用级 | 已经使用 hooks 体系,需要统一审计或拦截 |
tools 收窄可见工具集合,再用 allow/deny 规则设置静态规则,最后用权限回调做参数级判断。
方式一:工具集合与 allow/deny 规则
tools 控制本次会话可见的工具集合;allow/deny 规则控制权限。自定义 MCP 工具要使用完整工具名。
方式二:权限模式
权限模式用一行配置设置整次会话的默认权限行为。
| 模式 | 效果 |
|---|---|
default | 标准权限行为,敏感操作按规则或运行时策略处理 |
acceptEdits | 自动接受文件编辑类操作,其他敏感操作仍按权限策略处理 |
bypassPermissions | 跳过权限检查,必须同时显式确认跳过权限检查 |
yolo | bypassPermissions 的兼容别名,也必须同时显式确认 |
plan | 计划模式,适合先让模型产出方案 |
dontAsk | 不进行交互询问,未预授权或未被规则允许的操作会被拒绝 |
auto | 由运行时能力自动判断 allow 或 deny |
方式三:权限回调
权限回调(canUseTool / can_use_tool)会在工具调用前执行。你可以根据工具名、参数内容和审批上下文返回允许或拒绝。
| 返回 | 效果 |
|---|---|
| allow | 允许执行,使用原始参数 |
| allow + 更新后的入参 | 允许执行,并替换工具参数 |
deny + message | 拒绝执行,模型能看到原因并尝试其他方式 |
deny + message + interrupt: true | 拒绝并中断当前 agent loop |
方式四:hooks.PreToolUse
如果你已经使用 hooks 体系,可以通过 PreToolUse 统一拦截或审计工具调用。
PreToolUse 的 permissionDecision 可以是 allow、deny、ask 或 defer。更完整的权限策略见 权限控制。
SDK 如何处理 tool 返回的错误
工具 handler 有几类错误路径。
业务失败:显式返回错误标记
可预期的业务失败推荐返回错误标记(TypeScript 为 isError: true,Python 为 is_error: True)。SDK 会把这个结果转换成 MCP CallToolResult 交给 CLI,模型能看到失败内容,并可能重试或选择其他方式。
- 参数合法但业务上找不到结果,例如订单不存在。
- 安全策略拒绝执行,例如只允许
SELECT查询。 - 外部服务返回可理解的业务错误。
非预期异常:handler 抛错
如果 handler 抛出异常,MCP 层会把异常转换成错误结果,agent loop 不会因为普通工具异常直接崩掉。但模型通常只能看到异常消息,格式和内容不如显式返回错误标记可控。
畸形返回:SDK 包装为错误(Python)
Python SDK 会在运行时兜底检查 handler 返回值:
- 返回
None:转换为错误文本,说明 handler 必须返回包含"content"的 dict。 - 返回非 dict,例如字符串、数字、列表:转换为文本内容,并标记
isError=True。 - 返回 dict 但没有
"content":转换为错误文本,并列出实际 keys。 - 返回不支持的 content 类型:该内容块会被跳过并写 warning 日志。
Tool 返回值
工具 handler 返回 MCP 的 CallToolResult(Python 中返回 dict,SDK 会转换)。最常用的是文本内容:
| 类型 | 形状 | 说明 |
|---|---|---|
| 文本 | { type: 'text', text } | 最常用,适合自然语言或 JSON 字符串 |
| 图片 | { type: 'image', data, mimeType } | data 是 base64 |
| 音频 | { type: 'audio', data, mimeType } | data 是 base64(TypeScript) |
| 资源链接 | { type: 'resource_link', uri, name?, description?, mimeType? } | 返回可引用资源;Python 中降级为文本,把 name / uri / description 拼接给模型 |
| 内嵌资源 | { type: 'resource', resource } | 返回文本或二进制资源内容;Python 中内嵌文本资源转换为 TextContent |
- handler 返回 dict 顶层
_meta不会透传到CallToolResult。 - handler 返回错误标记时使用 Python 字段名
"is_error": True,不是 MCP/TypeScript 风格的isError。SDK 内部会映射到 MCP 结果。
常见踩坑
- 权限配置里写自定义工具时,要写
mcp__server__tool完整名称。 - TypeScript 中
tool()的第三个参数传 Zod raw shape,不要传z.object(...)。 - Python 简单 dict schema 的所有字段都是 required;需要可选字段时用
TypedDict + NotRequired或完整 JSON Schema。 - 需要 enum、数值范围、嵌套对象、字符串 pattern/format 时,Python 使用完整 JSON Schema dict。
- Python handler 必须是
async def,并返回包含"content"列表的 dict。 - 工具描述要写"什么时候用、做什么、返回什么",不要只写
query、helper这类模糊描述。 readOnlyHint是工具元信息和调度提示,不是权限开关;是否允许执行仍由权限配置决定。- 不要把大而全的业务入口都塞进一个万能工具。一个工具最好完成一类清晰动作。
继续阅读
- Tools Reference:内置工具列表、工具创建 API、
CallToolResult、内置工具输入输出类型。 - MCP 集成:in-process、stdio、SSE、HTTP、OAuth 等 MCP server 接入方式。
- 权限控制:权限模式、工具白名单、权限回调、权限规则更新。
- 子 Agent 使用指南:让不同 Agent 使用不同工具集。