Claude Code MCP Elicitation実践ガイド — MCPサーバーからの対話的入力要求とHooksによる自動応答パターン
MCP Elicitationとは何か — 従来のMCPに足りなかったピース
MCPの「一方通行」問題とElicitationの位置づけ
MCPサーバーは長らく「ツールを呼ばれたら結果を返す」という片方向の存在だった。クライアントがツールを呼び出し、サーバーが結果を返す。シンプルだが、実行途中で「あと1つ情報が足りない」「本当にこの操作を実行していいか確認したい」といった場面では、エラーを返して最初からやり直すしかなかった。
2026年3月14日リリースのClaude Code v2.1.76で追加されたElicitation機能は、この制約を根本から解消する。MCPサーバーがツール実行中にユーザーへ構造化された入力を要求し、その応答を受け取って処理を継続できる。human-in-the-loopがMCPの第一級プリミティブになった形だ。
Elicitation自体はMCP仕様 2025-06-18で定義され、JSON-RPCメソッド名は elicitation/create である。
FormモードとURLモード — 2つの入力経路
Elicitationには2つのモードがある。
Formモードは、JSONスキーマ駆動のフォームで非機密データを収集する。データはMCPクライアント(Claude Code)を経由してサーバーに届く。不足パラメータの補完や確認ダイアログに適している。
URLモードは、OAuth認証やAPIキー入力など機密操作向けだ。ブラウザで外部URLを開き、データはMCPクライアントを経由しない。MCP仕様 2025-11-25で追加された。
どちらのモードも、クライアントが初期化時にcapability negotiationで対応を宣言する必要がある。
// クライアント → サーバー(初期化時)
{
"capabilities": {
"elicitation": {
"form": {},
"url": {}
}
}
}
レスポンスの action は3種類ある。accept(ユーザーがデータを入力して送信)、decline(明示的に拒否)、cancel(ダイアログを閉じた)。サーバー側はこの3つを必ずハンドリングする。
// Formモードのリクエスト例
{
"method": "elicitation/create",
"params": {
"message": "デプロイ先の環境を選択してください",
"requestedSchema": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"enum": ["staging", "production"],
"description": "デプロイ環境"
}
},
"required": ["environment"]
}
}
}
// accept時のレスポンス
{
"action": "accept",
"content": {
"environment": "staging"
}
}
自作MCPサーバーでElicitationを発行する
TypeScript SDKでのElicitation実装
@modelcontextprotocol/sdk を使った実装を見ていこう。ツールハンドラ内で ctx.mcpReq.elicitInput() を呼び出すことでElicitationを発行する。重要な制約として、Elicitationはクライアントリクエスト(tools/call など)の処理中にのみ発行可能で、サーバー単独では発行できない。
以下はFormモードでフィードバックを収集するツールの実装例だ。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({
name: "feedback-server",
version: "1.0.0",
});
server.tool(
"collect-feedback",
"ユーザーからフィードバックを収集する",
{ projectName: z.string() },
async ({ projectName }, ctx) => {
const result = await ctx.mcpReq.elicitInput({
message: `「${projectName}」へのフィードバックをお願いします`,
requestedSchema: {
type: "object",
properties: {
rating: {
type: "string",
enum: ["great", "good", "neutral", "bad"],
description: "総合評価",
},
comment: {
type: "string",
description: "コメント(任意)",
},
},
required: ["rating"],
},
});
if (result.action === "accept") {
// result.content に { rating, comment } が入る
return {
content: [
{ type: "text", text: `フィードバック受領: ${result.content.rating}` },
],
};
}
return {
content: [
{ type: "text", text: "フィードバックがキャンセルされました" },
],
};
}
);
URLモードの場合は、OAuthプロバイダへのリダイレクトURLを指定する。
server.tool("authenticate", "OAuthで認証する", {}, async (_args, ctx) => {
const result = await ctx.mcpReq.elicitInput({
message: "GitHubアカウントで認証してください",
url: "https://your-server.example.com/oauth/start?session=abc123",
});
if (result.action !== "accept") {
return { content: [{ type: "text", text: "認証がキャンセルされました" }] };
}
// URLモード完了後、セッションから認証情報を取得して処理を継続
const token = await fetchTokenFromSession("abc123");
return { content: [{ type: "text", text: "認証成功" }] };
});
スキーマ設計のルールと制約
Formモードのスキーマにはいくつかの厳格な制約がある。
- フラットなオブジェクトのみ: ネストしたオブジェクトや配列は使えない
- プリミティブ型のみ:
string、number、integer、boolean、enum(oneOf+constまたはenum配列) format属性: string型にemail、uri、date、date-timeを指定でき、クライアント側でバリデーションが走る
正直、ネストが使えない制約は最初少し窮屈に感じた。だが、Elicitationは「ちょっとした追加入力」を想定した機能であり、複雑なフォームが必要ならURLモードでWebフォームに誘導するのが正しい設計判断だろう。
実務ユースケース別の実装パターン
パターン1: 不足パラメータの対話的取得
ツール引数がオプショナルで省略された場合、エラーにせずElicitationで補完するパターン。required フィールドと default 値の設計がポイントになる。
server.tool(
"deploy",
"アプリをデプロイする",
{ app: z.string(), env: z.string().optional() },
async ({ app, env }, ctx) => {
if (!env) {
const result = await ctx.mcpReq.elicitInput({
message: `${app} のデプロイ先を指定してください`,
requestedSchema: {
type: "object",
properties: {
env: { type: "string", enum: ["staging", "production"] },
},
required: ["env"],
},
});
if (result.action !== "accept") {
return { content: [{ type: "text", text: "デプロイを中止しました" }] };
}
env = result.content.env;
}
// デプロイ処理を継続
}
);
パターン2: OAuth/APIキー認証フロー
機密データを扱う認証フローにはURLモードを使う。データがMCPクライアントを通らないため安全だ。OAuthプロバイダへのリダイレクト後、notifications/elicitation/complete でサーバーに完了が通知され、セッションを継続できる。
使い分けの基準はシンプルで、機密データが含まれるならURLモード、それ以外はFormモードだ。
パターン3: 危険操作の確認ダイアログ
boolean 型を使った確認ダイアログは、地味だが実務では最も使用頻度が高いパターンだと思う。
server.tool(
"drop-table",
"テーブルを削除する",
{ table: z.string() },
async ({ table }, ctx) => {
const result = await ctx.mcpReq.elicitInput({
message: `テーブル「${table}」を削除します。この操作は取り消せません。`,
requestedSchema: {
type: "object",
properties: {
confirm: {
type: "boolean",
description: "本当に削除しますか?",
},
},
required: ["confirm"],
},
});
if (result.action !== "accept" || !result.content.confirm) {
return { content: [{ type: "text", text: "削除を中止しました" }] };
}
await db.run(`DROP TABLE ${table}`);
return { content: [{ type: "text", text: `${table} を削除しました` }] };
}
);
Elicitation Hooksで自動応答 — CI/CDと自律エージェントへの組み込み
Elicitation Hookの仕組みと設定
Elicitation HookはMCPサーバーがElicitationを発行した瞬間に発火する。対話的な応答をプログラムで自動化できるため、CI/CDパイプラインや自律エージェントへの組み込みに不可欠だ。
Hookは settings.json に設定する。type は command のみサポートされている。
// ~/.claude/settings.json
{
"hooks": {
"Elicitation": [
{
"matcher": "my-trusted-server",
"hooks": [
{
"type": "command",
"command": "/path/to/auto-respond.sh"
}
]
}
]
}
}
Hookへの入力フィールドは mcp_server_name、message、mode、url、elicitation_id、requested_schema で、これらをもとに応答を判断する。
- exit code 0 + JSON出力: 自動応答を返す
- exit code 2: Elicitationを拒否する
ElicitationResult Hookによるレスポンス制御
ElicitationResult Hookは、ユーザーが応答した後、MCPサーバーに送信される前に発火する。応答内容の上書きやブロックが可能で、監査ログの記録に適している。
自動化パターン: 信頼サーバーの自動承認・監査ログ・バリデーション
CI/CD環境で環境変数からAPIキーを読み取り、Elicitationに自動応答するシェルスクリプトの実装例を示す。
#!/bin/bash
# auto-respond.sh — Elicitation Hookの自動応答スクリプト
# stdinからHook入力を読み取る
INPUT=$(cat)
SERVER_NAME=$(echo "$INPUT" | jq -r '.mcp_server_name')
MESSAGE=$(echo "$INPUT" | jq -r '.message')
MODE=$(echo "$INPUT" | jq -r '.mode')
# URLモードのURLは自動で開かない(セキュリティ対策)
if [ "$MODE" = "url" ]; then
echo "URL elicitation blocked in CI" >&2
exit 2
fi
# 信頼サーバーのFormモードのみ自動応答
if [ "$SERVER_NAME" = "my-deploy-server" ]; then
# 環境変数から値を読み取って応答を構築
cat <<EOF
{
"hookSpecificOutput": {
"action": "accept",
"content": {
"environment": "${DEPLOY_ENV:-staging}",
"api_key": "${API_KEY}"
}
}
}
EOF
exit 0
fi
# 未知のサーバーからのElicitationは拒否
exit 2
matcher でサーバー名をフィルタリングし、信頼できるサーバーのみ自動承認するのが安全なパターンだ。自律エージェント環境(非対話環境)では、すべてのElicitationに対してHookが応答する設計が必須となる。Hookが設定されていなければ、Elicitationはタイムアウトで失敗する。
ハマりポイントと注意事項
実装時に遭遇しやすい問題をまとめておく。
- Formモードで機密データを要求しない: パスワードやAPIキーはFormモードで収集してはいけない。データがMCPクライアントを経由するため、必ずURLモードを使う
- スキーマにネストや配列を入れない: フラットなオブジェクト + プリミティブ型のみ。ネストした構造を渡すとクライアント側でエラーになる
- クライアントリクエストに紐づく制約: Elicitationはサーバー起動時の初期設定には使えない。必ず
tools/callなどのリクエスト処理中に発行する - capability negotiationの宣言漏れ: クライアントが
elicitationcapabilityを宣言しないと、サーバーからのElicitationはサイレントに無視される。デバッグしにくいので最初に確認すべきポイントだ - URLモードのanti-phishing対策: Elicitationを開始したユーザーと完了したユーザーが同一であることをサーバー側で検証する。セッショントークンの紐付けを怠ると、フィッシング攻撃の余地が生まれる
- エラーコード -32042: URLモードでの認証完了が前提のリクエストに対して返される
URL Elicitation Required Error。クライアント側でこのコードをハンドリングし、再認証フローに誘導する
MCP Elicitationは、MCPサーバーを「呼ばれたら答える」受動的な存在から「必要な情報を自ら問いかける」能動的な存在へ進化させるプリミティブだ。Formモードで構造化データを、URLモードで機密操作を安全に処理し、Elicitation Hooksで自動応答を組み込めば、対話的でありながら完全自動化も可能なMCPワークフローが構築できる。
まずは既存のMCPサーバーに確認ダイアログを1つ追加するところから試してみてほしい。
