agent-sdk/custom-tools.md +833 −0 created
1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# 为 Claude 提供自定义工具
6
7> 使用 Claude Agent SDK 的进程内 MCP 服务器定义自定义工具,以便 Claude 可以调用您的函数、访问您的 API 并执行特定领域的操作。
8
9自定义工具通过让您定义 Claude 在对话期间可以调用的自己的函数来扩展 Agent SDK。使用 SDK 的进程内 MCP 服务器,您可以让 Claude 访问数据库、外部 API、特定领域的逻辑或应用程序需要的任何其他功能。
10
11本指南涵盖如何使用输入架构和处理程序定义工具、将它们捆绑到 MCP 服务器中、将它们传递给 `query`,以及控制 Claude 可以访问哪些工具。它还涵盖错误处理、工具注释和返回非文本内容(如图像)。
12
13## 快速参考
14
15| 如果您想... | 执行此操作 |
16| :------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17| 定义工具 | 使用 [`@tool`](/zh-CN/agent-sdk/python#tool)(Python)或 [`tool()`](/zh-CN/agent-sdk/typescript#tool)(TypeScript),包含名称、描述、架构和处理程序。请参阅[创建自定义工具](#create-a-custom-tool)。 |
18| 向 Claude 注册工具 | 在 `create_sdk_mcp_server` / `createSdkMcpServer` 中包装并传递给 `query()` 中的 `mcpServers`。请参阅[调用自定义工具](#call-a-custom-tool)。 |
19| 预先批准工具 | 添加到您的允许工具列表。请参阅[配置允许的工具](#configure-allowed-tools)。 |
20| 从 Claude 的上下文中删除内置工具 | 传递仅列出您想要的内置工具的 `tools` 数组。请参阅[配置允许的工具](#configure-allowed-tools)。 |
21| 让 Claude 并行调用工具 | 在没有副作用的工具上设置 `readOnlyHint: true`。请参阅[添加工具注释](#add-tool-annotations)。 |
22| 处理错误而不停止循环 | 返回 `isError: true` 而不是抛出异常。请参阅[处理错误](#handle-errors)。 |
23| 返回图像或文件 | 在内容数组中使用 `image` 或 `resource` 块。请参阅[返回图像和资源](#return-images-and-resources)。 |
24| 返回机器可读的 JSON 结果 | 在结果上设置 `structuredContent`。请参阅[返回结构化数据](#return-structured-data)。 |
25| 扩展到许多工具 | 使用[工具搜索](/zh-CN/agent-sdk/tool-search)按需加载工具。 |
26
27## 创建自定义工具
28
29工具由四个部分定义,作为参数传递给 TypeScript 中的 [`tool()`](/zh-CN/agent-sdk/typescript#tool) 助手或 Python 中的 [`@tool`](/zh-CN/agent-sdk/python#tool) 装饰器:
30
31* **名称:** Claude 用来调用工具的唯一标识符。
32* **描述:** 工具的功能。Claude 读取此内容以决定何时调用它。
33* **输入架构:** Claude 必须提供的参数。在 TypeScript 中,这始终是 [Zod 架构](https://zod.dev/),处理程序的 `args` 会自动从中获得类型。在 Python 中,这是一个将名称映射到类型的字典,如 `{"latitude": float}`,SDK 会为您将其转换为 JSON Schema。Python 装饰器还接受完整的 [JSON Schema](https://json-schema.org/understanding-json-schema/about) 字典,当您需要枚举、范围、可选字段或嵌套对象时。
34* **处理程序:** 当 Claude 调用工具时运行的异步函数。它接收验证的参数,必须返回一个对象,包含:
35 * `content`(必需):结果块的数组,每个块的 `type` 为 `"text"`、`"image"` 或 `"resource"`。有关非文本块,请参阅[返回图像和资源](#return-images-and-resources)。
36 * `structuredContent`(可选):保存结果作为机器可读数据的 JSON 对象,与 `content` 一起返回。请参阅[返回结构化数据](#return-structured-data)。
37 * `isError`(可选):设置为 `true` 以表示工具失败,以便 Claude 可以对其做出反应。请参阅[处理错误](#handle-errors)。
38
39定义工具后,使用 [`createSdkMcpServer`](/zh-CN/agent-sdk/typescript#createsdkmcpserver)(TypeScript)或 [`create_sdk_mcp_server`](/zh-CN/agent-sdk/python#create_sdk_mcp_server)(Python)将其包装在服务器中。服务器在应用程序内进程内运行,而不是作为单独的进程。
40
41### 天气工具示例
42
43此示例定义了一个 `get_temperature` 工具并将其包装在 MCP 服务器中。它仅设置工具;要将其传递给 `query` 并运行它,请参阅下面的[调用自定义工具](#call-a-custom-tool)。
44
45<CodeGroup>
46 ```python Python theme={null}
47 from typing import Any
48 import httpx
49 from claude_agent_sdk import tool, create_sdk_mcp_server
50
51
52 # Define a tool: name, description, input schema, handler
53 @tool(
54 "get_temperature",
55 "Get the current temperature at a location",
56 {"latitude": float, "longitude": float},
57 )
58 async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
59 async with httpx.AsyncClient() as client:
60 response = await client.get(
61 "https://api.open-meteo.com/v1/forecast",
62 params={
63 "latitude": args["latitude"],
64 "longitude": args["longitude"],
65 "current": "temperature_2m",
66 "temperature_unit": "fahrenheit",
67 },
68 )
69 data = response.json()
70
71 # Return a content array - Claude sees this as the tool result
72 return {
73 "content": [
74 {
75 "type": "text",
76 "text": f"Temperature: {data['current']['temperature_2m']}°F",
77 }
78 ]
79 }
80
81
82 # Wrap the tool in an in-process MCP server
83 weather_server = create_sdk_mcp_server(
84 name="weather",
85 version="1.0.0",
86 tools=[get_temperature],
87 )
88 ```
89
90 ```typescript TypeScript theme={null}
91 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
92 import { z } from "zod";
93
94 // Define a tool: name, description, input schema, handler
95 const getTemperature = tool(
96 "get_temperature",
97 "Get the current temperature at a location",
98 {
99 latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
100 longitude: z.number().describe("Longitude coordinate")
101 },
102 async (args) => {
103 // args is typed from the schema: { latitude: number; longitude: number }
104 const response = await fetch(
105 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}¤t=temperature_2m&temperature_unit=fahrenheit`
106 );
107 const data: any = await response.json();
108
109 // Return a content array - Claude sees this as the tool result
110 return {
111 content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
112 };
113 }
114 );
115
116 // Wrap the tool in an in-process MCP server
117 const weatherServer = createSdkMcpServer({
118 name: "weather",
119 version: "1.0.0",
120 tools: [getTemperature]
121 });
122 ```
123</CodeGroup>
124
125有关完整的参数详细信息,包括 JSON Schema 输入格式和返回值结构,请参阅 [`tool()`](/zh-CN/agent-sdk/typescript#tool) TypeScript 参考或 [`@tool`](/zh-CN/agent-sdk/python#tool) Python 参考。
126
127<Tip>
128 要使参数可选:在 TypeScript 中,向 Zod 字段添加 `.default()`。在 Python 中,字典架构将每个键视为必需的,因此将参数从架构中省略,在描述字符串中提及它,并在处理程序中使用 `args.get()` 读取它。下面的 [`get_precipitation_chance` 工具](#add-more-tools)展示了两种模式。
129</Tip>
130
131### 调用自定义工具
132
133通过 `mcpServers` 选项将您创建的 MCP 服务器传递给 `query`。`mcpServers` 中的键成为每个工具的完全限定名称中的 `{server_name}` 段:`mcp__{server_name}__{tool_name}`。在 `allowedTools` 中列出该名称,以便工具运行而无需权限提示。
134
135这些代码片段重用上面[示例](#weather-tool-example)中的 `weatherServer` 来询问 Claude 特定位置的天气。
136
137<CodeGroup>
138 ```python Python theme={null}
139 import asyncio
140 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
141
142
143 async def main():
144 options = ClaudeAgentOptions(
145 mcp_servers={"weather": weather_server},
146 allowed_tools=["mcp__weather__get_temperature"],
147 )
148
149 async for message in query(
150 prompt="What's the temperature in San Francisco?",
151 options=options,
152 ):
153 # ResultMessage is the final message after all tool calls complete
154 if isinstance(message, ResultMessage) and message.subtype == "success":
155 print(message.result)
156
157
158 asyncio.run(main())
159 ```
160
161 ```typescript TypeScript theme={null}
162 import { query } from "@anthropic-ai/claude-agent-sdk";
163
164 for await (const message of query({
165 prompt: "What's the temperature in San Francisco?",
166 options: {
167 mcpServers: { weather: weatherServer },
168 allowedTools: ["mcp__weather__get_temperature"]
169 }
170 })) {
171 // "result" is the final message after all tool calls complete
172 if (message.type === "result" && message.subtype === "success") {
173 console.log(message.result);
174 }
175 }
176 ```
177</CodeGroup>
178
179### 添加更多工具
180
181一个服务器在其 `tools` 数组中列出的工具数量不限。如果有多个工具在一个服务器上,您可以在 `allowedTools` 中单独列出每个工具,或使用通配符 `mcp__weather__*` 来覆盖服务器公开的每个工具。
182
183下面的示例向[天气工具示例](#weather-tool-example)中的 `weatherServer` 添加第二个工具 `get_precipitation_chance`,并使用数组中的两个工具重建它。
184
185<CodeGroup>
186 ```python Python theme={null}
187 # Define a second tool for the same server
188 @tool(
189 "get_precipitation_chance",
190 "Get the hourly precipitation probability for a location. "
191 "Optionally pass 'hours' (1-24) to control how many hours to return.",
192 {"latitude": float, "longitude": float},
193 )
194 async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
195 # 'hours' isn't in the schema - read it with .get() to make it optional
196 hours = args.get("hours", 12)
197 async with httpx.AsyncClient() as client:
198 response = await client.get(
199 "https://api.open-meteo.com/v1/forecast",
200 params={
201 "latitude": args["latitude"],
202 "longitude": args["longitude"],
203 "hourly": "precipitation_probability",
204 "forecast_days": 1,
205 },
206 )
207 data = response.json()
208 chances = data["hourly"]["precipitation_probability"][:hours]
209
210 return {
211 "content": [
212 {
213 "type": "text",
214 "text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
215 }
216 ]
217 }
218
219
220 # Rebuild the server with both tools in the array
221 weather_server = create_sdk_mcp_server(
222 name="weather",
223 version="1.0.0",
224 tools=[get_temperature, get_precipitation_chance],
225 )
226 ```
227
228 ```typescript TypeScript theme={null}
229 // Define a second tool for the same server
230 const getPrecipitationChance = tool(
231 "get_precipitation_chance",
232 "Get the hourly precipitation probability for a location",
233 {
234 latitude: z.number(),
235 longitude: z.number(),
236 hours: z
237 .number()
238 .int()
239 .min(1)
240 .max(24)
241 .default(12) // .default() makes the parameter optional
242 .describe("How many hours of forecast to return")
243 },
244 async (args) => {
245 const response = await fetch(
246 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
247 );
248 const data: any = await response.json();
249 const chances = data.hourly.precipitation_probability.slice(0, args.hours);
250
251 return {
252 content: [{ type: "text", text: `Next ${args.hours} hours: ${chances.join("%, ")}%` }]
253 };
254 }
255 );
256
257 // Rebuild the server with both tools in the array
258 const weatherServer = createSdkMcpServer({
259 name: "weather",
260 version: "1.0.0",
261 tools: [getTemperature, getPrecipitationChance]
262 });
263 ```
264</CodeGroup>
265
266此数组中的每个工具在每个回合都会消耗上下文窗口空间。如果您定义了数十个工具,请参阅[工具搜索](/zh-CN/agent-sdk/tool-search)以按需加载它们。
267
268### 添加工具注释
269
270[工具注释](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations)是描述工具行为方式的可选元数据。在 TypeScript 中作为 `tool()` 助手的第五个参数传递,或在 Python 中通过 `@tool` 装饰器的 `annotations` 关键字参数传递。所有提示字段都是布尔值。
271
272| 字段 | 默认值 | 含义 |
273| :---------------- | :------ | :---------------------------- |
274| `readOnlyHint` | `false` | 工具不修改其环境。控制工具是否可以与其他只读工具并行调用。 |
275| `destructiveHint` | `true` | 工具可能执行破坏性更新。仅供参考。 |
276| `idempotentHint` | `false` | 使用相同参数的重复调用没有额外效果。仅供参考。 |
277| `openWorldHint` | `true` | 工具到达流程外的系统。仅供参考。 |
278
279注释是元数据,不是强制执行。标记为 `readOnlyHint: true` 的工具如果处理程序这样做,仍然可以写入磁盘。保持注释与处理程序准确。
280
281此示例向[天气工具示例](#weather-tool-example)中的 `get_temperature` 工具添加 `readOnlyHint`。
282
283<CodeGroup>
284 ```python Python theme={null}
285 from claude_agent_sdk import tool, ToolAnnotations
286
287
288 @tool(
289 "get_temperature",
290 "Get the current temperature at a location",
291 {"latitude": float, "longitude": float},
292 annotations=ToolAnnotations(
293 readOnlyHint=True
294 ), # Lets Claude batch this with other read-only calls
295 )
296 async def get_temperature(args):
297 return {"content": [{"type": "text", "text": "..."}]}
298 ```
299
300 ```typescript TypeScript theme={null}
301 tool(
302 "get_temperature",
303 "Get the current temperature at a location",
304 { latitude: z.number(), longitude: z.number() },
305 async (args) => ({ content: [{ type: "text", text: `...` }] }),
306 { annotations: { readOnlyHint: true } } // Lets Claude batch this with other read-only calls
307 );
308 ```
309</CodeGroup>
310
311请参阅 [TypeScript](/zh-CN/agent-sdk/typescript#toolannotations) 或 [Python](/zh-CN/agent-sdk/python#toolannotations) 参考中的 `ToolAnnotations`。
312
313## 控制工具访问
314
315[天气工具示例](#weather-tool-example)注册了一个服务器并在 `allowedTools` 中列出了工具。本部分涵盖工具名称的构造方式以及当您有多个工具或想要限制内置工具时如何限制访问。
316
317### 工具名称格式
318
319当 MCP 工具暴露给 Claude 时,它们的名称遵循特定格式:
320
321* 模式:`mcp__{server_name}__{tool_name}`
322* 示例:服务器 `weather` 中名为 `get_temperature` 的工具变成 `mcp__weather__get_temperature`
323
324### 配置允许的工具
325
326`tools` 选项和允许/不允许列表在不同的层上运行。`tools` 控制哪些内置工具出现在 Claude 的上下文中。允许和不允许的工具列表控制 Claude 尝试调用它们后是否批准或拒绝调用。
327
328| 选项 | 层 | 效果 |
329| :------------------------ | :-- | :------------------------------------------------------------------- |
330| `tools: ["Read", "Grep"]` | 可用性 | 仅列出的内置工具在 Claude 的上下文中。未列出的内置工具被删除。MCP 工具不受影响。 |
331| `tools: []` | 可用性 | 所有内置工具都被删除。Claude 只能使用您的 MCP 工具。 |
332| 允许的工具 | 权限 | 列出的工具运行而无需权限提示。未列出的工具保持可用;调用通过[权限流](/zh-CN/agent-sdk/permissions)进行。 |
333| 不允许的工具 | 权限 | 对列出的工具的每次调用都被拒绝。工具保留在 Claude 的上下文中,因此 Claude 可能仍会在调用被拒绝之前尝试它。 |
334
335要限制 Claude 可以使用哪些内置工具,优先使用 `tools` 而不是不允许的工具。从 `tools` 中省略工具会将其从上下文中删除,以便 Claude 永远不会尝试它;在 `disallowedTools` 中列出它(Python:`disallowed_tools`)会阻止调用但保留工具可见,因此 Claude 可能会浪费一个回合尝试它。有关完整的评估顺序,请参阅[配置权限](/zh-CN/agent-sdk/permissions)。
336
337## 处理错误
338
339您的处理程序报告错误的方式决定了代理循环是继续还是停止:
340
341| 发生的情况 | 结果 |
342| :---------------------------------------------------------- | :--------------------------------------- |
343| 处理程序抛出未捕获的异常 | 代理循环停止。Claude 永远看不到错误,`query` 调用失败。 |
344| 处理程序捕获错误并返回 `isError: true`(TS)/ `"is_error": True`(Python) | 代理循环继续。Claude 将错误视为数据,可以重试、尝试不同的工具或解释失败。 |
345
346下面的示例在处理程序内部捕获两种失败,而不是让它们抛出。非 200 HTTP 状态从响应中捕获并作为错误结果返回。网络错误或无效 JSON 由周围的 `try/except`(Python)或 `try/catch`(TypeScript)捕获,也作为错误结果返回。在这两种情况下,处理程序正常返回,代理循环继续。
347
348<CodeGroup>
349 ```python Python theme={null}
350 import json
351 import httpx
352 from typing import Any
353
354
355 @tool(
356 "fetch_data",
357 "Fetch data from an API",
358 {"endpoint": str}, # Simple schema
359 )
360 async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
361 try:
362 async with httpx.AsyncClient() as client:
363 response = await client.get(args["endpoint"])
364 if response.status_code != 200:
365 # Return the failure as a tool result so Claude can react to it.
366 # is_error marks this as a failed call rather than odd-looking data.
367 return {
368 "content": [
369 {
370 "type": "text",
371 "text": f"API error: {response.status_code} {response.reason_phrase}",
372 }
373 ],
374 "is_error": True,
375 }
376
377 data = response.json()
378 return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
379 except Exception as e:
380 # Catching here keeps the agent loop alive. An uncaught exception
381 # would end the whole query() call.
382 return {
383 "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
384 "is_error": True,
385 }
386 ```
387
388 ```typescript TypeScript theme={null}
389 tool(
390 "fetch_data",
391 "Fetch data from an API",
392 {
393 endpoint: z.string().url().describe("API endpoint URL")
394 },
395 async (args) => {
396 try {
397 const response = await fetch(args.endpoint);
398
399 if (!response.ok) {
400 // Return the failure as a tool result so Claude can react to it.
401 // isError marks this as a failed call rather than odd-looking data.
402 return {
403 content: [
404 {
405 type: "text",
406 text: `API error: ${response.status} ${response.statusText}`
407 }
408 ],
409 isError: true
410 };
411 }
412
413 const data = await response.json();
414 return {
415 content: [
416 {
417 type: "text",
418 text: JSON.stringify(data, null, 2)
419 }
420 ]
421 };
422 } catch (error) {
423 // Catching here keeps the agent loop alive. An uncaught throw
424 // would end the whole query() call.
425 return {
426 content: [
427 {
428 type: "text",
429 text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
430 }
431 ],
432 isError: true
433 };
434 }
435 }
436 );
437 ```
438</CodeGroup>
439
440## 返回图像和资源
441
442工具结果中的 `content` 数组接受 `text`、`image` 和 `resource` 块。您可以在同一响应中混合它们。
443
444### 图像
445
446图像块以 base64 编码的方式内联携带图像字节。没有 URL 字段。要返回位于 URL 的图像,在处理程序中获取它,读取响应字节,并在返回之前进行 base64 编码。结果作为视觉输入处理。
447
448| 字段 | 类型 | 注释 |
449| :--------- | :-------- | :------------------------------------------------------ |
450| `type` | `"image"` | |
451| `data` | `string` | Base64 编码的字节。仅原始 base64,没有 `data:image/...;base64,` 前缀 |
452| `mimeType` | `string` | 必需。例如 `image/png`、`image/jpeg`、`image/webp`、`image/gif` |
453
454<CodeGroup>
455 ```python Python theme={null}
456 import base64
457 import httpx
458
459
460 # Define a tool that fetches an image from a URL and returns it to Claude
461 @tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
462 async def fetch_image(args):
463 async with httpx.AsyncClient() as client: # Fetch the image bytes
464 response = await client.get(args["url"])
465
466 return {
467 "content": [
468 {
469 "type": "image",
470 "data": base64.b64encode(response.content).decode(
471 "ascii"
472 ), # Base64-encode the raw bytes
473 "mimeType": response.headers.get(
474 "content-type", "image/png"
475 ), # Read MIME type from the response
476 }
477 ]
478 }
479 ```
480
481 ```typescript TypeScript theme={null}
482 tool(
483 "fetch_image",
484 "Fetch an image from a URL and return it to Claude",
485 {
486 url: z.string().url()
487 },
488 async (args) => {
489 const response = await fetch(args.url); // Fetch the image bytes
490 const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
491 const mimeType = response.headers.get("content-type") ?? "image/png";
492
493 return {
494 content: [
495 {
496 type: "image",
497 data: buffer.toString("base64"), // Base64-encode the raw bytes
498 mimeType
499 }
500 ]
501 };
502 }
503 );
504 ```
505</CodeGroup>
506
507### 资源
508
509资源块嵌入由 URI 标识的内容片段。URI 是 Claude 引用的标签;实际内容位于块的 `text` 或 `blob` 字段中。当您的工具生成稍后按名称寻址有意义的内容时使用此功能,例如生成的文件或来自外部系统的记录。
510
511| 字段 | 类型 | 注释 |
512| :------------------ | :----------- | :---------------------------- |
513| `type` | `"resource"` | |
514| `resource.uri` | `string` | 内容的标识符。任何 URI 方案 |
515| `resource.text` | `string` | 内容,如果是文本。提供此项或 `blob`,不能两者都提供 |
516| `resource.blob` | `string` | 内容 base64 编码,如果是二进制 |
517| `resource.mimeType` | `string` | 可选 |
518
519此示例显示从工具处理程序内部返回的资源块。URI `file:///tmp/report.md` 是 Claude 可以稍后引用的标签;SDK 不从该路径读取。
520
521<CodeGroup>
522 ```typescript TypeScript theme={null}
523 return {
524 content: [
525 {
526 type: "resource",
527 resource: {
528 uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
529 mimeType: "text/markdown",
530 text: "# Report\n..." // The actual content, inline
531 }
532 }
533 ]
534 };
535 ```
536
537 ```python Python theme={null}
538 return {
539 "content": [
540 {
541 "type": "resource",
542 "resource": {
543 "uri": "file:///tmp/report.md", # Label for Claude to reference, not a path the SDK reads
544 "mimeType": "text/markdown",
545 "text": "# Report\n...", # The actual content, inline
546 },
547 }
548 ]
549 }
550 ```
551</CodeGroup>
552
553这些块形状来自 MCP `CallToolResult` 类型。有关完整定义,请参阅 [MCP 规范](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-result)。
554
555## 返回结构化数据
556
557`structuredContent` 是结果上的可选 JSON 对象,与 `content` 数组分开。使用它返回原始值,Claude 可以将其作为精确字段读取,而不是从文本字符串或图像中解析它们。
558
559当设置 `structuredContent` 时,Claude 接收 JSON 加上来自 `content` 的任何图像或资源块。来自 `content` 的文本块不被转发,因为假设它们复制结构化数据。下面的示例将图表呈现为图像块,并从同一处理程序的 `structuredContent` 中返回其后面的数据点。
560
561```typescript TypeScript theme={null}
562return {
563 content: [
564 {
565 type: "image",
566 data: chartPngBuffer.toString("base64"),
567 mimeType: "image/png"
568 }
569 ],
570 structuredContent: {
571 series: "temperature_2m",
572 unit: "fahrenheit",
573 points: [62.1, 63.4, 65.0, 64.2]
574 }
575};
576```
577
578<Note>
579 Python `@tool` 装饰器仅从处理程序的返回字典转发 `content` 和 `is_error`。要从 Python 返回 `structuredContent`,请运行[独立 MCP 服务器](/zh-CN/agent-sdk/mcp)而不是进程内 SDK 服务器。
580</Note>
581
582## 示例:单位转换器
583
584此工具在长度、温度和重量的单位之间转换值。用户可以询问"将 100 公里转换为英里"或"72°F 是多少摄氏度",Claude 从请求中选择正确的单位类型和单位。
585
586它演示了两种模式:
587
588* **枚举架构:** `unit_type` 被限制为一组固定值。在 TypeScript 中,使用 `z.enum()`。在 Python 中,字典架构不支持枚举,因此需要完整的 JSON Schema 字典。
589* **不支持的输入处理:** 当找不到转换对时,处理程序返回 `isError: true`,以便 Claude 可以告诉用户出了什么问题,而不是将失败视为正常结果。
590
591<CodeGroup>
592 ```python Python theme={null}
593 from typing import Any
594 from claude_agent_sdk import tool, create_sdk_mcp_server
595
596
597 # z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
598 # The dict schema has no equivalent, so full JSON Schema is required.
599 @tool(
600 "convert_units",
601 "Convert a value from one unit to another",
602 {
603 "type": "object",
604 "properties": {
605 "unit_type": {
606 "type": "string",
607 "enum": ["length", "temperature", "weight"],
608 "description": "Category of unit",
609 },
610 "from_unit": {
611 "type": "string",
612 "description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
613 },
614 "to_unit": {"type": "string", "description": "Unit to convert to"},
615 "value": {"type": "number", "description": "Value to convert"},
616 },
617 "required": ["unit_type", "from_unit", "to_unit", "value"],
618 },
619 )
620 async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
621 conversions = {
622 "length": {
623 "kilometers_to_miles": lambda v: v * 0.621371,
624 "miles_to_kilometers": lambda v: v * 1.60934,
625 "meters_to_feet": lambda v: v * 3.28084,
626 "feet_to_meters": lambda v: v * 0.3048,
627 },
628 "temperature": {
629 "celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
630 "fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
631 "celsius_to_kelvin": lambda v: v + 273.15,
632 "kelvin_to_celsius": lambda v: v - 273.15,
633 },
634 "weight": {
635 "kilograms_to_pounds": lambda v: v * 2.20462,
636 "pounds_to_kilograms": lambda v: v * 0.453592,
637 "grams_to_ounces": lambda v: v * 0.035274,
638 "ounces_to_grams": lambda v: v * 28.3495,
639 },
640 }
641
642 key = f"{args['from_unit']}_to_{args['to_unit']}"
643 fn = conversions.get(args["unit_type"], {}).get(key)
644
645 if not fn:
646 return {
647 "content": [
648 {
649 "type": "text",
650 "text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
651 }
652 ],
653 "is_error": True,
654 }
655
656 result = fn(args["value"])
657 return {
658 "content": [
659 {
660 "type": "text",
661 "text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
662 }
663 ]
664 }
665
666
667 converter_server = create_sdk_mcp_server(
668 name="converter",
669 version="1.0.0",
670 tools=[convert_units],
671 )
672 ```
673
674 ```typescript TypeScript theme={null}
675 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
676 import { z } from "zod";
677
678 const convert = tool(
679 "convert_units",
680 "Convert a value from one unit to another",
681 {
682 unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
683 from_unit: z
684 .string()
685 .describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
686 to_unit: z.string().describe("Unit to convert to"),
687 value: z.number().describe("Value to convert")
688 },
689 async (args) => {
690 type Conversions = Record<string, Record<string, (v: number) => number>>;
691
692 const conversions: Conversions = {
693 length: {
694 kilometers_to_miles: (v) => v * 0.621371,
695 miles_to_kilometers: (v) => v * 1.60934,
696 meters_to_feet: (v) => v * 3.28084,
697 feet_to_meters: (v) => v * 0.3048
698 },
699 temperature: {
700 celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
701 fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
702 celsius_to_kelvin: (v) => v + 273.15,
703 kelvin_to_celsius: (v) => v - 273.15
704 },
705 weight: {
706 kilograms_to_pounds: (v) => v * 2.20462,
707 pounds_to_kilograms: (v) => v * 0.453592,
708 grams_to_ounces: (v) => v * 0.035274,
709 ounces_to_grams: (v) => v * 28.3495
710 }
711 };
712
713 const key = `${args.from_unit}_to_${args.to_unit}`;
714 const fn = conversions[args.unit_type]?.[key];
715
716 if (!fn) {
717 return {
718 content: [
719 {
720 type: "text",
721 text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
722 }
723 ],
724 isError: true
725 };
726 }
727
728 const result = fn(args.value);
729 return {
730 content: [
731 {
732 type: "text",
733 text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
734 }
735 ]
736 };
737 }
738 );
739
740 const converterServer = createSdkMcpServer({
741 name: "converter",
742 version: "1.0.0",
743 tools: [convert]
744 });
745 ```
746</CodeGroup>
747
748定义服务器后,以与天气示例相同的方式将其传递给 `query`。此示例在循环中发送三个不同的提示,以显示同一工具处理不同的单位类型。对于每个响应,它检查 `AssistantMessage` 对象(包含 Claude 在该回合中进行的工具调用)并在打印最终 `ResultMessage` 文本之前打印每个 `ToolUseBlock`。这让您看到 Claude 何时使用工具与从其自己的知识中回答。
749
750<CodeGroup>
751 ```python Python theme={null}
752 import asyncio
753 from claude_agent_sdk import (
754 query,
755 ClaudeAgentOptions,
756 ResultMessage,
757 AssistantMessage,
758 ToolUseBlock,
759 )
760
761
762 async def main():
763 options = ClaudeAgentOptions(
764 mcp_servers={"converter": converter_server},
765 allowed_tools=["mcp__converter__convert_units"],
766 )
767
768 prompts = [
769 "Convert 100 kilometers to miles.",
770 "What is 72°F in Celsius?",
771 "How many pounds is 5 kilograms?",
772 ]
773
774 for prompt in prompts:
775 async for message in query(prompt=prompt, options=options):
776 if isinstance(message, AssistantMessage):
777 for block in message.content:
778 if isinstance(block, ToolUseBlock):
779 print(f"[tool call] {block.name}({block.input})")
780 elif isinstance(message, ResultMessage) and message.subtype == "success":
781 print(f"Q: {prompt}\nA: {message.result}\n")
782
783
784 asyncio.run(main())
785 ```
786
787 ```typescript TypeScript theme={null}
788 import { query } from "@anthropic-ai/claude-agent-sdk";
789
790 const prompts = [
791 "Convert 100 kilometers to miles.",
792 "What is 72°F in Celsius?",
793 "How many pounds is 5 kilograms?"
794 ];
795
796 for (const prompt of prompts) {
797 for await (const message of query({
798 prompt,
799 options: {
800 mcpServers: { converter: converterServer },
801 allowedTools: ["mcp__converter__convert_units"]
802 }
803 })) {
804 if (message.type === "assistant") {
805 for (const block of message.message.content) {
806 if (block.type === "tool_use") {
807 console.log(`[tool call] ${block.name}`, block.input);
808 }
809 }
810 } else if (message.type === "result" && message.subtype === "success") {
811 console.log(`Q: ${prompt}\nA: ${message.result}\n`);
812 }
813 }
814 }
815 ```
816</CodeGroup>
817
818## 后续步骤
819
820自定义工具在标准接口中包装异步函数。您可以在同一服务器中混合本页上的模式:单个服务器可以在彼此旁边保存数据库工具、API 网关工具和图像渲染器。
821
822从这里:
823
824* 如果您的服务器增长到数十个工具,请参阅[工具搜索](/zh-CN/agent-sdk/tool-search)以延迟加载它们,直到 Claude 需要它们。
825* 要连接到外部 MCP 服务器(文件系统、GitHub、Slack)而不是构建自己的,请参阅[连接 MCP 服务器](/zh-CN/agent-sdk/mcp)。
826* 要控制哪些工具自动运行与需要批准,请参阅[配置权限](/zh-CN/agent-sdk/permissions)。
827
828## 相关文档
829
830* [TypeScript SDK 参考](/zh-CN/agent-sdk/typescript)
831* [Python SDK 参考](/zh-CN/agent-sdk/python)
832* [MCP 文档](https://modelcontextprotocol.io)
833* [SDK 概述](/zh-CN/agent-sdk/overview)