跳转到主要内容
自定义工具通过让您定义 Claude 在对话期间可以调用的自己的函数来扩展 Agent SDK。使用 SDK 的进程内 MCP 服务器,您可以让 Claude 访问数据库、外部 API、特定领域的逻辑或应用程序需要的任何其他功能。 本指南涵盖如何使用输入架构和处理程序定义工具、将它们捆绑到 MCP 服务器中、将它们传递给 query,以及控制 Claude 可以访问哪些工具。它还涵盖错误处理、工具注释和返回非文本内容(如图像)。

快速参考

如果您想…执行此操作
定义工具使用 @tool(Python)或 tool()(TypeScript),包含名称、描述、架构和处理程序。请参阅创建自定义工具
向 Claude 注册工具create_sdk_mcp_server / createSdkMcpServer 中包装并传递给 query() 中的 mcpServers。请参阅调用自定义工具
预先批准工具添加到您的允许工具列表。请参阅配置允许的工具
从 Claude 的上下文中删除内置工具传递仅列出您想要的内置工具的 tools 数组。请参阅配置允许的工具
让 Claude 并行调用工具在没有副作用的工具上设置 readOnlyHint: true。请参阅添加工具注释
处理错误而不停止循环返回 isError: true 而不是抛出异常。请参阅处理错误
返回图像或文件在内容数组中使用 imageresource 块。请参阅返回图像和资源
返回机器可读的 JSON 结果在结果上设置 structuredContent。请参阅返回结构化数据
扩展到许多工具使用工具搜索按需加载工具。

创建自定义工具

工具由四个部分定义,作为参数传递给 TypeScript 中的 tool() 助手或 Python 中的 @tool 装饰器:
  • 名称: Claude 用来调用工具的唯一标识符。
  • 描述: 工具的功能。Claude 读取此内容以决定何时调用它。
  • 输入架构: Claude 必须提供的参数。在 TypeScript 中,这始终是 Zod 架构,处理程序的 args 会自动从中获得类型。在 Python 中,这是一个将名称映射到类型的字典,如 {"latitude": float},SDK 会为您将其转换为 JSON Schema。Python 装饰器还接受完整的 JSON Schema 字典,当您需要枚举、范围、可选字段或嵌套对象时。
  • 处理程序: 当 Claude 调用工具时运行的异步函数。它接收验证的参数,必须返回一个对象,包含:
    • content(必需):结果块的数组,每个块的 type"text""image""resource"。有关非文本块,请参阅返回图像和资源
    • 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],
)
有关完整的参数详细信息,包括 JSON Schema 输入格式和返回值结构,请参阅 tool() TypeScript 参考或 @tool Python 参考。
要使参数可选:在 TypeScript 中,向 Zod 字段添加 .default()。在 Python 中,字典架构将每个键视为必需的,因此将参数从架构中省略,在描述字符串中提及它,并在处理程序中使用 args.get() 读取它。下面的 get_precipitation_chance 工具展示了两种模式。

调用自定义工具

通过 mcpServers 选项将您创建的 MCP 服务器传递给 querymcpServers 中的键成为每个工具的完全限定名称中的 {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())

添加更多工具

一个服务器在其 tools 数组中列出的工具数量不限。如果有多个工具在一个服务器上,您可以在 allowedTools 中单独列出每个工具,或使用通配符 mcp__weather__* 来覆盖服务器公开的每个工具。 下面的示例向天气工具示例中的 weatherServer 添加第二个工具 get_precipitation_chance,并使用数组中的两个工具重建它。
# 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],
)
此数组中的每个工具在每个回合都会消耗上下文窗口空间。如果您定义了数十个工具,请参阅工具搜索以按需加载它们。

添加工具注释

工具注释是描述工具行为方式的可选元数据。在 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
    ),  # Lets Claude batch this with other read-only calls
)
async def get_temperature(args):
    return {"content": [{"type": "text", "text": "..."}]}
请参阅 TypeScriptPython 参考中的 ToolAnnotations

控制工具访问

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

工具名称格式

当 MCP 工具暴露给 Claude 时,它们的名称遵循特定格式:
  • 模式:mcp__{server_name}__{tool_name}
  • 示例:服务器 weather 中名为 get_temperature 的工具变成 mcp__weather__get_temperature

配置允许的工具

tools 选项和允许/不允许列表影响两个层:可用性(控制工具是否出现在 Claude 的上下文中)和权限(控制 Claude 尝试调用后是否批准调用)。tools 和裸名称 disallowedTools 条目改变可用性。allowedTools 和作用域 disallowedTools 规则仅改变权限。
选项效果
tools: ["Read", "Grep"]可用性仅列出的内置工具在 Claude 的上下文中。未列出的内置工具被删除。MCP 工具不受影响。
tools: []可用性所有内置工具都被删除。Claude 只能使用您的 MCP 工具。
允许的工具权限列出的工具运行而无需权限提示。未列出的工具保持可用;调用通过权限流进行。
不允许的工具两者裸工具名称(如 "Bash")将工具从 Claude 的上下文中删除,与从 tools 中省略它相同。作用域规则(如 "Bash(rm *)")将工具保留在上下文中,仅拒绝匹配的调用。
要完全删除内置工具,请从 tools 中省略它或在 disallowedTools 中列出其裸名称(Python:disallowed_tools);两者都将工具保留在上下文之外,以便 Claude 永远不会尝试它。作用域 disallowedTools 规则会阻止匹配的调用但保留工具可见,因此 Claude 可能会浪费一个回合尝试它。有关完整的评估顺序,请参阅配置权限

