Skip to main content
扩展能力

工具(Tools)

工具是模型执行任务时可以调用的能力。Qoder Agent SDK 支持两类工具:
  • 内置工具:由 Qoder CLI 提供,例如读文件、搜索、执行命令、调用子 Agent。
  • 自定义工具:由 SDK 使用者定义并以进程内 MCP server 的方式暴露给模型调用——TypeScript 使用 tool() + createSdkMcpServer(),Python 使用 @tool() 装饰器 + create_sdk_mcp_server()
本文重点讲如何自定义工具。MCP server 的更多接入方式见 MCP 集成,权限体系的完整说明见 权限控制,内置工具完整列表见 SDK References

内置工具

使用内置工具时,你不需要实现工具本身,只需要在 options 中控制本次会话能看到哪些工具、哪些工具预授权、哪些工具禁止。
query({
  prompt: 'Read this repository and summarize risks in the authentication module. Do not modify files.',
  options: {
    auth: accessTokenFromEnv(),
    cwd: '/path/to/project',
    tools: ['Read', 'Grep', 'Glob'],
    allowedTools: ['Read', 'Grep', 'Glob'],
  },
});
常用内置工具包括 ReadEditWriteBashGlobGrepWebFetchWebSearchAgent 等。工具名称由底层 Qoder CLI 决定;在权限配置里应使用 CLI 暴露给模型的工具名。完整清单、名称和输入输出结构见 SDK References

自定义工具

当你希望模型调用自己的业务能力时,可以自定义工具。例如查询订单、搜索内部知识库、调用审批系统、访问只读数据库等。 自定义工具通常分三步:
  1. 创建工具:TypeScript 用 tool(),Python 用 @tool() 装饰一个 async def handler。
  2. 注册到进程内 MCP server:createSdkMcpServer() / create_sdk_mcp_server()
  3. 在 options 中通过 MCP servers 配置接入,并用权限配置控制调用。

自定义工具接入步骤

先看一个完整的最小示例,然后按三步拆开说明每一步可以配置什么:
import {
  accessTokenFromEnv,
  createSdkMcpServer,
  query,
  tool,
} from '@qodercn-ai/qodercn-agent-sdk';
import { z } from 'zod';

const lookupOrder = tool(
  'lookup_order',
  'Look up an order by order ID.',
  {
    orderId: z.string().describe('Order ID, such as O-1001'),
  },
  async ({ orderId }) => {
    const order = await orders.find(orderId);

    if (!order) {
      return {
        isError: true,
        content: [{ type: 'text', text: `Order not found: ${orderId}` }],
      };
    }

    return {
      content: [{ type: 'text', text: JSON.stringify(order) }],
    };
  },
  { annotations: { readOnlyHint: true } },
);

const orderTools = createSdkMcpServer({
  name: 'orders',
  tools: [lookupOrder],
});

const messages = query({
  prompt: 'Check the status of order O-1001 and summarize it in one sentence.',
  options: {
    auth: accessTokenFromEnv(),
    mcpServers: { orders: orderTools },
    allowedTools: ['mcp__orders__lookup_order'],
  },
});

for await (const message of messages) {
  if (message.type === 'result') {
    console.log(message.result);
  }
}

第一步:创建工具

这一步负责定义工具本身,包括工具名、描述、输入参数、执行逻辑和工具元信息。

入参

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 工具注解,例如 readOnlyHintdestructiveHintopenWorldHint
Python 工具 handler 必须是 async 函数,通常接收一个 args dict。如果 handler 声明第二个位置参数,SDK 会传入 ToolInvocationContext,其中 signal 是一个 asyncio.Event,当 CLI 取消正在执行的工具调用时会被设置,适合长任务主动停止:
tool(
  'search_docs',
  'Search internal product documentation.',
  { query: z.string().describe('Search keywords') },
  async ({ query }) => ({ content: [{ type: 'text', text: query }] }),
);

配置输入参数

两个 SDK 的输入 schema 写法不同,最终都会被规范化为 MCP 协议的 JSON Schema。 TypeScript:传 Zod raw shape,也就是字段对象,不是 z.object(...)
{
  query: z.string().describe('Search keywords'),
  maxResults: z.number().int().min(1).max(10).optional()
    .describe('Maximum number of snippets to return'),
  source: z.enum(['docs', 'tickets', 'wiki']).default('docs')
    .describe('Where to search'),
}
需求TypeScript 写法
必填字符串z.string().describe('...')
可选参数z.string().optional().describe('...')
默认值z.number().default(5)
枚举值z.enum(['docs', 'tickets'])
数字范围z.number().min(1).max(10)
Python:支持三类写法。 写法一:简单 dict,适合几个简单参数的场景。key 是参数名,value 是 Python 类型;这种写法下所有 key 都是 required。可以用 typing.Annotated 给字段增加描述:
from typing import Annotated


