メインコンテンツまでスキップ

ツール設計パターンとアンチパターン

LLM が正しく選び、少ないコンテキストで使えるツールを設計するための指針を解説します。コンテキスト問題や CLI との比較の議論を踏まえた設計論です。プロトコル上のツールの定義と実行の仕様は Tools、サーバーの実装は MCPサーバーの実装 で扱い、ここでは「どういう単位で、どう書けば、モデルが迷わず、安全に使えるか」に絞ります。

1. このドキュメントで扱うこと​

  • API をそのまま MCP 化するアンチパターン
  • タスク指向のツール
  • description の書き方
  • 引数設計
  • 合成性の欠如を補う
  • 出力サイズの制御
  • LLM が自己修正できるエラー
  • 破壊的操作の安全設計
  • 冪等性と状態ハンドル
  • プロンプトキャッシュへの配慮

API をそのまま MCP 化するアンチパターン​

既存の REST API や OpenAPI の定義から、エンドポイントごとに 1 つのツールを自動生成する作り方は、手軽で、最初のデモは動きます。しかし、これは MCP の設計として最も避けたいパターンです。Thoughtworks Technology Radar は、この素朴な API からの変換を Hold (見送り) としています。この評価が対象とするのは、プロトコルではなく、設計の失敗です。

問題は 3 つあります。

  • 定義が肥大する: 40 個の CRUD のツールは、エージェントのためのインターフェースではなく、生成された API クライアントです。ツール定義は、使われるかどうかにかかわらず、コンテキストに常駐します。コンテキストの影響は コンテキスト肥大問題と対策 を参照してください
  • 選択の精度が下がる: 似た名前、似た説明のツールが大量にあると、モデルは正しいものを選べなくなる。get_user、get_user_by_email、list_users、search_users のどれを使えばよいか、モデルには判断できない
  • 1 つの作業に多数の呼び出しが要る: 利用者の依頼は、「先月の未払いの請求書を、顧客ごとにまとめて」のように、複数のエンドポイントをまたぐ。REST の粒度のままでは、モデルが中間データを抱えながら、何度も往復する。往復のたびに、トークンと時間と失敗の可能性が増える

API は、サーバーの内部で使うものであって、モデルに見せるものではありません。モデルに見せるのは、利用者が実際に頼む作業の単位です。

タスク指向のツール​

ツールは、API のエンドポイントではなく、利用者が頼む作業の単位で作ります。1 つのツールの中で、複数の API 呼び出しや、結果の絞り込み、整形を行い、モデルには、そのタスクの結果だけを返します。

観点エンドポイントごとタスク指向
ツールの数数十数個から十数個
呼び出し回数1 つの依頼に多数1 回から数回
中間データモデルのコンテキストを通るサーバー内で処理される
説明技術的な API の仕様利用者の目的の言葉

次の例は、同じ作業を、2 通りに設計したものです。

エンドポイントごと: list_customers, get_customer, list_invoices, get_invoice, list_payments, ...
タスク指向: find_overdue_invoices (顧客ごとにまとめた未払いの請求書の一覧を返す)
get_customer_account_summary (顧客の請求、入金、連絡先を 1 回で返す)

タスク指向にすると、ツールの数と定義のサイズが減り、モデルの選択も正確になります。一方で、何でもできる 1 つの万能のツール (call_api(path, method, body)) は、選択の問題は避けられても、引数の自由度が大きすぎて、検証も確認もできず、最小権限の考え方に反します。タスクの粒度は、利用者の言葉で説明できる単位を目安にします。

どのツールを作るかは、利用者が実際に頼む依頼の一覧から逆算します。既存の API の一覧から始めないことが、最初の設計の分かれ目です。

description の書き方​

モデルは、ツールの name と description、各引数の description を読んで、どのツールをいつ使うかを判断します。description の質は、ツールの数より、選択の精度に影響するという指摘もあります。次の 3 つを書きます。

  1. 何をするか: 動詞で始め、結果を 1 文で
  2. いつ使うか: どんな依頼のときに選ぶか
  3. いつ使わないか: 似たツールとの使い分け。隣のツールの名前を挙げる
悪い例: "請求書を取得する"
良い例: "未払いで、支払期限を過ぎた請求書を、顧客ごとにまとめて返す。『督促が必要な顧客』『延滞の一覧』の依頼に使う。
1 件の請求書の詳細は get_invoice を使う。"