处理错误

您的处理程序报告错误的方式决定了代理循环是继续还是停止:
发生的情况结果
处理程序抛出未捕获的异常代理循环停止。Claude 永远看不到错误,query 调用失败。
处理程序捕获错误并返回 isError: true(TS)/ "is_error": True(Python)代理循环继续。Claude 将错误视为数据,可以重试、尝试不同的工具或解释失败。
下面的示例在处理程序内部捕获两种失败,而不是让它们抛出。非 200 HTTP 状态从响应中捕获并作为错误结果返回。网络错误或无效 JSON 由周围的 try/except(Python)或 try/catch(TypeScript)捕获,也作为错误结果返回。在这两种情况下,处理程序正常返回,代理循环继续。
import json
import httpx
from typing import Any


@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:
        # Catching here keeps the agent loop alive. An uncaught exception
        # would end the whole query() call.
        return {
            "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
            "is_error": True,
        }

返回图像和资源

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

图像

图像块以 base64 编码的方式内联携带图像字节。没有 URL 字段。要返回位于 URL 的图像,在处理程序中获取它,读取响应字节,并在返回之前进行 base64 编码。结果作为视觉输入处理。
字段类型注释
type"image"
datastringBase64 编码的字节。仅原始 base64,没有 data:image/...;base64, 前缀
mimeTypestring必需。例如 image/pngimage/jpegimage/webpimage/gif
import base64
import httpx


# 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
            }
        ]
    }

资源

资源块嵌入由 URI 标识的内容片段。URI 是 Claude 引用的标签;实际内容位于块的 textblob 字段中。当您的工具生成稍后按名称寻址有意义的内容时使用此功能,例如生成的文件或来自外部系统的记录。
字段类型注释
type"resource"
resource.uristring内容的标识符。任何 URI 方案
resource.textstring内容,如果是文本。提供此项或 blob,不能两者都提供
resource.blobstring内容 base64 编码,如果是二进制
resource.mimeTypestring可选
此示例显示从工具处理程序内部返回的资源块。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
      }
    }
  ]
};
这些块形状来自 MCP CallToolResult 类型。有关完整定义,请参阅 MCP 规范

返回结构化数据

structuredContent 是结果上的可选 JSON 对象,与 content 数组分开。使用它返回原始值,Claude 可以将其作为精确字段读取,而不是从文本字符串或图像中解析它们。 当设置 structuredContent 时,Claude 接收 JSON 加上来自 content 的任何图像或资源块。来自 content 的文本块不被转发,因为假设它们复制结构化数据。下面的示例将图表呈现为图像块,并从同一处理程序的 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 装饰器仅从处理程序的返回字典转发 contentis_error。要从 Python 返回 structuredContent,请运行独立 MCP 服务器而不是进程内 SDK 服务器。

示例:单位转换器

此工具在长度、温度和重量的单位之间转换值。用户可以询问”将 100 公里转换为英里”或”72°F 是多少摄氏度”,Claude 从请求中选择正确的单位类型和单位。 它演示了两种模式:
  • 枚举架构: unit_type 被限制为一组固定值。在 TypeScript 中,使用 z.enum()。在 Python 中,字典架构不支持枚举,因此需要完整的 JSON Schema 字典。
  • 不支持的输入处理: 当找不到转换对时,处理程序返回 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],
)
定义服务器后,以与天气示例相同的方式将其传递给 query。此示例在循环中发送三个不同的提示,以显示同一工具处理不同的单位类型。对于每个响应,它检查 AssistantMessage 对象(包含 Claude 在该回合中进行的工具调用)并在打印最终 ResultMessage 文本之前打印每个 ToolUseBlock。这让您看到 Claude 何时使用工具与从其自己的知识中回答。
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())

后续步骤

自定义工具在标准接口中包装异步函数。您可以在同一服务器中混合本页上的模式:单个服务器可以在彼此旁边保存数据库工具、API 网关工具和图像渲染器。 从这里:
  • 如果您的服务器增长到数十个工具,请参阅工具搜索以延迟加载它们,直到 Claude 需要它们。
  • 要连接到外部 MCP 服务器(文件系统、GitHub、Slack)而不是构建自己的,请参阅连接 MCP 服务器
  • 要控制哪些工具自动运行与需要批准,请参阅配置权限

相关文档