input_schema = {
    "query": Annotated[str, "Search keywords"],
    "max_results": Annotated[int, "Maximum number of snippets to return"],
}
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):
from typing import Annotated, TypedDict

from typing_extensions import NotRequired


class SearchInput(TypedDict):
    query: Annotated[str, "Search keywords"]
    max_results: NotRequired[Annotated[int, "Maximum snippets to return"]]


@tool("search_docs", "Search internal product documentation.", SearchInput)
async def search_docs(args):
    limit = args.get("max_results", 5)
    return {"content": [{"type": "text", "text": f"{args['query']} ({limit})"}]}
写法三:完整 JSON Schema dict,需要 enum、数值范围、字符串格式约束或嵌套对象时使用:
input_schema = {
    "type": "object",
    "properties": {
        "query": {"type": "string", "description": "Search keywords"},
        "source": {
            "type": "string",
            "enum": ["docs", "tickets", "wiki"],
            "description": "Where to search",
        },
        "max_results": {"type": "integer", "minimum": 1, "maximum": 10},
    },
    "required": ["query"],
}

配置工具元信息

annotations 用来传 MCP 工具注解。SDK 会把它放到 MCP tool 定义里,CLI 可据此做调度、权限或状态展示。
tool(
  'search_docs',
  'Search internal product documentation.',
  { query: z.string().describe('Search keywords') },
  async ({ query }) => ({ content: [{ type: 'text', text: query }] }),
  {
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      openWorldHint: false,
    },
  },
);
常用字段:
字段类型语义
titlestring工具的人类可读标题
readOnlyHintboolean标记工具只读,不修改任何状态
destructiveHintboolean标记工具可能修改或删除数据
openWorldHintboolean标记工具会访问外部系统或网络
maxResultSizeChars(Python)intPython SDK 会通过 _meta["anthropic/maxResultSizeChars"] 传给 CLI,用于放宽工具返回长度限制
注意:这些字段不会替代权限配置。是否允许调用工具仍由 tools、allow/deny 规则、权限模式、权限回调和 hooks 决定。MCP status 中回显的 annotations 字段名也可能是 CLI 投影后的 readOnlydestructiveopenWorld,而不是 MCP 原始的 *Hint 名称;如果宿主 UI 需要展示这些信息,请在自己的工具定义侧保留映射。

第二步:注册到 MCP server

createSdkMcpServer() / create_sdk_mcp_server() 把一个或多个工具注册为同进程 MCP server。server 名会进入完整工具名,因此建议短、稳定。
const kbTools = createSdkMcpServer({
  name: 'kb',
  version: '1.0.0',
  tools: [searchDocs],
});
字段怎么填说明
namekbordersserver 名,会组成完整工具名 mcp__{name}__{tool}
version'1.0.0'信息性版本号,可省略
tools[searchDocs, lookupOrder]注册到这个 server 的工具列表
Python 的 create_sdk_mcp_server() 会做同步校验:server 名 / tool 名 / tool 描述必须是非空字符串,同一个 server 内不能有重复 tool 名。

第三步:接入 query()

把 server 放进 MCP servers 配置后,CLI 会发现其中的工具,并在模型需要时通过 SDK 调回你的 handler。
query({
  prompt: 'Search docs for the refund policy and summarize it.',
  options: {
    auth: accessTokenFromEnv(),
    mcpServers: { kb: kbTools },
    allowedTools: ['mcp__kb__search_docs'],
  },
});
自定义工具的完整名称格式是:
mcp__{serverName}__{toolName}
例如 server 名是 orders,tool 名是 lookup_order,完整工具名就是 mcp__orders__lookup_order。这个完整名称会用于工具白名单 / 黑名单、权限回调、hooks matcher 和子 Agent 的 tools 配置。 Python 的 QoderSDKClient 多轮会话也使用同样的 mcp_servers 配置:
from qodercn_agent_sdk import QoderSDKClient


options = QoderAgentOptions(
    auth=qodercli_auth(),
    mcp_servers={"kb": kb_tools},
    allowed_tools=["mcp__kb__search_docs"],
)

