ツール設計パターンとアンチパターン
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 件の請求書の詳細は 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 つを含めます。
- 何が悪いか: どの引数が、なぜ受け付けられなかったか
- どう直せばよいか: 許される値、形式、範囲。「使える値は pending、shipped、cancelled です」
- 現在の状況: 必要なら、現在の日付や、有効な 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 による確認、最小権限のスコープで守る。アノテーションは補助にすぎない
- 副作用のある操作は、再試行を前提に冪等にし、状態はハンドルで受け渡して、所有者を毎回検証する
- ツールは決定的な順序で返し、実行中に定義を変えない。プロンプトキャッシュを保つ