Agent SDK のカスタムツール:tool() と createSdkMcpServer で関数を呼ばせる

Claude Agent SDK エンジニアリング 8分で読めます

Claude Agent SDK のカスタムツールは、あなたのアプリ内の関数を Claude が会話の途中で呼び出せるようにするしくみです。SDK に組み込まれたインプロセスの MCP サーバーを使うので、別プロセスのサーバーを立てずに、データベースの照会、社内 API の呼び出し、業務固有の計算などをツールとして渡せます。

この記事では、公式ドキュメント「Give Claude custom tools」に沿って、TypeScript の tool() と Python の @tool によるツール定義、createSdkMcpServer(Python は create_sdk_mcp_server)での登録、allowedTools による事前承認、エラー・画像・構造化データの返し方までを整理します。外部の MCP サーバー(GitHub や Slack など)につなぐ方法は「Claude Code を MCP で外部ツールに接続する」も参考にしてください。

カスタムツールの4つの要素

ツールは次の4つで定義し、TypeScript では tool() ヘルパーの引数、Python では @tool デコレータの引数として渡します。

要素内容
名前Claude がツールを呼ぶときの一意な識別子(例 get_temperature)。
説明ツールが何をするか。Claude はこれを読んで、いつ呼ぶかを判断します。
入力スキーマTypeScript は Zod のスキーマ(.describe() で各フィールドの説明を付けられる)。Python は {"latitude": float} のような型の辞書で、SDK が JSON Schema に変換します。Annotated[float, "Latitude coordinate"] と書けば説明も付き、列挙値や範囲指定が要るときは JSON Schema の辞書をそのまま渡せます。
ハンドラClaude が呼んだときに実行される非同期関数。検証済みの引数を受け取り、結果オブジェクトを返します。

ハンドラの返り値は、必須の content(text・image・audio・resource・resource_link のいずれかの type を持つブロックの配列)に、任意の structuredContent(機械可読な JSON)と isError(失敗を示すフラグ)を加えた形です。

公式の例は、Open-Meteo の API から現在の気温を取ってくる 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]
});

Python では同じものを次のように書きます(HTTP 呼び出しには httpx を使っています)。

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

@tool(
    "get_temperature",
    "Get the current temperature at a location",
    {
        "latitude": Annotated[float, "Latitude coordinate"],
        "longitude": Annotated[float, "Longitude coordinate"],
    },
)
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"}]}

weather_server = create_sdk_mcp_server(name="weather", version="1.0.0", tools=[get_temperature])

作ったサーバーはアプリと同じプロセスの中で動きます。別プロセスとして起動する必要はありません。

サーバーに登録して query() から呼ぶ

サーバーは query() の mcpServers(Python は mcp_servers)オプションに渡します。このときのキー名が、ツールの完全修飾名 mcp__{サーバー名}__{ツール名} のサーバー名部分になります。その名前を allowedTools に入れておくと、権限の確認なしでツールが実行されます。

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);
  }
}

Python では ClaudeAgentOptions(mcp_servers={"weather": weather_server}, allowed_tools=["mcp__weather__get_temperature"]) を渡し、ResultMessage の subtype が "success" のものを取り出します。ツール定義と同じファイルにまとめ、TypeScript なら npx tsx weather.ts、Python なら uv run weather.py(または仮想環境で python weather.py)で実行できます。

1つのサーバーに複数のツールを載せる

サーバーの tools 配列にはいくつでもツールを並べられます。allowedTools には1つずつ書くほか、mcp__weather__* のようなワイルドカードでそのサーバーの全ツールをまとめて許可できます。

なお、ツール検索は既定で有効で、SDK の MCP ツールは名前だけが一覧で見え、詳細なスキーマは必要になったときに読み込まれます。ツール検索を無効にすると、配列内の全ツールが毎ターンのコンテキストを消費します。TypeScript では tool() の5番目の引数(extras)や createSdkMcpServer() のオプションに alwaysLoad: true を渡すと、そのツールのスキーマを最初のプロンプトに常に載せておけます。

TypeScript の createSdkMcpServer() は、ほかに instructions(モデルに渡すサーバーの説明)と timeout(このサーバーのツール呼び出しのタイムアウト。ミリ秒・1000以上の整数。SDK v0.3.248 以降)も受け付けます。

省略可能な引数とアノテーション

引数を省略可能にする

省略可能な引数は、スキーマで省略可能と宣言し、既定値はハンドラ側で補います。

@tool(
    "get_precipitation_chance",
    "Get the hourly precipitation probability for a location",
    {
        "type": "object",
        "properties": {
            "latitude": {"type": "number"},
            "longitude": {"type": "number"},
            "hours": {"type": "integer", "minimum": 1, "maximum": 24,
                      "description": "How many hours of forecast to return"},
        },
        "required": ["latitude", "longitude"],
    },
)
async def get_precipitation_chance(args):
    hours = args.get("hours", 12)
    ...

列挙値(enum)も同じで、TypeScript は z.enum([...])、Python は型の辞書形式では書けないため JSON Schema の辞書を使います。

ツールアノテーション

