跳到正文
FunCoding

搜索

搜索文档、智能体、博客、Skill 和 MCP

给 Claude 自定义工具

用 Agent SDK 的进程内 MCP 服务器定义自定义工具:快速参考、创建与调用工具、添加更多工具、工具注解、控制工具访问、错误处理、返回图片与资源、返回结构化数据,以及单位转换器完整示例。

自定义工具扩展 Agent SDK:让你定义自己的函数,供 Claude 在对话中调用。借助 SDK 的进程内 MCP 服务器,你可以让 Claude 访问数据库、外部 API、领域专属逻辑或你的应用需要的任何其他能力。

快速参考

你想做什么怎么做
定义工具用 @tool(Python)或 tool()(TypeScript),带名字、描述、schema 和处理函数(见「创建自定义工具」)
向 Claude 注册工具包进 create_sdk_mcp_server / createSdkMcpServer,并传给 query() 里的 mcpServers(见「调用自定义工具」)
预批准工具加到你的允许工具里(见「配置允许的工具」)
把内置工具从 Claude 的上下文里移除传一个只列出你想要的内置工具的 tools 数组(见「配置允许的工具」)
让 Claude 并行调用工具对没有副作用的工具设 readOnlyHint: true(见「添加工具注解」)
控制 Claude 读到的错误消息返回 isError: true 来自己撰写消息,而不是暴露原始异常(见「处理错误」)
返回图片或文件在 content 数组里用 image 或 resource 块(见「返回图片与资源」)
返回机器可读的 JSON 结果在结果上设 structuredContent(见「返回结构化数据」)
扩展到许多工具用工具搜索按需加载工具

创建自定义工具

工具由四部分定义,作为参数传给 TypeScript 里的 tool() 辅助函数或 Python 里的 @tool 装饰器:

  • 名字:Claude 用来调用工具的唯一标识。
  • 描述:工具做什么;Claude 读它来决定何时调用。
  • 输入 schema:Claude 必须提供的参数。在 TypeScript 里它总是 Zod schema,处理函数的 args 由它自动获得类型;在 Python 里它是把名字映射到类型的字典,如 {"latitude": float},SDK 会替你把它转换成 JSON Schema;需要枚举、范围、可选字段或嵌套对象时,Python 装饰器也直接接受完整的 JSON Schema 字典。
  • 处理函数: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


# 定义工具:名字、描述、输入 schema、处理函数
@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()

    # 返回 content 数组——Claude 把它看作工具结果
    return {
        "content": [
            {
                "type": "text",
                "text": f"Temperature: {data['current']['temperature_2m']}°F",
            }
        ]
    }


# 把工具包进进程内 MCP 服务器
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";

// 定义工具:名字、描述、输入 schema、处理函数
const getTemperature = tool(
  "get_temperature",
  "Get the current temperature at a location",
  {
    latitude: z.number().describe("Latitude coordinate"), // .describe() 添加 Claude 能看到的字段描述
    longitude: z.number().describe("Longitude coordinate")
  },
  async (args) => {
    // args 的类型来自 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();

    // 返回 content 数组——Claude 把它看作工具结果
    return {
      content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
    };
  }
);

// 把工具包进进程内 MCP 服务器
const weatherServer = createSdkMcpServer({
  name: "weather",
  version: "1.0.0",
  tools: [getTemperature]
});

完整的参数细节(包括 JSON Schema 输入格式和返回值结构)见 TypeScript 的 tool() 参考或 Python 的 @tool 参考。要让参数可选:在 TypeScript 里给 Zod 字段加 .optional() 并在处理函数里应用默认值;在 Python 里,字典 schema 把每个键都当作必填,所以把该参数留在 schema 之外、在描述字符串里提到它,并在处理函数里用 args.get() 读取。下面的 get_precipitation_chance 工具展示了两种模式。

调用自定义工具

