こんにちは。グループ経営ソリューション事業部 グループ経営コンサルティング第2ユニット サイクロス製品開発部の座間です。
この記事は、MCPが提供しているElicitationという機能が公式TypeScript SDKで使用できるようになっていたので、キャッチアップがてら少し触ってみたという内容でお送りします。
Elicitationについて
定義
公式ドキュメントではElicitationについて以下のように記載されています。
The Model Context Protocol (MCP) provides a standardized way for servers to request additional information from users through the client during interactions. This flow allows clients to maintain control over user interactions and data sharing while enabling servers to gather necessary information dynamically.
つまり、Elicitationは「MCP Serverからユーザーに追加の情報を要求できる標準化された方法」と言えそうです。
Elicitationは公式にはdraft段階ですが、実装側ではTypeScript SDK、MCPホスト側ではGitHub Copilotなどがすでに対応しています。
使い道
MCP公式TypeScript SDKのREADME記載の実装例では、おおまかに以下のような流れの中でElicitationを使用しています。
- ユーザーが日時を指定してレストランを予約するようエージェントに指示する
- エージェントは指定された日時とレストラン名で予約を試みるが、すでに予約が埋まってしまっている
- エージェントはElicitation機能を利用して、ユーザーに別の日時を選択することを求める
- エージェントはユーザーから受け取った新しい日時を基にレストランを予約する
上記フローはユーザーが見える範囲にフォーカスして記述しています。
実際にElicitationを使う判断をしているのはMCP Server Toolです。
「Toolの実行途中に追加の情報を取得できる」という特性を考えると、上記以外にも以下のようなことに使えそうですね。
- Toolが途中まで処理した結果をユーザーに提示し承認を求める。OKなら処理を進め、NGなら実行をキャンセルする。
- Tool呼び出し時にエージェントが推論で補完しきれなかった引数をユーザーが追加で入力するように求める。
MCP公式TypeScript SDKで実装してみた
まずはElicitationがどのようなものなのかを感じ取るために、挨拶をしてくれるだけの簡単なMCP Serverを実装してみます。
環境
- Node.js 18.20.8
- @modelcontextprotocol/sdk 1.17.3
MCP Serverを利用するホストとしては、GitHub CopilotをVS Codeから使用します。
MCP Serverの実装
最初に、ただテキストを返すだけのMCP Serverを実装します。詳細な実装方法は公式のクイックスタート等、すばらしい記事がたくさんあるので割愛します。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new McpServer({ name: "elicitation-tutorial", version: "1.0.0", }); server.tool( "Greet", "Say Hello to you", {}, async () => { return { content: [ { type: "text", text: `こんにちは` } ] }; } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });
Elicitation機能の追加
上記で作成したMCP ServerにElicitation機能を追加してみましょう。TypeScript SDKではelicitInput
というメソッドを使用します。
let yourName: string | undefined; const result = await server.server.elicitInput({ message: "あなたについて教えてください", requestedSchema: { type: "object", properties: { yourName: { type: "string", title: "氏名", description: "あなたの名前を入力してください" } }, required: ["yourName"] } }); if (result.action === "accept" && result.content && result.content.yourName) { yourName = String(result.content.yourName); }
少しだけ解説します。elicitInput
で指定する引数の構造は以下のとおりです。これ以外にも指定できる項目もあるので、詳細はMCPの公式ドキュメントなどをご確認ください。
message
: ユーザーに入力を求めるときに表示するテキストrequestedSchema
: 期待するレスポンスの形式を定義するJSONスキーマtype
: 期待するデータ型だが、基本的に構造化データを受け取りたいのでobject
固定properties
: ユーザーに入力を求めるプロパティ${プロパティ名}
: 上記の例では最初に定義したyourName
を受け取りたいのでyourName
を指定type
: 期待するデータ型。string
number
boolean
から指定可能title
: タイトルdescription
: プロパティについての説明
required
: 入力を必須とするプロパティ名の配列
また、elicitInput
メソッドの戻り値にはaction
属性が含まれます。action
はユーザーの操作結果によって以下の3種類の値をとります。
accept
: ユーザーが承認し、データを入力したdecline
: ユーザーが追加の入力を明示的に拒否したcancel
: ダイアログを閉じる等でユーザーが明示的に選択せずに終了した
Elicitationを組み込んだserver.toolの実装は以下のようになります。
server.tool( "Greet", "Say Hello to you", {}, async () => { let yourName: string | undefined; const result = await server.server.elicitInput({ message: "あなたについて教えてください", requestedSchema: { type: "object", properties: { yourName: { type: "string", title: "氏名", description: "あなたの名前を入力してください" } }, required: ["yourName"] } }); if (result.action === "accept" && result.content && result.content.yourName) { yourName = String(result.content.yourName); } return { content: [ { type: "text", text: `こんにちは、${yourName}さん` } ] }; } );
これで、挨拶をしようとするとユーザーの名前を聞いてくるToolの完成です。早速使ってみましょう!
まずはGitHub Copilotに挨拶ツールを使わせます。すると
message
で定義したメッセージが表示され、名前を入力するよう求められます。
Respondボタンを押すと上部に入力ボックスが出てくるので、名前を入力します。
名前を渡すとToolの実行が再開します。無事入力した名前で挨拶してくれました。
このようにElicitationを利用すると、Toolの実行を中断して追加の情報を与えることができます。
試してみた:Elicitationを組み込んだ健康アドバイザーToolの実装
ここまででElicitationについてざっと理解できたので、ちょっとした遊びも兼ねて健康アドバイスを実施してくれるToolを実装してみました。
あらかじめ設定しておいた質問をエージェントが順々に投げかけ、集めた回答をもとに簡単な健康アドバイスを提供してくれます。
実演
「健康診断して」と伝えて開始します。基本情報を求められるので入力します。
最初は睡眠に関する質問です。
type: number
を指定しているので、数値以外はエラーが出るようになっています。
properties
で複数のプロパティを定義すると、Respondボタンは表示されずにそのまま次の質問に移ります。プロパティの中でenum
を定義すると選択式にもできます。
続いて運動習慣に関する質問です。
type: boolean
を使用するとtrue/falseで選択できます。
最後に朝食に関する質問です。
required
に含めなかったプロパティはNone No selection
を選択してスキップすることもできます。
質問をすべて入力し終えるとToolが最後まで実行されます。健康アドバイスを実施する旨を記載したプロンプトを戻り値にしているため、Toolの実行結果を受け取ったエージェントが考えてアドバイスをしてくれます。
無事アドバイスをしてくれました!現在の実装では質問を固定していますが、質問自体も生成AIが考えられるようにしても面白そうですね。
実装
だいぶ長いですが参考として実装も載せておきます。基本的には最初に実装したelicitInput
を少し変えながら連続して配置しています。
今回は
action: accept
のパターンしか処理を書いていませんが、実際はaction: decline
やaction: cancel
の場合の処理も書いてあげた方が良いです。ユーザーがRespondではなくCancelボタンを押した場合でもToolの実行は継続され、次のセクションの質問に移ってしまいます。
server.tool( "CheckHealth", "いくつかの質問からユーザの健康状態の診断と改善アドバイスを実施します", {}, async () => { let userName: string | undefined; let age: number | undefined; let sleepTime: number | undefined; let sleepQuality: string | undefined; let doesExerciseRegularly: boolean | undefined; let hasBreakfastEveryday: boolean | undefined; let breakfastMenu: string | undefined; const userInfoResult = await server.server.elicitInput({ message: "まずは、あなた自身について教えてください", requestedSchema: { type: "object", properties: { name: { type: "string", title: "氏名", description: "あなたの名前を教えてください" }, age: { type: "number", title: "年齢", description: "あなたの年齢を教えてください" } }, required: ["name", "age"] } }); if (userInfoResult.action === "accept" && userInfoResult.content) { if (userInfoResult.content.name) { userName = String(userInfoResult.content.name); } if (userInfoResult.content.age) { age = Number(userInfoResult.content.age); } } const sleepInfoResult = await server.server.elicitInput({ message: `${userName}さんの睡眠について教えてください`, requestedSchema: { type: "object", properties: { question1: { type: "number", title: "睡眠時間", description: "あなたの平均的な睡眠時間を教えてください" }, question2: { type: "string", title: "睡眠の質", description: "最近よく眠れている実感はありますか?", enum: ["常にある", "ある", "あまりない", "ない"] } }, required: ["question1", "question2"] } }) if (sleepInfoResult.action === "accept" && sleepInfoResult.content) { if (sleepInfoResult.content.question1) { sleepTime = Number(sleepInfoResult.content.question1); } if (sleepInfoResult.content.question2) { sleepQuality = String(sleepInfoResult.content.question2); } } const exerciseInfoResult = await server.server.elicitInput({ message: `${userName}さんの運動習慣について教えてください`, requestedSchema: { type: "object", properties: { question3: { type: "boolean", title: "運動習慣", description: "日常的に運動していますか?" } }, required: ["question3"] } }) if (exerciseInfoResult.action === "accept" && exerciseInfoResult.content && exerciseInfoResult.content.question3 !== undefined) { doesExerciseRegularly = Boolean(exerciseInfoResult.content.question3); } const breakfastInfoResult = await server.server.elicitInput({ message: `${userName}さんの朝食事情について教えてください`, requestedSchema: { type: "object", properties: { question4: { type: "boolean", title: "毎日食べているか", description: "朝ごはんは毎日食べていますか?" }, question5: { type: "string", title: "朝食の内容", description: "よく食べる朝ごはんのメニューを教えてください" } }, required: ["question4"] } }) if (breakfastInfoResult.action === "accept" && breakfastInfoResult.content) { if (breakfastInfoResult.content.question4 !== undefined) { hasBreakfastEveryday = Boolean(breakfastInfoResult.content.question4); } breakfastMenu = breakfastInfoResult.content.question5 ? String(breakfastInfoResult.content.question5) : "未回答"; } const prompt = [ `以下の情報を基に${userName}さんの健康状態を判断し、適切なアドバイスをしてください。`, `・年齢:${age}歳`, `・睡眠時間:平均${sleepTime}時間`, `・日頃から眠れているか:${sleepQuality}`, `・日常的に運動しているか:${doesExerciseRegularly}`, `・朝食を毎日食べているか:${hasBreakfastEveryday}`, `・朝食の内容:${breakfastMenu}` ].join('\n'); return { content: [ { type: "text", text: String(prompt) } ] }; } );
おわりに
本記事では、MCPの機能の1つであるElicitationについて、簡単な実装例とともに解説しました。
「Toolの実行途中にユーザーが入力できるボックスを表示させる」という特性は様々な用途に使えそうですね。MCPにはElicitation以外にもSamplingなど面白そうな機能がまだまだあるので、色々試していきたいです。
最後までお読みいただきありがとうございました!本記事が少しでも皆様のご参考になれば幸いです。
執筆:@zama.kazuki
レビュー:@nakamura.toshihiro
(Shodoで執筆されました)