そのほかの指針です。

  • 重要な情報は、先頭に置く。クライアントによっては、description を切り詰める (Claude Code は既定で 2,048 文字) ため、長い説明の末尾は届かない可能性がある
  • 長くしすぎない。description は、定義のサイズの大部分を占める。選択に必要な情報に絞り、詳しい使い方は、結果やエラーのメッセージで伝える
  • 副作用や制約は、description に書く。「課金が発生する」「24 時間で失効する」のように、モデルが呼ぶ前に知るべきことを書く
  • 引数の description にも、形式の例を書く (「ISO 8601 の日付 (例: 2026-07-28)」)
  • title は、利用者に見せる表示名で、モデルが読む description とは別。name は、1〜128 文字で、英数字、_、-、. だけにし、サーバーの中で一意にする (大文字と小文字は区別される)。動詞と対象で、一貫した命名にする (search_orders、get_order、cancel_order)
  • 外部のサーバーのツールを集約するホストでは、名前が衝突し得る。サーバー側でも、search のような汎用的すぎる名前は避ける

description は、モデルが読む指示なので、信頼できない外部の入力を埋め込んではなりません。ツールの定義の書き換えによる攻撃は AIエージェント特有の脅威 を参照してください。

引数設計​

引数は、モデルが埋めるフォームです。モデルが間違えにくく、間違えても検出できるように作ります。

  • 必須と任意を分ける: 必須は最小限にする。モデルが推測しなければ埋められない引数は、必須にしない。任意の引数には、安全で妥当な既定値を持たせる (limit の既定は 20 など)
  • 列挙型を使う: 取り得る値が決まっている引数は、enum にする。自由な文字列にすると、"pending" と "Pending" と "未処理" が混ざる
  • 範囲を宣言する: minimum、maximum、maxLength、pattern で、スキーマに制約を書く。SDK がスキーマで検証するので、ハンドラーには検証済みの値が届く
  • 平坦にする: 深くネストしたオブジェクトや、oneOf / anyOf を多用したスキーマは、モデルが正しく組み立てにくい。引数は、できるだけ平坦な構造にする
  • ID より名前を受ける: 利用者の言葉 (顧客名、プロジェクト名) で呼べるなら、サーバー内で解決する。モデルに、まず ID を検索させる必要をなくす。曖昧なときは、候補を返してモデルに選ばせる
  • 引数のないツール: { "type": "object", "additionalProperties": false } を inputSchema にする (仕様が推奨する形)。null は使えない
  • スキーマの方言: $schema がなければ JSON Schema 2020-12 になる。2020-12 を使うことが推奨される

outputSchema による出力の型付け: ツールは、結果の構造を outputSchema で宣言できます。宣言すると、サーバーはそれに適合する structuredContent を返さなければならず (MUST)、クライアントは、結果を検証することが推奨されます (SHOULD)。structuredContent は、任意の JSON の値 (オブジェクト、配列、文字列など) です。互換性のために、構造化した結果を返すツールは、シリアライズした JSON を、content の text にも入れることが推奨されます。outputSchema があると、クライアントは Code Mode で型付きの API を生成でき、モデルは必要なフィールドだけを扱えるため、コンテキストの節約にもなります。

ヘッダーへの写し (x-mcp-header): Streamable HTTP では、引数の一部を Mcp-Param-{name} のヘッダーに写せます。ゲートウェイが、本体を解析せずに、リージョンやテナントのような値でルーティングや制限をするための仕組みです。プロパティのスキーマに "x-mcp-header": "Region" のように付けます。対象は、文字列、整数、真偽値で、number は使えず、スキーマのルートから properties だけをたどって届く場所に限ります。ヘッダーは、ネットワークの中間装置から見えるため、パスワード、API キー、トークン、個人情報の引数には付けてはなりません。

合成性の欠如を補う​

CLI では、grep、sort、head、jq をパイプでつなげば、結果をその場で絞り込めます。MCP のツールの結果は、そのままモデルのコンテキストに入るため、この合成の手段がありません。この差を補うのが、ツールの引数です。パイプに相当する次の引数を、サーバー側で用意します。