通过 mcpServers 选项把你创建的 MCP 服务器传给 query。mcpServers 里的键成为每个工具完全限定名里的 {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 是所有工具调用完成后的最终消息
        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" 是所有工具调用完成后的最终消息
  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 定义。

# 为同一个服务器定义第二个工具
@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' 不在 schema 里——用 .get() 读取使它可选
    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))}%",
            }
        ]
    }


# 用数组里的两个工具重建服务器
weather_server = create_sdk_mcp_server(
    name="weather",
    version="1.0.0",
    tools=[get_temperature, get_precipitation_chance],
)
// 为同一个服务器定义第二个工具
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)
      .optional() // .optional() 让 Claude 能省略该参数
      .describe("How many hours of forecast to return")
  },
  async (args) => {
    const hours = args.hours ?? 12; // 在处理函数里应用默认值
    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, hours);

    return {
      content: [{ type: "text", text: `Next ${hours} hours: ${chances.join("%, ")}%` }]
    };
  }
);

// 用数组里的两个工具重建服务器
const weatherServer = createSdkMcpServer({
  name: "weather",
  version: "1.0.0",
  tools: [getTemperature, getPrecipitationChance]
});

工具搜索默认开启并推迟 SDK MCP 工具:Claude 在紧凑的列表里看到每个工具的名字,并按需加载它的完整 schema。关闭工具搜索时,这个数组里的每个工具每轮都占用上下文窗口空间。在 TypeScript 里,在 tool() 的 extras 参数或 createSdkMcpServer() 的选项里传 alwaysLoad: true,可以把某个工具的完整 schema 保留在初始提示里。

添加工具注解

工具注解是描述工具行为的可选元数据。把它们作为 TypeScript 里 tool() 辅助函数的第五个参数传入,或通过 Python 里 @tool 装饰器的 annotations 关键字参数传入。所有提示字段都是布尔值。

字段默认含义
readOnlyHintfalse工具不修改其环境;控制该工具能否与其他只读工具并行调用
destructiveHinttrue工具可能做破坏性更新;仅供参考
idempotentHintfalse用相同参数重复调用没有额外影响;仅供参考
openWorldHinttrue工具触及你进程之外的系统;仅供参考

注解是元数据,不是强制:标为 readOnlyHint: true 的工具如果处理函数这样做,仍然可以写磁盘;要让注解与处理函数保持一致。这个例子给天气工具示例里的 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
    ),  # 让 Claude 能把它与其他只读调用批量处理
)
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 } } // 让 Claude 能把它与其他只读调用批量处理
);

控制工具访问

天气工具示例注册了服务器并在 allowedTools 里列出工具。本节讲当你有多个工具或想限制内置工具时如何限定访问范围(工具名如何构成见「调用自定义工具」)。

配置允许的工具

tools 选项和允许/拒绝列表影响两层:可用性——控制工具是否出现在 Claude 的上下文里;权限——控制 Claude 尝试之后调用是否被批准。tools 和裸名的 disallowedTools 条目改变可用性;allowedTools 和带范围的 disallowedTools 规则改变权限。如果你在 allowedTools 里点名任务跟踪工具之一,Claude Code 也会让会话选择加入。

选项层效果
tools: ["Read", "Grep"]可用性Claude 的上下文里只有所列的内置工具;未列出的内置工具被移除;MCP 工具不受影响
tools: []可用性所有内置工具都被移除;Claude 只能使用你的 MCP 工具
允许的工具权限所列工具无需权限提示就运行;其他未列出的工具仍可用,调用走权限流程
拒绝的工具两者"Bash" 这样的裸工具名把该工具从 Claude 的上下文里移除,与在 tools 里省略它相同;"Bash(rm *)" 这样带范围的规则让工具留在上下文里,只拒绝按所写匹配的调用

要完全移除某个内置工具,在 tools 里省略它,或在 disallowedTools(Python:disallowed_tools)里列出它的裸名;两者都让工具留在上下文之外,使 Claude 从不尝试它。带范围的 disallowedTools 规则阻止匹配的调用但让工具保持可见,所以 Claude 可能浪费一个轮次去尝试它(完整的评估顺序见「配置权限」)。

