Skip to content

Agent SDK 的自定义工具让您把自己的函数包装成 Claude 能调用的 MCP 工具,用于查询数据库、调用外部 API 或执行领域特定逻辑。关键操作:使用 @tooltool() 定义名称、描述、输入模式和 handler,通过 createSdkMcpServer 注册到服务器,在 querymcpServers 选项中传入并设置 allowedTools 控制权限。返回错误时用 isError: true 让 loop 继续,支持返回图片、文件和结构化数据。

Claude Agent SDK 自定义工具配置:定义、注册与访问控制

通过 Agent SDK 的 in-process MCP 服务器定义自定义工具,让 Claude 调用您的函数、访问您的 API 并执行领域特定操作。

自定义工具扩展了 Agent SDK,允许您定义自己的函数,在对话中由 Claude 调用。使用 SDK 的 in-process MCP 服务器,您可以赋予 Claude 访问数据库、外部 API、领域特定逻辑或应用程序需要的任何其他能力。

本文涵盖如何定义带有输入模式和 handler 的工具,将它们打包到 MCP 服务器中,传递给 query,并控制 Claude 可访问的工具。还包括错误处理、工具注解以及返回图片等非文本内容。

快速参考

目标操作
定义一个工具使用 @tool(Python)或 tool()(TypeScript),传入名称、描述、模式和 handler。参见创建自定义工具
向 Claude 注册一个工具包装在 create_sdk_mcp_server / createSdkMcpServer 中并通过 mcpServers 传递给 query()。参见调用自定义工具
预审批一个工具将其加入 allowedTools。参见配置允许的工具
从 Claude 的上下文中移除内置工具传入仅包含所需内置工具的 tools 数组。参见配置允许的工具
允许 Claude 并行调用工具在无副作用的工具上设置 readOnlyHint: true。参见添加工具注解
处理错误而不停止循环返回 isError: true 而不是抛出异常。参见处理错误
返回图片或文件在 content 数组中使用 imageresource 块。参见返回图片和资源
返回机器可读的 JSON 结果在结果中设置 structuredContent。参见返回结构化数据
扩展至大量工具使用工具搜索按需加载工具。

创建自定义工具

一个工具由四个部分组成,通过参数传递给 TypeScript 的 tool() 辅助函数或 Python 的 @tool 装饰器:

  • 名称: Claude 调用工具时使用的唯一标识符。
  • 描述: 工具的功能说明。Claude 据此判断何时调用它。
  • 输入模式: Claude 必须提供的参数。在 TypeScript 中始终是 Zod schema,handler 的 args 自动从此获得类型。在 Python 中是一个名称到类型的映射,如 {"latitude": float},SDK 会为您转换为 JSON Schema。当需要枚举、范围、可选字段或嵌套对象时,Python 装饰器也直接接受完整的 JSON Schema 字典。
  • Handler: Claude 调用工具时执行的异步函数。它接收经过验证的参数,必须返回一个对象,包含:
    • content(必需):结果块的数组,每个块具有 "text""image""resource"type。非文本块参见返回图片和资源
    • structuredContent(可选):一个 JSON 对象,以机器可读的数据形式返回结果,与 content 一同返回。参见返回结构化数据
    • isError(可选):设为 true 表示工具失败,以便 Claude 做出反应。参见处理错误

定义工具后,使用 createSdkMcpServer(TypeScript)或 create_sdk_mcp_server(Python)将其包装在服务器中。该服务器在您的应用程序内部运行(in-process),而不是作为单独的进程。

天气工具示例

此示例定义一个 get_temperature 工具并将其包装在 MCP 服务器中。它仅设置工具;要将其传递给 query 并运行,请参见下面的调用自定义工具

python
from typing import Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server

# 定义一个工具:名称、描述、输入模式、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 {
        "content": [
            {
                "type": "text",
                "text": f"Temperature: {data['current']['temperature_2m']}°F",
            }
        ]
    }

# 将工具包装在 in-process MCP 服务器中
weather_server = create_sdk_mcp_server(
    name="weather",
    version="1.0.0",
    tools=[get_temperature],
)
typescript
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const getTemperature = tool(
  "get_temperature",
  "Get the current temperature at a location",
  {
    latitude: z.number().describe("Latitude coordinate"),
    longitude: z.number().describe("Longitude coordinate")
  },
  async (args) => {
    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 {
      content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
    };
  }
);

const weatherServer = createSdkMcpServer({
  name: "weather",
  version: "1.0.0",
  tools: [getTemperature]
});

有关完整的参数细节(包括 JSON Schema 输入格式和返回值结构),请参见 TypeScript 的 tool() 参考或 Python 的 @tool 参考。

要使参数可选:在 TypeScript 中,为 Zod 字段添加 .default()。在 Python 中,字典模式将每个键视为必需,因此将参数从模式中排除,在描述字符串中提及,并在 handler 中使用 args.get() 读取。下面的 get_precipitation_chance 工具示例展示了这两种模式。