CLI のパイプツールの引数例
grepフィルターstatus、customer、since、query
sort並び替えsort_by、order (enum)
head件数の上限limit (既定と最大を決める)
cut / jqフィールドの選択fields (enum の配列)。必要な列だけを返す
wc -l集計count_only、あるいは集計専用のツール

重要なのは、絞り込みをサーバー側で行うことです。モデルが、1,000 件を受け取ってから、頭の中で絞り込む設計は、トークンを無駄にし、誤りも増えます。集計や変換が多様なら、クライアントが Code Mode (コードを書いて、サンドボックスで実行する方式) で絞り込めるように、outputSchema を宣言する方法もあります。

出力サイズの制御​

ツールの結果は、すべてモデルのコンテキストに入ります。出力サイズは、設計の段階で制御します。

  • ページネーション: limit と cursor を持たせ、結果にも、次のページを示す nextCursor と、全体の件数 (分かれば) を入れる。「結果が途中で切れていること」を、モデルが分かるようにする
  • 既定の件数を絞る: 既定の limit は小さく (10〜50)、最大も決めて、サーバーで強制する。最大を超える要求は、エラーではなく、最大に丸めて、そう伝える
  • 要約して返す: 一覧は、詳細な全フィールドではなく、ID、名前、状態のような、選ぶのに足りる最小限の項目にする。詳細は、別のツール (get_order) で取得させる
  • 巨大なデータは、リソースやファイルの参照で返す: ログ、ドキュメント、画像のような大きな内容は、結果に埋め込まず、resource_link で URI を返し、必要なときにクライアントが resources/read で取得する。リソースのリンクは、resources/list の結果に出るとは限らない。小さな内容なら、埋め込みリソース (resource) で返してもよい
  • 切り詰めを明示する: 出力を切るときは、切ったことと、全体を得る方法 (次のページ、絞り込みの引数) を、結果に書く。黙って切ると、モデルは完全な結果だと信じる

画像と音声は、Base64 で結果に入るため、サイズが大きく、トークンの消費も大きくなります。必要なときだけ返します。

LLM が自己修正できるエラー​

ツールのエラーは 2 種類あります。この区別が、自己修正できるかどうかを決めます。

種類例返し方
プロトコルエラー未知のツール、スキーマに合わない不正なリクエスト、サーバーの障害JSON-RPC の error (例: -32602)。モデルは回復しにくい
ツール実行エラーAPI の失敗、入力値の検証エラー、ビジネスルール違反成功のレスポンスで isError: true

LLM が引数を直して再試行できる失敗は、後者にします。仕様は、クライアントはツール実行エラーを、自己修正を可能にするためにモデルへ渡すべきだとしています。メッセージには、次の 3 つを含めます。

  1. 何が悪いか: どの引数が、なぜ受け付けられなかったか
  2. どう直せばよいか: 許される値、形式、範囲。「使える値は pending、shipped、cancelled です」
  3. 現在の状況: 必要なら、現在の日付や、有効な ID の候補のような、直すための情報
悪い例: "Invalid input"
良い例: "出発日 2026-01-05 は使えません。出発日は未来の日付である必要があります。現在の日付は 2026-10-13 です。"

汎用的なエラーメッセージは、モデルが直せず、再試行が無駄になります。逆に、メッセージに、内部のスタックトレースや、接続文字列、他の利用者の情報を含めてはなりません。エラーの本文も、モデルが読む出力であり、情報の漏洩の経路になり得ます。権限がない場合 (403) は、存在の有無を漏らさないよう、「見つからない」と同じ形で返す選択もあります。

破壊的操作の安全設計​

削除、送金、送信、公開のように、取り消せない操作を持つツールは、複数の層で守ります。

1. アノテーションで性質を宣言する: ツールには、readOnlyHint や destructiveHint のような、動作を説明するアノテーションを付けられます。クライアントが UI の見せ方や確認の方針を決める参考になります。ただし、仕様は、クライアントはアノテーションを、信頼できるサーバーから得たもの以外は信用してはならないとしています (MUST)。したがって、アノテーションは安全の仕組みそのものではなく、補助です。サーバー側でも、必ず制限を実装します。

2. dry-run で、確認の材料を作る: 破壊的な操作は、dry_run のような引数 (または、プレビュー専用のツール) を持たせ、実行せずに、何が起こるか (削除される件数、対象、金額) を返します。モデルは、それを利用者に見せ、承認を得てから、本番の実行に進めます。