处理错误

处理函数的错误不会停止智能体循环。SDK 的进程内 MCP 服务器捕获未捕获的异常并把它们作为错误结果返回,所以你怎么报告错误决定的是 Claude 读到什么,而不是查询是否失败:

发生什么结果
处理函数抛出未捕获的异常MCP 服务器把它转换成携带原始异常消息的错误结果;Claude 看到该消息,智能体循环继续
处理函数捕获错误并返回 isError: true(TS)/ "is_error": True(Python)Claude 看到你撰写的消息;你可以加上原始异常缺少的上下文,如哪个请求失败了或该试什么替代办法

两种情况下 Claude 都可以重试、尝试不同的工具或解释失败。当原始异常消息不足以让 Claude 采取行动时,自己捕获错误。下面的例子在处理函数里捕获两类失败并撰写 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},  # 简单 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:
                # 把失败作为工具结果返回,让 Claude 能对它作出反应。
                # is_error 把它标为失败的调用,而不是看起来奇怪的数据。
                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:
        # 撰写 Claude 读到的消息。未捕获的异常会以
        # 不带上下文的原始 str(e) 到达 Claude。
        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) {
        // 把失败作为工具结果返回,让 Claude 能对它作出反应。
        // isError 把它标为失败的调用,而不是看起来奇怪的数据。
        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) {
      // 撰写 Claude 读到的消息。未捕获的抛出会以
      // 不带上下文的原始错误消息到达 Claude。
      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 把音频块保存到磁盘,Claude 收到带保存文件路径的文本块;在 Python 里,SDK 从工具结果里丢弃音频块并记录警告。Claude 把每个资源链接块作为含链接名、URI 和描述的文本块收到;在 TypeScript 里,你的应用还会以用户消息 tool_use_result 上的 resourceLinks 收到链接本身;在 Python 里,SDK 在 CLI 看到结果之前就把它们展平成文本,所以进程内工具永远不会产生 Python 的 resourceLinks 键。

图片

图片块把图片字节内联携带,用 base64 编码,没有 URL 字段。要返回位于某个 URL 的图片,在处理函数里获取它、读取响应字节,并在返回前做 base64 编码。PNG、JPEG、GIF 或 WebP 图片作为视觉输入到达 Claude;任何其他类型的图片会被保存到磁盘,Claude 改为收到它的文件路径文本。

字段类型说明
type"image"
datastringbase64 编码的字节;只要原始 base64,不要 data:image/...;base64, 前缀
mimeTypestring必填,例如 image/png、image/jpeg、image/webp、image/gif
import base64
import httpx
from claude_agent_sdk import tool


# 定义一个从 URL 获取图片并返回给 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:  # 获取图片字节
        response = await client.get(args["url"])

    return {
        "content": [
            {
                "type": "image",
                "data": base64.b64encode(response.content).decode(
                    "ascii"
                ),  # 对原始字节做 base64 编码
                "mimeType": response.headers.get(
                    "content-type", "image/png"
                ),  # 从响应里读取 MIME 类型
            }
        ]
    }
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); // 获取图片字节
    const buffer = Buffer.from(await response.arrayBuffer()); // 读入 Buffer 以便 base64 编码
    const mimeType = response.headers.get("content-type") ?? "image/png";

    return {
      content: [
        {
          type: "image",
          data: buffer.toString("base64"), // 对原始字节做 base64 编码
          mimeType
        }
      ]
    };
  }
);

资源

资源块嵌入一段由 URI 标识的内容,实际内容放在块的 text 或 blob 字段里。当你的工具产生生成的文件或来自外部系统的记录时使用它。

字段类型说明
type"resource"
resource.uristring内容的标识,任何 URI 方案
resource.textstring内容(文本时);提供它或 blob,不要两个都提供
resource.blobstringbase64 编码的内容(二进制时);仅 TypeScript:Python SDK 从工具结果里丢弃二进制资源并记录警告
resource.mimeTypestring可选

