Agent SDKカスタムツールの作り方 — インプロセスMCPサーバーで機能拡張
Agent SDKのインプロセスMCPサーバーで独自ツールを定義する手順。天気ツールの実装例からエラーハンドリング、structuredContentでの構造化データ返却まで扱います。
Agent SDKカスタムツールとは — インプロセスMCPサーバーで独自機能を足す仕組み
Agent SDKのカスタムツールとは、SDKが内蔵するインプロセスMCPサーバー(自分のアプリケーションと同じプロセス内で動くMCPサーバー)を使って、Claudeが呼び出せる独自の関数を追加する機能です。外部サービスのAPI呼び出しやデータベースへのクエリ、社内システム固有のロジックなど、組み込みツール(Read / Write / Bashなど)ではカバーできない処理をClaudeに渡せます。コードを書く代わりにドメイン知識をファイルとして持たせたい場合は、Managed AgentsのカスタムSkillという選択肢もあります。
ツールは名前・説明・入力スキーマ・ハンドラの4要素で定義します。別プロセスを立ち上げる必要はなく、関数を書いてサーバーオブジェクトに登録するだけで動き始めるのが最大の特徴です。query() そのものの基本的な使い方はAgent SDKクイックスタートで解説しています。
カスタムツールが動くまでの流れ
- 1
ツールを定義する
TypeScriptは
tool()関数、Pythonは@toolデコレータで、4要素を1つのツールにまとめます。 - 2
サーバーにまとめる
createSdkMcpServer()(Pythonはcreate_sdk_mcp_server())にtools配列として渡します。 - 3
queryに登録する
query()のmcpServersにサーバーを渡します。キー名が、Claudeに見えるツール名の一部になります。 - 4
許可リストに載せる
mcp__{サーバー名}__{ツール名}をallowedToolsに入れると、承認プロンプトなしで動きます。
外部の標準MCPサーバーに接続する話は別の切り口になります。すでに動いているMCPサーバー(GitHubやSlackなど)を利用したい場合はAgent SDKからMCPサーバーに接続する手順を、npmやMCPレジストリで配布できる独立したサーバーを一から作りたい場合はMCPサーバー自作ガイドを参照してください。本記事が扱うのは、あくまで自分のアプリケーションの中だけで完結するツール追加です。
手順1 — 天気ツールを定義する
ハンドラは非同期関数で、content 配列を含むオブジェクトを返します。次の例は緯度・経度を受け取り、Open-Meteo APIから気温を取得する get_temperature ツールです。
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}¤t=temperature_2m`
);
const data: any = await response.json();
return {
content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°C` }]
};
}
);
const weatherServer = createSdkMcpServer({
name: "weather",
version: "1.0.0",
tools: [getTemperature]
});Pythonでは辞書スキーマを使います。{"latitude": float} のような型指定は、SDK内部でJSON Schemaへ変換されます。列挙値や範囲、オプション引数が必要な場合だけ、フルのJSON Schema辞書を直接渡します(enumはPythonの簡易辞書スキーマでは表現できないため必須)。
from claude_agent_sdk import tool, create_sdk_mcp_server
import httpx
@tool(
"get_temperature",
"Get the current temperature at a location",
{"latitude": float, "longitude": float},
)
async def get_temperature(args):
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"},
)
data = response.json()
return {"content": [{"type": "text", "text": f"Temperature: {data['current']['temperature_2m']}°C"}]}
weather_server = create_sdk_mcp_server(name="weather", version="1.0.0", tools=[get_temperature])TypeScriptの .describe() に当たるものとして、Pythonの簡易辞書では Annotated[float, "Latitude coordinate"] のように型へ説明文を添えられます。
オプション引数の書き方は言語で違う
TypeScriptはZodのフィールドに .optional() を付け、既定値はハンドラ側で埋めます。Pythonの辞書スキーマは全キーが必須として扱われるため、省略可能にしたい引数はJSON Schema形式で渡し、required から外します。ハンドラでは args.get() で読みます。
@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},
},
"required": ["latitude", "longitude"], # hours は必須から外す
},
)
async def get_precipitation_chance(args):
hours = args.get("hours", 12) # 未指定のときの既定値はここで決める
...TypeScript側は hours: z.number().int().min(1).max(24).optional() と書き、ハンドラの冒頭で args.hours ?? 12 とします。
手順2 — サーバーをqueryに登録して呼び出す
mcpServers のキーが {server_name} の部分になり、ツール名は mcp__{server_name}__{tool_name} という形式でClaudeに見えます。上の例なら mcp__weather__get_temperature です。この名前を allowedTools に列挙すると、実行のたびに許可を求められずに動きます。接頭辞を外す環境変数はAgent SDKのMCP接頭辞を外す方法にあります。
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);
}
}npx tsx weather.tsサーバーに複数のツールを持たせるときは、tools 配列にまとめて渡し、allowedTools は個別列挙かワイルドカード mcp__weather__* のどちらかで許可します。許可側のワイルドカードは、サーバー名まで書いた形にしか効きません。mcp__* のような書き方は警告とともに無視され、何も自動承認されません。mcp__weather__get_* のようにツール名の前半だけを固定する書き方は使えます。
拒否側は事情が逆です。disallowedTools なら mcp__* が全サーバーのMCPツールにそのまま効きます。
手順3 — エラーハンドリングを実装する
ハンドラが例外を投げても、それだけでエージェントループは止まりません。SDKのインプロセスMCPサーバーが例外を捕まえ、エラー結果としてClaudeに渡すからです。ただし、どう報告するかでClaudeが読む内容が変わります。
エラーの報告方法でClaudeが読む内容が変わる
例外を投げっぱなしにする
SDKが例外を捕まえ、生の例外メッセージを持つエラー結果にしてClaudeへ渡します。ループは続きます。
自前で捕まえて isError: true を返す
開発者が書いた文面が渡ります。失敗したリクエストの内容や、次に試す手立てを添えられます。
どちらの場合もClaudeは再試行や別ツールへの切り替え、失敗の説明ができます。生の例外文字列だけでは行動の手がかりが乏しいときに、自前で文面を組み立てます。Pythonでは isError ではなく "is_error": True と書きます。
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
};
}
return { content: [{ type: "text", text: JSON.stringify(await response.json()) }] };
} catch (error) {
return {
content: [{ type: "text", text: `Failed to fetch data: ${String(error)}` }],
isError: true
};
}
}構造化データを返す — structuredContentとPythonでの制限
content 配列とは別に、structuredContent というJSONフィールドを結果に付けられます。グラフの画像とその元データを同時に返すような場面で、Claudeが数値を文字列から読み取り直す必要がなくなります。
return {
content: [{ type: "image", data: chartPngBase64, mimeType: "image/png" }],
structuredContent: { series: "temperature_2m", unit: "celsius", points: [16.7, 17.2, 18.0] }
};ツールの振る舞いを申告する(アノテーション)
アノテーションは、ツールの性質をClaudeやクライアントに伝えるメタデータです。TypeScriptでは tool() の第5引数に { annotations: { readOnlyHint: true } } の形で渡し、Pythonでは @tool の annotations キーワード引数に ToolAnnotations(readOnlyHint=True) を渡します。値はすべて真偽値です。
| フィールド | 既定値 | 意味 |
|---|---|---|
readOnlyHint | 既定値false | 意味環境を変更しない。他の読み取り専用ツールと並列に呼べるかを左右する |
destructiveHint | 既定値true | 意味破壊的な更新を行う可能性がある |
idempotentHint | 既定値false | 意味同じ引数で繰り返し呼んでも追加の効果がない |
openWorldHint | 既定値true | 意味プロセス外のシステムに到達する(Web検索など) |
readOnlyHint 以外の3つは情報提供のみで、動作を変えません。readOnlyHint: true を付けたツールがディスクに書き込んでいても実行時エラーにはならないため、ハンドラの実際の挙動とアノテーションを合わせるのは書き手の仕事です。
画像やリソースを結果として返す
content 配列には text 以外に image / audio / resource / resource_link ブロックも混在させられます。resource ブロックは、生成したファイルや外部システムのレコードのように、あとで名前を付けて参照させたい内容に向いています。URIはClaudeが参照するためのラベルで、SDKがそのパスを実際に読みに行くわけではありません。resource_link ブロックはClaudeにテキストとして渡り、TypeScriptでは tool_use_result.resourceLinks から取り出せます。Pythonではこのブロックは生成されません。中身は text か、バイナリならbase64の blob に入れます。
return {
content: [
{
type: "resource",
resource: { uri: "file:///tmp/report.md", mimeType: "text/markdown", text: "# Report\n..." }
}
]
};音声ブロックの扱いは言語によって差があります。TypeScriptはファイルに保存してパスをテキストブロックとしてClaudeに返しますが、Pythonは音声ブロックをそのまま結果から取り除き、警告をログに出すだけです。この非対称があるので、音声を多用するツールは両言語で返り値を動作確認しておくと確実です。Python SDKは、バイナリデータを持つ resource ブロック(resource.blob)も結果から取り除いて警告を出します。バイナリのリソースを返すならTypeScript側で実装するか、テキストベースのresourceに絞ります。
ツールへのアクセス制御 — 見えるかどうかと、承認がいるかどうか
tools オプションと許可リストは、性質が異なる2つの層に効きます。Managed Agentsでも標準ツールの一覧と有効化・無効化は同じくavailabilityとpermissionを分けて設定します。availability(Claudeの視界に入るか)とpermission(呼び出しに承認がいるか)は別物で、両者の区別が権限設計の出発点です。
| 設定 | 層 | 効果 |
|---|---|---|
tools: ["Read", "Grep"] | 層availability | 効果列挙した組み込みツールだけが見える。MCPツールには影響しない |
tools: [] | 層availability | 効果組み込みツールを全部外し、MCPツールだけを見せる |
allowedTools | 層permission | 効果列挙したツールは承認プロンプトなしで実行される |
disallowedTools(裸のツール名) | 層両方 | 効果ツール自体を視界から外す |
disallowedTools("Bash(rm *)" のようなスコープ付き) | 層permission | 効果視界には残るが、一致する呼び出しだけ拒否する |
組み込みツールを完全に無効化したいときは tools から外すか裸の名前で disallowedTools に入れます。スコープ付きのdisallowedルールはツールを視界に残したまま特定の呼び出しだけを拒否するので、Claudeが無駄なターンを使って弾かれる呼び出しを試すことがあります。
出力が大きいとき・遅いとき — 数値の上限
ハンドラの結果サイズと所要時間には、SDKが適用する上限があります。Claude Codeと同じMCP出力の制限が、SDKにも掛かります。
インプロセスMCPで気にする上限
出力トークン
25,000超
画像を含まない成功結果はファイルに保存され、パスを示すエラーメッセージに置き換わる。MAX_MCP_OUTPUT_TOKENS で変更
テキスト結果
50,000字超
anthropic/maxResultSizeChars を宣言していなければ、トークン上限に関係なくファイルに保存される
接続待ち
30秒
既定値。MCP_TIMEOUT(ミリ秒)で変更。ツール呼び出しの所要時間は MCP_TOOL_TIMEOUT
精度の目安
30〜50個
同時にロードするツールがこれを超えると選択精度が落ちる。カタログ上限は10,000個
TypeScriptでは、1つのSDK MCPサーバーのツール呼び出し制限を createSdkMcpServer() の timeout で個別に設定できます。単位はミリ秒で、1000以上の整数を指定します。この値は MCP_TOOL_TIMEOUT の代わりに適用され、TypeScript Agent SDK v0.3.248以降で使えます。大きな結果を返すツールは、要約や絞り込みをハンドラ側で済ませてから返すと、保存先ファイルを読み直すターンを減らせます。
ツールが増えたときの注意 — Tool Searchとの関係
Tool Searchは既定でオンになっており、SDK MCPサーバーのツールも自動的に遅延ロードの対象になります。Claudeにはツール名の一覧だけが見え、必要になったタイミングで完全なスキーマを取りに行きます。常に使う少数のツールをコンテキストに残しておきたいときは、tool() の第5引数(TypeScript)で alwaysLoad: true を渡すか、createSdkMcpServer() の alwaysLoad オプションでサーバー単位にまとめて指定します。数百〜数千のツールを扱う運用の詳細はTool Searchの使い方にまとめています。
早見表 — 3つの選択肢のどれを使うか
「独自の処理をClaudeに渡したい」というゴールは同じでも、実装先は3通りあります。
| 選択肢 | 実行場所 | 配布・共有 | 向く場面 |
|---|---|---|---|
| SDKカスタムツール(本記事) | 実行場所自分のアプリと同じプロセス | 配布・共有不可(コードに埋め込み) | 向く場面自社アプリ専用の軽い処理(Pythonでは、structuredContentを使わない場合) |
| 外部MCPサーバーへの接続 | 実行場所別プロセス・別サーバー | 配布・共有既存サーバーをそのまま利用 | 向く場面GitHub / Slack / DBなど既製のMCPサーバーがある場合 |
| MCPサーバー自作(標準SDK) | 実行場所別プロセス | 配布・共有npmやMCPレジストリで配布可能 | 向く場面複数プロジェクトで再利用したい、PythonでstructuredContentが必要 |
よくあるつまずき
allowedToolsへの登録漏れ: ツール自体は動いていても、mcp__weather__get_temperatureをallowedToolsに入れ忘れると、呼び出しは権限フローに回され、最終的にcanUseToolコールバックが判断します。dontAskモードではこの段階が省かれて呼び出しが拒否されるため、登録漏れがそのままツールの使えない状態になります。- Pythonのenumが書けない:
{"unit": str}のような簡易スキーマにはenum制約を書く手段がありません。選択肢を絞りたい場合はフルJSON Schema辞書("enum": [...])に切り替える必要があります。 - 画像をURLのまま返そうとする:
imageブロックにURLフィールドはありません。ハンドラ側で画像を取得し、バイト列をbase64エンコードしてからdataに入れます。data:image/...;base64,の接頭辞は付けず、mimeTypeは必須です。画像として視覚入力になるのはPNG・JPEG・GIF・WebPで、それ以外の形式はディスクに保存され、パスだけが文字で渡ります。 - ツールが一覧から消える(TypeScript): 入力スキーマをJSON Schemaに変換できないと、そのツールだけが一覧から外れ、警告が出ます。Node.jsでは
CLAUDE_SDK_MCP_TOOL_SCHEMA_UNCONVERTIBLEというコードの警告です。TypeScript Agent SDK v0.3.286より前は、1つ変換できないだけでサーバー全体のツール一覧が失敗し、どのツールもClaudeに届きませんでした。 - 並列実行されないと思い込む: カスタムツールは既定で直列に呼び出されます。副作用のないツールに
readOnlyHint: trueを付けると、他の読み取り専用ツールと並列実行の対象になります。並列呼び出しそのものが失敗する場合は、SDK MCPサーバー特有の既知の問題を扱ったStream closedエラーの原因と回避策も確認してください。
まとめ
動かないときに疑う先は、許可リストの書き方、Pythonのスキーマ、結果サイズの上限の3点です。ここを押さえれば、自分のアプリの中で完結するツールはSDKカスタムツールだけで足ります。