Documentation Index
Fetch the complete documentation index at: https://docs.qoder.com/llms.txt
Use this file to discover all available pages before exploring further.
工具是模型执行任务时可以调用的能力。Qoder Agent SDK Python 版支持两类工具:
- 内置工具:由 Qoder CLI 提供,例如读文件、搜索、执行命令、调用子 Agent。
- 自定义工具:由 SDK 使用者通过
@tool() 和 create_sdk_mcp_server() 定义 Python 函数,并以进程内 MCP server 的方式暴露给模型调用。
本文重点讲如何自定义工具。MCP server 的更多接入方式见 MCP 集成,权限体系的完整说明见 权限控制。
内置工具
使用内置工具时,你不需要实现工具本身,只需要在 QoderAgentOptions 中控制本次会话能看到哪些工具、哪些工具预授权、哪些工具禁止。
import asyncio
from qoder_agent_sdk import QoderAgentOptions, qodercli_auth, query
async def main():
options = QoderAgentOptions(
auth=qodercli_auth(),
cwd="/path/to/project",
tools=["Read", "Grep", "Glob"],
allowed_tools=["Read", "Grep", "Glob"],
)
async for message in query(
prompt=(
"Read this repository and summarize risks in the authentication "
"module. Do not modify files."
),
options=options,
):
print(message)
asyncio.run(main())
常用内置工具包括 Read、Edit、Write、Bash、Glob、Grep、WebFetch、WebSearch、Agent 等。工具名称由底层 Qoder CLI 决定;在权限配置里应使用 CLI 暴露给模型的工具名,例如 Read、Write、Bash。完整内置工具列表见 Tools Reference - 内置工具列表。
自定义工具
当你希望模型调用自己的业务能力时,可以自定义工具。例如查询订单、搜索内部知识库、调用审批系统、访问只读数据库等。
Python 版自定义工具通常分三步:
- 用
@tool() 装饰一个 async def handler。
- 用
create_sdk_mcp_server() 把一个或多个工具注册到进程内 MCP server。
- 在
QoderAgentOptions 中通过 mcp_servers 接入,并用权限配置控制调用。
自定义工具接入步骤
先看一个完整的最小示例,然后按三步拆开说明每一步可以配置什么:
import asyncio
import json
from mcp.types import ToolAnnotations
from qoder_agent_sdk import (
QoderAgentOptions,
create_sdk_mcp_server,
qodercli_auth,
query,
tool,
)
orders = {
"O-1001": {"order_id": "O-1001", "status": "shipped", "eta": "2026-05-20"},
}
@tool(
"lookup_order",
"Look up an order by order ID and return its status as JSON.",
{"order_id": str},
annotations=ToolAnnotations(readOnlyHint=True),
)
async def lookup_order(args):
order_id = args["order_id"]
order = orders.get(order_id)
if order is None:
return {
"is_error": True,
"content": [{"type": "text", "text": f"Order not found: {order_id}"}],
}
return {"content": [{"type": "text", "text": json.dumps(order)}]}
order_tools = create_sdk_mcp_server(
name="orders",
tools=[lookup_order],
)
async def main():
options = QoderAgentOptions(
auth=qodercli_auth(),
mcp_servers={"orders": order_tools},
allowed_tools=["mcp__orders__lookup_order"],
)
async for message in query(
prompt="Check the status of order O-1001 and summarize it in one sentence.",
options=options,
):
print(message)
asyncio.run(main())
这一步负责定义工具本身,包括工具名、描述、输入参数、执行逻辑和工具元信息。
@tool() 是一个装饰器。它有 4 个入参:
def tool(
name: str,
description: str,
input_schema: type | dict[str, Any],
annotations: ToolAnnotations | None = None,
): ...
| 入参 | 类型 | 是否必填 | 语义 |
|---|
name | str | 是 | 工具在当前 MCP server 内的唯一标识 |
description | str | 是 | 给模型看的工具说明,描述工具何时使用、做什么、返回什么 |
input_schema | type | dict[str, Any] | 是 | 定义工具输入参数,支持简单 dict、TypedDict 和完整 JSON Schema dict |
annotations | ToolAnnotations | None | 否 | MCP 工具注解,例如 readOnlyHint、destructiveHint、openWorldHint |
完整 API 签名和返回的 SdkMcpTool 类型见 Tools Reference - tool()。
工具 handler 必须是 async 函数,通常接收一个 args dict:
@tool("search_docs", "Search internal product documentation.", {"query": str})
async def search_docs(args):
return {"content": [{"type": "text", "text": f"Searching: {args['query']}"}]}
如果 handler 声明第二个位置参数,SDK 会传入 ToolInvocationContext。其中 signal 是一个 asyncio.Event,当 CLI 取消正在执行的工具调用时会被设置,适合长任务主动停止:
@tool("watch", "Watch a counter until max.", {"max": int})
async def watch(args, extra):
for i in range(args["max"]):
if extra.signal.is_set():
return {"content": [{"type": "text", "text": f"aborted at {i}"}]}
await asyncio.sleep(0.01)
return {"content": [{"type": "text", "text": "done"}]}
配置输入参数
Python 版 input_schema 支持三类写法。它们最终都会被规范化为 MCP 协议的 JSON Schema。
写法一:简单 dict
适合几个简单参数的场景。dict 的 key 是参数名,value 是 Python 类型;这种写法下所有 key 都是 required。
input_schema = {
"query": str,
"max_results": int,
"include_archived": bool,
}
可以用 typing.Annotated 给字段增加描述:
from typing import Annotated
input_schema = {
"query": Annotated[str, "Search keywords"],
"max_results": Annotated[int, "Maximum number of snippets to return"],
}
常见类型会被转换为 JSON Schema:
| 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:
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})"}]}
Python 3.11 及以上可以直接从 typing 导入 NotRequired;Python 3.10 需要从 typing_extensions 导入。
写法三:完整 JSON Schema dict
需要 enum、数值范围、字符串格式约束或嵌套对象时,使用完整 JSON Schema:
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"],
}
关于可选参数:简单 dict 写法中所有字段都是 required。要表达可选字段,优先使用 TypedDict + NotRequired 或完整 JSON Schema 的 required 列表。
配置工具元信息
annotations 使用 mcp.types.ToolAnnotations。SDK 会把它放到 MCP tool 定义里,CLI 可据此做调度、权限或状态展示。
from mcp.types import ToolAnnotations
@tool(
"search_docs",
"Search internal product documentation.",
{"query": str},
annotations=ToolAnnotations(
title="Search docs",
readOnlyHint=True,
destructiveHint=False,
openWorldHint=False,
),
)
async def search_docs(args):
return {"content": [{"type": "text", "text": args["query"]}]}
常用字段:
| 字段 | 类型 | 语义 |
|---|
title | str | 工具的人类可读标题 |
readOnlyHint | bool | 标记工具只读,不修改任何状态 |
destructiveHint | bool | 标记工具可能修改或删除数据 |
openWorldHint | bool | 标记工具会访问外部系统或网络 |
maxResultSizeChars | int | Python SDK 会通过 _meta["anthropic/maxResultSizeChars"] 传给 CLI,用于放宽工具返回长度限制 |
注意:annotations 不是权限配置的替代品。是否允许调用工具仍由 tools、allowed_tools、disallowed_tools、permission_mode、can_use_tool 和 hooks 决定。get_mcp_status() / MCP status 中回显的 annotations 字段名也可能是 CLI 投影后的 readOnly、destructive、openWorld,而不是 MCP 原始的 *Hint 名称。
第二步:注册到 MCP server
create_sdk_mcp_server() 把一个或多个工具注册为同进程 MCP server。server 名会进入完整工具名,因此建议短、稳定。
kb_tools = create_sdk_mcp_server(
name="kb",
version="1.0.0",
tools=[search_docs],
)
| 字段 | 怎么填 | 说明 |
|---|
name | 如 kb、orders | server 名,会组成完整工具名 mcp__{name}__{tool} |
version | 如 "1.0.0" | 信息性版本号,默认 "1.0.0" |
tools | [search_docs, lookup_order] | 注册到这个 server 的工具列表 |
返回值是 McpSdkServerConfig,可直接传给 QoderAgentOptions.mcp_servers。
完整返回类型见 Tools Reference - create_sdk_mcp_server()。
create_sdk_mcp_server() 会做同步校验:
- server 名必须是非空字符串。
- tool 名必须是非空字符串。
- tool 描述必须是非空字符串。
- 同一个 server 内不能有重复 tool 名。
第三步:接入 query()
把 server 放进 options.mcp_servers 后,CLI 会发现其中的工具,并在模型需要时通过 SDK 调回你的 handler。
options = QoderAgentOptions(
auth=qodercli_auth(),
mcp_servers={"kb": kb_tools},
allowed_tools=["mcp__kb__search_docs"],
)
async for message in query(
prompt="Search docs for the refund policy and summarize it.",
options=options,
):
print(message)
自定义工具的完整名称格式是:
mcp__{serverName}__{toolName}
例如 server 名是 orders,tool 名是 lookup_order,完整工具名就是 mcp__orders__lookup_order。这个完整名称会用于 allowed_tools、disallowed_tools、can_use_tool、hooks matcher 和子 Agent 的 tools 配置。
QoderSDKClient 多轮会话也使用同样的 mcp_servers 配置:
from qoder_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)
当模型调用工具时,SDK 提供多层权限控制。你可以决定:
- 本次会话提供哪些工具。
- 哪些工具可以默认放行。
- 哪些工具明确禁止。
- 每次工具调用前是否交给宿主应用动态判断。
权限控制方式总览
| 方式 | 作用 | 粒度 | 适用场景 |
|---|
tools | 限制本次会话可见工具集合 | 会话级 | 想从源头收窄模型能看到的工具 |
allowed_tools / disallowed_tools | 预授权或禁止指定工具 | 工具级 | 明确知道要允许或禁止哪些工具 |
permission_mode | 设置整次会话的默认权限策略 | 全局 | 快速切换计划模式、自动接受编辑、跳过权限等 |
can_use_tool | 每次调用前执行自定义判断逻辑 | 调用级 | 需要根据参数内容动态决策 |
hooks["PreToolUse"] | 通过 hooks 生命周期拦截工具调用 | 调用级 | 已经使用 hooks 体系,需要统一审计或拦截 |
这些方式可以组合使用。常见做法是:先用 tools 收窄可见工具集合,再用 allowed_tools / disallowed_tools 设置静态规则,最后用 can_use_tool 做参数级判断。
tools 控制本次会话可见的工具集合;allowed_tools 和 disallowed_tools 控制权限规则。自定义 MCP 工具要使用完整工具名。
# Only expose read/search tools to this session.
QoderAgentOptions(
tools=["Read", "Glob", "Grep"],
allowed_tools=["Read", "Glob", "Grep"],
)
# Explicitly deny high-risk tools.
QoderAgentOptions(
disallowed_tools=["Bash", "Write", "Edit"],
)
# Use full names for custom MCP tools.
QoderAgentOptions(
mcp_servers={"orders": order_tools},
allowed_tools=["mcp__orders__lookup_order"],
)
# Disable all tools. The model can only answer from its context.
QoderAgentOptions(tools=[])
当同一个工具同时匹配允许和禁止规则时,禁止规则优先。
方式二:permission_mode
permission_mode 用一行配置设置整次会话的默认权限行为。
QoderAgentOptions(
permission_mode="acceptEdits",
)
| 模式 | 效果 |
|---|
"default" | 标准权限行为,敏感操作按规则或运行时策略处理 |
"acceptEdits" | 自动接受文件编辑类操作,其他敏感操作仍按权限策略处理 |
"bypassPermissions" | 跳过权限检查,必须同时设置 allow_dangerously_skip_permissions=True |
"yolo" | "bypassPermissions" 的兼容别名,也必须同时设置 allow_dangerously_skip_permissions=True |
"plan" | 计划模式,适合先让模型产出方案 |
"dontAsk" | 不进行交互询问,未预授权或未被规则允许的操作会被拒绝 |
"auto" | 由运行时能力自动判断 allow 或 deny |
can_use_tool 会在工具调用前执行。你可以根据工具名、参数内容和审批上下文返回允许或拒绝。
from typing import Any
from qoder_agent_sdk import (
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
async def can_use_tool(
tool_name: str,
input_data: dict[str, Any],
context: ToolPermissionContext,
):
if tool_name != "mcp__orders__lookup_order":
return PermissionResultDeny(
message="Only order lookup is allowed in this workflow.",
)
return PermissionResultAllow(updated_input=input_data)
options = QoderAgentOptions(
auth=qodercli_auth(),
mcp_servers={"orders": order_tools},
allowed_tools=["mcp__orders__lookup_order"],
can_use_tool=can_use_tool,
)
常见返回值:
| 返回 | 效果 |
|---|
PermissionResultAllow() | 允许执行,使用原始参数 |
PermissionResultAllow(updated_input={...}) | 允许执行,并替换工具参数 |
PermissionResultDeny(message="reason") | 拒绝执行,模型能看到原因并尝试其他方式 |
PermissionResultDeny(message="reason", interrupt=True) | 拒绝并中断当前 agent loop |
ToolPermissionContext 中常用字段包括 tool_use_id、agent_id、signal、title、display_name、description、suggestions、blocked_path 和 decision_reason。更完整的权限策略见 权限控制。
在子 Agent 中使用自定义工具时,也使用完整工具名:
from qoder_agent_sdk import AgentDefinition
options = QoderAgentOptions(
auth=qodercli_auth(),
mcp_servers={"orders": order_tools},
allowed_tools=["Agent"],
agents={
"order-support": AgentDefinition(
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 统一拦截或审计工具调用。
from qoder_agent_sdk import HookMatcher
async def block_dangerous_bash(inp, tool_use_id, context):
command = inp.get("tool_input", {}).get("command", "")
if "rm -rf" in command:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "rm -rf is not allowed",
}
}
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
}
}
options = QoderAgentOptions(
allowed_tools=["Bash"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[block_dangerous_bash]),
],
},
)
PreToolUse 的 permissionDecision 可以是 "allow"、"deny"、"ask" 或 "defer"。
工具 handler 有三类错误路径。
业务失败:返回 is_error: True
可预期的业务失败推荐返回 is_error: True。SDK 会把这个结果转换成 MCP CallToolResult 交给 CLI,模型能看到失败内容,并可能重试或选择其他方式。
return {
"is_error": True,
"content": [
{
"type": "text",
"text": json.dumps(
{
"error": "VALIDATION_ERROR",
"message": "Only SELECT statements are allowed.",
}
),
}
],
}
适合使用 is_error: True 的场景:
- 参数合法但业务上找不到结果,例如订单不存在。
- 安全策略拒绝执行,例如只允许
SELECT 查询。
- 外部服务返回可理解的业务错误。
非预期异常:handler 抛错
如果 handler 抛出异常,MCP 层会把异常转换成错误结果,agent loop 不会因为普通工具异常直接崩掉。但模型通常只能看到异常消息,格式和内容不如显式返回 is_error: True 可控。
@tool("fetch_user", "Fetch a user by ID.", {"user_id": str})
async def fetch_user(args):
response = await user_service.fetch(args["user_id"])
if not response.ok:
raise RuntimeError("User service failed")
return {"content": [{"type": "text", "text": await response.text()}]}
建议:业务上可预期的失败用 is_error: True;真正意外的异常再抛出。
畸形返回:SDK 包装为错误
Python SDK 会在运行时兜底检查 handler 返回值:
- 返回
None:转换为错误文本,说明 handler 必须返回包含 "content" 的 dict。
- 返回非 dict,例如字符串、数字、列表:转换为文本内容,并标记
isError=True。
- 返回 dict 但没有
"content":转换为错误文本,并列出实际 keys。
- 返回不支持的 content 类型:该内容块会被跳过并写 warning 日志。
这些兜底能避免模型看到空成功结果,但文档和业务代码里仍应始终返回标准结构。
工具 handler 返回一个 dict,SDK 会把它转换为 MCP 的 CallToolResult。最常用的是文本内容:
return {
"content": [{"type": "text", "text": "done"}],
}
也可以返回结构化 JSON 字符串,方便模型理解和继续处理:
return {
"content": [
{
"type": "text",
"text": json.dumps(
{
"order_id": "O-1001",
"status": "shipped",
"eta": "2026-05-20",
}
),
}
],
}
常见内容块:
| 类型 | 形状 | Python SDK 行为 |
|---|
| 文本 | {"type": "text", "text": "..."} | 转换为 TextContent |
| 图片 | {"type": "image", "data": "...", "mimeType": "image/png"} | 转换为 ImageContent,data 是 base64 |
| 资源链接 | {"type": "resource_link", "uri": "...", "name": "...", "description": "..."} | 降级为文本,把 name / uri / description 拼接给模型 |
| 内嵌文本资源 | {"type": "resource", "resource": {"text": "..."}} | 转换为 TextContent |
Python 版还有两个需要注意的结果差异:
- handler 返回 dict 顶层
_meta 不会透传到 CallToolResult。
- handler 返回错误标记时使用 Python 字段名
"is_error": True,不是 MCP/TypeScript 风格的 isError。SDK 内部会映射到 MCP 结果。
常见踩坑
- 权限配置里写自定义工具时,要写
mcp__server__tool 完整名称。
- Python 简单 dict schema 的所有字段都是 required;需要可选字段时用
TypedDict + NotRequired 或完整 JSON Schema。
- 需要 enum、数值范围、嵌套对象、字符串 pattern/format 时,使用完整 JSON Schema dict。
- handler 必须是
async def,并返回包含 "content" 列表的 dict。
- 工具描述要写“什么时候用、做什么、返回什么”,不要只写
query、helper 这类模糊描述。
readOnlyHint 是工具元信息和调度/权限提示,不是权限开关;是否允许执行仍由权限配置决定。
- 不要把大而全的业务入口都塞进一个万能工具。一个工具最好完成一类清晰动作。
继续阅读
- MCP 集成:in-process、stdio、SSE、HTTP、OAuth 等 MCP server 接入方式。
- 权限控制:
permission_mode、allowed_tools、can_use_tool、hooks、权限规则更新。
- Hooks:
PreToolUse、PostToolUse、PermissionRequest 等生命周期扩展。
- 子 Agent 使用指南:让不同 Agent 使用不同工具集。