async with QoderSDKClient(options=options) as client:
    await client.query("Search docs for the refund policy.")

    async for message in client.receive_response():
        print(message)

控制 tool 权限

当模型调用工具时,SDK 提供多层权限控制。你可以决定:
  • 本次会话提供哪些工具。
  • 哪些工具可以默认放行。
  • 哪些工具明确禁止。
  • 每次工具调用前是否交给宿主应用动态判断。

权限控制方式总览

方式(TypeScript / Python)作用粒度适用场景
tools限制本次会话可见工具集合会话级想从源头收窄模型能看到的工具
allowedTools / allowed_toolsdisallowedTools / disallowed_tools预授权或禁止指定工具工具级明确知道要允许或禁止哪些工具
permissionMode / permission_mode设置整次会话的默认权限策略全局快速切换计划模式、自动接受编辑、跳过权限等
canUseTool / can_use_tool每次调用前执行自定义判断逻辑调用级需要根据参数内容动态决策
hooks.PreToolUse通过 hooks 生命周期拦截工具调用调用级已经使用 hooks 体系,需要统一审计或拦截
这些方式可以组合使用。常见做法是:先用 tools 收窄可见工具集合,再用 allow/deny 规则设置静态规则,最后用权限回调做参数级判断。

方式一:工具集合与 allow/deny 规则

tools 控制本次会话可见的工具集合;allow/deny 规则控制权限。自定义 MCP 工具要使用完整工具名。
// Only expose read/search tools to this session.
query({
  prompt: 'Analyze the repository without editing files.',
  options: {
    tools: ['Read', 'Glob', 'Grep'],
    allowedTools: ['Read', 'Glob', 'Grep'],
  },
});

// Explicitly deny high-risk tools.
query({
  prompt: 'Review the project and report issues.',
  options: {
    disallowedTools: ['Bash', 'Write', 'Edit'],
  },
});

// Use full names for custom MCP tools.
query({
  prompt: 'Check order O-1001.',
  options: {
    mcpServers: { orders: orderTools },
    allowedTools: ['mcp__orders__lookup_order'],
  },
});

// Disable all tools. The model can only answer from its context.
query({
  prompt: 'Explain what this SDK does at a high level.',
  options: { tools: [] },
});
当同一个工具同时匹配允许和禁止规则时,禁止规则优先。

方式二:权限模式

权限模式用一行配置设置整次会话的默认权限行为。
query({
  prompt: 'Refactor the code.',
  options: {
    permissionMode: 'acceptEdits',
  },
});
模式效果
default标准权限行为,敏感操作按规则或运行时策略处理
acceptEdits自动接受文件编辑类操作,其他敏感操作仍按权限策略处理
bypassPermissions跳过权限检查,必须同时显式确认跳过权限检查
yolobypassPermissions 的兼容别名,也必须同时显式确认
plan计划模式,适合先让模型产出方案
dontAsk不进行交互询问,未预授权或未被规则允许的操作会被拒绝
auto由运行时能力自动判断 allow 或 deny

方式三:权限回调

权限回调(canUseTool / can_use_tool)会在工具调用前执行。你可以根据工具名、参数内容和审批上下文返回允许或拒绝。
query({
  prompt: 'Check order O-1001.',
  options: {
    auth: accessTokenFromEnv(),
    mcpServers: { orders: orderTools },
    allowedTools: ['mcp__orders__lookup_order'],
    async canUseTool(toolName, input, options) {
      if (toolName !== 'mcp__orders__lookup_order') {
        return {
          behavior: 'deny',
          message: 'Only order lookup is allowed in this workflow.',
          toolUseID: options.toolUseID,
        };
      }

      return {
        behavior: 'allow',
        updatedInput: input,
        toolUseID: options.toolUseID,
      };
    },
  },
});
常见返回值:
返回效果
allow允许执行,使用原始参数
allow + 更新后的入参允许执行,并替换工具参数
deny + message拒绝执行,模型能看到原因并尝试其他方式
deny + message + interrupt: true拒绝并中断当前 agent loop
在子 Agent 中使用自定义工具时,也使用完整工具名:
query({
  prompt: 'Use the order-support agent to check order O-1001.',
  options: {
    auth: accessTokenFromEnv(),
    mcpServers: { orders: orderTools },
    allowedTools: ['Agent'],
    agents: {
      'order-support': {
        description: 'Handles order lookup and explains order status.',
        prompt: 'Use order tools to answer order status questions clearly.',
        tools: ['mcp__orders__lookup_order'],
      },
    },
  },
});