3. Elicitation で、実行の直前に確認する: サーバーは、実行の直前に、利用者への確認を求められます。tools/call への応答を InputRequiredResult にし、elicitation/create の form モードで、確認の内容を示します。クライアントは、利用者の入力を集めて、元のリクエストを再送します (MRTR)。確認を、モデルの判断ではなく、サーバーが強制する形にできる点が重要です。ただし、次の点に注意します。

  • クライアントが Elicitation に対応しているとは限らない。対応を宣言していないクライアントには、MissingRequiredClientCapability (-32021) を返すか、代替の経路 (dry-run だけを許し、実行は拒否する) を用意する
  • form モードで、パスワード、API キー、トークン、支払い情報のような機密を求めてはならない。そうした入力は、URL モードで画面外に出す
  • requestState は、クライアントを経由する値なので、改ざんされ得る。署名や暗号化で保護し、検証する。MRTR の詳細は ElicitationとMulti Round-Trip Requests を参照

4. 最小権限のスコープ: 全権限を持つ単一のトークンは、漏れたときの影響が最大になります。読み取りと書き込みを、別のスコープに分け、ツールごとに必要なスコープを決めます。仕様は、tools/list が返す集合を、リクエストの認可 (付与されたスコープ) に応じて変えてよいとしています。読み取りのスコープしか持たない呼び出し元には、書き込みのツールを見せない設計ができます。ただし、一覧に出さないことは、認可の代わりにはなりません。ツールの実行時にも、必ずスコープを検証します。不足するスコープは、403 と insufficient_scope で返し、クライアントがステップアップできるようにします (MCPの認可 を参照)。また、cacheScope を public にした一覧は、認可の異なる呼び出し元の間で共有され得るため、利用者ごとに変わる一覧は private にします。

5. 仕様が定める、サーバーの義務: すべての入力を検証する、アクセス制御を実装する、ツールの呼び出しにレート制限を掛ける、出力をサニタイズする。クライアントの確認 UI があることを、これらの代わりにしません。

冪等性と状態ハンドル​

副作用のある操作は、ネットワークの障害やクライアントの再試行で、2 回実行されることがあります。ストリームが切れると、クライアントは新しいリクエスト ID で、最初から送り直し、サーバーから見ると別のリクエストになるためです。作成、送信、課金のツールは、再試行されても安全に設計します。

  • 副作用のあるツールに、クライアントが生成する idempotency_key を持たせ、同じキーでの 2 回目の呼び出しには、1 回目の結果を返す
  • 「同じ内容は 1 回」を、ストア側の一意制約で保証する
  • 「状態を変える」代わりに、「望ましい状態を宣言する」形にする (「在庫を 1 減らす」ではなく、「在庫を 9 にする」。「メンバーを追加する」ではなく、「メンバーが含まれる状態にする」)

2026-07-28 には、プロトコル上のセッションがありません。複数の呼び出しにまたがる状態 (カゴ、開いたブラウザ、データベースのトランザクション) は、サーバーが発行する明示的なハンドルを、ふつうのツールの引数として受け渡します。公式の仕様のガイダンス (非規範的) は、次を求めています。

  • 所有者の検証: ハンドルは名前であって、権限ではない。毎回、呼び出し元の認可をハンドルに対して検証する。認証のないサーバーでは、ハンドルがそのまま bearer トークンなので、十分なエントロピー (UUIDv4 など) で生成し、有効期間を限る
  • 不透明性: 内部の構造を符号化したハンドルは、解析や推測を招く。意味を持たない識別子にする
  • 有効期間の明示: 保持の方針を、作成するツールの description に書く ("baskets expire after 24 hours of inactivity" のように)
  • 失効のエラー: 期限切れや未知のハンドルは、そのことを伝えるツール実行エラーを返し、モデルが新しく作り直せるようにする

ハンドルの乗っ取りの攻撃は MCPセキュリティベストプラクティス を、実装は MCPサーバーの実装 を参照してください。

プロンプトキャッシュへの配慮​

