Перейти к основному содержанию
Пользовательские инструменты расширяют Agent SDK, позволяя вам определять собственные функции, которые Claude может вызывать во время разговора. Используя встроенный MCP-сервер SDK, вы можете предоставить Claude доступ к базам данных, внешним API, логике, специфичной для вашей области, или любым другим возможностям, которые требует ваше приложение. В этом руководстве рассказывается, как определять инструменты с входными схемами и обработчиками, объединять их в MCP-сервер, передавать их в query и контролировать, к каким инструментам Claude может получить доступ. Оно также охватывает обработку ошибок, аннотации инструментов и возврат нетекстового содержимого, такого как изображения.

Краткая справка

Если вы хотите…Сделайте это
Определить инструментИспользуйте @tool (Python) или tool() (TypeScript) с именем, описанием, схемой и обработчиком. См. Создание пользовательского инструмента.
Зарегистрировать инструмент с ClaudeОберните в create_sdk_mcp_server / createSdkMcpServer и передайте в mcpServers в query(). См. Вызов пользовательского инструмента.
Предварительно одобрить инструментДобавьте в разрешённые инструменты. См. Настройка разрешённых инструментов.
Удалить встроенный инструмент из контекста ClaudeПередайте массив tools, содержащий только встроенные инструменты, которые вы хотите. См. Настройка разрешённых инструментов.
Позволить Claude вызывать инструменты параллельноУстановите readOnlyHint: true на инструментах без побочных эффектов. См. Добавление аннотаций инструментов.
Обработать ошибки без остановки циклаВерните isError: true вместо выброса исключения. См. Обработка ошибок.
Вернуть изображения или файлыИспользуйте блоки image или resource в массиве содержимого. См. Возврат изображений и ресурсов.
Вернуть результат в формате машиночитаемого JSONУстановите structuredContent на результат. См. Возврат структурированных данных.
Масштабировать до множества инструментовИспользуйте поиск инструментов для загрузки инструментов по требованию.

Создание пользовательского инструмента

Инструмент определяется четырьмя частями, передаваемыми в качестве аргументов вспомогательной функции tool() в TypeScript или декоратору @tool в Python:
  • Имя: уникальный идентификатор, который Claude использует для вызова инструмента.
  • Описание: что делает инструмент. Claude читает это, чтобы решить, когда его вызывать.
  • Входная схема: аргументы, которые должен предоставить Claude. В TypeScript это всегда схема Zod, и типы args обработчика автоматически выводятся из неё. В Python это словарь, отображающий имена на типы, например {"latitude": float}, который SDK преобразует в JSON Schema для вас. Декоратор Python также принимает полный словарь JSON Schema непосредственно, когда вам нужны перечисления, диапазоны, необязательные поля или вложенные объекты.
  • Обработчик: асинхронная функция, которая запускается, когда Claude вызывает инструмент. Она получает проверенные аргументы и должна вернуть объект с:
    • content (обязательно): массив блоков результатов, каждый с типом "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],
)
См. справку tool() TypeScript или справку @tool Python для полных деталей параметров, включая форматы входных JSON Schema и структуру возвращаемого значения.
Чтобы сделать параметр необязательным: в TypeScript добавьте .default() к полю Zod. В Python словарь схемы рассматривает каждый ключ как обязательный, поэтому оставьте параметр вне схемы, упомяните его в строке описания и читайте его с помощью args.get() в обработчике. Инструмент get_precipitation_chance ниже показывает оба паттерна.

Вызов пользовательского инструмента

Передайте созданный MCP-сервер в query через опцию mcpServers. Ключ в 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 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__*, чтобы охватить каждый инструмент, который сервер предоставляет. Пример ниже добавляет второй инструмент, 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],
)
Каждый инструмент в этом массиве потребляет пространство контекстного окна на каждом ходу. Если вы определяете десятки инструментов, см. поиск инструментов для загрузки их по требованию.

Добавление аннотаций инструментов

