本页目录17
- 自定义工具由名称、描述、输入 schema、处理函数(handler)四部分构成,TypeScript 用 tool() + Zod schema,Python 用 @tool 装饰器 + dict/JSON Schema
- 工具需用 createSdkMcpServer / create_sdk_mcp_server 包装成进程内 MCP 服务器,再通过 query() 的 mcpServers 选项注册,完整工具名格式为 mcp__{server_name}__{tool_name}
- tools 数组和 allowedTools/disallowedTools 分别控制「可见性(availability)」与「权限(permission)」两层,二者作用不同
- handler 抛出未捕获异常也不会中断 agent 循环,但可通过返回 isError: true 自定义 Claude 看到的错误信息
- content 数组支持 text/image/audio/resource/resource_link 多种类型,structuredContent 可返回机器可读 JSON,但 Python 的 @tool 装饰器目前不转发 structuredContent
- 工具数量多时默认开启的 tool search 会按需加载 schema,TypeScript 可用 alwaysLoad: true 强制常驻上下文
本文是对 Claude Agent SDK 官方文档「Give Claude custom tools」页面的中文整理,完整与最新内容以原文为准:https://code.claude.com/docs/en/agent-sdk/custom-tools
概述
自定义工具让你在 Agent SDK 中定义自己的函数,供 Claude 在对话中调用。通过 SDK 的进程内 MCP 服务器,可以让 Claude 访问数据库、外部 API、领域特定逻辑,或应用需要的任何其他能力。
快速参考
| 如果你想要... | 这样做 |
|---|---|
| 定义一个工具 | 使用 @tool(Python)或 tool()(TypeScript),提供名称、描述、schema 和处理函数。见「创建自定义工具」 |
| 向 Claude 注册工具 | 用 create_sdk_mcp_server / createSdkMcpServer 包装,传给 query() 的 mcpServers 选项。见「调用自定义工具」 |
| 预先批准某个工具 | 加入你的 allowed tools 列表。见「配置允许的工具」 |
| 从 Claude 的上下文中移除某个内置工具 | 传入只列出你想保留的内置工具的 tools 数组。见「配置允许的工具」 |
| 让 Claude 并行调用工具 | 对无副作用的工具设置 readOnlyHint: true。见「添加工具注解」 |
| 控制 Claude 读到的错误信息 | 返回 isError: true 来自定义消息,而不是暴露原始异常。见「处理错误」 |
| 返回图片或文件 | 在 content 数组中使用 image 或 resource 块。见「返回图片和资源」 |
| 返回机器可读的 JSON 结果 | 在结果上设置 structuredContent。见「返回结构化数据」 |
| 扩展到大量工具 | 使用 tool search 按需加载工具 |
创建自定义工具
一个工具由四部分组成,作为参数传给 TypeScript 中的 tool() 辅助函数,或 Python 中的 @tool 装饰器:
- 名称(Name):Claude 用来调用该工具的唯一标识符。
- 描述(Description):说明工具的作用。Claude 会读取这段描述来决定何时调用它。
- 输入 schema:Claude 必须提供的参数。TypeScript 中始终使用 Zod schema,handler 的
args会根据它自动推断类型。Python 中是一个把名称映射到类型的 dict,例如{"latitude": float},SDK 会自动将其转换为 JSON Schema。当需要枚举、范围、可选字段或嵌套对象时,Python 装饰器也接受直接传入完整的 JSON Schema dict。 - 处理函数(Handler):Claude 调用工具时运行的异步函数。它接收经过校验的参数,必须返回一个包含以下字段的对象:
content(必需):结果块组成的数组,每个块的type为"text"、"image"、"audio"、"resource"或"resource_link"之一。非文本块见「返回图片和资源」。structuredContent(可选):一个 JSON 对象,作为机器可读数据随content一起返回。见「返回结构化数据」。isError(可选):设为true表示工具调用失败,以便 Claude 做出反应。见「处理错误」。
定义好工具后,用 createSdkMcpServer(TypeScript)或 create_sdk_mcp_server(Python)将其包装为服务器。该服务器运行在你的应用进程内,而不是作为单独的进程。
示例:天气工具
下面的示例定义了一个 get_temperature 工具,并将其包装进一个 MCP 服务器。它只是完成了工具的搭建;要将其传给 query 并运行,见下文「调用自定义工具」。
from typing import Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server
# Define a tool: name, description, input schema, handler
@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 a content array - Claude sees this as the tool result
return {
"content": [
{
"type": "text",
"text": f"Temperature: {data['current']['temperature_2m']}°F",
}
]
}
# Wrap the tool in an in-process MCP server
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature],
)
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
// Define a tool: name, description, input schema, handler
const getTemperature = tool(
"get_temperature",
"Get the current temperature at a location",
{
latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
longitude: z.number().describe("Longitude coordinate")
},
async (args) => {
// args is typed from the schema: { latitude: number; longitude: number }
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}¤t=temperature_2m&temperature_unit=fahrenheit`
);
const data: any = await response.json();
// Return a content array - Claude sees this as the tool result
return {
content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
};
}
);
// Wrap the tool in an in-process MCP server
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature]
});
完整参数细节(包括 JSON Schema 输入格式和返回值结构)请见 tool() TypeScript 参考或 @tool Python 参考。
提示:要让某个参数变为可选:在 TypeScript 中,给对应的 Zod 字段加上
.default()。在 Python 中,dict schema 会把每个 key 都视为必需,因此把该参数留在 schema 之外、在描述字符串中提及它,并在 handler 里用args.get()读取。下文「添加更多工具」中的get_precipitation_chance工具同时展示了这两种写法。
调用自定义工具
通过 mcpServers 选项把创建好的 MCP 服务器传给 query。mcpServers 中的 key 会成为每个工具完整名称中的 {server_name} 部分:mcp__{server_name}__{tool_name}。把这个名称列入 allowedTools,该工具运行时就不会弹出权限提示。
下面的代码复用上文「示例:天气工具」中的 weatherServer,询问 Claude 某地的天气。
import asyncio
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,
):
# ResultMessage is the final message after all tool calls complete
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "What's the temperature in San Francisco?",
options: {
mcpServers: { weather: weatherServer },
allowedTools: ["mcp__weather__get_temperature"]
}
})) {
// "result" is the final message after all tool calls complete
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
把这段代码与「示例:天气工具」中的工具和服务器定义放在同一个文件里,然后用 Python 的 python weather.py 或 TypeScript 的 npx tsx weather.ts 运行。Claude 会调用 get_temperature,脚本会打印一行关于旧金山当前气温的回答。
添加更多工具
一个服务器可以容纳 tools 数组中列出的任意多个工具。当一个服务器上有多个工具时,你可以在 allowedTools 中逐个列出,也可以用通配符 mcp__weather__* 覆盖该服务器暴露的全部工具。
下面的示例定义了第二个工具 get_precipitation_chance,并用一个把两个工具都列入数组的定义替换了「示例:天气工具」中的 weatherServer。
# Define a second tool for the same server
@tool(
"get_precipitation_chance",
"Get the hourly precipitation probability for a location. "
"Optionally pass 'hours' (1-24) to control how many hours to return.",
{"latitude": float, "longitude": float},
)
async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
# 'hours' isn't in the schema - read it with .get() to make it optional
hours = args.get("hours", 12)
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": args["latitude"],
"longitude": args["longitude"],
"hourly": "precipitation_probability",
"forecast_days": 1,
},
)
data = response.json()
chances = data["hourly"]["precipitation_probability"][:hours]
return {
"content": [
{
"type": "text",
"text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
}
]
}
# Rebuild the server with both tools in the array
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature, get_precipitation_chance],
)
// Define a second tool for the same server
const getPrecipitationChance = tool(
"get_precipitation_chance",
"Get the hourly precipitation probability for a location",
{
latitude: z.number(),
longitude: z.number(),
hours: z
.number()
.int()
.min(1)
.max(24)
.default(12) // .default() makes the parameter optional
.describe("How many hours of forecast to return")
},
async (args) => {
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
);
const data: any = await response.json();
const chances = data.hourly.precipitation_probability.slice(0, args.hours);
return {
content: [{ type: "text", text: `Next ${args.hours} hours: ${chances.join("%, ")}%` }]
};
}
);
// Rebuild the server with both tools in the array
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature, getPrecipitationChance]
});
tool search 默认开启,会延迟加载 SDK MCP 工具:Claude 只会先看到一份紧凑的工具名称列表,需要时再按需加载完整 schema。如果禁用 tool search,该数组中的每个工具都会在每一轮对话中占用上下文窗口空间。在 TypeScript 中,可以在 tool() 的 extras 参数中,或 createSdkMcpServer() 的选项中传入 alwaysLoad: true,让某个工具的完整 schema 始终留在初始 prompt 中。
添加工具注解
工具注解(tool annotations)是描述工具行为的可选元数据。在 TypeScript 中作为 tool() 的第五个参数传入,在 Python 中通过 @tool 装饰器的 annotations 关键字参数传入。所有 hint 字段都是布尔值。
| 字段 | 默认值 | 含义 |
|---|---|---|
readOnlyHint | false | 工具不修改其运行环境。决定该工具是否可以与其他只读工具并行调用。 |
destructiveHint | true | 工具可能执行破坏性更新。仅作信息说明用途。 |
idempotentHint | false | 使用相同参数重复调用没有额外效果。仅作信息说明用途。 |
openWorldHint | true | 工具会访问你进程之外的系统。仅作信息说明用途。 |
注解只是元数据,并非强制约束。一个标注为 readOnlyHint: true 的工具,如果 handler 就是这样实现的,依然可以写入磁盘。请让注解与 handler 的实际行为保持一致。
下面的示例为「示例:天气工具」中的 get_temperature 工具添加了 readOnlyHint。
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
), # Lets Claude batch this with other read-only calls
)
async def get_temperature(args):
return {"content": [{"type": "text", "text": "..."}]}
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
tool(
"get_temperature",
"Get the current temperature at a location",
{ latitude: z.number(), longitude: z.number() },
async (args) => ({ content: [{ type: "text", text: `...` }] }),
{ annotations: { readOnlyHint: true } } // Lets Claude batch this with other read-only calls
);
完整定义见 TypeScript 或 Python 参考中的 ToolAnnotations。
控制工具访问
「示例:天气工具」注册了一个服务器,并把工具列入 allowedTools。本节介绍当你有多个工具、或想限制内置工具时,如何配置访问范围。关于工具名称的构成方式,见上文「调用自定义工具」。
配置允许的工具
tools 选项与 allowed/disallowed 列表会影响两个层面:可见性(availability),决定某个工具是否出现在 Claude 的上下文中;以及权限(permission),决定 Claude 尝试调用时该次调用是否被批准。tools 和裸名称形式的 disallowedTools 条目改变可见性;allowedTools 和带范围限定的 disallowedTools 规则改变权限。如果你在 allowedTools 中列出了某个任务追踪工具,Claude Code 也会让该会话选择启用它。
| 选项 | 层面 | 效果 |
|---|---|---|
tools: ["Read", "Grep"] | 可见性 | 只有列出的内置工具出现在 Claude 的上下文中,未列出的内置工具被移除。MCP 工具不受影响。 |
tools: [] | 可见性 | 所有内置工具都被移除。Claude 只能使用你的 MCP 工具。 |
| allowed tools | 权限 | 列出的工具运行时不会弹出权限提示。其他未列出的工具仍然可用,调用会走权限流程。 |
| disallowed tools | 两者 | 裸工具名(如 "Bash")会把该工具从 Claude 的上下文中移除,效果等同于在 tools 中省略它。带范围限定的规则(如 "Bash(rm *)")会让工具仍留在上下文中,但拒绝匹配的调用。 |
要彻底移除某个内置工具,在 tools 中省略它,或在 disallowedTools(Python 中为 disallowed_tools)中列出其裸名称;两种方式都会让该工具不进入上下文,Claude 也就永远不会尝试调用它。带范围限定的 disallowedTools 规则只会阻止匹配的调用,但工具仍然可见,Claude 可能会浪费一轮尝试调用它。完整的判定顺序见配置权限。
处理错误
handler 出错并不会终止 agent 循环。SDK 的进程内 MCP 服务器会捕获未处理的异常并将其转换为错误结果,因此你如何报告错误决定的是 Claude 读到什么内容,而不是查询是否失败:
| 发生的情况 | 结果 |
|---|---|
| handler 抛出未捕获的异常 | MCP 服务器会将其转换为携带原始异常信息的错误结果。Claude 会看到该消息,agent 循环继续。 |
handler 捕获错误并返回 isError: true(TS)/ "is_error": True(Python) | Claude 会看到你自行组织的消息。你可以补充原始异常缺失的上下文,比如是哪个请求失败了、接下来该尝试什么。 |
无论哪种情况,Claude 都可以重试、换一个工具,或向用户解释失败原因。当原始异常信息不足以让 Claude 采取行动时,应自行捕获错误。
下面的示例在 handler 内部捕获了两类失败,并组织了 Claude 会读到的错误信息:非 200 的 HTTP 状态码从响应中捕获并作为错误结果返回;网络错误或无效 JSON 由外层的 try/except(Python)或 try/catch(TypeScript)捕获,同样作为错误结果返回。两种情况下 Claude 收到的都是描述失败原因的消息,而不是一个裸的异常字符串。
import json
import httpx
from typing import Any
from claude_agent_sdk import tool
@tool(
"fetch_data",
"Fetch data from an API",
{"endpoint": str}, # Simple schema
)
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 the failure as a tool result so Claude can react to it.
# is_error marks this as a failed call rather than odd-looking data.
return {
"content": [
{
"type": "text",
"text": f"API error: {response.status_code} {response.reason_phrase}",
}
],
"is_error": True,
}
data = response.json()
return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
except Exception as e:
# Composes the message Claude reads. An uncaught exception would
# reach Claude as the raw str(e) with no context.
return {
"content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
"is_error": True,
}
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
tool(
"fetch_data",
"Fetch data from an API",
{
endpoint: z.string().url().describe("API endpoint URL")
},
async (args) => {
try {
const response = await fetch(args.endpoint);
if (!response.ok) {
// Return the failure as a tool result so Claude can react to it.
// isError marks this as a failed call rather than odd-looking data.
return {
content: [
{
type: "text",
text: `API error: ${response.status} ${response.statusText}`
}
],
isError: true
};
}
const data = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2)
}
]
};
} catch (error) {
// Composes the message Claude reads. An uncaught throw would
// reach Claude as the raw error message with no context.
return {
content: [
{
type: "text",
text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
}
);
返回图片和资源
工具结果中的 content 数组接受 text、image、audio、resource、resource_link 类型的块,可以在同一个响应里混用。在 TypeScript 中,SDK 会把 audio 块保存到磁盘,Claude 收到的是一个包含保存路径的文本块;在 Python 中,SDK 会从工具结果中丢弃 audio 块并记录一条警告。SDK 会把 resource link 块转换为一个包含该链接名称、URI 和描述的文本块。
图片
image 块以 base64 编码的形式内联携带图片字节数据,不存在 URL 字段。要返回一张位于某个 URL 的图片,需要在 handler 中先拉取该 URL,读取响应字节,再进行 base64 编码后返回。返回结果会作为视觉输入被处理。
| 字段 | 类型 | 说明 |
|---|---|---|
type | "image" | |
data | string | base64 编码的字节数据。只能是原始 base64,不能带 data:image/...;base64, 前缀 |
mimeType | string | 必需。例如 image/png、image/jpeg、image/webp、image/gif |
import base64
import httpx
from claude_agent_sdk import tool
# Define a tool that fetches an image from a URL and returns it to Claude
@tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
async def fetch_image(args):
async with httpx.AsyncClient() as client: # Fetch the image bytes
response = await client.get(args["url"])
return {
"content": [
{
"type": "image",
"data": base64.b64encode(response.content).decode(
"ascii"
), # Base64-encode the raw bytes
"mimeType": response.headers.get(
"content-type", "image/png"
), # Read MIME type from the response
}
]
}
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
tool(
"fetch_image",
"Fetch an image from a URL and return it to Claude",
{
url: z.string().url()
},
async (args) => {
const response = await fetch(args.url); // Fetch the image bytes
const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
const mimeType = response.headers.get("content-type") ?? "image/png";
return {
content: [
{
type: "image",
data: buffer.toString("base64"), // Base64-encode the raw bytes
mimeType
}
]
};
}
);
资源
resource 块封装了一段由 URI 标识的内容。这个 URI 只是 Claude 用来引用它的一个标签,实际内容存放在该块的 text 或 blob 字段中。当你的工具产出了某种适合在之后按名称引用的东西时(比如生成的文件,或来自外部系统的一条记录),可以使用这种方式。
| 字段 | 类型 | 说明 |
|---|---|---|
type | "resource" | |
resource.uri | string | 内容的标识符。可以是任意 URI scheme |
resource.text | string | 如果内容是文本,放在这里。与 blob 二选一,不能同时提供 |
resource.blob | string | 如果内容是二进制数据,base64 编码后放在这里。仅限 TypeScript:Python SDK 会从工具结果中丢弃二进制资源并记录一条警告 |
resource.mimeType | string | 可选 |
下面的示例展示了从工具 handler 内部返回的一个 resource 块。URI file:///tmp/report.md 只是一个供 Claude 引用的标签;SDK 并不会去读取该路径。
return {
content: [
{
type: "resource",
resource: {
uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
mimeType: "text/markdown",
text: "# Report\n..." // The actual content, inline
}
}
]
};
return {
"content": [
{
"type": "resource",
"resource": {
"uri": "file:///tmp/report.md", # Label for Claude to reference, not a path the SDK reads
"mimeType": "text/markdown",
"text": "# Report\n...", # The actual content, inline
},
}
]
}
这些块的结构来自 MCP 的 CallToolResult 类型。完整定义见 MCP 规范。
返回结构化数据
structuredContent 是结果对象上一个可选的 JSON 字段,独立于 content 数组。用它来返回原始数值,让 Claude 可以直接读取精确的字段,而不必从文本字符串或图片中解析。
设置了 structuredContent 后,Claude 会收到该 JSON,以及 content 中的任意 image 或 resource 块。content 中的 text 块不会被转发,因为它们被认为与结构化数据是重复的。下面的例子在同一个 handler 中,将图表渲染为 image 块,并在 structuredContent 中返回图表背后的数据点。片段中的 chartPngBuffer 是一个持有已渲染 PNG 字节数据的 Buffer。
return {
content: [
{
type: "image",
data: chartPngBuffer.toString("base64"),
mimeType: "image/png"
}
],
structuredContent: {
series: "temperature_2m",
unit: "fahrenheit",
points: [62.1, 63.4, 65.0, 64.2]
}
};
注意:Python 的
@tool装饰器只会从 handler 的返回 dict 中转发content和is_error。要从 Python 返回structuredContent,需要运行一个独立的 MCP 服务器,而不是进程内的 SDK 服务器。
示例:单位转换器
这个工具在长度、温度和重量单位之间进行换算。用户可以问「把 100 公里换算成英里」或「72°F 是多少摄氏度」,Claude 会根据请求挑选正确的单位类型和具体单位。
它展示了两种模式:
- 枚举 schema:
unit_type被限定为一组固定的取值。在 TypeScript 中使用z.enum();在 Python 中,dict schema 不支持枚举,因此需要使用完整的 JSON Schema dict。 - 处理不支持的输入:当找不到对应的换算组合时,handler 会返回
isError: true,以便 Claude 能告诉用户出了什么问题,而不是把失败当作正常结果处理。
from typing import Any
from claude_agent_sdk import tool, create_sdk_mcp_server
# z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
# The dict schema has no equivalent, so full JSON Schema is required.
@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, e.g. kilometers, fahrenheit, pounds",
},
"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]:
conversions = {
"length": {
"kilometers_to_miles": lambda v: v * 0.621371,
"miles_to_kilometers": lambda v: v * 1.60934,
"meters_to_feet": lambda v: v * 3.28084,
"feet_to_meters": lambda v: v * 0.3048,
},
"temperature": {
"celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
"fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
"celsius_to_kelvin": lambda v: v + 273.15,
"kelvin_to_celsius": lambda v: v - 273.15,
},
"weight": {
"kilograms_to_pounds": lambda v: v * 2.20462,
"pounds_to_kilograms": lambda v: v * 0.453592,
"grams_to_ounces": lambda v: v * 0.035274,
"ounces_to_grams": lambda v: v * 28.3495,
},
}
key = f"{args['from_unit']}_to_{args['to_unit']}"
fn = conversions.get(args["unit_type"], {}).get(key)
if not fn:
return {
"content": [
{
"type": "text",
"text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
}
],
"is_error": True,
}
result = fn(args["value"])
return {
"content": [
{
"type": "text",
"text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
}
]
}
converter_server = create_sdk_mcp_server(
name="converter",
version="1.0.0",
tools=[convert_units],
)
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const convert = tool(
"convert_units",
"Convert a value from one unit to another",
{
unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
from_unit: z
.string()
.describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
to_unit: z.string().describe("Unit to convert to"),
value: z.number().describe("Value to convert")
},
async (args) => {
type Conversions = Record<string, Record<string, (v: number) => number>>;
const conversions: Conversions = {
length: {
kilometers_to_miles: (v) => v * 0.621371,
miles_to_kilometers: (v) => v * 1.60934,
meters_to_feet: (v) => v * 3.28084,
feet_to_meters: (v) => v * 0.3048
},
temperature: {
celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
celsius_to_kelvin: (v) => v + 273.15,
kelvin_to_celsius: (v) => v - 273.15
},
weight: {
kilograms_to_pounds: (v) => v * 2.20462,
pounds_to_kilograms: (v) => v * 0.453592,
grams_to_ounces: (v) => v * 0.035274,
ounces_to_grams: (v) => v * 28.3495
}
};
const key = `${args.from_unit}_to_${args.to_unit}`;
const fn = conversions[args.unit_type]?.[key];
if (!fn) {
return {
content: [
{
type: "text",
text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
}
],
isError: true
};
}
const result = fn(args.value);
return {
content: [
{
type: "text",
text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
}
]
};
}
);
const converterServer = createSdkMcpServer({
name: "converter",
version: "1.0.0",
tools: [convert]
});
定义好服务器后,像天气示例一样把它传给 query。下面的例子在循环中发送三个不同的 prompt,展示同一个工具处理不同的单位类型。对于每个响应,代码会检查 AssistantMessage 对象(其中包含 Claude 在该轮做出的工具调用),打印每个 ToolUseBlock,最后打印 ResultMessage 的文本。这样可以看出 Claude 何时在使用工具,何时在凭自身知识作答。
由于 tool search 默认开启,输出中还可能包含一次 ToolSearch 调用,这是 Claude 在加载被延迟的工具 schema。
import asyncio
from claude_agent_sdk import (
query,
ClaudeAgentOptions,
ResultMessage,
AssistantMessage,
ToolUseBlock,
)
async def main():
options = ClaudeAgentOptions(
mcp_servers={"converter": converter_server},
allowed_tools=["mcp__converter__convert_units"],
)
prompts = [
"Convert 100 kilometers to miles.",
"What is 72°F in Celsius?",
"How many pounds is 5 kilograms?",
]
for prompt in prompts:
try:
async for message in query(prompt=prompt, options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, ToolUseBlock):
print(f"[tool call] {block.name}({block.input})")
elif isinstance(message, ResultMessage) and message.subtype == "success":
print(f"Q: {prompt}\nA: {message.result}\n")
except Exception as error:
# A single-shot query() raises after yielding an error result. Only success
# results are printed above, so handle the failure here and continue with
# the next prompt.
print(f"Call failed: {error}")
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
const prompts = [
"Convert 100 kilometers to miles.",
"What is 72°F in Celsius?",
"How many pounds is 5 kilograms?"
];
for (const prompt of prompts) {
try {
for await (const message of query({
prompt,
options: {
mcpServers: { converter: converterServer },
allowedTools: ["mcp__converter__convert_units"]
}
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "tool_use") {
console.log(`[tool call] ${block.name}`, block.input);
}
}
} else if (message.type === "result" && message.subtype === "success") {
console.log(`Q: ${prompt}\nA: ${message.result}\n`);
}
}
} catch (error) {
// A single-shot query() throws after yielding an error result. Only success
// results are logged above, so handle the failure here and continue with
// the next prompt.
console.error(`Call failed: ${error}`);
}
}
后续步骤
可以在同一个服务器中混用本文中的各种模式:一个服务器可以同时容纳数据库工具、API 网关工具和图片渲染工具。
接下来可以了解:
- 如果你的服务器工具数量增长到几十个,参见 tool search,了解如何延迟加载它们,直到 Claude 真正需要时才加载。
- 如果想连接外部 MCP 服务器(文件系统、GitHub、Slack)而不是自己搭建,参见连接 MCP 服务器。
- 如果想控制哪些工具自动运行、哪些需要审批,参见配置权限。