调用自定义工具

通过 mcpServers 选项将您创建的 MCP 服务器传递给 querymcpServers 中的键成为每个工具完全限定名称中的 {server_name} 段:mcp__{server_name}__{tool_name}。在 allowedTools 中列出该名称,以便工具运行无需权限提示。

以下代码片段复用了上面天气工具示例中的 weatherServer,询问 Claude 特定位置的天气。

python
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,
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)

asyncio.run(main())
typescript
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"]
  }
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

添加更多工具

一个服务器可以容纳您在其 tools 数组中列出的任意数量的工具。当服务器上有多个工具时,您可以单独列出每个工具在 allowedTools 中,或使用通配符 mcp__weather__* 覆盖服务器公开的所有工具。

下面的示例向天气工具示例中的 weatherServer 添加了第二个工具 get_precipitation_chance,并使用两个工具重建了数组。

python
@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 = args.get("hours", 12)  # optional, defaults to 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],
)
typescript
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)
      .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("%, ")}%` }]
    };
  }
);

const weatherServer = createSdkMcpServer({
  name: "weather",
  version: "1.0.0",
  tools: [getTemperature, getPrecipitationChance]
});

此数组中的每个工具都会在每轮占用上下文窗口空间。如果您定义了数十个工具,请参见工具搜索以按需加载它们。

添加工具注解

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

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

注解是元数据,不是强制。标记为 readOnlyHint: true 的工具如果 handler 实际写入磁盘,仍然可以写入。请保持注解与 handler 一致。

以下示例为天气工具示例中的 get_temperature 添加了 readOnlyHint

python
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": "..."}]}
typescript
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 将此调用与其他只读调用批量执行
);

请参见 TypeScript 或 Python 参考中的 ToolAnnotations 文档(TypeScriptPython)。

控制工具访问

天气工具示例注册了一个服务器并在 allowedTools 中列出了工具。本节介绍工具名称的构造方式以及当您有多个工具或想要限制内置工具时如何限定访问范围。

工具名称格式

当 MCP 工具暴露给 Claude 时,其名称遵循特定格式:

  • 模式:mcp__{server_name}__{tool_name}
  • 示例:名为 get_temperature 的工具在服务器 weather 中变为 mcp__weather__get_temperature

配置允许的工具

tools 选项和允许/禁止列表影响两个层面:可用性(控制工具是否出现在 Claude 上下文中)和权限(控制 Claude 尝试调用时是否批准)。tools 和裸名称 disallowedTools 条目影响可用性。allowedTools 和限定范围的 disallowedTools 规则仅影响权限。

选项层面效果
tools: ["Read", "Grep"]可用性只有列出的内置工具出现在 Claude 上下文中。未列出的内置工具被移除。MCP 工具不受影响。
tools: []可用性所有内置工具被移除。Claude 只能使用您的 MCP 工具。
allowedTools权限列出的工具在运行时无需权限提示。未列出的工具仍然可用;调用会经过权限流程
disallowedTools两者兼有裸工具名称(如 "Bash")将工具从 Claude 上下文中移除,与从 tools 中省略效果相同。限定范围的规则(如 "Bash(rm *)")保留工具在上下文中,仅拒绝匹配的调用。

要完全移除一个内置工具,可以从 tools 中省略它,或在 disallowedTools(Python: disallowed_tools)中列出其裸名称;两者都将工具排除在上下文之外,因此 Claude 永远不会尝试它。限定范围的 disallowedTools 规则会阻止匹配的调用,但工具仍然可见,因此 Claude 可能浪费一轮尝试。完整的评估顺序请参见配置权限

处理错误

您的 handler 如何报告错误决定了代理循环是继续还是停止:

情况结果
Handler 抛出未捕获的异常代理循环停止。Claude 看不到错误,query 调用失败。
Handler 捕获错误并返回 isError: true(TS)/ "is_error": True(Python)代理循环继续。Claude 将错误视为数据,可以重试、尝试其他工具或解释失败。

下面的示例在 handler 内部捕获两种故障类型,而不是让它们抛出。非 200 的 HTTP 状态从响应中捕获并以错误结果返回。网络错误或无效 JSON 由周围的 try/except(Python)或 try/catch(TypeScript)捕获,同样作为错误结果返回。两种情况下 handler 正常返回,代理循环继续。

python
import json
import httpx
from typing import Any

@tool(
    "fetch_data",
    "Fetch data from an API",
    {"endpoint": str},
)
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 {
                    "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:
        return {
            "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
            "is_error": True,
        }
typescript
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 {
          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) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
          }
        ],
        isError: true
      };
    }
  }
);

返回图片和资源

工具结果中的 content 数组接受 textimageresource 块。您可以在同一个响应中混合使用它们。

图片

图片块以内联形式携带图片字节,编码为 base64。没有 URL 字段。要返回位于 URL 的图片,在 handler 中获取它,读取响应字节,并在返回前进行 base64 编码。结果作为视觉输入处理。

字段类型说明
type"image"
datastringBase64 编码的字节。仅 raw base64,不含 data:image/...;base64, 前缀
mimeTypestring必需。例如 image/pngimage/jpegimage/webpimage/gif
python
import base64
import httpx

@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"),
                "mimeType": response.headers.get("content-type", "image/png"),
            }
        ]
    }
typescript
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());
    const mimeType = response.headers.get("content-type") ?? "image/png";
    return {
      content: [{ type: "image", data: buffer.toString("base64"), mimeType }]
    };
  }
);

资源

资源块嵌入由 URI 标识的一段内容。URI 是供 Claude 引用的标签;实际内容位于块的 textblob 字段中。当您的工具生成以后可以通过名称引用的内容(如生成的文件或外部系统的记录)时使用此块。

字段类型说明
type"resource"
resource.uristring内容的标识符。任何 URI 方案。
resource.textstring文本内容。提供此字段或 blob,不要同时提供。
resource.blobstring二进制内容的 base64 编码。
resource.mimeTypestring可选。

以下示例展示从工具 handler 内部返回的资源块。URI file:///tmp/report.md 是一个标签,Claude 可以稍后引用;SDK 不会从该路径读取。

typescript
return {
  content: [
    {
      type: "resource",
      resource: {
        uri: "file:///tmp/report.md",
        mimeType: "text/markdown",
        text: "# Report\n..."
      }
    }
  ]
};
python
return {
    "content": [
        {
            "type": "resource",
            "resource": {
                "uri": "file:///tmp/report.md",
                "mimeType": "text/markdown",
                "text": "# Report\n...",
            },
        }
    ]
}

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

返回结构化数据

structuredContent 是结果中的一个可选 JSON 对象,独立于 content 数组。使用它返回原始值,Claude 可以直接读取精确字段,而不需要从文本字符串或图片中解析。

设置 structuredContent 后,Claude 收到 JSON 以及 content 中的任何图片或资源块。content 中的文本块不会被转发,因为假定它们重复了结构化数据。以下示例从同一个 handler 中将图表渲染为图片块,并在 structuredContent 中返回其背后的数据点。

typescript
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 的返回字典中转发 contentis_error。要从 Python 返回 structuredContent,请运行独立 MCP 服务器而不是 in-process 的 SDK 服务器。

示例:单位转换器

此工具在长度、温度和重量单位之间转换值。用户可以询问“将 100 公里转换为英里”或“72°F 是多少摄氏度”,Claude 会根据请求选择正确的单位类型和单位。

它演示了两种模式:

  • 枚举模式: unit_type 被限制为一组固定值。在 TypeScript 中使用 z.enum()。在 Python 中,字典模式不支持枚举,因此需要完整的 JSON Schema 字典。
  • 不支持的输入处理: 当找不到转换对时,handler 返回 isError: true,以便 Claude 可以告知用户出错原因,而不是将失败视为正常结果。
python
from typing import Any
from claude_agent_sdk import tool, create_sdk_mcp_server

# z.enum() 在 TypeScript 中变为 JSON Schema 中的 "enum" 约束。
# 字典模式没有等效项,因此需要完整的 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],
)
typescript
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 在该轮中进行的工具调用)并打印每个 ToolUseBlock,然后打印最终的 ResultMessage 文本。这使您可以观察 Claude 何时使用工具与何时依靠自身知识回答。

python
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:
        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")

asyncio.run(main())
typescript
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) {
  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`);
    }
  }
}

