メインコンテンツへスキップ
カスタムツールは Agent SDK を拡張し、Claude が会話中に呼び出せる独自の関数を定義できるようにします。SDK のインプロセス MCP サーバーを使用すると、Claude にデータベース、外部 API、ドメイン固有のロジック、またはアプリケーションが必要とするその他の機能へのアクセスを提供できます。 このガイドでは、入力スキーマとハンドラーを使用してツールを定義し、それらを MCP サーバーにバンドルし、query に渡し、Claude がアクセスできるツールを制御する方法について説明します。また、エラーハンドリング、ツール注釈、および画像などの非テキストコンテンツを返す方法についても説明します。

クイックリファレンス

実行したい操作方法
ツールを定義するPython では @tool、TypeScript では tool() を使用して、名前、説明、スキーマ、ハンドラーを指定します。カスタムツールを作成するを参照してください。
Claude にツールを登録するcreate_sdk_mcp_server / createSdkMcpServer でラップし、query()mcpServers に渡します。カスタムツールを呼び出すを参照してください。
ツールを事前承認する許可されたツールに追加します。許可されたツールを設定するを参照してください。
Claude のコンテキストから組み込みツールを削除する必要な組み込みのみをリストする tools 配列を渡します。許可されたツールを設定するを参照してください。
Claude がツールを並列で呼び出せるようにする副作用のないツールに readOnlyHint: true を設定します。ツール注釈を追加するを参照してください。
ループを停止せずにエラーを処理するthrow の代わりに isError: true を返します。エラーを処理するを参照してください。
画像またはファイルを返すコンテンツ配列で image または resource ブロックを使用します。画像とリソースを返すを参照してください。
マシン可読 JSON 結果を返す結果に structuredContent を設定します。構造化データを返すを参照してください。
多くのツールにスケーリングするツール検索を使用して、オンデマンドでツールを読み込みます。

カスタムツールを作成する

ツールは 4 つの部分で定義され、TypeScript の tool() ヘルパーまたは Python の @tool デコレーターに引数として渡されます。
  • 名前: Claude がツールを呼び出すために使用する一意の識別子。
  • 説明: ツールが何をするかを説明します。Claude はこれを読んで、ツールをいつ呼び出すかを決定します。
  • 入力スキーマ: Claude が提供する必要がある引数。TypeScript では常に Zod スキーマであり、ハンドラーの args は自動的に型付けされます。Python では {"latitude": float} のような名前から型へのマッピングであり、SDK が JSON Schema に変換します。Python デコレーターは、列挙型、範囲、オプションフィールド、またはネストされたオブジェクトが必要な場合、完全な JSON Schema 辞書も受け入れます。
  • ハンドラー: Claude がツールを呼び出すときに実行される非同期関数。検証された引数を受け取り、以下を含むオブジェクトを返す必要があります。
    • content(必須):結果ブロックの配列。各ブロックは "text""image"、または "resource"type を持ちます。非テキストブロックについては、画像とリソースを返すを参照してください。
    • 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


# ツールを定義:名前、説明、入力スキーマ、ハンドラー
@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()

    # コンテンツ配列を返す - 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],
)
完全なパラメーター詳細については、tool() TypeScript リファレンスまたは @tool Python リファレンスを参照してください。JSON Schema 入力形式と戻り値の構造を含みます。
パラメーターをオプションにするには:TypeScript では、Zod フィールドに .default() を追加します。Python では、辞書スキーマはすべてのキーを必須として扱うため、パラメーターをスキーマから除外し、説明文字列で言及し、ハンドラーで args.get() で読み取ります。以下の get_precipitation_chance ツールは両方のパターンを示しています。

カスタムツールを呼び出す

作成した MCP サーバーを mcpServers オプション経由で 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())

さらにツールを追加する

サーバーは tools 配列にリストされた数だけのツールを保持します。複数のツールがサーバーにある場合、allowedTools で各ツールを個別にリストするか、ワイルドカード mcp__weather__* を使用してサーバーが公開するすべてのツールをカバーできます。 以下の例は、天気ツールの例weatherServer に 2 番目のツール get_precipitation_chance を追加し、両方のツールを配列で再構築します。
# 同じサーバーの 2 番目のツールを定義します
@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' はスキーマにありません - .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],
)
この配列内のすべてのツールは、毎ターン、コンテキストウィンドウスペースを消費します。数十のツールを定義している場合は、ツール検索を参照して、代わりにオンデマンドで読み込みます。

ツール注釈を追加する

ツール注釈は、ツールの動作を説明するオプションのメタデータです。TypeScript の tool() ヘルパーの 5 番目の引数として、または 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": "..."}]}
TypeScript または Python リファレンスで ToolAnnotations を参照してください。

ツールアクセスを制御する

天気ツールの例はサーバーを登録し、allowedTools にツールをリストしました。このセクションでは、ツール名がどのように構成されるか、および複数のツールがある場合や組み込みを制限したい場合にアクセスをスコープする方法について説明します。

ツール名形式

MCP ツールが Claude に公開されるとき、それらの名前は特定の形式に従います。
  • パターン:mcp__{server_name}__{tool_name}
  • 例:weather サーバーの get_temperature という名前のツールは mcp__weather__get_temperature になります

許可されたツールを設定する

tools オプションと許可/禁止リストは 2 つのレイヤーに影響します。可用性はツールが 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 はエラーをデータとして見て、再試行、別のツールを試す、または失敗を説明できます。
以下の例は、スロー させる代わりに、ハンドラー内で 2 種類の障害をキャッチします。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},  # シンプルなスキーマ
)
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:
        # ここでキャッチすることでエージェントループを生かしておきます。キャッチされない例外
        # は query() 呼び出し全体を終了させます。
        return {
            "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
            "is_error": True,
        }

画像とリソースを返す

ツール結果の content 配列は textimage、および resource ブロックを受け入れます。同じ応答でそれらを混ぜることができます。

画像

画像ブロックは画像バイトをインラインで、base64 としてエンコードされた状態で運びます。URL フィールドはありません。URL に存在する画像を返すには、ハンドラーで取得し、応答バイトを読み取り、返す前に base64 エンコードします。結果は視覚入力として処理されます。
フィールド注釈
type"image"
datastringBase64 エンコードされたバイト。data:image/...;base64, プレフィックスなしの生の base64 のみ
mimeTypestring必須。例えば image/pngimage/jpegimage/webpimage/gif
import base64
import httpx


# 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 タイプを読み取ります
            }
        ]
    }

リソース

リソースブロックは 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", // Claude が参照するためのラベル。SDK が読み取るパスではありません
        mimeType: "text/markdown",
        text: "# Report\n..." // 実際のコンテンツ、インライン
      }
    }
  ]
};
これらのブロック形状は 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 を返すには、インプロセス SDK サーバーの代わりに スタンドアロン MCP サーバーを実行します。

例:単位変換ツール

このツールは長さ、温度、重量の単位間で値を変換します。ユーザーは「100 キロメートルをマイルに変換」または「72°F は摂氏何度か」と尋ねることができ、Claude はリクエストから正しい単位タイプと単位を選択します。 2 つのパターンを示しています。
  • 列挙型スキーマ: 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


# TypeScript の z.enum() は 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],
)
サーバーが定義されたら、天気の例と同じ方法で query に渡します。この例は、同じツールが異なる単位タイプを処理することを示すために、ループで 3 つの異なるプロンプトを送信します。各応答について、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 サーバーを接続するを参照してください。
  • どのツールが自動的に実行されるか、承認が必要かを制御するには、許可を設定するを参照してください。

関連ドキュメント