Claude Media
Agent SDKカスタムツールの作り方 — インプロセスMCPサーバーで機能拡張

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. 1

    ツールを定義する

    TypeScriptは tool() 関数、Pythonは @tool デコレータで、4要素を1つのツールにまとめます。

  2. 2

    サーバーにまとめる

    createSdkMcpServer()(Pythonは create_sdk_mcp_server())に tools 配列として渡します。

  3. 3

    queryに登録する

    query() の mcpServers にサーバーを渡します。キー名が、Claudeに見えるツール名の一部になります。

  4. 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}&current=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個

ツール数の目安と上限はTool Searchのページの値です

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カスタムツールだけで足ります。

この記事を共有:XはてブLinkedIn