MCPの(ほぼ)全機能を実装したズンドコMCPサーバーをFastMCP 2.0で実装し、MCPを完全に理解した話の続き。 前回の記事はこれ

今回が最終編で、MCPユーティリティ機能のProgressの実装について。

なお、この記事には生成AIの出力が含まれる。

ズンドコMCPサーバー・クライアント

MCPのいろいろな機能を試すため、ズンドコMCPサーバーとズンドコクライアントをFastMCP 2.0で開発した。

前回までの記事では以下の機能の実装について書いた。

  • MCPサーバー機能
    • Tools
      • get_zundoko: 「ズン」か「ドコ」をランダムにひとつ生成する。
      • check_kiyoshi: 「キ・ヨ・シ!」条件を満たすかチェックする。
      • reset_zundoko_kiyoshi: ズンドコ履歴とキヨシ状態をリセットする。
    • Resources
      • zundoko://history: get_zundokoで生成したズンドコの履歴。
        • ズンドコ履歴の変更通知に対応。
      • zundoko://kiyoshi: 「キ・ヨ・シ!」をしたことがあれば、その記録。(i.e. キヨシ状態)
        • キヨシ状態の変更通知に対応。
    • Resource Templates
      • zundoko://history/{index}: ズンドコ履歴内の特定の回次のズンドコ。
    • Prompts
      • explain_zundoko_kiyoshi: ズンドコキヨシのやりかたを説明するプロンプト。
    • Logging
      • Tools処理中のログを送信する。
  • MCPクライアント機能
    • Elicitation
      • check_kiyoshi時にユーザーが「キ・ヨ・シ!」コールできる。
    • Sampling
      • check_kiyoshi時にユーザーが「キ・ヨ・シ!」以外のコールをしたとき、気の利いたレスポンスを返す。
  • MCPユーティリティ機能
    • Ping
      • MCPサーバーの生存確認ができる。

今回の記事では以下の実装について書く。

  • MCPユーティリティ機能
    • Progress
      • get_zundokoリクエストの処理中に、「キ・ヨ・シ!」条件達成までの進捗をレポートする。

書いたソースはGitHubに置いてある。

FastMCPでProgressを実装

ProgressはMCPのユーティリティ機能で、MCPサーバーかMCPクライアントいずれかから送ったリクエストに対して、処理の進捗を通知する機能。 進捗通知を受けたい場合はリクエストのprogressTokenパラメータにリクエスト固有な値をいれて送って、進捗通知にはprogressToken、処理の総量(total)、処理済みの量(progress)、進捗メッセージ(message)を入れて送る。

ズンドコMCPサーバー・ズンドコクライアントにおいては、この機能は、「キ・ヨ・シ!」条件達成までの進捗の通知に使う。 通知はズンドコMCPサーバーからで、タイミングはget_zundokoツールでズンドコを生成した後。

「キ・ヨ・シ!」条件は複数のget_zundoko実行により達成されるので、通知する進捗は複数のリクエストにまたがるものになり、プロトコルに反するけど気にしない。 「キ・ヨ・シ!」条件達成進捗は「ドコ」により0に後退することがあり、後退はプロトコルに反してるけどそれも気にしない。

ズンドコMCPサーバー側の実装

進捗(i.e. Progress)の通知は、以前の記事のリソース変更通知と同様に、Contextオブジェクトのメソッドで送れる。 進捗通知のメソッドはreport_progress()

get_zundokoツールの処理の中で、ズンドコ履歴にズンドコを追加した後にreport_progress()で進捗通知するようにエンハンスする。

 @mcp.tool
 async def get_zundoko(ctx: Context) -> str:
     """
     Returns either "Zun" or "Doko" randomly.
     """
     choices = ["Zun", "Doko"]
     result = random.choice(choices)
     zundoko_history.append(result)
     await ctx.send_resource_list_changed()

+    zundoko_progress = _calc_zundoko_progress()
+    await ctx.report_progress(
+        progress=zundoko_progress[0], total=5, message=zundoko_progress[1]
+    )
+
     history_count = len(zundoko_history)
     await ctx.info(
         f"Zundoko history now contains {history_count} item(s)",
         extra={"count": history_count, "latest": result}
     )

     return result

こんな感じ。 _calc_zundoko_progress()progressmessageを作ってるけど、その処理内容はMCPに関係ないので割愛。

一応処理内容はこれ

ズンドコクライアント側の実装

進捗通知を受信するクライアントは、前回記事のSamplingのクライアントをベースにする。

進捗通知の処理は、リソース変更通知と同様に、Clientインスタンスに設定するハンドラ関数に書くことができる。

async def progress_handler(progress: int, total: int, message: str = None):
    percentage = progress / total * 100
    print(f"Progress: {progress}/{total} ({percentage:.0f}%) - {message}")

ハンドラ関数の中身はこんな感じ。進捗通知の内容を標準出力に吐くだけ。

このハンドラ関数は、Clientインスタンスのprogress_handlerプロパティに設定する。

 async def main():
     async with Client(
         "http://127.0.0.1:8080/mcp",
         log_handler=handle_log,
         elicitation_handler=elicitation_handler,
         sampling_handler=sampling_handler,
+        progress_handler=progress_handler,
     ) as client:
         while True:

このクライアントを実行すると以下のような出力が得られる。(ログハンドラの出力除く。)

<snip>
Doko
Pattern not found. Last 5 items: ['Doko', 'Zun', 'Zun', 'Doko', 'Doko']
Progress: 1.0/5.0 (20%) - 1 consecutive Zun(s)
Zun
Pattern not found. Last 5 items: ['Zun', 'Zun', 'Doko', 'Doko', 'Zun']
Progress: 2.0/5.0 (40%) - 2 consecutive Zun(s)
Zun
Pattern not found. Last 5 items: ['Zun', 'Doko', 'Doko', 'Zun', 'Zun']
Progress: 0.0/5.0 (0%) - Waiting for a Zun
Doko
Pattern not found. Last 5 items: ['Doko', 'Doko', 'Zun', 'Zun', 'Doko']
Progress: 1.0/5.0 (20%) - 1 consecutive Zun(s)
Zun
Pattern not found. Last 5 items: ['Doko', 'Zun', 'Zun', 'Doko', 'Zun']
Progress: 2.0/5.0 (40%) - 2 consecutive Zun(s)
<snip>

「キ・ヨ・シ!」条件達成進捗が表示できた。

FastMCP Cloud

FastMCPはFastMCP CloudというMCPサーバーのホスティングサービスもやっていて、今のところ無料でデプロイできる。

ズンドコMCPサーバーをFastMCP Cloudにデプロイしたら、https://zundoko.fastmcp.app/mcpでアクセスできるようになった。

ただこのエンドポイントでズンドコすると、なぜかcheck_kiyoshiのElicitationリクエストが受け取れずにスタックしてしまう。 FastMCP Cloudからクライアントへのリクエストは対応してない?