这个例子展示从工具处理函数内部返回的资源块。SDK 不会从例子里的 URI(file:///tmp/report.md)读取任何东西。

return {
  content: [
    {
      type: "resource",
      resource: {
        uri: "file:///tmp/report.md", // 供 Claude 引用的标签,不是 SDK 读取的路径
        mimeType: "text/markdown",
        text: "# Report\n..." // 实际内容,内联
      }
    }
  ]
};
return {
    "content": [
        {
            "type": "resource",
            "resource": {
                "uri": "file:///tmp/report.md",  # 不是 SDK 读取的路径
                "mimeType": "text/markdown",
                "text": "# Report\n...",  # 实际内容,内联
            },
        }
    ]
}

这些块的形状来自 MCP 的 CallToolResult 类型(完整定义见 MCP 规范)。

返回结构化数据

structuredContent 是结果上可选的 JSON 对象,与 content 数组分开。用它返回原始值,让 Claude 能把它们作为精确字段读取,而不必从文本字符串或图片里解析出来。设置了 structuredContent 时,Claude 收到该 JSON 加上 content 里的任何图片或资源块;content 里的文本块不会被转发,因为它们被假定重复了结构化数据。下面的例子把图表渲染为图片块,并从同一个处理函数在 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 装饰器只转发处理函数返回字典里的 content 和 is_error。要从 Python 返回 structuredContent,要运行独立的 MCP 服务器,而不是进程内的 SDK 服务器。

示例:单位转换器

这个工具在长度、温度和重量单位之间转换值。用户可以问"把 100 公里转换成英里"或"72°F 是多少摄氏度",Claude 从请求里选出正确的单位类型和单位。它演示两种模式:

  • 枚举 schema:unit_type 被限制在一组固定值里。在 TypeScript 里用 z.enum();在 Python 里,字典 schema 不支持枚举,所以需要完整的 JSON Schema 字典。
  • 不支持的输入处理:找不到转换对时,处理函数返回 isError: true,让 Claude 能告诉用户哪里出了问题,而不是把失败当作正常结果。
from typing import Any
from claude_agent_sdk import tool, create_sdk_mcp_server


# TypeScript 里的 z.enum() 在 JSON Schema 里变成 "enum" 约束。
# 字典 schema 没有等价物,所以需要完整的 JSON Schema。
@tool(
    "convert_units",
    "Convert a value from one unit to another",
    {
        "type": "object",
        "properties": {
            "unit_type": {
                "type": "string",
                "enum": ["length", "temperature", "weight"],
                "description": "Category of unit",
            },
            "from_unit": {
                "type": "string",
                "description": "Unit to convert from, 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。这个例子在循环里发送三个不同的提示,展示同一个工具处理不同的单位类型。对每个响应,它检查 AssistantMessage 对象(含 Claude 在该轮次发出的工具调用),并在打印最终 ResultMessage 文本之前打印每个 ToolUseBlock,让你看到 Claude 何时在使用工具、何时凭自己的知识回答。因为工具搜索默认开启,输出里也可能包含 Claude 加载被推迟的工具 schema 时的 ToolSearch 调用。

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:
            # 单次 query() 在产出错误结果之后抛出。上面只打印成功的
            # 结果,所以在这里处理失败并继续下一个提示。
            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) {
    // 单次 query() 在产出错误结果之后抛出。上面只记录成功的
    // 结果,所以在这里处理失败并继续下一个提示。
    console.error(`Call failed: ${error}`);
  }
}

下一步

你可以在同一个服务器里混合本页的各种模式:单个服务器可以同时持有数据库工具、API 网关工具和图片渲染器。从这里出发:如果你的服务器增长到几十个工具,见工具搜索,把加载推迟到 Claude 需要它们时;要连接外部 MCP 服务器(文件系统、GitHub、Slack)而不是自己构建,见「连接 MCP 服务器」;要控制哪些工具自动运行、哪些需要批准,见「配置权限」。