方式四:hooks.PreToolUse

如果你已经使用 hooks 体系,可以通过 PreToolUse 统一拦截或审计工具调用。
query({
  prompt: 'Run the test command.',
  options: {
    allowedTools: ['Bash'],
    hooks: {
      PreToolUse: [
        {
          matcher: 'Bash',
          hooks: [
            async (input) => {
              const command = (input.tool_input as { command: string }).command;
              if (command.includes('rm -rf')) {
                return {
                  hookSpecificOutput: {
                    hookEventName: 'PreToolUse',
                    permissionDecision: 'deny',
                    permissionDecisionReason: 'rm -rf is not allowed',
                  },
                };
              }

              return {
                hookSpecificOutput: {
                  hookEventName: 'PreToolUse',
                  permissionDecision: 'allow',
                },
              };
            },
          ],
        },
      ],
    },
  },
});
PreToolUsepermissionDecision 可以是 allowdenyaskdefer。更完整的权限策略见 权限控制

SDK 如何处理 tool 返回的错误

工具 handler 有几类错误路径。

业务失败:显式返回错误标记

可预期的业务失败推荐返回错误标记(TypeScript 为 isError: true,Python 为 is_error: True)。SDK 会把这个结果转换成 MCP CallToolResult 交给 CLI,模型能看到失败内容,并可能重试或选择其他方式。
return {
  isError: true,
  content: [{
    type: 'text',
    text: JSON.stringify({
      error: 'VALIDATION_ERROR',
      message: 'Only SELECT statements are allowed.',
    }),
  }],
};
适合使用错误标记的场景:
  • 参数合法但业务上找不到结果,例如订单不存在。
  • 安全策略拒绝执行,例如只允许 SELECT 查询。
  • 外部服务返回可理解的业务错误。

非预期异常:handler 抛错

如果 handler 抛出异常,MCP 层会把异常转换成错误结果,agent loop 不会因为普通工具异常直接崩掉。但模型通常只能看到异常消息,格式和内容不如显式返回错误标记可控。
const toolThatMayThrow = tool(
  'fetch_user',
  'Fetch a user by ID.',
  { userId: z.string() },
  async ({ userId }) => {
    const response = await userService.fetch(userId);
    if (!response.ok) {
      throw new Error('User service failed');
    }
    return { content: [{ type: 'text', text: await response.text() }] };
  },
);
建议:业务上可预期的失败用显式错误标记;真正意外的异常再抛出。

畸形返回:SDK 包装为错误(Python)

Python SDK 会在运行时兜底检查 handler 返回值:
  • 返回 None:转换为错误文本,说明 handler 必须返回包含 "content" 的 dict。
  • 返回非 dict,例如字符串、数字、列表:转换为文本内容,并标记 isError=True
  • 返回 dict 但没有 "content":转换为错误文本,并列出实际 keys。
  • 返回不支持的 content 类型:该内容块会被跳过并写 warning 日志。
这些兜底能避免模型看到空成功结果,但业务代码里仍应始终返回标准结构。

Tool 返回值

工具 handler 返回 MCP 的 CallToolResult(Python 中返回 dict,SDK 会转换)。最常用的是文本内容:
return {
  content: [{ type: 'text', text: 'done' }],
};
也可以返回结构化 JSON 字符串,方便模型理解和继续处理:
return {
  content: [{
    type: 'text',
    text: JSON.stringify({
      orderId: 'O-1001',
      status: 'shipped',
      eta: '2026-05-20',
    }),
  }],
};
常见内容块:
类型形状说明
文本{ 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
Python 版还有两个需要注意的结果差异:
  • 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。
  • 工具描述要写"什么时候用、做什么、返回什么",不要只写 queryhelper 这类模糊描述。
  • readOnlyHint 是工具元信息和调度提示,不是权限开关;是否允许执行仍由权限配置决定。
  • 不要把大而全的业务入口都塞进一个万能工具。一个工具最好完成一类清晰动作。

继续阅读

  • Tools Reference:内置工具列表、工具创建 API、CallToolResult、内置工具输入输出类型。
  • MCP 集成:in-process、stdio、SSE、HTTP、OAuth 等 MCP server 接入方式。
  • 权限控制:权限模式、工具白名单、权限回调、权限规则更新。
  • 子 Agent 使用指南:让不同 Agent 使用不同工具集。