Claude Code MCP Elicitation実践ガイド — MCPサーバーからユーザー入力を要求する双方向ワークフローの構築
MCPサーバーは「呼ばれたら応える」受動的な存在だった。2026年3月、Claude Code v2.1.76で追加されたElicitation機能がこの前提を覆す。MCPサーバーがタスク実行中にユーザーへ構造化フォームを提示し、入力を待ち、その結果に基づいて処理を分岐できるようになった。本記事では、formモードの実装からHooksによるカスタム制御、さらにheadlessモードのdefer決定を組み合わせたCI/CD承認ゲートまで、実際に動くコード付きで解説する。
Elicitationとは何か — MCPの「逆方向」通信
従来のMCP通信フローとの違い
従来のMCPはクライアント→サーバーの一方向リクエストだった。クライアントが tools/call でツールを呼び、サーバーが結果を返す。ユーザーの意思決定が必要な場面でも、サーバー側から能動的に問い合わせる手段はなかった。
Elicitationはこの方向を逆転させる。プロトコルメソッド elicitation/create により、サーバー→クライアント→ユーザーという逆方向の問い合わせが可能になった。モードは2種類ある。
- formモード — クライアント内に構造化フォームを表示し、入力データをMCPチャネル経由で受け取る
- urlモード — ブラウザでURLを開き、OAuthやAPI Key入力など機密データをMCP外で処理する
// formモード: サーバー→クライアントへのリクエスト
{
"method": "elicitation/create",
"params": {
"message": "デプロイ先の環境を選択してください",
"requestedSchema": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"enum": ["staging", "production"],
"description": "デプロイ環境"
},
"confirm": {
"type": "boolean",
"description": "本当にデプロイしますか?"
}
},
"required": ["environment", "confirm"]
}
}
}
3つのレスポンスアクション: accept / decline / cancel
ユーザーの応答は3種類に分かれる。
| アクション | 意味 | contentフィールド |
|---|---|---|
accept | 入力を送信 | フォームデータを含む |
decline | 明示的に拒否 | なし |
cancel | ダイアログを閉じた(Escキーなど) | なし |
サーバー側はこの3パターンすべてをハンドリングする必要がある。正直、最初は「declineとcancelを分ける必要あるのか?」と思ったが、ユーザーが意図的に拒否したのか、単に閉じてしまったのかで後続処理を変えたい場面は確かにある。
スキーマの制約: requestedSchema はフラットなオブジェクトとプリミティブ型のみ。ネストしたオブジェクトや配列は使えない。複雑な入力が必要な場合は、複数回のElicitationに分割する設計が必要になる。
formモード実装 — MCPサーバーから構造化フォームを提示する
MCPサーバー側の実装(TypeScript SDK)
MCP TypeScript SDKの server.server.elicitInput() を使った実装例を示す。デプロイ承認を求めるMCPツールの完全な実装だ。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "deploy-gate",
version: "1.0.0",
});
server.tool(
"deploy",
{ service: { type: "string", description: "デプロイ対象サービス" } },
async ({ service }, { server: mcpServer }) => {
// Elicitationでユーザーに承認を求める
const result = await mcpServer.elicitInput({
message: `${service} をデプロイします。環境を選択してください。`,
requestedSchema: {
type: "object",
properties: {
environment: {
type: "string",
enum: ["staging", "production"],
description: "デプロイ先環境",
},
skip_tests: {
type: "boolean",
description: "テストをスキップする",
},
},
required: ["environment"],
},
});
// accept / decline / cancel の3パターンを必ずハンドリング
if (result.action === "decline") {
return {
content: [{ type: "text", text: "デプロイがユーザーにより拒否されました。" }],
};
}
if (result.action === "cancel") {
return {
content: [{ type: "text", text: "デプロイがキャンセルされました。再度実行してください。" }],
};
}
// accept: 入力値を使って処理続行
const env = result.content?.environment as string;
const skipTests = (result.content?.skip_tests as boolean) ?? false;
// 実際のデプロイ処理...
return {
content: [{
type: "text",
text: `${service} を ${env} にデプロイしました(テストスキップ: ${skipTests})`,
}],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
フォームフィールド設計パターン
ユースケース別に、よく使うフィールド設計を3つ紹介する。
1. デプロイ前確認(boolean) — 最もシンプル。Yes/Noの1問で済む場面に。
2. 環境選択(enum) — enum で選択肢を提示するパターンが最も実用的。ユーザーに自由入力させるとtypoリスクがある場面で重宝する。
3. パラメータ入力(string + number複合) — バージョン番号やレプリカ数など、複数の入力を一画面で収集する。
Elicitation Hooksでクライアント側を制御する
Elicitation / ElicitationResult フックの仕組み
Claude CodeのHooksシステムを使うと、Elicitationの挙動をクライアント側でカスタム制御できる。.claude/settings.json に定義する。
フック入力として標準入力に渡されるJSONには、session_id、mcp_server_name、tool_name、tool_input、form_fields の5フィールドが含まれる。
終了コードの意味は以下の通り。
- 0: 成功。stdoutのJSONに従って処理
- 2: ブロック。Elicitationを拒否
自動承認・自動拒否・ログ記録の実装例
特定のMCPサーバーからのElicitationのみ自動承認し、それ以外はブロックする例を示す。
// .claude/settings.json
{
"hooks": {
"Elicitation": [
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "node /path/to/elicitation-gate.mjs"
}]
}
]
}
}
// elicitation-gate.mjs
import { readFileSync } from "fs";
const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));
// 信頼済みMCPサーバーリスト
const TRUSTED_SERVERS = ["deploy-gate", "config-manager"];
if (TRUSTED_SERVERS.includes(input.mcp_server_name)) {
// 自動承認: デフォルト値で応答
const content = {};
for (const field of input.form_fields) {
if (field.type === "boolean") content[field.name] = true;
}
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: "Elicitation",
action: "accept",
content,
},
}));
} else {
// 信頼されていないサーバーはブロック
process.stderr.write(`Blocked elicitation from: ${input.mcp_server_name}`);
process.exit(2);
}
ElicitationResultフックでは、ユーザーの入力内容を外部に転送できる。監査ログやSlack通知に活用する場面で有用だ。
headless + defer で実現するCI/CD承認ゲート
defer決定とは — headlessセッションの一時停止と再開
v2.1.89で追加されたPreToolUseフックの defer 決定は、headlessセッションを一時停止する仕組みだ。ツールは実行されず、プロセスは stop_reason: "tool_deferred" で終了する。セッション状態はディスクに保存され、--resume で再開できる。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "defer"
}
}
制約として、deferが機能するのはClaudeが単一のツール呼び出しを行ったターンに限られる。複数ツールの同時呼び出し時はdeferは無視される。
GitHub Actions承認ゲートの構築例
Elicitation + defer を組み合わせた、CI/CDパイプラインでの承認フローの概要を示す。
# .github/workflows/deploy.yml
name: Deploy with approval
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Claude headless
id: claude
run: |
result=$(claude -p "deploy-gateツールでproductionデプロイを実行して" \
--headless \
--permission-mode default \
--output-format json)
echo "result=$result" >> "$GITHUB_OUTPUT"
- name: Check for deferred tool
id: check
run: |
stop_reason=$(echo '${{ steps.claude.outputs.result }}' | jq -r '.stop_reason')
session_id=$(echo '${{ steps.claude.outputs.result }}' | jq -r '.session_id')
echo "stop_reason=$stop_reason" >> "$GITHUB_OUTPUT"
echo "session_id=$session_id" >> "$GITHUB_OUTPUT"
- name: Wait for Slack approval
if: steps.check.outputs.stop_reason == 'tool_deferred'
run: |
# Slack承認ボタン送信 → Webhook待受
# 承認後にresumeを実行
claude -p --resume ${{ steps.check.outputs.session_id }} \
--permission-mode default
タイムアウト設計も忘れてはならない。deferされたセッションに有効期限はないため、CI/CD側でジョブタイムアウトを設定し、期限切れ時はセッションを破棄する運用が必要だ。
ハマりポイントと制約事項
Elicitationを実装する上で、事前に知っておくべき制約をまとめる。
スキーマはフラットオブジェクトのみ。 ネストや配列が必要なら、複数回のElicitationに分割する。ただし1つのツール呼び出し内で何度でも elicitInput() を呼べるので、段階的に聞いていく設計は可能だ。
URLモードのエラーコード -32042 (URLElicitationRequiredError)。ツール呼び出しへのエラーレスポンスとして返され、クライアントにURL遷移を促す。notifications/elicitation/complete 通知後にリトライする設計が必要になる。
Elicitation非対応クライアントへの後方互換。 これは地味だが重要なポイントだ。capabilities.elicitation の存在チェックを必ず行い、非対応時はデフォルト値で続行するフォールバックを用意する。
server.tool("smart-deploy", schema, async (args, { server: mcpServer }) => {
// クライアントがElicitation対応かチェック
const capabilities = mcpServer.getClientCapabilities?.();
if (!capabilities?.elicitation) {
// 非対応: デフォルト値で続行
return executeDeploy(args.service, "staging", false);
}
// 対応: ユーザーに確認
const result = await mcpServer.elicitInput({ /* ... */ });
// ...
});
タイムアウト設計。 ユーザーが長時間応答しない場合、サーバー側のリクエストがハングする可能性がある。AbortController でタイムアウトを設けることを推奨する。
まとめ — 自律と介入の境界を設計する
Elicitationは「完全自律」と「常時監視」の間に、必要な瞬間だけ人間を介入させる設計を可能にした。
使い分けの指針は3層で考えるとよい。
- formモード — 日常的な確認・入力。デプロイ先選択、パラメータ指定など
- Hooks — 組織ポリシーに基づく自動制御。信頼済みサーバーの自動承認、監査ログ
- defer + resume — CI/CDレベルの承認フロー。外部システム(Slack、GitHub)との連携
MCPサーバーを設計する際は、「このツールのどのステップでユーザー判断が必要か」を最初に考えることが重要だ。すべてを自動化するのでもなく、すべてに確認を挟むのでもなく、判断が必要な瞬間を見極めてElicitationを配置する。それが、自律エージェント時代のヒューマン・イン・ザ・ループ設計の要点になる。
