AIエージェントが最も危ないのは、暴走する瞬間ではなく、「何をすべきかわからないのに黙って進む」瞬間だ。
MCPのElicitationは、その問題に対処するために設計された機能だ。AIエージェントが処理の途中で人間に構造化された確認を求め、必要な情報を得てから次のステップに進む仕組みを提供する。
「全自動か全手動か」という誤った二択
AIエージェントに仕事を任せようとすると、二つの極端な設計に行き着きがちだ。
一方は全自動——AIに全てを委ねて、途中で何も確認しない。作業は速いが、誤った前提で進んでいても誰も止められない。もう一方は全手動——ステップごとに人間が確認する。安全だが、エージェントを使う意味が薄れる。
本番環境へのデプロイ、データベースの削除、ファイルの一括変更。こういった「取り返しのつかない操作」を前にしたとき、「自動で進む」設計は怖い。かといって「全て人間が判断する」設計は、エージェントの価値を消してしまう。
MCP Elicitationは、このどちらでもない第三の設計を可能にする。「必要な時だけ人間に聞く」というパターンだ。
一発勝負のツール呼び出しと、その限界
従来のMCPツール呼び出しは一発勝負だ。AIが判断してツールを呼び出し、結果が返ってくる。ブランチ名が不明ならデフォルト値を使うか、エラーを返すかの二択しかない。
# ❌ 旧来: 欠損パラメータはエラーかサイレントなデフォルト値
@mcp.tool()
async def deploy_to_production(branch: str = "main") -> str:
# 本番デプロイ、取り消せない。
# branchが本当に正しいか確認する手段がない
return f"Deployed {branch} to production"
このコードには問題がある。branch が指定されなければ "main" をデフォルトにするが、AIが「main以外のブランチのつもりで指示していた」可能性がある。エラーにしても、AIは「情報が足りないからタスク失敗」と判断するだけで、ユーザーに聞き返す手段がない。
Elicitationを使えば、ツール実行の途中でユーザーに構造化フォームを表示し、必要な情報を取得してから処理を続けられる。
# ✅ Elicitation あり: 必要な時だけ人間に確認
from pydantic import BaseModel
from fastmcp import Context
class DeployConfirm(BaseModel):
branch: str
confirmed: bool
@mcp.tool()
async def deploy_to_production(ctx: Context) -> str:
result = await ctx.elicit(
message="本番環境にデプロイします。ブランチと操作を確認してください",
schema=DeployConfirm,
)
if result.action != "accept" or not result.data.confirmed:
return "デプロイをキャンセルしました"
return f"Deployed {result.data.branch} to production"
ユーザーの画面にはフォームが表示される。ブランチ名を入力し、確認チェックボックスをオンにしてから「実行」を押す。それを受け取ったエージェントが処理を続ける。
elicitation/create の仕組み
ElicitationはMCPの仕様バージョン2025-06-18で導入されたRPCメソッドだ。通常のMCPはクライアント(AIアプリ)からサーバー(ツール)へのリクエストが基本だが、Elicitationはサーバーからクライアントへの逆方向リクエストになっている。
MCPサーバーが elicitation/create を発行すると、クライアントはUIにフォームを表示してユーザーの入力を待つ。入力が完了するとクライアントからサーバーへ結果が返り、処理が再開する。
リクエストのJSON構造はこうなる:
{
"jsonrpc": "2.0",
"id": 1,
"method": "elicitation/create",
"params": {
"message": "デプロイ先の環境を選択してください",
"requestedSchema": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"title": "環境",
"enum": ["staging", "production"],
"enumNames": ["ステージング(テスト)", "本番環境"]
},
"confirmed": {
"type": "boolean",
"title": "確認",
"description": "本番への変更は取り消せません"
}
},
"required": ["environment", "confirmed"]
}
}
}
requestedSchema に使える型はシンプルに限定されている——string(format: email/uri/date/date-time対応)、number/integer(minimum/maximum指定可)、boolean(default指定可)、enum の4種類だ。ネストしたオブジェクトは使えない。クライアント側のUI生成を単純にするための意図的な設計だ。
クライアントはMCPの初期化ハンドシェイク時にElicitation対応を宣言する必要がある:
{ "capabilities": { "elicitation": {} } }
この宣言がないクライアントに対しては、MCPサーバーはElicitationを使うべきでない。
フォームモードとURLモード
Elicitationには2つのモードがある。
フォームモード(インバンド)は基本のモードで、MCPプロトコル経由でデータがやり取りされる。ユーザーが入力した情報はMCPクライアントを通ってサーバーに届く。名前や日付、選択肢のような一般的な入力に適している。
URLモードは仕様バージョン2025-11-25で追加された。クライアントがブラウザでURLを開き、ユーザーは外部のWebページで認証などの操作を完了する。操作結果はMCPプロトコルを通過しない。つまり、認証情報がLLMのコンテキストやMCPクライアントを経由しない設計になっている。
GitHubやGoogleへのOAuthフローはURLモードに向いている。アクセストークンがAIのコンテキストに混入するリスクを避けられる。
URLモードは非同期で動く。ユーザーがブラウザで操作を完了するまで数分かかることもある。サーバーは完了時に notifications/elicitation/complete でクライアントに通知する。
3つのアクション: accept / decline / cancel
Elicitationへのレスポンスには3種類のアクションがある。仕様書とWorkOSのElicitation解説が詳しく説明している。
accept はユーザーが明示的に承認した状態だ。content フィールドに入力データが含まれ、サーバーはそのデータを使って処理を続ける。
decline はユーザーが明示的に拒否した状態だ。「このタスクはやらない」という意図的な判断なので、サーバーはリトライせず別の対応を取るべきだ。
cancel はユーザーが判断を保留した状態だ。Escapeキーを押した、画面を閉じたなどが該当する。「後でやる」の可能性があるのでリトライが許容される。
この3つの区別は重要だ。サーバー側が decline と cancel を同じに扱ってしまうと、ユーザーが「やらない」と言っているのに同じ確認ダイアログが繰り返し表示される、不快な体験が生まれる。
パスワードを聞いてはいけない理由
フォームモードでMCPサーバーがパスワードやAPIキーを要求することを、仕様書は禁じている(MUST NOT)。
なぜか。フォームモードのデータはMCPプロトコルを経由してLLMのコンテキストに入る可能性がある。一度LLMのコンテキストに機密情報が入れば、ログに残ったり、別のツール呼び出しで露出したりするリスクがある。
機密情報が必要な場合はURLモードを使い、ブラウザの外部フローで処理する設計が正しい。
クライアント側にも実装上の義務がある。仕様書はクライアントがレート制限を実装すべき(SHOULD)と定めている。悪意あるMCPサーバーがElicitationを使ってユーザーを誘導したり、フィッシング的なフォームを大量表示したりすることを防ぐためだ。
Python SDKでの実装パターン
FastMCP(v2.10.0以降)を使うと、Elicitationをシンプルなコードで実装できる。MCP Python SDK公式のelicitation.pyを参考にした実装例を示す。
レストランの予約ツールで、満席だった場合に代替日を聞く例だ:
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel
from fastmcp.server.elicitation import AcceptedElicitation
mcp = FastMCP("Restaurant MCP")
class AlternativeBooking(BaseModel):
try_another_date: bool
preferred_date: str # 例: "2024-12-26"
@mcp.tool()
async def book_table(date: str, party_size: int, ctx: Context) -> str:
"""指定日にテーブルを予約する"""
if is_fully_booked(date):
result = await ctx.elicit(
message=f"{date}は満席です。別の日を試しますか?",
schema=AlternativeBooking,
)
if isinstance(result, AcceptedElicitation) and result.data.try_another_date:
return await book_table(result.data.preferred_date, party_size, ctx)
return "予約をキャンセルしました"
return f"{date}に{party_size}名でご予約しました"
「12月25日は満席」→ フォームが表示される → ユーザーが「12月26日にする」と回答 → 予約完了。この一連の流れがエージェントとユーザーの対話として実現する。
対応済みクライアント
Elicitation対応のクライアントはすでに複数登場している。
GitHub Copilot in VS Code v1.102(2025年6月リリース)は、Elicitation対応した最初の主要クライアントの一つだ。GitHub Blogの解説記事では、チックタックトーゲームのMCPサーバーが「難易度」「プレイヤー名」「先攻後攻」をElicitationで問い合わせ、VS Code上にモーダルが表示される様子が示されている。
Claude Codeはバージョン2.1.76(2026年3月14日リリース)でElicitationに対応した。Claude Code Changelogによると、フォームモードとURLモードの両方に対応しており、Elicitation フックと ElicitationResult フックも追加されている。フックを使うことで、elicitationリクエストをインターセプトして独自の処理を挟むことも可能だ。

対話型ワークフローという設計の変化
Elicitationが示しているのは、AIエージェントの設計に関する考え方の変化だ。
これまでのエージェントは一方通行だった。入力を受け取り、ツールを呼び出し、結果を返す。処理の途中でユーザーに確認を取る仕組みがなかった。
Elicitationで双方向になる。エージェントが処理の途中で立ち止まり、「ここは私が判断するより聞いたほうがいい」と判断して問い合わせる。ユーザーが答えたら処理を再開する。
空港の搭乗ゲートに近いイメージだ。全員を一度に止めて全行程をチェックするのではなく、「こちらの便のお客様だけこちらへ」と必要な時だけ呼び止める。それ以外は進める。
「暴走するAI」への不安と「毎回手動でチェックしなければならない」面倒さ。その両方に対して、Elicitationは一つの設計上の回答を出している。全自動でも全手動でもなく、「判断が必要な場所だけ人間を巻き込む」という設計だ。
MCPを使ったツール開発をするなら、Elicitationの活用を設計の選択肢に入れておく価値がある。