多くのモデル提供元は、プロンプトの先頭 (ツール定義の配列を含む) をキャッシュします。ツールの定義を途中で足したり消したり並び替えたりすると、このキャッシュが切れ、キャッシュミスが、減らしたはずの定義より多くのトークンを使うことがあります。サーバーの側では、次の点を守ります。

  • ツールを決定的な順序で返す: 基になる集合が変わらなければ、毎回同じ順序で返す (SHOULD)。クライアントが一覧を確実にキャッシュでき、プロンプトキャッシュの命中率も上がる。マップの反復順や、並行処理の完了順に依存して、順序が揺れないようにする
  • 実行中に定義を変えない: 2026-07-28 では、tools/list が返す集合は、接続ごと、または他のリクエストの副作用として変わってはならない (MUST NOT)。変えてよいのは、時間の経過 (list_changed)、またはリクエストの認可に応じた違いだけ
  • 変更の頻度を絞る: 変更するときは、notifications/tools/list_changed で知らせる。クライアントは、通知を受けると、TTL の期限前でもキャッシュを古いものとして扱う
  • ttlMs と cacheScope を適切に返す: すべての利用者に同じ一覧なら、public で、TTL を長めにする。利用者ごとに違うなら、private
  • description を頻繁に書き換えない: 説明文に、現在時刻や、変化する数値を入れると、そのたびにキャッシュが切れる。動的な値は、結果に入れる

2. よくあるアンチパターンと改善​

アンチパターン問題改善
REST の全エンドポイントをツール化定義が肥大化し、選択精度も下がるユーザーのタスク単位に集約する
description が一行だけいつ使うべきか判断できない用途と使い分けを明記する
巨大な結果をそのまま返すコンテキストを圧迫する絞り込み引数とページネーションを用意する
汎用的なエラーメッセージLLM が修正できず再試行が無駄になる原因と修正方法を返す
全権限を持つ単一のトークン漏洩時の影響が大きい操作ごとのスコープに分ける
何でもできる万能ツール引数が自由すぎて検証も確認もできないタスクごとに、引数を絞ったツールに分ける
副作用のあるツールが冪等でない再試行で二重に実行される冪等性キーと、ストア側の一意制約
一覧の順序や定義が揺れるプロンプトキャッシュが切れる決定的な順序で、実行中は定義を変えない

3. 開発者がつまずきやすいポイント​

OpenAPI から全自動で生成して終わる​

最初のデモは動きますが、ツールが多すぎ、description が機械的で、モデルが選べません。生成は、叩き台にとどめ、利用者の依頼から逆算して、タスクの単位に作り直します。

description に、API の仕様書をそのまま貼る​

長く、技術的で、いつ使うかが書かれていません。何をするか、いつ使うか、いつ使わないかに絞ります。

絞り込みを、モデルに任せる​

全件を返して、モデルに絞り込ませると、トークンを浪費し、見落としも起きます。フィルター、並び替え、件数の上限、フィールドの選択を、引数として用意します。

結果が途中で切れていることを伝えない​

件数の上限で黙って切ると、モデルは完全な結果だと信じます。nextCursor や、「上位 20 件のみ。さらに絞り込むには since を指定」のような説明を、結果に含めます。

エラーを例外やプロトコルエラーで返す​

入力値の誤りをプロトコルエラーにすると、モデルは回復できません。直せる失敗は、isError: true のツール実行エラーとして、原因と直し方を書きます。

アノテーションだけで安全だと考える​

destructiveHint を付けても、クライアントが信用するとは限らず、悪意あるサーバーは偽装できます。サーバー側でも、スコープの検証と、確認の強制を実装します。

一覧に出さないことを、認可の代わりにする​

呼び出し元の権限に応じて、tools/list から書き込みのツールを外しても、名前を知っていれば、呼べてしまいます。実行時にも、必ず権限を検証します。

確認をモデルの判断に任せる​

「実行してよいか、利用者に尋ねてください」という description は、モデルが守るとは限りません。確認が必要な操作は、Elicitation や、dry-run と本番の分離で、サーバーが強制します。

ハンドルの所有者を確認しない​

ハンドルを知っていれば、他の利用者の状態を操作できる実装は、乗っ取りの対象です。毎回、呼び出し元とハンドルの所有者を照合します。

一覧の順序が、実行のたびに変わる​

マップの反復順や、並行処理の完了順で、ツールの順序が揺れると、プロンプトキャッシュが切れ、キャッシュの命中率が下がります。名前などでソートして、決定的にします。

description に、変化する値を入れる​

