Claude Code 学习站

Claude Agent SDK 自定义工具(Custom Tools)参考

整理 Claude Agent SDK 官方文档:如何用 tool()/@tool 定义自定义工具、注册进程内 MCP 服务器、控制权限、处理错误及返回图片/结构化数据。

本页目录17
AI 摘要 · 已核查整理于 2026-08-10原文:Give Claude custom tools(Anthropic)Agent SDK自定义工具MCPClaude Code
要点速览
  • 自定义工具由名称、描述、输入 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 数组中使用 imageresource 块。见「返回图片和资源」
返回机器可读的 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}&current=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 服务器传给 querymcpServers 中的 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 字段都是布尔值。

字段默认值含义
readOnlyHintfalse工具不修改其运行环境。决定该工具是否可以与其他只读工具并行调用。
destructiveHinttrue工具可能执行破坏性更新。仅作信息说明用途。
idempotentHintfalse使用相同参数重复调用没有额外效果。仅作信息说明用途。
openWorldHinttrue工具会访问你进程之外的系统。仅作信息说明用途。

注解只是元数据,并非强制约束。一个标注为 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
);

完整定义见 TypeScriptPython 参考中的 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 数组接受 textimageaudioresourceresource_link 类型的块,可以在同一个响应里混用。在 TypeScript 中,SDK 会把 audio 块保存到磁盘,Claude 收到的是一个包含保存路径的文本块;在 Python 中,SDK 会从工具结果中丢弃 audio 块并记录一条警告。SDK 会把 resource link 块转换为一个包含该链接名称、URI 和描述的文本块。

图片

image 块以 base64 编码的形式内联携带图片字节数据,不存在 URL 字段。要返回一张位于某个 URL 的图片,需要在 handler 中先拉取该 URL,读取响应字节,再进行 base64 编码后返回。返回结果会作为视觉输入被处理。

字段类型说明
type"image"
datastringbase64 编码的字节数据。只能是原始 base64,不能带 data:image/...;base64, 前缀
mimeTypestring必需。例如 image/pngimage/jpegimage/webpimage/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 用来引用它的一个标签,实际内容存放在该块的 textblob 字段中。当你的工具产出了某种适合在之后按名称引用的东西时(比如生成的文件,或来自外部系统的一条记录),可以使用这种方式。

字段类型说明
type"resource"
resource.uristring内容的标识符。可以是任意 URI scheme
resource.textstring如果内容是文本,放在这里。与 blob 二选一,不能同时提供
resource.blobstring如果内容是二进制数据,base64 编码后放在这里。仅限 TypeScript:Python SDK 会从工具结果中丢弃二进制资源并记录一条警告
resource.mimeTypestring可选

下面的示例展示了从工具 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 中转发 contentis_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 服务器
  • 如果想控制哪些工具自动运行、哪些需要审批,参见配置权限

相关文档