MCPツールに足りない引数をどう補うか
2026/4/5
はじめに
LLM ベースのエージェントが MCP ツールを呼び出すとき、ユーザーの指示が曖昧でパラメータが足りないケースは日常的に発生します。
「KR ダッシュボードのデータ見せて」
このリクエストを受けたエージェントは kr_dashboard_get_records ツールを呼び出したいが、どの KR データかが指定されていない。売上 KR? 採用 KR? 品質 KR?
この「足りない引数」を補う手段は、現在 3 つ存在します。
| アプローチ | 誰が聞くか | いつ聞くか |
|---|---|---|
| LLM が聞く | LLM 自身 | ツール呼び出し前 |
| リッチフォーム(Generative UI) | LLM がフォーム UI を生成 | ツール呼び出し前 |
| MCP Elicitation | MCP サーバー(コード) | ツール実行中 |
本記事では、MCP 仕様 2025-06-18 で追加された Elicitation の仕組みを解説し、既存のリッチフォーム手法との使い分けを整理します。さらに、実プロジェクトで実装を試みた際に発覚した AgentCore Gateway の構造的制約 を記録します。
注: 本記事は MCP 仕様 2025-06-18 を基準に整理しています。2025-11-25 では URL mode の追加など Elicitation の拡張が行われていますが、AgentCore Gateway がサポートする仕様バージョンに合わせて 2025-06-18 を採用しました。最新の仕様は MCP Elicitation Specification (2025-11-25) を参照してください。
MCP Elicitation とは
MCP Elicitation は、MCP 仕様 2025-06-18 で追加されたプロトコル機能です。ツール実行中に MCP サーバーがクライアントを介してユーザーに質問し、回答を受けて処理を続行できます。厳密には「サーバー → クライアント → ユーザー」の経路で、クライアントが UI 表示と承認制御を握ります。
Claude Code を使ったことがある方なら AskUserQuestion を想像してください。近い概念ですが、決定的な違いがあります。
AskUserQuestion との対比
AskUserQuestion — LLM が聞く:
ユーザー: 「KRデータ見せて」
LLM: 「どのKRですか?売上、採用、品質がありますが」 ← LLMが文脈から推測
ユーザー: 「売上」
LLM → ツール呼び出し
MCP Elicitation — サーバーが聞く:
ユーザー: 「KRデータ見せて」
LLM → ツール呼び出し(パラメータ不足のまま)
→ MCPサーバーが実行中に「パラメータ不足」と判断
→ elicitation/create でクライアント経由でユーザーに質問
→ ユーザーが回答
→ MCPサーバーが処理続行 → 結果返却
LLM ← ツール結果を受け取り表示
注目すべきは、Elicitation のやり取りが LLM をバイパスしている点です。MCP サーバーがクライアントを介してユーザーに質問し、回答を受け取る。LLM のトークン消費もラウンドトリップも発生しません。
質問を決めるのはサーバーコード
ここが直感に反するポイントです。Elicitation の質問文も選択肢も、LLM は一切関与しない。すべて MCP サーバー側の開発者が書いたコードが決めます。
// MCPサーバー側のツール実装(開発者が書くコード)
server.tool("get-kr-dashboard", async ({ params }, { sendElicitation }) => {
// DBから利用可能なKRを動的に取得
const availableKRs = await fetchAvailableKRs(params.orgId);
// サーバーが質問文とスキーマを決める。LLMは関与しない
// 仕様上、requestedSchema のトップレベルは type: "object" 固定
const result = await sendElicitation({
message: "どのデータを表示しますか?",
requestedSchema: {
type: "object",
properties: {
selected: {
type: "string",
enum: availableKRs.map((kr) => kr.name),
description: "表示するKRデータ",
},
},
required: ["selected"],
},
});
if (result.action === "accept") {
// content は requestedSchema と同じ shape の object
return fetchDashboardData(result.content.selected);
}
});
LLM は「ツールを呼ぶ」だけ。質問文も選択肢もサーバーのロジック次第です。これが AskUserQuestion と根本的に異なる点であり、サーバーだけが知っている情報 — DB の中身、ユーザー権限で見えるリソース、API の現在の状態 — を選択肢にできるのが強みです。
LLM に利用可能な KR の一覧を教える必要がない。MCP サーバーが DB を引いて「この組織で今見れるのはこの 3 つ」と動的に出せます。
AI SDK のサポート状況
@ai-sdk/mcp v1.0.15 / @modelcontextprotocol/sdk v1.27.1 で完全サポートされています。
import { createMCPClient } from "@ai-sdk/mcp";
const client = await createMCPClient({
transport: { type: "sse", url, headers },
capabilities: {
elicitation: {}, // Elicitation サポートを宣言
},
});
client.onElicitationRequest(ElicitationRequestSchema, async (request) => {
// request.params.message — サーバーからの質問
// request.params.requestedSchema — 期待されるレスポンスのJSON Schema
return {
action: "accept", // "accept" | "decline" | "cancel"
content: {
/* ユーザーの回答 */
},
};
});
応答の action は 3 つ。accept(回答する)、decline(質問を拒否)、cancel(操作をキャンセル)。JSON Schema で応答形式を制約できるため、自由テキストではなく選択肢やフォーマット付きの入力を強制できます。
セキュリティ上の注意: 仕様上、サーバーは Elicitation でパスワードやトークンなどの機密情報を要求してはなりません。Elicitation の経路はそうした情報の安全な伝送を想定しておらず、2025-11-25 では機密情報の収集には別途 URL mode が追加されています。
Generative UIとの比較
ここで問題になるのが、「足りない引数を補う」という同じ課題を解決する既存手法との重なりです。
筆者が担当しているプロジェクトでは、LLM がフォーム UI を JSON-L 形式で生成し、クライアントが DatePicker や Select などのリッチコンポーネントとして描画する仕組み(Generative UI)を既に構築していました。休暇申請フォーム、ユーザー検索フィルタなど、複数フィールドの一括入力に使っています。
これも「MCP ツールに足りない引数を補う」手段です。Elicitation と役割が被っている。
なお、Generative UI はアプリケーション層の実装であり、Elicitation は MCP プロトコルの機能です。レイヤーが異なるものの比較ですが、「足りない引数を補う」という同じ課題に対するアプローチとして並べています。
比較表
| Generative UI | MCP Elicitation | |
|---|---|---|
| 誰が判断 | LLM | MCP サーバー(コード) |
| いつ聞く | ツール呼び出し前 | ツール実行中 |
| UI の表現力 | 高い(DatePicker, Table 等) | flat primitive schema に制限、リッチ UI はクライアント実装依存 |
| 一度に聞ける量 | 複数フィールド一括 | flat object で複数フィールド可能だが、ネストや複雑な構造は不可 |
| LLM 再呼び出し | 必要(フォーム送信 → 新リクエスト) | 不要(ツール内で完結) |
| トークンコスト | LLM 往復が 1 回多い | ツール内完結で節約 |
| 選択肢の情報源 | LLM の知識 or 事前のツール呼び出し | サーバーが DB/API から動的生成 |
判断フレームワーク(私見)
以下は筆者のプロジェクトで実際に運用してみた上での整理であり、公式なベストプラクティスではありません。プロダクトの規模やアーキテクチャによって最適解は変わるはずです。
判断軸は大きく 2 つあります。誰が質問を決めるか(LLM か サーバーコードか)と、UI にどこまでの表現力が必要かです。
- Generative UI を選ぶ: 複数フィールドの一括入力、DatePicker や Table などの入力支援、リッチな UI 表現が必要なとき。LLM が考える UI。
- Elicitation を選ぶ: ツール実行中に不足引数が判明する、候補をサーバーが DB/API から動的生成する、LLM を介さず確定的に聞きたいとき。サーバーコードが決める追問。プロトコル上は構造化された追問だが、UI の表現力は Generative UI より狭い。
- LLM の聞き返しを選ぶ: 曖昧さの解消自体に会話の文脈理解が必要なとき。
- HITL 承認を選ぶ: 破壊的操作の最終確認。既存の仕組みで十分。
具体例で整理すると:
| ユースケース | 推奨手段 | 理由 |
|---|---|---|
| 複数フィールドの一括入力(休暇申請: 期間+理由+種別) | Generative UI | リッチ UI、DatePicker 等の表現力が必要 |
| リソース選択(どの KR? どの組織?) | Elicitation | サーバーだけが正しい選択肢を知っている(権限付き org 一覧、現在有効な候補など) |
| 曖昧な意図の確認(削除?更新?) | LLM の聞き返し | 文脈理解が必要 |
| 破壊的操作の最終確認 | HITL 承認 | 既存の仕組みで十分 |
なお、インフラやクライアントが Elicitation を素直に扱えない環境なら、Generative UI やアプリ層の独自実装で同等の UX を提供する判断も自然です。手段はあくまで手段であり、ユーザー体験を優先すべきです。
AgentCore Gateway の壁
現在のアーキテクチャ
筆者が担当しているプロジェクトでは、AWS Bedrock AgentCore Gateway + Lambda インターセプターでMCPサーバーを構成しています。
クライアント → AgentCore Gateway → Lambda ハンドラー → 結果返却
(ステートレス)
Lambda ハンドラーは「引数を受け取り → 処理 → 結果を返す」一方通行のリクエスト-レスポンスモデルです。
なぜ Elicitation が動かないか
MCP Elicitation はツール実行中の双方向通信を前提としています。
MCPサーバー: ツール実行開始
→ 「パラメータ足りない」
→ elicitation/create を送信(実行を一時停止)
→ ユーザーの回答を待つ ← ここで Lambda は死んでいる
→ 回答を受けて実行を再開
→ 結果を返却
この「一時停止して応答を待つ」がステートレスな Lambda では構造的に不可能です。Lambda はリクエストを受けたら結果を返して終了。途中で外部に質問を送って応答を待つ、という対話的なフローを挟む仕組みがありません。
AgentCore Runtime という選択肢
AWS Bedrock AgentCore には Gateway とは別に Runtime が存在します。
| AgentCore Gateway | AgentCore Runtime | |
|---|---|---|
| 実行モデル | Lambda(ステートレス) | サーバーレスランタイム(stateful session 対応) |
| Elicitation | 現行サポート範囲では扱えない | stateful MCP session で対応 |
| ツール実行中の対話 | 不可(tools/list / tools/call のみ) | 可(stateful mode 有効時) |
| インフラ | Lambda + インターセプター | Runtime + MCP サーバー |
| 変更コスト | — | Terraform / インフラ全面変更 |
Runtime は stateful MCP session をサポートしており、ツール実行中に「一時停止 → 追問 → 再開」の双方向フローが成立します。MCP の Elicitation をプロトコル準拠で動かすなら、Runtime が最有力な選択肢です。
なぜ Runtime が必要なのか:stateful session の有無
Gateway の公式ドキュメントが明示しているサポート範囲は tools/list と tools/call です。AWS が「Gateway は Elicitation 非対応」と明示的に書いているわけではありませんが、現行のサポート範囲から elicitation/create が通らないと読むのが自然です。
一方、Runtime は MCP の stateful mode をサポートしています。デフォルトは stateless な streamable HTTP ですが、Elicitation や sampling を使う場合は stateful mode を有効にできます。stateful session 内では、ツール実行中に sendElicitation() で一時停止し、ユーザーの回答を待ち、そのコンテキストを保持したまま処理を再開できる。
これは MCP Elicitation だけの話ではありません。MCP 仕様が拡張していく双方向機能 — サーバーからの通知、プログレス報告など — は、stateful session を前提としています。Gateway のアーキテクチャでは、仕様の進化に追随することが難しくなる可能性があります。
今回の判断:Runtime への移行
この制約を受けて、プロジェクトとしては AgentCore Runtime への移行を検討しています。まず LangFuse 分析エージェントをパイロットとして Runtime に移行し、うまくいけば他のエージェントも順次切り替えていく想定です。
移行の動機は Elicitation だけではありません。実際に移行を進める中で、Runtime でなければ実現が難しい要件が複数見えてきました。
- Elicitation: ツール実行中にユーザーへ追加質問し、回答を受けて処理を続行する
- Progress 通知: エージェント実行中の進捗(どのツールを呼んでいるか、どの段階か)をリアルタイムにチャット画面へ表示する
- HITL(Human-in-the-Loop): 破壊的操作の承認フローを MCP ネイティブで実現する
- 長時間実行: LangGraph ベースの Python エージェントは複数ツールを連鎖的に呼び出すため、Lambda のタイムアウト制約(最大 15 分)では厳しいケースがある
これらはいずれも「ツール実行中に状態を保持したまま外部とやり取りする」という共通の要件を持っており、Gateway のステートレスモデルでは構造的に対応しきれません。
クライアント側は先行して Elicitation を受信・表示・応答する仕組みを実装済みなので、サーバー側が Runtime に切り替わり MCP ネイティブの elicitation/create を送るようになれば、そのまま繋がります。
まとめ
MCP ツールに「足りない引数」を補う手段は 1 つではありません。
- LLM の聞き返し — 文脈理解が必要な曖昧さの解消に
- Generative UI — 複数フィールドの一括入力に
- MCP Elicitation — サーバーが動的に選択肢を生成するリソース選択に
- HITL 承認 — 破壊的操作の最終確認に
どれか一つに統一するのではなく、それぞれの強みを活かして共存させるのが現実的な設計です。
そしてインフラ面では、同等の UX はアプリケーション層の独自実装でも実現できます。ただし、MCP ネイティブ準拠で Elicitation を動かしたいなら、stateful session をサポートする AgentCore Runtime が最有力な選択肢です。Gateway のステートレスアーキテクチャでは現行のサポート範囲に制約があり、MCP 仕様の進化に追随していくなら Runtime への移行を検討する価値があるでしょう。