現在時刻や、件数を description に入れると、そのたびにキャッシュが切れ、定義の変更を検知する仕組み (Rug pull の検知) も、誤検知が増えます。動的な値は、結果に入れます。

4. コード例​

エンドポイントごとの設計とタスク指向の設計​

悪い設計 (エンドポイントごと)。

[
{ "name": "get_customers", "description": "顧客を取得する" },
{ "name": "get_customer_by_id", "description": "顧客を ID で取得する" },
{ "name": "get_invoices", "description": "請求書を取得する" },
{ "name": "get_invoice_by_id", "description": "請求書を ID で取得する" },
{ "name": "get_payments", "description": "入金を取得する" }
]

良い設計 (タスク指向)。description で用途と使い分けを示し、絞り込みの引数、列挙型、既定値、outputSchema を持たせます。

{
"name": "find_overdue_invoices",
"title": "延滞している請求書の検索",
"description": "支払期限を過ぎた未払いの請求書を、顧客ごとにまとめて返す。『督促が必要な顧客』『延滞の一覧』の依頼に使う。1 件の請求書の詳細は get_invoice を使う。結果は金額の大きい順で、最大 limit 件。続きがあれば next_cursor を返す。",
"inputSchema": {
"type": "object",
"properties": {
"min_days_overdue": {
"type": "integer",
"minimum": 1,
"default": 1,
"description": "延滞の日数の下限。既定は 1"
},
"customer_name": {
"type": "string",
"description": "顧客名の一部。指定すると、その顧客だけに絞る"
},
"sort_by": {
"type": "string",
"enum": ["amount", "days_overdue"],
"default": "amount",
"description": "並び順。amount は金額の大きい順"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 20,
"description": "返す顧客の件数の上限。既定は 20、最大は 100"
},
"cursor": {
"type": "string",
"description": "前回の結果の next_cursor。続きを取得するときに指定する"
}
},
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"properties": {
"customers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"customer_id": { "type": "string" },
"name": { "type": "string" },
"overdue_total": { "type": "number", "description": "延滞の合計額 (円)" },
"oldest_days_overdue": { "type": "integer" }
},
"required": ["customer_id", "name", "overdue_total", "oldest_days_overdue"]
}
},
"next_cursor": { "type": "string" },
"truncated": { "type": "boolean", "description": "上限で切った場合は true" }
},
"required": ["customers", "truncated"]
},
"annotations": { "readOnlyHint": true }
}

この定義のポイントは次のとおりです。

  • description に、何をするか、いつ使うか、いつ使わないか (get_invoice)、並び順、続きの取得方法を書いている
  • 絞り込み (customer_name、min_days_overdue)、並び替え (sort_by)、件数の上限 (limit)、ページネーション (cursor) が引数にある
  • enum、default、minimum、maximum でスキーマが制約している
  • outputSchema で、結果の構造と、切り詰めの有無 (truncated) を宣言している
  • readOnlyHint は、クライアントの見せ方の参考値にとどまり、サーバーは別に権限を検証する

結果のサイズと切り詰めの伝え方​

{
"resultType": "complete",
"content": [
{
"type": "text",
"text": "延滞している顧客は 137 社。金額の大きい上位 20 社を返します。続きは cursor を指定して取得できます。"
}
],
"structuredContent": {
"customers": [
{ "customer_id": "c_001", "name": "株式会社サンプル", "overdue_total": 2400000, "oldest_days_overdue": 62 }
],
"next_cursor": "eyJvZmZzZXQiOjIwfQ",
"truncated": true
}
}

大きな内容は、結果に埋め込まず、リンクで返します。

{
"type": "resource_link",
"uri": "reports://overdue/2026-10",
"name": "overdue-2026-10.csv",
"description": "延滞している全請求書の CSV (137 社、約 4,200 行)",
"mimeType": "text/csv"
}

自己修正できるエラー​

{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "sort_by に 'date' は使えません。使える値は amount、days_overdue です。金額の大きい順なら amount を指定して、もう一度呼んでください。"
}
],
"isError": true
}
}

破壊的操作の dry-run と確認​

破壊的なツールは、dry_run を持たせ、既定を true にします (確認なしに実行されない向きに倒します)。