后续步骤

自定义工具将异步函数包装在标准接口中。您可以在同一个服务器中混合本文中的模式:一个服务器可以同时拥有数据库工具、API 网关工具和图片渲染器。

从这里开始:

  • 如果您的服务器增长到数十个工具,请参见工具搜索以延迟加载它们直到 Claude 需要。
  • 要连接到外部 MCP 服务器(文件系统、GitHub、Slack)而不是自己构建,请参见连接 MCP 服务器
  • 要控制哪些工具自动运行以及哪些需要审批,请参见配置权限

相关文档

常见问题

自定义工具调用失败后为什么代理循环停止了?

当 handler 抛出未捕获的异常时,query 调用会失败,循环停止。正确的做法是捕获所有异常,在返回值中设置 isError: true(TypeScript)或 "is_error": True(Python),让 Claude 看到错误信息并决定重试或解释。

自定义工具能返回图片给 Claude 吗?

可以。在 handler 的 content 数组中返回 type: "image" 的块,其中 data 是图片字节的 base64 编码(不带 data:image/...;base64, 前缀),mimeType 指定图片格式。如果图片来自 URL,需要在 handler 中先获取到字节。

如何限制 Claude 只能使用我的自定义工具,而不能使用内置工具?

query 的选项中设置 tools: [],这将移除所有内置工具,Claude 只能访问您通过 mcpServers 提供的工具。或者,使用 disallowedTools 列出您不想暴露的内置工具名称(如 "Bash")也可以将其从上下文中移除。