Аннотации инструментов — это необязательные метаданные, описывающие поведение инструмента. Передайте их в качестве пятого аргумента вспомогательной функции tool() в TypeScript или через аргумент ключевого слова annotations для декоратора @tool в Python. Все поля подсказок являются логическими значениями.
ПолеПо умолчаниюЗначение
readOnlyHintfalseИнструмент не изменяет свою среду. Контролирует, может ли инструмент вызываться параллельно с другими инструментами только для чтения.
destructiveHinttrueИнструмент может выполнять деструктивные обновления. Только информационное.
idempotentHintfalseПовторные вызовы с одинаковыми аргументами не имеют дополнительного эффекта. Только информационное.
openWorldHinttrueИнструмент обращается к системам вне вашего процесса. Только информационное.
Аннотации — это метаданные, а не принуждение. Инструмент, отмеченный как readOnlyHint: true, всё ещё может писать на диск, если это то, что делает обработчик. Держите аннотацию точной для обработчика. Этот пример добавляет readOnlyHint к инструменту get_temperature из примера инструмента погоды.
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": "..."}]}
См. ToolAnnotations в справке TypeScript или Python.

Контроль доступа к инструментам

Пример инструмента погоды зарегистрировал сервер и перечислил инструменты в 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.
разрешённые инструментыРазрешениеПеречисленные инструменты работают без запроса разрешения. Неперечисленные инструменты остаются доступными; вызовы проходят через поток разрешений.
запрещённые инструментыОбаИмя инструмента без области действия, такое как "Bash", удаляет инструмент из контекста Claude, как если бы его не было в tools. Правило с областью действия, такое как "Bash(rm *)", оставляет инструмент в контексте и отклоняет только совпадающие вызовы.
Чтобы полностью удалить встроенный инструмент, пропустите его из tools или перечислите его имя без области действия в disallowedTools (Python: disallowed_tools); оба способа держат инструмент вне контекста, чтобы Claude никогда не попытался его использовать. Правило disallowedTools с областью действия блокирует совпадающие вызовы, но оставляет инструмент видимым, поэтому Claude может потратить ход, пытаясь его использовать. См. Настройка разрешений для полного порядка оценки.

Обработка ошибок

То, как ваш обработчик сообщает об ошибках, определяет, продолжается ли цикл агента или останавливается:
Что происходитРезультат
Обработчик выбрасывает неперехваченное исключениеЦикл агента останавливается. Claude никогда не видит ошибку, и вызов query завершается ошибкой.
Обработчик перехватывает ошибку и возвращает isError: true (TS) / "is_error": True (Python)Цикл агента продолжается. Claude видит ошибку как данные и может повторить попытку, попробовать другой инструмент или объяснить сбой.
Пример ниже перехватывает два вида сбоев внутри обработчика вместо того, чтобы позволить им выбросить исключение. Статус HTTP, отличный от 200, перехватывается из ответа и возвращается как результат ошибки. Ошибка сети или неверный 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 в результате инструмента принимает блоки text, image и resource. Вы можете смешивать их в одном ответе.

Изображения

Блок изображения содержит байты изображения встроенным образом, закодированные как base64. Нет поля URL. Чтобы вернуть изображение, которое находится по URL, получите его в обработчике, прочитайте байты ответа и закодируйте их в base64 перед возвратом. Результат обрабатывается как визуальный ввод.
ПолеТипПримечания
type"image"
datastringБайты в кодировке Base64. Только сырой base64, без префикса data:image/...;base64,
mimeTypestringОбязательно. Например image/png, image/jpeg, image/webp, image/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, чтобы ссылаться на неё; фактическое содержимое находится в поле text или blob блока. Используйте это, когда ваш инструмент производит что-то, что имеет смысл адресовать по имени позже, например сгенерированный файл или запись из внешней системы.
ПолеТипПримечания
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 пересылает только content и is_error из словаря возврата обработчика. Чтобы вернуть structuredContent из Python, запустите автономный 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 сделал на этом ходу) и выводит каждый ToolUseBlock перед выводом финального текста ResultMessage. Это позволяет вам увидеть, когда 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-серверов.
  • Чтобы контролировать, какие инструменты работают автоматически, а какие требуют одобрения, см. Настройка разрешений.

Связанная документация