{
"name": "delete_stale_projects",
"description": "90 日以上更新のないプロジェクトを削除する。削除は取り消せない。dry_run が true (既定) のときは、削除せず、対象の一覧と件数だけを返す。実行するときは、まず dry_run で対象を利用者に見せ、承認を得てから dry_run を false にして呼ぶ。",
"inputSchema": {
"type": "object",
"properties": {
"older_than_days": { "type": "integer", "minimum": 90, "default": 90 },
"dry_run": { "type": "boolean", "default": true, "description": "true のときは削除せず、対象だけを返す" }
},
"additionalProperties": false
},
"annotations": { "destructiveHint": true }
}

さらに、サーバーが確認を強制するには、dry_run が false の呼び出しに対して、Elicitation を要求します。InputRequiredResult を返します。

{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "input_required",
"inputRequests": {
"confirm_delete": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "12 件のプロジェクトを削除します。この操作は取り消せません。",
"requestedSchema": {
"type": "object",
"properties": {
"confirmed": { "type": "boolean", "description": "削除を実行する" }
},
"required": ["confirmed"]
}
}
}
},
"requestState": "<署名したサーバーの状態>"
}
}

クライアントが、inputResponses と requestState を付けて再送し、confirmed が true のときだけ、サーバーは削除を実行します。requestState はクライアントを経由するので、署名して、検証します。

状態ハンドルを返すツール​

{
"name": "create_basket",
"description": "注文用のカゴを作る。カゴは 24 時間操作がないと失効する。結果の basket_id を、add_item と place_order に渡す。",
"inputSchema": { "type": "object", "additionalProperties": false }
}
{
"content": [{ "type": "text", "text": "カゴ bsk_a1b2c3 を作りました。24 時間操作がないと失効します。" }],
"structuredContent": { "basket_id": "bsk_a1b2c3", "expires_in_hours": 24 }
}

失効したハンドルが渡されたときは、モデルが回復できるエラーを返します。

{
"content": [{ "type": "text", "text": "カゴ bsk_a1b2c3 は失効しているか、存在しません。create_basket で新しく作成してください。" }],
"isError": true
}

設計のチェックリスト​

項目確認すること
粒度利用者の依頼の言葉で説明できる単位か。エンドポイントの 1 対 1 になっていないか
description何をするか、いつ使うか、いつ使わないかが、先頭に書かれているか
引数必須が最小か。列挙型、既定値、範囲があるか。絞り込み、並び替え、上限、フィールド選択があるか
出力既定の件数が小さいか。切り詰めを伝えているか。outputSchema があるか。巨大なデータは参照で返しているか
エラー直せる失敗は isError: true で、原因と直し方を返しているか。内部情報を含んでいないか
破壊的操作dry-run があるか。サーバーが確認を強制しているか。実行時にスコープを検証しているか
冪等性再試行で二重に実行されないか。ハンドルの所有者を検証しているか
キャッシュツールの順序が決定的か。実行中に定義を変えていないか。ttlMs と cacheScope が適切か

5. まとめ​

  • REST のエンドポイントごとにツールを作る設計は避ける。定義が肥大し、選択の精度が下がり、1 つの依頼に多数の呼び出しが要る。Thoughtworks Technology Radar は Hold としている
  • ツールは、利用者が頼む作業の単位 (タスク) で作り、ツールの数と定義のサイズを絞る。ただし、万能の 1 ツールにはしない
  • description には、何をするか、いつ使うか、いつ使わないかを、先頭から書く。選択の精度を左右する
  • 引数は、必須を最小にし、列挙型、既定値、範囲を宣言し、平坦にする。outputSchema で出力を型付けする
  • CLI のパイプの代わりに、フィルター、並び替え、件数の上限、フィールドの選択を引数として用意し、絞り込みをサーバー側で行う
  • 出力は、ページネーション、要約、リソースへの参照で小さくし、切り詰めたことを明示する
  • 直せる失敗は、isError: true のツール実行エラーとして、何が悪く、どう直すかを返す
  • 破壊的操作は、サーバー側の検証、dry-run、Elicitation による確認、最小権限のスコープで守る。アノテーションは補助にすぎない
  • 副作用のある操作は、再試行を前提に冪等にし、状態はハンドルで受け渡して、所有者を毎回検証する
  • ツールは決定的な順序で返し、実行中に定義を変えない。プロンプトキャッシュを保つ

関連ドキュメント​

参考資料​