第10章:自定义工具 —— 给 Agent 装上新技能
一句话:创建你自己的工具,让 Agent 能做任何你想做的事。
本章目标
- 理解为什么需要自定义工具,以及它和内置工具的区别
- 掌握
createSdkMcpServer()和tool()的用法 - 能独立编写天气查询、数据库操作、待办事项管理等自定义工具
- 了解工具设计的最佳实践和常见陷阱
前置知识
- 需要先看完第4章(query 函数)和第5章(内置工具)
- 需要基本了解 TypeScript 和 Zod 库
- 建议先看完第9章(结构化输出),因为里面提到了 Zod
10.1 为什么需要自定义工具?
内置工具够用吗?
在第5章里,我们学了 Agent 自带的那些工具 —— Read、Write、Bash、Grep 等等。这些工具很强大,能帮 Agent 读写文件、执行命令、搜索内容。
但是,想想这些场景:
- 你想让 Agent 查一下北京今天的天气
- 你想让 Agent 从你公司的数据库里查一条订单
- 你想让 Agent 调用你公司内部的审批 API
- 你想让 Agent 往 Slack 频道里发一条消息
- 你想让 Agent 管理一个待办事项清单
这些事情,内置工具统统做不到。内置工具就像一个瑞士军刀 —— 日常够用,但你要砍树的话,得去买一把电锯。
自定义工具 = 给 Agent 装新手臂
自定义工具就是你自己写的函数,然后"注册"给 Agent,让它在需要的时候可以调用。
打个比方:
- 内置工具 = Agent 出厂自带的手和脚
- 自定义工具 = 你给 Agent 加装的机械臂、望远镜、对讲机...
你想让 Agent 有什么新能力,就给它做一个对应的工具就行了。理论上,只要你能用代码实现的功能,都可以包装成一个工具给 Agent 用。
工作原理简单说
整个流程是这样的:
1. 你定义一个工具:名字、描述、参数、执行函数
2. 你把工具注册到一个"工具服务器"
3. 你把工具服务器传给 query()
4. Agent 运行时,看到工具列表,根据描述判断什么时候该用
5. Agent 决定用某个工具时,传入参数,你的函数被调用
6. 函数执行完,返回结果给 Agent
7. Agent 拿到结果,继续思考下一步
是不是很简单?就是"定义 -> 注册 -> 使用"三步走。
10.2 用 createSdkMcpServer() 创建工具服务器
核心概念
在 Claude Agent SDK 里,自定义工具不是直接丢给 query() 的。你需要先创建一个工具服务器(MCP Server),然后把工具服务器传给 query()。
为什么要多这一步?因为 SDK 采用了 MCP(Model Context Protocol)协议来管理工具。MCP 是 Anthropic 发明的一个开放标准,下一章会专门讲。现在你只需要知道:工具服务器就是一个装工具的盒子。
两个关键函数
| 函数 | 干什么的 | 比喻 |
|---|---|---|
createSdkMcpServer() |
创建一个工具服务器 | 造一个工具箱 |
tool() |
定义一个工具 | 造一把具体的工具 |
最小示例
先看一个最简单的例子,感受一下整体结构:
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
// 第一步:用 tool() 定义一个工具
// 第二步:用 createSdkMcpServer() 把工具放进服务器
const myServer = createSdkMcpServer({
name: "my-tools", // 服务器名字(随便起)
version: "1.0.0", // 版本号
tools: [ // 工具列表,可以放多个
tool(
"say_hello", // 工具名字
"向指定的人打招呼", // 工具描述(Agent 靠这个决定什么时候用)
{ name: z.string().describe("要打招呼的人的名字") }, // 参数 schema
async ({ name }) => { // 执行函数
return {
content: [{ type: "text", text: `你好,${name}!很高兴认识你!` }]
};
}
)
]
});
// 第三步:把工具服务器传给 query()
for await (const message of query({
prompt: "请跟小明打个招呼",
options: {
mcpServers: {
"my-tools": myServer // 键名要和服务器 name 一致
},
allowedTools: [
"mcp__my-tools__say_hello" // 允许 Agent 使用这个工具
]
}
})) {
if (message.type === "assistant") {
console.log(message.content);
}
}
逐行解析
tool() 函数的四个参数:
tool(
name, // 第1个参数:工具名字(string)
description, // 第2个参数:工具描述(string)
schema, // 第3个参数:参数定义(Zod schema 对象)
handler // 第4个参数:执行函数(async function)
)
| 参数 | 类型 | 说明 | 比喻 |
|---|---|---|---|
name |
string |
工具的唯一标识,建议用 动词_名词 格式 |
工具的名牌 |
description |
string |
告诉 Agent 这个工具干什么用的 | 工具的说明书 |
schema |
Zod 对象 | 定义工具需要哪些参数 | 工具的操作面板 |
handler |
async (args) => result |
工具被调用时执行的代码 | 工具的实际功能 |
createSdkMcpServer() 的参数:
createSdkMcpServer({
name: "server-name", // 服务器名字
version: "1.0.0", // 版本号
tools: [ // 工具列表
tool(...),
tool(...),
tool(...)
]
})
工具命名规则:
在 allowedTools 里引用自定义工具时,格式是:
mcp__<服务器名>__<工具名>
比如服务器叫 my-tools,工具叫 say_hello,那就是:
mcp__my-tools__say_hello
你也可以用通配符允许服务器里的所有工具:
mcp__my-tools__*
tool() 的返回值格式
每个工具的执行函数必须返回一个固定格式的对象:
{
content: [
{ type: "text", text: "这里是返回给 Agent 的文本内容" }
]
}
content 是一个数组,里面可以放多条内容。最常用的是 type: "text",即文本内容。
10.3 完整示例:天气查询工具
来做一个真正有用的工具 —— 查天气。我们用免费的 Open-Meteo API(不需要 API Key)。
完整代码
// weather-agent.ts
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
// ============================================
// 第一步:定义一个城市坐标的映射表
// (真实项目中你可以调用地理编码 API 来获取坐标)
// ============================================
const cityCoordinates: Record<string, { lat: number; lon: number }> = {
"北京": { lat: 39.9042, lon: 116.4074 },
"上海": { lat: 31.2304, lon: 121.4737 },
"广州": { lat: 23.1291, lon: 113.2644 },
"深圳": { lat: 22.5431, lon: 114.0579 },
"杭州": { lat: 30.2741, lon: 120.1551 },
"成都": { lat: 30.5728, lon: 104.0668 },
"东京": { lat: 35.6762, lon: 139.6503 },
"纽约": { lat: 40.7128, lon: -74.0060 },
"伦敦": { lat: 51.5074, lon: -0.1278 },
};
// ============================================
// 第二步:创建天气工具服务器
// ============================================
const weatherServer = createSdkMcpServer({
name: "weather-tools",
version: "1.0.0",
tools: [
tool(
"get_weather",
"查询指定城市的当前天气信息,包括温度、湿度、风速等",
{
city: z.string().describe("城市名称,例如:北京、上海、东京、纽约")
},
async ({ city }) => {
// 查找城市坐标
const coords = cityCoordinates[city];
if (!coords) {
return {
content: [{
type: "text",
text: `抱歉,暂时不支持查询"${city}"的天气。支持的城市有:${Object.keys(cityCoordinates).join("、")}`
}]
};
}
try {
// 调用 Open-Meteo API(免费,无需 API Key)
const url = `https://api.open-meteo.com/v1/forecast?latitude=${coords.lat}&longitude=${coords.lon}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code&timezone=auto`;
const response = await fetch(url);
const data = await response.json();
const current = data.current;
// 把天气代码翻译成中文描述
const weatherDesc = getWeatherDescription(current.weather_code);
const result = [
`城市:${city}`,
`天气:${weatherDesc}`,
`温度:${current.temperature_2m}°C`,
`湿度:${current.relative_humidity_2m}%`,
`风速:${current.wind_speed_10m} km/h`,
].join("\n");
return {
content: [{ type: "text", text: result }]
};
} catch (error) {
return {
content: [{
type: "text",
text: `查询天气失败:${error instanceof Error ? error.message : "未知错误"}`
}]
};
}
}
)
]
});
// 辅助函数:天气代码转中文
function getWeatherDescription(code: number): string {
const weatherMap: Record<number, string> = {
0: "晴天",
1: "大部晴朗", 2: "局部多云", 3: "多云",
45: "有雾", 48: "雾凇",
51: "小毛毛雨", 53: "中毛毛雨", 55: "大毛毛雨",
61: "小雨", 63: "中雨", 65: "大雨",
71: "小雪", 73: "中雪", 75: "大雪",
80: "小阵雨", 81: "中阵雨", 82: "大阵雨",
95: "雷暴", 96: "雷暴伴冰雹", 99: "强雷暴伴冰雹",
};
return weatherMap[code] || `未知天气(代码:${code})`;
}
// ============================================
// 第三步:创建 Agent 并使用天气工具
// ============================================
async function main() {
console.log("天气查询 Agent 启动!\n");
for await (const message of query({
prompt: "请帮我查一下北京和上海的天气,然后告诉我哪个城市更适合今天出门散步。",
options: {
mcpServers: {
"weather-tools": weatherServer
},
allowedTools: [
"mcp__weather-tools__get_weather"
],
maxTurns: 10
}
})) {
if (message.type === "assistant") {
// 打印 Agent 的回复
for (const block of message.message.content) {
if (block.type === "text") {
console.log(block.text);
} else if (block.type === "tool_use") {
console.log(`\n[调用工具] ${block.name}(${JSON.stringify(block.input)})`);
}
}
} else if (message.type === "result") {
console.log("\n--- Agent 执行完毕 ---");
}
}
}
main().catch(console.error);
运行效果
运行这段代码后,你会看到类似这样的输出:
天气查询 Agent 启动!
[调用工具] get_weather({"city":"北京"})
[调用工具] get_weather({"city":"上海"})
根据查询结果:
- 北京:晴天,温度 22°C,湿度 35%,风速 12 km/h
- 上海:多云,温度 25°C,湿度 68%,风速 8 km/h
建议去北京散步!虽然上海温度稍高,但北京是晴天而且湿度低,
体感会更舒适。不过北京风稍大一些,建议带件薄外套。
--- Agent 执行完毕 ---
代码要点
- Agent 自己决定调用顺序:你只说"查北京和上海的天气",Agent 自己决定先查哪个、查几次。
- Agent 能综合分析:拿到两个城市的数据后,Agent 会自己对比分析,给出建议。
- 错误处理很重要:如果用户问了一个不支持的城市,工具会返回友好提示,而不是让程序崩溃。
10.4 完整示例:数据库查询工具
让 Agent 能查数据库 —— 这是企业场景里最常见的需求。我们用 SQLite 做示例。
安装依赖
npm install better-sqlite3
npm install -D @types/better-sqlite3
完整代码
// db-agent.ts
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
import Database from "better-sqlite3";
// ============================================
// 第一步:创建并初始化数据库(带示例数据)
// ============================================
const db = new Database(":memory:"); // 用内存数据库做演示
// 创建表
db.exec(`
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
product_id INTEGER,
customer TEXT NOT NULL,
quantity INTEGER NOT NULL,
order_date TEXT NOT NULL,
FOREIGN KEY (product_id) REFERENCES products(id)
);
`);
// 插入示例数据
const insertProduct = db.prepare(
"INSERT INTO products (name, category, price, stock) VALUES (?, ?, ?, ?)"
);
const insertOrder = db.prepare(
"INSERT INTO orders (product_id, customer, quantity, order_date) VALUES (?, ?, ?, ?)"
);
const products = [
["iPhone 15", "手机", 5999, 100],
["MacBook Pro", "电脑", 14999, 50],
["AirPods Pro", "耳机", 1299, 200],
["iPad Air", "平板", 4799, 80],
["Apple Watch", "手表", 2999, 150],
];
products.forEach(p => insertProduct.run(...p));
const orders = [
[1, "张三", 2, "2024-01-15"],
[2, "李四", 1, "2024-01-16"],
[3, "王五", 3, "2024-01-17"],
[1, "赵六", 1, "2024-01-18"],
[4, "张三", 1, "2024-01-19"],
[5, "李四", 2, "2024-01-20"],
[3, "钱七", 1, "2024-01-21"],
];
orders.forEach(o => insertOrder.run(...o));
// ============================================
// 第二步:创建数据库工具服务器
// ============================================
const dbServer = createSdkMcpServer({
name: "database",
version: "1.0.0",
tools: [
// 工具1:执行 SQL 查询(只读)
tool(
"query_database",
"执行 SQL 查询语句来查询数据库。注意:只支持 SELECT 查询,不允许修改数据。数据库包含 products 表(id, name, category, price, stock)和 orders 表(id, product_id, customer, quantity, order_date)。",
{
sql: z.string().describe("SQL 查询语句,只支持 SELECT 语句")
},
async ({ sql }) => {
// 安全检查:只允许 SELECT 语句
const trimmed = sql.trim().toUpperCase();
if (!trimmed.startsWith("SELECT")) {
return {
content: [{
type: "text",
text: "错误:只允许执行 SELECT 查询语句,不能修改数据。"
}]
};
}
try {
const rows = db.prepare(sql).all();
if (rows.length === 0) {
return {
content: [{ type: "text", text: "查询结果为空,没有找到匹配的数据。" }]
};
}
// 把结果格式化成表格
const headers = Object.keys(rows[0] as object);
const table = [
headers.join(" | "),
headers.map(() => "---").join(" | "),
...rows.map(row =>
headers.map(h => String((row as Record<string, unknown>)[h])).join(" | ")
)
].join("\n");
return {
content: [{
type: "text",
text: `查询到 ${rows.length} 条结果:\n\n${table}`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `SQL 执行错误:${error instanceof Error ? error.message : "未知错误"}`
}]
};
}
}
),
// 工具2:查看数据库结构
tool(
"list_tables",
"查看数据库中有哪些表,以及每个表的字段信息",
{}, // 不需要参数
async () => {
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table'"
).all() as { name: string }[];
const result: string[] = [];
for (const table of tables) {
const columns = db.prepare(
`PRAGMA table_info(${table.name})`
).all() as { name: string; type: string }[];
result.push(`表名:${table.name}`);
result.push(`字段:${columns.map(c => `${c.name}(${c.type})`).join(", ")}`);
result.push("");
}
return {
content: [{ type: "text", text: result.join("\n") }]
};
}
)
]
});
// ============================================
// 第三步:使用数据库工具
// ============================================
async function main() {
console.log("数据库查询 Agent 启动!\n");
for await (const message of query({
prompt: "请帮我分析一下销售数据:哪个产品卖得最多?哪个客户消费最多?整理成一个简单的报告。",
options: {
mcpServers: {
database: dbServer
},
allowedTools: [
"mcp__database__query_database",
"mcp__database__list_tables"
],
maxTurns: 15
}
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text") {
console.log(block.text);
} else if (block.type === "tool_use") {
console.log(`\n[SQL 查询] ${JSON.stringify(block.input)}\n`);
}
}
}
}
}
main().catch(console.error);
关键设计点
只读限制:数据库工具只允许 SELECT 查询,这是安全红线。你不会想让 Agent 随便 DELETE 或者 DROP TABLE。
工具描述里写清楚表结构:注意
query_database的description里提到了两个表名和字段名。这非常重要!Agent 是靠描述来写 SQL 的,你不告诉它有什么表、什么字段,它就写不出正确的 SQL。提供
list_tables工具:让 Agent 可以自己探索数据库结构。这样即使描述里漏了某些表信息,Agent 也能自己去查。结果格式化:把查询结果格式化成 Markdown 表格,Agent 看起来更清晰。
10.5 完整示例:待办事项管理工具
这个例子展示如何在一个服务器里放多个工具,实现完整的增删改查(CRUD)功能。
完整代码
// todo-agent.ts
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
// ============================================
// 数据存储(内存中的简单实现)
// ============================================
interface TodoItem {
id: number;
title: string;
completed: boolean;
createdAt: string;
priority: "high" | "medium" | "low";
}
let nextId = 1;
const todos: TodoItem[] = [];
// ============================================
// 创建待办事项工具服务器(4 个工具)
// ============================================
const todoServer = createSdkMcpServer({
name: "todo-manager",
version: "1.0.0",
tools: [
// 工具1:添加待办事项
tool(
"add_todo",
"添加一个新的待办事项到列表中",
{
title: z.string().describe("待办事项的标题,描述要做什么"),
priority: z.enum(["high", "medium", "low"])
.describe("优先级:high=紧急, medium=一般, low=不急")
.default("medium")
},
async ({ title, priority }) => {
const item: TodoItem = {
id: nextId++,
title,
completed: false,
createdAt: new Date().toISOString().slice(0, 10),
priority
};
todos.push(item);
return {
content: [{
type: "text",
text: `已添加待办事项 #${item.id}:${item.title}(优先级:${item.priority})`
}]
};
}
),
// 工具2:列出所有待办事项
tool(
"list_todos",
"列出所有待办事项,可以按状态过滤",
{
filter: z.enum(["all", "pending", "completed"])
.describe("过滤条件:all=全部, pending=未完成, completed=已完成")
.default("all")
},
async ({ filter }) => {
let filtered = todos;
if (filter === "pending") {
filtered = todos.filter(t => !t.completed);
} else if (filter === "completed") {
filtered = todos.filter(t => t.completed);
}
if (filtered.length === 0) {
return {
content: [{ type: "text", text: "当前没有待办事项。" }]
};
}
// 按优先级排序:high > medium > low
const priorityOrder = { high: 0, medium: 1, low: 2 };
filtered.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
const list = filtered.map(t => {
const status = t.completed ? "[x]" : "[ ]";
const priorityEmoji =
t.priority === "high" ? "[!!!]" :
t.priority === "medium" ? "[!!]" : "[!]";
return `${status} #${t.id} ${priorityEmoji} ${t.title} (${t.createdAt})`;
}).join("\n");
return {
content: [{
type: "text",
text: `待办事项列表(共 ${filtered.length} 条):\n\n${list}`
}]
};
}
),
// 工具3:完成待办事项
tool(
"complete_todo",
"将指定的待办事项标记为已完成",
{
id: z.number().describe("待办事项的 ID 编号")
},
async ({ id }) => {
const item = todos.find(t => t.id === id);
if (!item) {
return {
content: [{ type: "text", text: `错误:找不到 ID 为 ${id} 的待办事项。` }]
};
}
if (item.completed) {
return {
content: [{ type: "text", text: `待办事项 #${id} 已经是完成状态了。` }]
};
}
item.completed = true;
return {
content: [{
type: "text",
text: `已完成:#${id} ${item.title}`
}]
};
}
),
// 工具4:删除待办事项
tool(
"delete_todo",
"删除指定的待办事项",
{
id: z.number().describe("要删除的待办事项的 ID 编号")
},
async ({ id }) => {
const index = todos.findIndex(t => t.id === id);
if (index === -1) {
return {
content: [{ type: "text", text: `错误:找不到 ID 为 ${id} 的待办事项。` }]
};
}
const removed = todos.splice(index, 1)[0];
return {
content: [{
type: "text",
text: `已删除:#${removed.id} ${removed.title}`
}]
};
}
)
]
});
// ============================================
// 使用待办事项 Agent
// ============================================
async function main() {
console.log("待办事项管理 Agent 启动!\n");
for await (const message of query({
prompt: `请帮我管理今天的待办事项:
1. 添加:写周报(高优先级)
2. 添加:买菜(低优先级)
3. 添加:回复客户邮件(高优先级)
4. 添加:整理书桌(中优先级)
5. 把"回复客户邮件"标记为已完成
6. 最后列出所有未完成的待办事项`,
options: {
mcpServers: {
"todo-manager": todoServer
},
// 用通配符允许所有 todo-manager 的工具
allowedTools: [
"mcp__todo-manager__*"
],
maxTurns: 20
}
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text") {
console.log(block.text);
} else if (block.type === "tool_use") {
console.log(`\n[操作] ${block.name}(${JSON.stringify(block.input)})`);
}
}
}
}
}
main().catch(console.error);
运行效果
Agent 会依次调用 add_todo 四次,complete_todo 一次,list_todos 一次,最后给你一个整理好的报告。
设计亮点
- 通配符
mcp__todo-manager__*:一次性允许这个服务器里的所有工具,不用一个一个列。 - 默认参数:
priority有默认值"medium",用户不说优先级时会自动用中等。 - Zod 枚举:用
z.enum()限制参数取值范围,防止 Agent 传入乱七八糟的值。 - 幂等检查:完成一个已完成的事项不会出错,而是返回友好提示。
10.6 工具设计最佳实践
好的工具设计能让 Agent 更聪明、更可靠。下面是我总结的几条经验。
实践1:工具描述必须清晰
Agent 完全靠 description 来决定什么时候用这个工具。描述写得好不好,直接决定 Agent 用得对不对。
// 差的描述 —— Agent 看了一头雾水
tool("query", "查询数据", ...)
// 好的描述 —— Agent 清楚知道这个工具干什么、什么时候该用
tool(
"query_database",
"执行 SQL SELECT 查询来获取数据。数据库包含 users 表(id, name, email, created_at)和 orders 表(id, user_id, amount, status, created_at)。只支持 SELECT 查询,不能修改数据。",
...
)
描述里应该包含:
- 这个工具做什么
- 什么时候该用它
- 有什么限制
- 如果有数据来源,说明数据结构
实践2:参数一定要加 .describe()
// 差 —— Agent 不知道 q 是什么意思
{ q: z.string() }
// 好 —— Agent 清楚该传什么
{ query: z.string().describe("搜索关键词,支持中英文,可以用空格分隔多个关键词") }
参数名也很重要,用完整的英文单词,不要用缩写。
实践3:返回值要信息量充足
Agent 拿到工具返回值后,需要根据返回内容来决定下一步。信息量太少,Agent 就会迷茫。
// 差 —— Agent 只知道成功了,不知道具体情况
return { content: [{ type: "text", text: "成功" }] };
// 好 —— Agent 拿到了足够的信息来做下一步判断
return {
content: [{
type: "text",
text: `成功添加了待办事项 #${id}:${title}。当前共有 ${todos.length} 条待办事项,其中 ${pendingCount} 条未完成。`
}]
};
实践4:错误处理要友好
工具出错时,不要让程序直接崩溃,而是返回一个清晰的错误信息给 Agent。Agent 看到错误信息后,可能会换个方式重试,或者告诉用户出了什么问题。
async ({ sql }) => {
try {
const result = db.prepare(sql).all();
return {
content: [{ type: "text", text: JSON.stringify(result) }]
};
} catch (error) {
// 返回错误信息给 Agent,而不是 throw
return {
content: [{
type: "text",
text: `SQL 执行失败:${error instanceof Error ? error.message : "未知错误"}。请检查 SQL 语法是否正确。`
}]
};
}
}
实践5:一个工具只做一件事
不要做一个"万能工具",把所有功能都塞进去。Agent 更擅长从多个简单工具中选择合适的组合,而不是理解一个复杂工具的所有参数。
// 差 —— 一个工具干太多事
tool("manage_todo", "管理待办事项", {
action: z.enum(["add", "delete", "complete", "list"]),
title: z.string().optional(),
id: z.number().optional(),
filter: z.string().optional()
}, ...)
// 好 —— 拆成多个简单工具
tool("add_todo", "添加新的待办事项", { title: z.string(), priority: z.string() }, ...)
tool("delete_todo", "删除待办事项", { id: z.number() }, ...)
tool("complete_todo", "完成待办事项", { id: z.number() }, ...)
tool("list_todos", "列出待办事项", { filter: z.enum(["all", "pending", "completed"]) }, ...)
实践6:命名统一用 动词_名词
工具名就像函数名,要一看就知道它干什么。
// 推荐的命名模式
"get_weather" // 获取天气
"search_products" // 搜索产品
"create_order" // 创建订单
"delete_user" // 删除用户
"list_todos" // 列出待办
"send_email" // 发送邮件
"calculate_tax" // 计算税费
// 不推荐
"weather" // 动词还是名词?
"doStuff" // 太模糊
"handler1" // 看不出干什么
10.7 工具的输入输出格式
输入:Zod Schema 定义
工具的参数用 Zod schema 定义。Zod 是一个 TypeScript 类型验证库,SDK 内部会自动把 Zod schema 转换成 JSON Schema,告诉 Claude 工具接受什么参数。
常用的 Zod 类型:
import { z } from "zod";
// 基本类型
z.string() // 字符串
z.number() // 数字
z.boolean() // 布尔值
// 带约束
z.string().min(1).max(100) // 1-100 个字符的字符串
z.number().int().min(0) // 非负整数
z.number().positive() // 正数
// 枚举
z.enum(["low", "medium", "high"]) // 只能是这三个值之一
// 可选参数(有默认值)
z.string().optional() // 可以不传
z.string().default("hello") // 不传时用默认值
// 数组
z.array(z.string()) // 字符串数组
// 对象
z.object({
name: z.string(),
age: z.number()
})
// 记得加 describe!
z.string().describe("用户的邮箱地址,格式如 user@example.com")
一个完整的参数定义示例:
tool(
"search_products",
"在商品库中搜索产品",
{
keyword: z.string().describe("搜索关键词"),
category: z.enum(["电子产品", "服装", "食品", "全部"])
.describe("商品分类")
.default("全部"),
minPrice: z.number().min(0).describe("最低价格(元)").optional(),
maxPrice: z.number().min(0).describe("最高价格(元)").optional(),
limit: z.number().int().min(1).max(50)
.describe("返回结果数量上限")
.default(10)
},
async (args) => { /* ... */ }
)
输出:content 数组
工具的返回值必须是这个格式:
{
content: [
{ type: "text", text: "文本内容" }
]
}
文本输出(最常用):
return {
content: [{
type: "text",
text: "查询到 5 条结果..."
}]
};
多段文本输出:
return {
content: [
{ type: "text", text: "=== 搜索结果 ===" },
{ type: "text", text: "1. iPhone 15 - ¥5999" },
{ type: "text", text: "2. MacBook Pro - ¥14999" },
{ type: "text", text: `共找到 2 条结果` }
]
};
图片输出(高级场景):
return {
content: [{
type: "image",
data: base64EncodedImage, // base64 编码的图片数据
mimeType: "image/png"
}]
};
返回错误信息:
// 不要 throw error,而是用 isError 标记
return {
content: [{ type: "text", text: "查询失败:连接超时" }],
isError: true
};
完整的错误处理模式
推荐在每个工具里都用 try/catch 包一下:
tool(
"do_something",
"做某事",
{ param: z.string() },
async ({ param }) => {
try {
// 你的业务逻辑
const result = await someOperation(param);
return {
content: [{ type: "text", text: `操作成功:${result}` }]
};
} catch (error) {
// 出错了,返回错误信息而不是 throw
const errorMessage = error instanceof Error
? error.message
: "未知错误";
return {
content: [{
type: "text",
text: `操作失败:${errorMessage}。请检查输入参数是否正确。`
}],
isError: true
};
}
}
)
10.8 进阶:异步工具和超时控制
处理耗时操作
有些工具可能需要较长时间执行(比如调用慢速 API、处理大量数据)。建议加上超时控制:
tool(
"slow_api_call",
"调用外部 API(可能需要几秒钟)",
{ query: z.string() },
async ({ query }) => {
// 用 AbortController 实现超时
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10 秒超时
try {
const response = await fetch(`https://api.example.com/search?q=${query}`, {
signal: controller.signal
});
clearTimeout(timeout);
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
};
} catch (error) {
clearTimeout(timeout);
if (error instanceof Error && error.name === "AbortError") {
return {
content: [{ type: "text", text: "请求超时(10秒),请稍后重试。" }],
isError: true
};
}
return {
content: [{ type: "text", text: `请求失败:${error}` }],
isError: true
};
}
}
)
工具之间共享状态
多个工具之间可以通过闭包共享状态。前面待办事项的例子已经展示了这个模式 —— todos 数组被多个工具共同读写:
// 共享状态定义在工具外面
const sharedState = {
todos: [] as TodoItem[],
nextId: 1
};
const server = createSdkMcpServer({
name: "stateful-tools",
version: "1.0.0",
tools: [
tool("add_item", "添加项目", { ... }, async (args) => {
// 读写 sharedState
sharedState.todos.push({ ... });
sharedState.nextId++;
// ...
}),
tool("list_items", "列出项目", { ... }, async () => {
// 读取 sharedState
return {
content: [{ type: "text", text: sharedState.todos.map(...).join("\n") }]
};
})
]
});
动手练习
练习1:计算器工具
做一个简单的计算器工具,支持四则运算。Agent 应该能处理类似"帮我算一下 (15 + 27) * 3 - 40 / 8"的请求。
要求:
- 四个工具:
add(加)、subtract(减)、multiply(乘)、divide(除) divide要处理除零的情况- 所有参数都是数字
- 把工具服务器命名为
calculator
参考代码框架:
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const calculatorServer = createSdkMcpServer({
name: "calculator",
version: "1.0.0",
tools: [
tool(
"add",
"将两个数字相加,返回它们的和",
{
a: z.number().describe("第一个加数"),
b: z.number().describe("第二个加数")
},
async ({ a, b }) => {
const result = a + b;
return {
content: [{ type: "text", text: `${a} + ${b} = ${result}` }]
};
}
),
tool(
"subtract",
"用第一个数字减去第二个数字,返回它们的差",
{
a: z.number().describe("被减数"),
b: z.number().describe("减数")
},
async ({ a, b }) => {
const result = a - b;
return {
content: [{ type: "text", text: `${a} - ${b} = ${result}` }]
};
}
),
tool(
"multiply",
"将两个数字相乘,返回它们的积",
{
a: z.number().describe("第一个因数"),
b: z.number().describe("第二个因数")
},
async ({ a, b }) => {
const result = a * b;
return {
content: [{ type: "text", text: `${a} * ${b} = ${result}` }]
};
}
),
tool(
"divide",
"用第一个数字除以第二个数字,返回商。注意:除数不能为零。",
{
a: z.number().describe("被除数"),
b: z.number().describe("除数,不能为零")
},
async ({ a, b }) => {
if (b === 0) {
return {
content: [{ type: "text", text: "错误:不能除以零!" }],
isError: true
};
}
const result = a / b;
return {
content: [{ type: "text", text: `${a} / ${b} = ${result}` }]
};
}
)
]
});
// 试试看:让 Agent 算一个复杂表达式
async function main() {
for await (const message of query({
prompt: "请帮我计算:(100 + 50) * 2 - 75 / 3,请一步步算。",
options: {
mcpServers: { calculator: calculatorServer },
allowedTools: ["mcp__calculator__*"],
maxTurns: 15
}
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text") console.log(block.text);
if (block.type === "tool_use") {
console.log(`[计算] ${block.name}(${JSON.stringify(block.input)})`);
}
}
}
}
}
main().catch(console.error);
练习2:文件统计工具
做一个文件统计工具,能统计指定目录下的文件信息。
要求:
count_files:统计指定目录中的文件数量get_file_sizes:获取指定目录中每个文件的大小find_largest_file:找到指定目录中最大的文件
提示: 用 Node.js 的 fs 和 path 模块。
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
const fileStatsServer = createSdkMcpServer({
name: "file-stats",
version: "1.0.0",
tools: [
tool(
"count_files",
"统计指定目录中的文件数量(不包含子目录中的文件)",
{
directory: z.string().describe("要统计的目录路径,例如:/home/user/projects")
},
async ({ directory }) => {
try {
const entries = fs.readdirSync(directory, { withFileTypes: true });
const fileCount = entries.filter(e => e.isFile()).length;
const dirCount = entries.filter(e => e.isDirectory()).length;
return {
content: [{
type: "text",
text: `目录 ${directory} 中有 ${fileCount} 个文件和 ${dirCount} 个子目录。`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `无法访问目录 ${directory}:${error instanceof Error ? error.message : "未知错误"}`
}],
isError: true
};
}
}
),
tool(
"get_file_sizes",
"获取指定目录中所有文件的名称和大小",
{
directory: z.string().describe("目录路径")
},
async ({ directory }) => {
try {
const entries = fs.readdirSync(directory, { withFileTypes: true });
const files = entries.filter(e => e.isFile());
if (files.length === 0) {
return {
content: [{ type: "text", text: `目录 ${directory} 中没有文件。` }]
};
}
const fileInfos = files.map(f => {
const stats = fs.statSync(path.join(directory, f.name));
const sizeKB = (stats.size / 1024).toFixed(1);
return ` ${f.name} — ${sizeKB} KB`;
});
return {
content: [{
type: "text",
text: `目录 ${directory} 中的文件:\n${fileInfos.join("\n")}`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `获取文件信息失败:${error instanceof Error ? error.message : "未知错误"}`
}],
isError: true
};
}
}
),
tool(
"find_largest_file",
"找到指定目录中体积最大的文件",
{
directory: z.string().describe("目录路径")
},
async ({ directory }) => {
try {
const entries = fs.readdirSync(directory, { withFileTypes: true });
const files = entries.filter(e => e.isFile());
if (files.length === 0) {
return {
content: [{ type: "text", text: "目录中没有文件。" }]
};
}
let largest = { name: "", size: 0 };
for (const f of files) {
const stats = fs.statSync(path.join(directory, f.name));
if (stats.size > largest.size) {
largest = { name: f.name, size: stats.size };
}
}
const sizeMB = (largest.size / 1024 / 1024).toFixed(2);
return {
content: [{
type: "text",
text: `最大的文件是:${largest.name},大小为 ${sizeMB} MB`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `查找失败:${error instanceof Error ? error.message : "未知错误"}`
}],
isError: true
};
}
}
)
]
});
练习3:翻译工具
做一个翻译工具,调用免费的翻译 API。
要求:
- 支持中英互译
- 参数包括:要翻译的文本、目标语言
- 做好错误处理
提示: 你可以用 LibreTranslate 的免费 API 或者模拟翻译来练习。下面是一个简单的模拟版本:
tool(
"translate_text",
"将文本从一种语言翻译成另一种语言。支持中文和英文之间的互译。",
{
text: z.string().describe("要翻译的文本内容"),
targetLanguage: z.enum(["zh", "en"])
.describe("目标语言:zh=中文, en=英文")
},
async ({ text, targetLanguage }) => {
try {
// 这里调用真实的翻译 API
// 以下是使用 LibreTranslate 免费 API 的示例
const sourceLanguage = targetLanguage === "zh" ? "en" : "zh";
const response = await fetch("https://libretranslate.com/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
q: text,
source: sourceLanguage,
target: targetLanguage,
format: "text"
})
});
const data = await response.json();
return {
content: [{
type: "text",
text: `原文(${sourceLanguage}):${text}\n翻译(${targetLanguage}):${data.translatedText}`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `翻译失败:${error instanceof Error ? error.message : "未知错误"}`
}],
isError: true
};
}
}
)
本章小结
恭喜你!读完这一章,你已经掌握了自定义工具的核心技能。来回顾一下要点:
为什么需要自定义工具:内置工具不够用时,自定义工具让 Agent 获得无限扩展能力。
核心 API:
tool(name, description, schema, handler)—— 定义一个工具createSdkMcpServer({ name, version, tools })—— 创建工具服务器- 通过
mcpServers选项传给query()—— 注册到 Agent
工具命名规则:
mcp__<服务器名>__<工具名>,支持*通配符。输入格式:用 Zod schema 定义参数,记得加
.describe()。输出格式:
{ content: [{ type: "text", text: "..." }] }。最佳实践:
- 描述写清楚,Agent 才知道什么时候该用
- 一个工具只做一件事
- 错误要返回而不是抛出
- 命名用
动词_名词格式
安全意识:数据库工具只允许 SELECT,文件操作要限制路径,敏感操作要校验权限。
下一章预告
这一章我们学会了在代码里创建自定义工具。但如果别人已经做好了一个工具,你怎么直接拿来用?如果你做的工具想分享给别人呢?
下一章我们来学 MCP(Model Context Protocol) —— Agent 世界的"USB 接口"。MCP 是一个开放标准,让不同人做的工具可以互相兼容、即插即用。你可以连接到 GitHub MCP Server 操作仓库,连接到数据库 MCP Server 查询数据,甚至一次连接多个 MCP Server,让 Agent 拥有来自各方的超能力。