メインコンテンツへスキップ
ブログ一覧

Claude Code MCP Elicitation実践ガイド — MCPサーバーからの対話的入力要求とHooksによる自動応答パターン

Claude CodeMCPElicitation自動化CI/CD

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で対応を宣言する必要がある。

jsonc
// クライアント → サーバー(初期化時)
{
  "capabilities": {
    "elicitation": {
      "form": {},
      "url": {}
    }
  }
}

レスポンスの action は3種類ある。accept(ユーザーがデータを入力して送信)、decline(明示的に拒否)、cancel(ダイアログを閉じた)。サーバー側はこの3つを必ずハンドリングする。

jsonc
// 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モードでフィードバックを収集するツールの実装例だ。

typescript
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を指定する。

typescript
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モードのスキーマにはいくつかの厳格な制約がある。

  • フラットなオブジェクトのみ: ネストしたオブジェクトや配列は使えない
  • プリミティブ型のみ: stringnumberintegerbooleanenumoneOf + const または enum 配列)
  • format 属性: string型に emailuridatedate-time を指定でき、クライアント側でバリデーションが走る

正直、ネストが使えない制約は最初少し窮屈に感じた。だが、Elicitationは「ちょっとした追加入力」を想定した機能であり、複雑なフォームが必要ならURLモードでWebフォームに誘導するのが正しい設計判断だろう。

実務ユースケース別の実装パターン

パターン1: 不足パラメータの対話的取得

ツール引数がオプショナルで省略された場合、エラーにせずElicitationで補完するパターン。required フィールドと default 値の設計がポイントになる。

typescript
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 型を使った確認ダイアログは、地味だが実務では最も使用頻度が高いパターンだと思う。

typescript
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 に設定する。typecommand のみサポートされている。

jsonc
// ~/.claude/settings.json
{
  "hooks": {
    "Elicitation": [
      {
        "matcher": "my-trusted-server",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/auto-respond.sh"
          }
        ]
      }
    ]
  }
}

Hookへの入力フィールドは mcp_server_namemessagemodeurlelicitation_idrequested_schema で、これらをもとに応答を判断する。

  • exit code 0 + JSON出力: 自動応答を返す
  • exit code 2: Elicitationを拒否する

ElicitationResult Hookによるレスポンス制御

ElicitationResult Hookは、ユーザーが応答した後、MCPサーバーに送信される前に発火する。応答内容の上書きやブロックが可能で、監査ログの記録に適している。

自動化パターン: 信頼サーバーの自動承認・監査ログ・バリデーション

CI/CD環境で環境変数からAPIキーを読み取り、Elicitationに自動応答するシェルスクリプトの実装例を示す。

bash
#!/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の宣言漏れ: クライアントが elicitation capabilityを宣言しないと、サーバーからのElicitationはサイレントに無視される。デバッグしにくいので最初に確認すべきポイントだ
  • URLモードのanti-phishing対策: Elicitationを開始したユーザーと完了したユーザーが同一であることをサーバー側で検証する。セッショントークンの紐付けを怠ると、フィッシング攻撃の余地が生まれる
  • エラーコード -32042: URLモードでの認証完了が前提のリクエストに対して返される URL Elicitation Required Error。クライアント側でこのコードをハンドリングし、再認証フローに誘導する

MCP Elicitationは、MCPサーバーを「呼ばれたら答える」受動的な存在から「必要な情報を自ら問いかける」能動的な存在へ進化させるプリミティブだ。Formモードで構造化データを、URLモードで機密操作を安全に処理し、Elicitation Hooksで自動応答を組み込めば、対話的でありながら完全自動化も可能なMCPワークフローが構築できる。

まずは既存のMCPサーバーに確認ダイアログを1つ追加するところから試してみてほしい。

もっと読む他の技術記事も読む