Table of Contents
MCPの(ほぼ)全機能を実装したズンドコMCPサーバーをFastMCP 2.0で実装し、MCPを完全に理解した話の続き。 前回の記事はこれ。
今回は主にMCPクライアント機能のElicitationの実装について。
Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから
— てくも (@kumiromilk) 2016年3月9日
「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了って関数作ったら満点で単位貰ってた
なお、この記事には生成AIの出力が含まれる。
ズンドコMCPサーバー・クライアント
MCPのいろいろな機能を試すため、ズンドコMCPサーバーとズンドコクライアントをFastMCP 2.0で開発した。
前回の記事では以下のMCPサーバー機能の実装について書いた。
- MCPサーバー機能
- Tools
get_zundoko: 「ズン」か「ドコ」をランダムにひとつ生成する。
- Resources
zundoko://history:get_zundokoで生成したズンドコの履歴。- ズンドコ履歴の変更通知に対応。
- Resource Templates
zundoko://history/{index}: ズンドコ履歴内の特定の回次のズンドコ。
- Prompts
explain_zundoko_kiyoshi: ズンドコキヨシのやりかたを説明するプロンプト。
- Logging
- Tools処理中のログを送信する。
- Tools
今回の記事では以下の実装について書く。
- MCPサーバー機能
- Tools
check_kiyoshi: 「キ・ヨ・シ!」条件を満たすかチェックする。reset_zundoko_kiyoshi: ズンドコ履歴とキヨシ状態をリセットする。
- Resources
zundoko://kiyoshi: 「キ・ヨ・シ!」をしたことがあれば、その記録。(i.e. キヨシ状態)- キヨシ状態の変更通知に対応。
- Tools
- MCPクライアント機能
- Elicitation
check_kiyoshi時にユーザーが「キ・ヨ・シ!」コールできる。
- Elicitation
書いたソースはGitHubに置いてある。
FastMCPでElicitationを実装
ElicitationはMCPクライアントの機能で、MCPサーバーから受信したリクエストに応じてユーザーに入力を求めて、入力結果をサーバーに返すことができる。 ズンドコMCPサーバー・ズンドコクライアントにおいては、この機能をユーザーに「キ・ヨ・シ!」コールさせるために使う。
ズンドコMCPサーバー側の実装
「キ・ヨ・シ!」は、「ズン」、「ズン」、「ズン」、「ズン」、「ドコ」の後にコールするので、いったんElicitationおいておいて、この「キ・ヨ・シ!」条件をズンドコ履歴が満たしているかを判定するだけのMCPツールcheck_kiyoshiを実装する。
@mcp.tool
async def check_kiyoshi(ctx: Context) -> str:
"""
Checks if the zundoko history matches the winning pattern (Zun Zun Zun Zun Doko)
and elicits "Ki-yo-shi!" from the user if it does.
"""
history_count = len(zundoko_history)
if history_count < 5:
return f"History has only {history_count} item(s), need at least 5 to check for the pattern."
if zundoko_history[-5:] == ["Zun", "Zun", "Zun", "Zun", "Doko"]:
return "Pattern found!"
else:
return f"Pattern not found. Last 5 items: {zundoko_history[-5:]}"これはまだ単に「キ・ヨ・シ!」条件の判定結果を返すだけのツール。
これを拡張して、「キ・ヨ・シ!」条件を満たしたときに即"Pattern found!"を返す代わりに、Elicitationでユーザーから返ってきたコールの内容によってレスポンスを変えるようにする。
Elicitationリクエストは、前回記事のLoggingと同様に、Contextオブジェクトのメソッドで送れる。 Elicitationのメソッドはelicit()で、ユーザーに向けたメッセージと、ユーザーに入力してほしい値の型を指定できる。
Elicitationリクエストに対しては、accept、decline、cancelの3通りのレスポンスがプロトコルに規定されていて、elicit()の戻り値はそれらに対応したAcceptedElicitation、DeclinedElicitation、CancelledElicitationのいずれかになり、AcceptedElicitationのときだけユーザー入力を受け取れる。
check_kiyoshiツールの処理の中で、「キ・ヨ・シ!」条件を満たしたときにelicit()を呼んで、その戻り値の型によってツールのレスポンスを変えるようにする。
from fastmcp import FastMCP, Context
+from fastmcp.server.elicitation import (
+ AcceptedElicitation,
+ DeclinedElicitation,
+ CancelledElicitation,
+)
<snip>
if zundoko_history[-5:] == ["Zun", "Zun", "Zun", "Zun", "Doko"]:
- return "Pattern found!"
+ elicit_result = await ctx.elicit(
+ "It's time to say Ki-yo-shi!",
+ response_type=str
+ )
+
+ match elicit_result:
+ case AcceptedElicitation(data=response):
+ if "Ki-yo-shi!" == response:
+ return "Perfect!'"
+ else:
+ return f"Pattern found! But you said '{response}' instead of 'Ki-yo-shi!'"
+ case DeclinedElicitation():
+ return "Pattern found! You declined to say 'Ki-yo-shi!'"
+ case CancelledElicitation():
+ return "Pattern found! You cancelled Ki-yo-shi!"
else:
return f"Pattern not found. Last 5 items: {zundoko_history[-5:]}"
elicit()の結果がAcceptedElicitationの時は、ユーザーの入力値を取得して、入力値がKi-yo-shi!かそれ以外によってツールのレスポンスを変えるようにした。
ズンドコクライアント側の実装
Elicitationリクエストを処理するクライアントは、前回記事のLoggingのクライアントをベースにする。
Elicitationリクエストの処理は、Loggingやリソース変更通知同様に、Clientインスタンスに設定するハンドラ関数に書くことができる。
from fastmcp.client.elicitation import ElicitResult
kiyoshi_answered = False
# ハンドラ関数の実装
async def elicitation_handler(message: str, response_type: type, params, context):
global kiyoshi_answered
# Elicitationリクエストのメッセージをコンソールに表示して、ユーザーの入力を受け取る
user_input = input(f"{message}: ")
kiyoshi_answered = True
# ズンドコMCPサーバーにユーザー入力を返す
return ElicitResult(action="accept", content=response_type(value=user_input))ハンドラ関数の中身はこんな感じ。今回はレスポンスはaccept一択。
このハンドラ関数は、Clientインスタンスのelicitation_handlerプロパティに設定する。
async def main():
async with Client(
"http://127.0.0.1:8080/mcp",
log_handler=handle_log,
+ elicitation_handler=elicitation_handler,
) as client:
await client.call_tool("get_zundoko", {})
クライアントの今の実装は、一回get_zundokoツールを呼ぶだけになってるけど、これを一連のズンドコキヨシをするように直す。
elicitation_handler=elicitation_handler,
) as client:
- await client.call_tool("get_zundoko", {})
+ while True:
+ # 「キ・ヨ・シ!」条件達成まで無限ループ
+ if kiyoshi_answered:
+ break
+
+ try:
+ # ズンドコを一つ取得
+ zundoko = await client.call_tool("get_zundoko", {})
+ print(zundoko.content[0].text)
+
+ # 「キ・ヨ・シ!」条件チェック
+ kiyoshi_check_result = await client.call_tool("check_kiyoshi", {})
+ print(kiyoshi_check_result.content[0].text)
+
+ await asyncio.sleep(1)
+ except Exception as e:
+ print(f"Error: {e}")
+ break
このクライアントを実行すると以下のような出力が得られる。(ログハンドラの出力除く。)
<snip>
Zun
Pattern not found. Last 5 items: ['Zun', 'Doko', 'Zun', 'Zun', 'Zun']
Zun
Pattern not found. Last 5 items: ['Doko', 'Zun', 'Zun', 'Zun', 'Zun']
Doko
It's time to say Ki-yo-shi!:ここで入力待ちになるので、「キ・ヨ・シ!」コールする。
<snip>
Zun
Pattern not found. Last 5 items: ['Zun', 'Doko', 'Zun', 'Zun', 'Zun']
Zun
Pattern not found. Last 5 items: ['Doko', 'Zun', 'Zun', 'Zun', 'Zun']
Doko
It's time to say Ki-yo-shi!: Ki-yo-shi!
Perfect!'MCPサーバー側にコールを返せた。
zundoko://kiyoshiリソースとreset_zundoko_kiyoshiツールの実装
「キ・ヨ・シ!」コール済みか(i.e. キヨシ状態)をリソースとして取得したり、その状態やズンドコ履歴をリセットするツールを実装したけど、実装内容は前回の記事と大差ないので割愛。
実装のコミットはこれ。