MCP のツールアノテーションは、ツールの振る舞いを示す任意のメタデータです。TypeScript では tool() の5番目の引数に { annotations: { ... } }、Python では @tool の annotations=ToolAnnotations(...) で渡します。

フィールド既定値意味
readOnlyHintfalse環境を変更しないツール。ほかの読み取り専用ツールと並列に呼べるかがこれで決まります。
destructiveHinttrue破壊的な更新をし得る(情報のみ)。
idempotentHintfalse同じ引数で繰り返し呼んでも追加の影響がない(情報のみ)。
openWorldHinttrueプロセス外のシステムに触れる(情報のみ)。
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 } }
);

アノテーションは宣言であって強制ではありません。readOnlyHint: true を付けても、ハンドラがディスクに書けば書き込みは起きます。ハンドラの実際の動きと一致させておきます。

ツールの利用範囲を制御する

tools オプションと許可・拒否リストは、2つの層に効きます。利用可能性(ツールが Claude のコンテキストに見えるか)と、権限(呼ばれたときに承認されるか)です。

オプション層効果
tools: ["Read", "Grep"]利用可能性列挙した組み込みツールだけがコンテキストに残ります。MCP ツールには影響しません。
tools: []利用可能性組み込みツールをすべて外し、Claude は自作の MCP ツールだけを使えます。
allowedTools権限列挙したツールは確認なしで実行。列挙していないツールも使えますが、通常の権限フローを通ります。
disallowedTools両方"Bash" のような名前だけの指定はツールをコンテキストから外します。"Bash(rm *)" のようなスコープ付きの指定はツールを残したまま一致する呼び出しだけを拒否します。

組み込みツールを完全に使わせたくないときは、tools から外すか、disallowedTools に名前だけで書きます。スコープ付きの拒否ルールだとツール自体は見えたままなので、Claude が試して1ターン無駄にすることがあります。評価順の全体は「Agent SDK の権限設定」で解説しています。

エラーの返し方

ハンドラでエラーが起きてもエージェントのループは止まりません。インプロセスの MCP サーバーが未処理の例外を捕まえてエラー結果に変換するためです。違いが出るのは「Claude が何を読むか」です。

起きること結果
ハンドラが例外を投げたまま例外メッセージがそのままエラー結果になり、Claude はそれを読んでループが続きます。
ハンドラが捕まえて isError: true(Python は "is_error": True)を返すClaude はあなたが組み立てたメッセージを読みます。どのリクエストが失敗したか、代わりに何を試すべきかといった文脈を足せます。

どちらの場合も、Claude は再試行したり、別のツールを使ったり、失敗を説明したりできます。生の例外メッセージだけでは Claude が次の行動を決めにくいときに、自分で捕まえて返します。

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
    };
  }
}

公式の単位変換ツールの例でも、対応していない単位の組み合わせが来たときは isError: true で「Unsupported conversion」を返し、失敗を普通の結果として扱わせないようにしています。

画像・リソース・構造化データを返す

画像

content 配列には複数の種類のブロックを混ぜられます。画像ブロックはバイト列を base64 で埋め込む形で、URL を渡すフィールドはありません。URL の画像を返したいときは、ハンドラで取得して base64 に変換します。

フィールド内容
type"image"
database64 文字列。data:image/...;base64, の接頭辞は付けない
mimeType必須。image/png・image/jpeg・image/webp・image/gif など

PNG・JPEG・GIF・WebP は画像として Claude に渡ります。それ以外の形式はディスクに保存され、Claude にはファイルパスがテキストで渡ります。

リソースとリソースリンク

リソースブロックは、URI で識別される内容を resource.text(テキスト)か resource.blob(base64 のバイナリ)に入れて返します。URI は Claude が参照するためのラベルで、SDK がそのパスを読みに行くわけではありません。blob は TypeScript のみで、Python SDK はバイナリのリソースを落として警告を出します。resource_link ブロックは、名前・URI・説明を含むテキストとして Claude に渡ります。

音声

音声ブロックは、TypeScript では SDK がディスクに保存して Claude にはそのパスがテキストで渡り、Python では結果から落とされて警告が出ます。

構造化データ(structuredContent)

structuredContent は content とは別の JSON オブジェクトで、Claude が文字列や画像から値を読み取らなくても、正確なフィールドとして受け取れるようにします。設定すると、Claude には JSON と content 内の画像・リソースブロックが渡り、content 内のテキストブロックは転送されません(構造化データと重複するとみなされるため)。

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 を返したい場合は、インプロセスの SDK サーバーではなく独立した MCP サーバーを使います。

ツールが数十個に増えたらツール検索で必要なときだけ読み込む、既存の外部 MCP サーバーを使うなら MCP 接続、自動実行と承認の線引きは権限設定、と公式は次の参照先を示しています。1つのサーバーにデータベース用・API 用・画像生成用のツールを混在させることもできます。

本ページは Give Claude custom tools の内容に基づく非公式の日本語解説で、tool() と createSdkMcpServer() の引数は Agent SDK reference - TypeScript と照合しています(確認日 2026-10-13)。関数名・フィールド名は公式表記のまま記載しています。最新の仕様は公式ドキュメントを確認してください。