
自定义工具是 Claude Code SDK 把 Agent 能力扩展到任意外部系统的核心机制。本篇完整拆解工具四要素(Name/Description/Input Schema/Handler)、Python @tool 装饰器与 create_sdk_mcp_server 注册流程、mcp__{server}__{tool} 命名规范与通配符放行、readOnlyHint 并行加速、throw 与 is_error 的错误处理差异、image/resource/structuredContent 非文本返回,以及 JSON Schema 枚举参数全写法,附完整可运行 Python 示例和五条实践建议。
@tool 装饰器把一个普通 async 函数包装成工具,再把它挂到 query() 上,Claude 就能在对话中直接调用你的代码——查数据库、打外部 API、做域内计算,全部可以。本篇完整拆解自定义工具的定义方式、注册流程、权限配置、错误处理,以及返回图片/结构化数据的写法,附可直接运行的 Python 示例。1@tool 装饰器或 TypeScript 的 tool() 函数:| 要素 | 作用 | 写法 |
|---|---|---|
| Name | Claude 调用时使用的唯一标识符 | 字符串,如 "get_temperature" |
| Description | 告诉 Claude 这个工具做什么、何时调用 | 越具体越好,直接影响模型选工具的准确率 |
| Input Schema | Claude 调用时必须提供的参数及类型 | Python:{"latitude": float};TypeScript:Zod schema |
| Handler | 实际执行逻辑的 async 函数 | 接收 args: dict,返回含 content 字段的 dict |
content(结果块数组),可选 structuredContent(机器可读 JSON)和 is_error(标记失败)。get_temperature 工具,从公开气象 API 拉取当前温度:1from typing import Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool(
"get_temperature",
"Get the current temperature at a location",
{"latitude": float, "longitude": float},
)
async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": args["latitude"],
"longitude": args["longitude"],
"current": "temperature_2m",
"temperature_unit": "fahrenheit",
},
)
data = response.json()
return {
"content": [
{
"type": "text",
"text": f"Temperature: {data['current']['temperature_2m']}°F",
}
]
}
# 打包成 in-process MCP server
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature],
)create_sdk_mcp_server 的关键点:它跑在进程内部,不是一个独立子进程,也不需要 stdio 通信。与 #8 MCP 集成篇里介绍的外部 MCP server 相比,这种方式延迟更低,部署更简单。query() 才能跑起来。MCP server 挂在 mcp_servers 字典里,字典的键名就是 server name,自动决定工具的完整名称格式:mcp__{server_name}__{tool_name}server name="weather",tool name="get_temperature",最终暴露给 Claude 的工具名是 mcp__weather__get_temperature。把它加进 allowed_tools,Claude 调用时就不需要人工确认:from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
options = ClaudeAgentOptions(
mcp_servers={"weather": weather_server},
allowed_tools=["mcp__weather__get_temperature"],
)
async for message in query(
prompt="What's the temperature in San Francisco?",
options=options,
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)allowed_tools 可以逐个列出,也可以用通配符 mcp__weather__* 一次性放行该 server 下的所有工具。| 字段 | 默认值 | 含义 |
|---|---|---|
readOnlyHint | false | 工具不修改环境(控制是否允许并行调用) |
destructiveHint | true | 工具可能执行破坏性写操作(仅提示,不强制) |
idempotentHint | false | 重复调用同参数无副作用(仅提示,不强制) |
openWorldHint | true | 工具会访问进程外系统(仅提示,不强制) |
readOnlyHint 是唯一影响实际执行行为的字段:标记为 true 后,Claude 可以把多个只读工具批量并行调用,显著缩短多步查询的总耗时。只查不改的工具都应该加上这个标记。from claude_agent_sdk import tool, ToolAnnotations
@tool(
"get_temperature",
"Get the current temperature at a location",
{"latitude": float, "longitude": float},
annotations=ToolAnnotations(readOnlyHint=True), # 允许并行
)
async def get_temperature(args):
return {"content": [{"type": "text", "text": "..."}]}query() 调用失败。is_error: True → Agent loop 继续,Claude 看到错误内容,可以重试、换工具或告知用户原因。try/except 所有可能出错的路径,把失败信息包在 content 里返回:
@tool("fetch_data", "Fetch data from an API", {"endpoint": str})
async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
try:
async with httpx.AsyncClient() as client:
response = await client.get(args["endpoint"])
if response.status_code != 200:
return {
"content": [{
"type": "text",
"text": f"API error: {response.status_code} {response.reason_phrase}",
}],
"is_error": True, # 通知 Claude 这次调用失败了
}
data = response.json()
return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
except Exception as e:
return {
"content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
"is_error": True,
}is_error: True 让 Claude 把工具返回当成错误信号来处理——它可能重试,可能换思路,但不会把错误字符串当正常数据吞掉。content 数组支持三种类型的块:text、image、resource,可以混用。
import base64
import httpx
@tool("fetch_image", "Fetch an image from a URL", {"url": str})
async def fetch_image(args):
async with httpx.AsyncClient() as client:
response = await client.get(args["url"])
return {
"content": [{
"type": "image",
"data": base64.b64encode(response.content).decode("ascii"),
"mimeType": response.headers.get("content-type", "image/png"),
}]
}text 或 blob 字段里。URI 只是 Claude 用来引用的标签,SDK 不会真的去读这个路径:{
"type": "resource",
"resource": {
"uri": "file:///tmp/report.md", # Claude 引用这个标签,不是 SDK 实际路径
"mimeType": "text/markdown",
"text": "# Report\n..." # 实际内容内联
}
}content 之外追加机器可读 JSON,Claude 直接读字段而不是解析文本。Python @tool 暂不支持 structuredContent,需要改用独立 MCP server 才能使用。{"latitude": float})不支持枚举约束,遇到需要限定取值范围的参数时,必须换成完整的 JSON Schema 格式:@tool(
"convert_units",
"Convert a value from one unit to another",
{
"type": "object",
"properties": {
"unit_type": {
"type": "string",
"enum": ["length", "temperature", "weight"], # 枚举约束
"description": "Category of unit",
},
"from_unit": {"type": "string", "description": "Unit to convert from"},
"to_unit": {"type": "string", "description": "Unit to convert to"},
"value": {"type": "number", "description": "Value to convert"},
},
"required": ["unit_type", "from_unit", "to_unit", "value"],
},
)
async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
...z.enum(["length", "temperature", "weight"]) 即可,Zod 自动转成 JSON Schema。.default() 指定默认值;Python 则把参数从 schema 里去掉(schema 里的每个键默认 required),在 handler 里用 args.get("hours", 12) 读取。"Get weather data" 不如 "Get the current temperature at a latitude/longitude coordinate in Fahrenheit",Claude 在多工具场景下选择更准确。readOnlyHint=True。一个常见场景是同时查多个地点温度——标记后 Claude 会批量并行发起,不需要串行等待。query() 调用,调试很麻烦。统一用 is_error: True 把错误作为数据回传,让 Claude 处理。mcp__server__* 放行。一个 server 里工具多了后逐个列 allowed_tools 很繁琐,通配符更好维护。如果只想暴露其中几个,逐个列出;想全部放行,用通配符。docs.anthropic.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.