【MCP】AIエージェントをSDKのみで実装　「素」のMCP実装を徹底解説

![アプリ画面_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/f7591f59-e86c-46ed-81d7-dd0c4c3f50de.png)

##  はじめに ##

&nbsp;
MCP(Model Context Protocol) は、最近、頻繁に聞くようになった技術用語です。
MCP自体はただのC/Sでのプロトコルですが、そもそもAIと既存の様々なシステム資産との標準化された接続を目的に生まれたものであり、AIエージェントという文脈の中でも、とても有用なプロトコルです。

AIエージェントのデモアプリを、そのMCPで実装してみました。
**[MCPの公式SDK](https://github.com/modelcontextprotocol/python-sdk)は使用しましたが、ちょっと前にSDKに統合されたFastMCPは使用しておらず、MCPサーバーとMCPクライアントを全部自前で実装しました。**

本稿では、SDKに統合されたFastMCPを除いた<strong>「元祖SDK」</strong>での、
実装コード、実装のルールや注意点、その理由や背景
を記してあります。

読者は、流行りつつあるFastMCPを使用していない本来の「素」のMCPとはどういうものかを知ることができ、今後、それはMCPと関わるうえで何らかのプラスになるでしょう。
**筆者自身も、「素」のMCPガチ実装から得るところが多かったです。**

冒頭のキャプチャ画面を見ると、やっていることは<font color="red">おバカっぽく見えます</font>ですが、
中身の実装コードやその説明は、それなりに考え抜いたものです。
例えば、GPTを用いたFunction callingとMCPの絡ませ方のあたりは、同等のものが無く、他に転用できるようなものだと思っています。

### 読者が得られるもの ###
* MCP実装の基本形
これからMCP実装に挑戦したい人に向けて書きました。
FastMCPを使用していないので、本来のMCPというものが見えます。
今後、それが分かったうえでFastMCPを使用すると、今自分が何をやっているのか認識したうえで実装できます。
* How toだけでなくリクツの理解
なぜそうするのか、わかります。
* 他ではなかなか見つからない実装テンプレート
例えば、本稿の実装例では、AIエージェント内部のLLMにGPTを使用して、なおかつ汎用性を考えています。
こういうものはなかなか無いです。

### 前提 ###
* [MCP公式ドキュメント](https://modelcontextprotocol.io/introduction)を事前に読むことをお勧めします
MCPについて、しっかりとした説明をする余裕（紙面スペース＆筆者の気力）が、本稿にはもうありません・・・。
本稿では超簡単な説明しかできませんので、公式ドキュメントを事前に読んでいただくのがいいです。
* MCPクライアント⇔MCPサーバー間の通信は**標準入出力に限定**
MCPでは、MCPクライアントとMCPサーバー間の通信のTransport Layerとして、主に「標準入出力」と「HTTP with SSE」の[2通りを想定](https://modelcontextprotocol.io/docs/concepts/architecture#transport-layer)しています。
**本稿では標準入出力に限定します。** 
両者は利用想定もパラダイムも全く違い、MCPの公式SDKのカバー範囲も違います。統一的に扱うことはできません。

<BR>
※「AIエージェント」の定義

| 本稿での定義 | 一般的定義 |
|:-----------|:------------|
|ユーザーの代理人（エージェント）として挙動|環境を知覚し、その知覚に基づいて行動を選択し、環境に影響を与えるシステム|
|<font color="RED">以下①→②→③が自律して行われる。</font>|<font color="red">以下①→②→③が自律して行われる。</font>|
|<font color="blue">①ユーザーの自然言語での依頼に応じて</font>|<font color="blue">①知覚：環境（＝ユーザーの発話など）から入力を得る</font>|
|<font color="orange">②自ら必要な処理を選択し|</font><font color="orange">②判断や推論：その入力を処理・解釈する</font>|
|<font color="limegreen">③それを実行する</font>|<font color="limegreen">③行動：何らかの出力や操作を実行する</font>|

※本稿は、MCPの実装の説明のみをします。ですので、本WEBアプリの実行情報やMCPとは無関係なUI関連のコードの提供はしません。

##  イメージで速習「MCPとは？」 ##

<details><summary>クリックして、MCPとは何か、イメージで感じてみる</summary>

冒頭で書いた通り、MCPそのものの説明は、ここでMCPを説明する画像や表を紙芝居のように並べるだけにとどめます。
MCPを知らない人は、これら紙芝居を眺めて、MCPとはどういうものか、イメージで感じてください。

[MCP公式ドキュメント](https://modelcontextprotocol.io/introduction)
[MCP公式ドキュメント Core architecture](https://modelcontextprotocol.io/docs/concepts/architecture)

>MCP is an open <strong>protocol that standardizes how applications provide context to LLMs.</strong> Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.

>The Model Context Protocol (MCP) is built on a flexible, extensible architecture that enables seamless communication between LLM applications and integrations.

![公式_説明画像01_加工.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/d9f98b60-2218-4fa1-b213-b27e6ed68561.png)

![公式_説明画像02_加工.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/fd6df474-afdf-45b2-a856-e470e66f48b4.png)

### MCP用語 ###

#### プレイヤー3人「Host」「MCPクライアント」「MCPサーバー」 ####

| プレイヤー | 現実に即した定義 |
|:-----------:|:------------|
|Host|アプリケーション全体。MCPクライアントを含む。|
|MCPクライアント|別プロセスにいるMCPサーバーとMCPに従ってやり取りする、C/Sのクライアント側。<BR>仕事は、<BR>・ツールを実行させる、リソースを得る、など<br>・（AIエージェントの場合）LLMと協働してFunction callingを行う|
|MCPサーバー|別プロセスにいるMCPクライアントとMCPに従ってやり取りする、C/Sのサーバー側。<BR>仕事は、MCPクライアントの求めに応じてタスクを実行すること（ツールを実行する、リソースを返す、など）。<BR>具体的には、以下4つ。<BR>・全ツール（関数）の仕様のリストを提供<BR>・特定のツールを実行<BR>・全リソースの付属情報（メタデータ）のリストを提供<BR>・特定のリソースを提供|

#### 「ツール」と「リソース」 ####

| ツール／<br>リソース | 現実に即した定義 |
|:-----------:|:------------|
|ツール|MCPサーバーが保持する機能。いわゆる関数。<BR>**実行する**対象。|
|リソース|MCPサーバーがアクセスできる何らかのデータやデータの状態。<BR>**知る**対象。|

</details>

##  ＜本題の前に＞MCPの有用なユースケース ##

<details><summary>クリックして、前座の話を聞いてみる</summary>

ここでは、クライアント⇔サーバー間のProtocolでしかないMCP(Model Context **Protocol**)の有用なユースケースの1例を取り上げます。

まずは、AIが無い状態での、MCPの有用なユースケースの1例を見てみましょう。

### MCPの有用なユースケース1例（MCPを導入したC/S、AI無し） ###

今、異なる2つの系統のシステム「販売系」「勘定系」が混在しているとします。
それぞれのAPIは、それぞれ独自仕様、独自プロトコルとします。
これらを、ユーザーに近いサイドでは、全く同一のものとして扱いたい。
そういう要求が出てくるのは必然でしょう。
そういう時に、MCPというプロトコルが有用となる場合があります。
「AI」はどこにも出てきていません。

![MCPの効用_普通のCS（AIエージェント化前）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/1d8bb1fd-cc56-4f03-b46b-e7c7782f48b2.png)

図で示した通り、
ユーザーに近いサイドのMCPクライアントでは、異なる系統のAPIなんてことは全く意識することなく、それらを「MCPツール」として一括平等に扱うことができています。

ポイントは、
**<FONT COLOR="RED">・共通プロトコルMCPで呼び出せるMCPツール群を単一のMCPサーバーが提供する</FONT>**
**<FONT COLOR="RED">・MCPクライアントは、その単一MCPサーバーから提供されるそれらMCPツール群を一括平等に扱える</FONT>**
です。
上図の例は、MCPの効用を最大化するため、敢えて、異なるシステム発祥の、独自仕様・独自プロトコルのAPI群を単一MCPサーバーで束ねる、という例にしました。
が、そうでなくてもよく、**上記2ポイントが実現されている**ことが重要です。
AI無しでも、十分MCPの効用があります。

これら2ポイントは、次の**AIエージェント化のための必須条件**にもなります。

というわけで、このMCPを導入したC/Sを、思い切ってAIエージェント化してみましょう。

### MCPの有用なユースケース1例（MCPを導入したC/S、AIエージェント化後） ###

ユーザーが自然言語で業務の指示をし、AIエージェントがそれに応える、というものを想定します。

![MCPの効用_AIエージェント化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/55800b1e-9ee2-41b7-875f-a4181707d10b.png)

LLMがユーザー発話から必要なMCPツールを必要なだけ選択し、MCPクライアントがそれらMCPツールをMCPサーバーに実行させます。

このようなことができるのは、
**<FONT COLOR="RED">・共通プロトコルMCPで呼び出せるMCPツール群を単一のMCPサーバーが提供しており、
<FONT COLOR="RED">・MCPクライアントがそれらMCPツール群を一括平等に扱える</FONT>**
からです。

**AIであるLLMと既存のシステム資産を連携させるのに、MCPという共通のプロトコルが基盤になっている**、ということが目視できたと思います。

</details>

## AIエージェントアプリ　全体の概要 ##

このミニWEBアプリ「玉の箱を管理するAIエージェント」は、大きく以下の**2つの独立した機能**で構成されています。

1つ目の機能は、本アプリのメインとなるAIエージェントの機能です。ユーザーの依頼にもとづき、玉を入れる仮想上の箱を管理します。

2つ目の機能は、ユーザーと上記AIエージェントの会話を別のAIが評論する、というものです。

**どちらもMCPに準拠したC/S**ですが、1つ目のメイン機能の方がAIエージェントです。

![アプリ画面_02_2機能.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/aa63ae85-7229-49ad-903b-114f9b3ab59c.png)

それぞれをまとめるとこうなります。
|機能|特徴|概要|ほんわか<BR>イメージ|
|:-----------|:------------|:------------|:------------:|
|<font color="orangered">メイン機能：</font><br>AIエージェントが玉の箱を管理|・同一マシン内での標準入出力を介したMCP準拠のC/S<BR>・**<font color="orangered">AIエージェント</font>、LLMにgpt-4o**|ユーザーの自然言語での依頼に応じて、AIエージェントが仮想上の箱に玉を入れたり、箱の中の玉の個数を数えたり、箱の中の玉を検索したりする。|<img width="200" alt="GPT_玉の箱管理_イメージ_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/5222880f-36d8-4a2c-bfea-56c5c67f371c.png">|
|<font color="blue">サブ機能：</font><br>別AIがユーザーとAIエージェントとの会話を評論|・同一マシン内での標準入出力を介したMCP準拠のC/S<BR>・<font color="blue">AIエージェントではなく</font>、MCP準拠のただのC/S|ユーザーとAIエージェントとの会話に、別AI（LLMはgpt-4.1）が上から目線で評論を加える。|<img width="200" alt="GPT_別AIによる評論_イメージ加工_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/3ff8245e-4267-4475-9580-e32079710794.png">|

2機能のモジュール構成です。
アプリの中に、独立した2つのMCP準拠のC/S（標準入出力）があるのがわかります。
![2機能のモジュール構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/d95fe4b5-be32-4416-ab27-c82d3077b2ae.png)

<BR>先に、メイン機能「AIエージェントが玉の箱を管理」の実装を見てみます。
その次に、サブ機能「別AIがユーザーとAIエージェントとの会話を評論」の実装を見ることにします。

## メイン機能「AIエージェントが玉の箱を管理」について ##

<img width="660" alt="アプリ画面_02_メイン機能（AIエージェント）部分.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/8adce613-91c1-437c-b0b8-f1b144df83d6.png">

### 挙動 ###

一意の文字列が書いてある玉を入れる、仮想上の箱があります。
ユーザー（「あなた」）は、AIエージェントに対して、この箱や玉に関する依頼を自然言語で行います。
依頼の基本形は、
玉に文字列を書いて箱に入れる、箱から特定の文字列が書かれた玉を探し出す、箱の中の玉の数を数える、
などです。


<img width="200" alt="GPT_玉の箱管理_イメージ_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/5222880f-36d8-4a2c-bfea-56c5c67f371c.png">

1回の依頼の中に**複数の依頼**を含む場合にも、AIエージェントは**1回で対応**します。
```:1回の依頼の中に複数の依頼を含む例
あなた：玉に中曽根って書いて箱に入れてくれ。鳩山（由）という玉はある？
AI：玉に「中曽根」と書いて箱に入れました。「鳩山（由）」と書いてある玉はあります。
```

### MCPクライアントとMCPサーバー ###

MCPクライアントとMCPサーバーは、同一マシン内の**別プロセス**で走ります。
両者の通信は、**標準入出力**を介して行われます。

![メイン機能（AIエージェント）のモジュール構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/875fd8a2-bb6b-4704-ae6d-3763a307c3f6.png)

<BR>まずは、MCPサーバー（mcp_server_ball.py）から実装を見て行きましょう。

## メイン機能「AIエージェントが玉の箱を管理」のMCPサーバーの実装 ##

<details><summary>クリックして、メイン機能（AIエージェント）のMCPサーバーの実装を見る</summary>

MCPサーバーは、MCPクライアントの求めに応じて、実際に箱の中に玉を入れたり、玉を検索したりします（ツールの実行）。

### MCPサーバーが持つツール ###

ツールは2個です。
|ツール名|機能|
|:-----------:|:------------|
|add_ball|玉に指定された文字列を書いて箱に入れる。<BR>ball.txtに、指定された文字列の1行を書き加える。|
|get_balls_status|玉の箱の状態を見る。指定された文字列の玉があるか、箱に何個玉があるか、など。<br>ball.txtを読む。|

「玉の箱」の実体は、ball.txtというファイルで、中身は、1行＝1個の玉に書かれた文字列、です。

### MCPサーバー「mcp_server_ball.py」　コード全部 ###

```python:メイン機能「AIエージェントが玉の箱を管理」のMCPサーバー（mcp_server_ball.py）
import os
import json
import logging
import asyncio
from datetime import datetime
from typing import Dict, List, Optional, Any, Union
from mcp.server import Server
from mcp.server.lowlevel.server import NotificationOptions
import mcp.types as types
from pydantic import AnyUrl
from mcp.types import Resource

# ロギング設定（出力フォーマット追加）
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [MCP-SERVER-BALL] %(message)s"
)
logger = logging.getLogger("mcp-server-ball")

# 玉の箱の実体
BALLS_FILE = "balls.txt"

# サーバーの初期化
server = Server("mcp-container-server")

# リソース定義 - URIによって識別されるリソース。ここでは空の辞書で生成しておく。
resources = {}

def initialize_files():
    """ファイルが存在しない場合は作成する"""
    if not os.path.exists(BALLS_FILE):
        with open(BALLS_FILE, 'w', encoding='utf-8') as f:
            pass        

def read_file(file_path: str) -> list[str]:
    """ファイルから行ごとに読み込む"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return [line.strip() for line in f.readlines() if line.strip()]
    except Exception as e:
        logger.error(f"ファイル読み込みエラー: {e}")
        return []

def write_to_file(file_path: str, text: str) -> bool:
    """ファイルに新しい行を追加する"""
    try:
        with open(file_path, 'a', encoding='utf-8') as f:
            f.write(f"{text}\n")
        return True
    except Exception as e:
        logger.error(f"ファイル書き込みエラー: {e}")
        return False

def check_text_exists(file_path: str, text: str) -> bool:
    """ファイル内に特定のテキストが存在するか確認する"""
    items = read_file(file_path)
    return text in items

async def get_balls_resource() -> str:
    """リソース関数　箱のデータリソースを取得してJSON文字列を返す"""
    items = read_file(BALLS_FILE)
    data = {
        "items": items,
        "count": len(items)
    }
    return json.dumps(data, ensure_ascii=False)

async def get_box_appearance_resource() -> str:
    """リソース関数　箱の外観のJSON文字列を返す"""
    data = {
        "length": "50cm",
        "width": "60cm",
        "height": "70cm",
        "color": "transparent"
    }
    return json.dumps(data, ensure_ascii=False)

async def add_ball(text: str) -> Dict[str, Any]:
    """
    玉に文字を書いて箱に入れる関数

    Args:
        text: 玉に書く文字

    Returns:
        status: 処理結果のステータス
        data: 追加した玉のデータ
    """
    # 既に同じ文字の玉があるかチェック
    if check_text_exists(BALLS_FILE, text):
        return {"status": "error", "data": {"error": "既に存在する文字", "text": text}}

    if write_to_file(BALLS_FILE, text):
        return {"status": "success", "data": {"text": text}}
    else:
        return {"status": "error", "data": {"error": "玉の追加に失敗しました"}}

async def get_balls_status(search_text: Optional[str] = None, count_only: bool = False) -> Dict[str, Any]:
    """
    箱の状態を取得する関数

    Args:
        search_text: 検索する文字（オプション）
        count_only: 数だけを返すかどうか（オプション）

    Returns:
        status: 処理結果のステータス
        data: 取得した玉のデータや統計情報
    """
    # 玉のリストを取得
    balls = read_file(BALLS_FILE)

    # 玉の総数
    total_count = len(balls)

    # count_only が True なら玉の数だけを返す
    if count_only:
        return {"status": "success", "data": {"count": total_count}}

    # 特定の文字を検索
    if search_text:
        found = search_text in balls
        return {"status": "success", "data": {"text": search_text, "found": found}}

    return {"status": "success", "data": {"balls": balls, "count": total_count}}

# リソース関数を登録
resources["mcp://resources/balls"] = get_balls_resource
resources["mcp://resources/box_appear"] = get_box_appearance_resource

# 玉の箱の実体ファイルの初期化
initialize_files()

# 全ツールの仕様のリスト
@server.list_tools()
async def list_tools() -> list[types.Tool]:
    """
    利用可能なツールの仕様のリストを返す
    AIエージェントでは、LLMがその仕様のリストを使用する
    ユーザーの依頼を実現するために、LLMは、どのツールが適切か判断し、そのツールの引数を生成する
    """
    return [
        types.Tool(
            name="add_ball",
            description="玉に文字を書いて箱に入れる。（プロンプト例：玉に鈴木と書いて箱に入れて。）",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {
                        "type": "string",
                        "description": "箱に入れる玉に書く文字。"
                    }
                },
                "required": ["text"]
            }
            ),
            types.Tool(
            name="get_balls_status",
            description="箱に入っている玉や箱の状態を取得する。（プロンプト例：鈴木と書いてある玉はある？）（プロンプト例：今、玉は箱にいくつある？）",
            inputSchema={
                "type": "object",
                "properties": {
                    "search_text": {
                        "type": "string",
                        "description": "この文字が書かれた玉を箱の中から検索（オプション）。"
                    },
                    "count_only": {
                        "type": "boolean",
                        "description": "箱の中の玉の数を取得するか（オプション）。"
                    }
                }
            }
        )
    ]

# 特定のツールの呼び出し
@server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> list[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:
    """
    指定されたツールを引数を使って実行する
    LLMが選択したツールと生成した引数に基づいて処理を行う
    """

    # 各ツールを呼び出し
    result = None

    if name == "add_ball":
        text = arguments.get("text")
        result = await add_ball(text)
    elif name == "get_balls_status":
        search_text = arguments.get("search_text")
        count_only = arguments.get("count_only", False)
        result = await get_balls_status(search_text, count_only)
    else:
        result = {"status": "error", "data": {"error": "不明なツール"}}

    # 結果を返す
    return [types.TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]

# 全リソースの付属情報のリスト
@server.list_resources()
async def list_resources_info() -> list[Resource]:
    return [
        Resource(
            uri="mcp://resources/balls",
            name="箱の状態",
            description="現在、箱に入っている玉の情報（JSON）",
            mimeType="application/json"
        ),
        Resource(
            uri="mcp://resources/box_appear",
            name="箱の外観",
            description="箱のサイズと色（JSON）",
            mimeType="application/json"
        )
    ]

# 特定のリソースの取得
@server.read_resource()
async def read_resource(resource_uri: AnyUrl) -> str:
   
    str_resource_uri = str(resource_uri)
    
    if str_resource_uri in resources:        
        resource_getter = resources[str_resource_uri]        
        if resource_getter is None:
            logger.error("登録されているリソース関数無し")
            raise ValueError(f"このURIで登録されているリソース関数がありません。: {str_resource_uri}")            
        # resource_getterが直接シリアライズした文字列を返すのでそのまま返す
        resource_ret = await resource_getter()       
        return resource_ret
    else:
        raise ValueError(f"リソースが見つかりません: {str_resource_uri}")

# メイン関数 - MCPサーバーが独立プロセスとしてスクリプト実行された時の事実上のエントリーポイント
async def main():
    """
    MCPサーバーを実行するメイン関数
    独立したプロセスとしてスクリプト実行された際のエントリーポイント
    """
    # MCPサーバー実行
    from mcp.server.stdio import stdio_server
    try:
        # MCPクライアントと標準入出力を介したやり取りができる状態にする
        async with stdio_server() as (read_stream, write_stream):
            from mcp.server.models import InitializationOptions
            # MCPサーバーを実際に動かして、標準入出力を介した待ち受け状態を作る
            await server.run(
                read_stream,
                write_stream,
                InitializationOptions(
                    server_name="mcp-container-server",
                    server_version="1.0.0",
                    capabilities=server.get_capabilities(
                        notification_options=NotificationOptions(),
                        experimental_capabilities={}
                    )
                )
            )        
    except Exception as e:
        logger.error(f"MCPサーバー（玉の箱の管理）の起動中にエラーが発生しました: {e}")
        raise

# MCPサーバーが独立プロセスとしてスクリプト実行された時のエントリーポイント
if __name__ == "__main__":
    asyncio.run(main())
```

### MCPサーバー実装のポイント ###

MCPサーバー実装のポイントを列挙していきます。
MCPサーバー実装の際の参考にしてください。

[その1：`if __name__ == "__main__"`でサーバーを起動、MCPクライアントからの要求待ち受け状態を作る](#その1if-__name__--__main__でサーバーを起動mcpクライアントからの要求待ち受け状態を作る)
[その2：MCPサーバーに必須の`@server.`付き4関数を必ず実装](#その2mcpサーバーに必須のserver付き4関数を必ず実装)
[その3：（AIエージェント限定）list_tool()のdescriptionは、LLMが迷わないように明確に書く](#その3aiエージェント限定list_toolのdescriptionはllmが迷わないように明確に書く)

#### その1：`if __name__ == "__main__"`でサーバーを起動、MCPクライアントからの要求待ち受け状態を作る ####

```python:該当箇所
# メイン関数 - MCPサーバーが独立プロセスとしてスクリプト実行された時の事実上のエントリーポイント
async def main():
    """
    MCPサーバーを実行するメイン関数
    独立したプロセスとしてスクリプト実行された際のエントリーポイント
    """
    # MCPサーバー実行
    from mcp.server.stdio import stdio_server
    try:
        # MCPクライアントと標準入出力を介したやり取りができる状態にする
        async with stdio_server() as (read_stream, write_stream):
            from mcp.server.models import InitializationOptions
            # MCPサーバーを実際に動かして、標準入出力を介した待ち受け状態を作る
            await server.run(
                read_stream,
                write_stream,
                InitializationOptions(
                    server_name="mcp-container-server",
                    server_version="1.0.0",
                    capabilities=server.get_capabilities(
                        notification_options=NotificationOptions(),
                        experimental_capabilities={}
                    )
                )
            )        
    except Exception as e:
        logger.error(f"MCPサーバー（玉の箱の管理）の起動中にエラーが発生しました: {e}")
        raise

# MCPサーバーが独立プロセスとしてスクリプト実行された時のエントリーポイント
if __name__ == "__main__":
    asyncio.run(main())
```
MCPサーバーが1番最初にしなければならないことは、
**<FONT COLOR="red">MCPクライアントからの（標準入出力を介した）要求を待ち受ける状態を作る</font>**
ということです。
それを、`if __name__ == "__main__"`で行います。

個々のコードがやっていることは、コード中のコメントを参照してください（その程度の理解で十分）。
MCPサーバーを実装する場合は、**上記の該当箇所丸パクリ**で構いません。

MCPクライアント側で、MCPサーバーを別プロセスでスクリプトとして実行すると（[後のMCPクライアント実装において、このやり方を示します](#その1mcpサーバーとのsessionは定型文で都度構築)）、
**MCPサーバーの`if __name__ == "__main__"`が必ず走る**ことになります。
そして無事に、MCPサーバーがMCPクライアントからの（標準入出力を介した）要求を待ち受ける状態になります。

#### その2：MCPサーバーに必須の`@server.`付き4関数を必ず実装 ####

MCPサーバーに必須の`@server.`付き4関数とは、**MCPサーバーが担う4つの役割**をそれぞれ実行する関数であり、具体的には以下です。

| MCPサーバーに必須の関数<BR>（デコレーター名） | その意味、役割、存在意義 |
|:-----------|:------------|
|`@server.list_tools()`|<FONT COLOR="BLUE">全ツールの仕様のリスト[(MCP形式)](https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure)をMCPクライアントに返す。</FONT><BR>そのリストは、自分がやれる仕事とその依頼の仕方の一覧表みたいなもの。<br>**AIエージェントにおけるMCPサーバーでは、MCPクライアントから必ず呼ばれる。**|
|`@server.call_tool()`|<FONT COLOR="BLUE">MCPクライアントの要求により、特定のツールを実行する。</FONT><br>MCPクライアントは、**必ずこの`@server.call_tool()`を介して、MCPサーバーの特定のツールを実行**する。|
|`@server.list_resources()`|<FONT COLOR="BLUE">全リソースの付属情報のリストをMCPクライアントに返す。</FONT>|
|`@server.read_resource()`|<FONT COLOR="BLUE">特定のリソースをMCPクライアントに返す。</FONT>|

**MCPサーバーの一般的な役割は、表中の<FONT COLOR="BLUE">青字</FONT>で示した4つ**です。
**それぞれの役割に、それ専用の`@server.`付き関数が1：1で対応**しているのがわかります。
ですので、<FONT COLOR="RED">この4関数を必ず実装</FONT>する必要があります。

「`@server.list_tools()`」「`@server.call_tool()`」「`@server.list_resources()`」「`@server.read_resource()`」という**デコレーター名は厳守**ですが、@表記の1行下の実際の関数名は自由に付けてもいいです。そっちの方は、別プロセスにいるMCPクライアントからは使いようが無いからです。

:::note alert
「`@server.list_tools()`」「`@server.call_tool()`」「`@server.list_resources()`」「`@server.read_resource()`」は、<FONT COLOR="RED">これらデコレーター名を変えてもダメだし、4関数のどれか1つが欠けてもダメ</FONT>です。
これらの**デコレーター名と関数の存在の双方が、MCPのSDK及びMCPのプロトコルでの約束事**です。
:::

このデコレーター名「**`@server.`**」は、MCPのSDKのものです。
**別プロセスにいるMCPクライアントが標準入出力を介してMCPサーバーのこれらの4関数をコールしてその結果を受け取る際の、MCPの「プロトコル」の低階層部分をSDKが代わりにやってくれる**、というものです。

例えば、プロトコルでは、MCPクライアント⇔MCPサーバー間は、JSON-RPC2.0形式で行え、とあります。
が、SDKを使用すると、JSON-RPC2.0での通信なんて、我々は実装しなくていいです。

必然的に、
**別プロセスにいるMCPクライアントから標準入出力経由で呼べる関数は、これら「`@server.`」付きの4関数のみ**
ということになります。


#### その3：（AIエージェント限定）list_tool()のdescriptionは、LLMが迷わないように明確に書く ####

```python:該当箇所　@server.list_tools()
# 全ツールの仕様のリスト
@server.list_tools()
async def list_tools() -> list[types.Tool]:
    """
    利用可能なツールの仕様のリストを返す
    AIエージェントでは、LLMがその仕様のリストを使用する
    ユーザーの依頼を実現するために、LLMは、どのツールが適切か判断し、そのツールの引数を生成する
    """
    return [
        types.Tool(
            name="add_ball",
            description="玉に文字を書いて箱に入れる。（プロンプト例：玉に鈴木と書いて箱に入れて。）",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {
                        "type": "string",
                        "description": "箱に入れる玉に書く文字。"
                    }
                },
                "required": ["text"]
            }
            ),
            types.Tool(
            name="get_balls_status",
            description="箱に入っている玉や箱の状態を取得する。（プロンプト例：鈴木と書いてある玉はある？）（プロンプト例：今、玉は箱にいくつある？）",
            inputSchema={
                "type": "object",
                "properties": {
                    "search_text": {
                        "type": "string",
                        "description": "この文字が書かれた玉を箱の中から検索（オプション）。"
                    },
                    "count_only": {
                        "type": "boolean",
                        "description": "箱の中の玉の数を取得するか（オプション）。"
                    }
                }
            }
        )
    ]
```
この関数`@server.list_tool()`は「`@server.`」付きなので、別プロセスにいるMCPクライアントから標準入出力経由で呼ぶことができます。

この関数が実際にMCPクライアントに返しているのは以下のものです。

##### `@server.list_tools()` の戻り値　ツール一覧のlist（MCP形式） #####
```python:@server.list_tools() の戻り値　ツール一覧のlist（MCP形式）
[
  {
    "name": "add_ball",
    "description": "玉に文字を書いて箱に入れる。（プロンプト例：玉に鈴木と書いて箱に入れて。）",
    "inputSchema": {
      "type": "object",
      "properties": {
        "text": {
          "type": "string",
          "description": "箱に入れる玉に書く文字。"
        }
      },
      "required": [
        "text"
      ]
    }
  },
  {
    "name": "get_balls_status",
    "description": "箱に入っている玉や箱の状態を取得する。（プロンプト例：鈴木と書いてある玉はある？）（プロンプト例：今、玉は箱にいくつある？）",
    "inputSchema": {
      "type": "object",
      "properties": {
        "search_text": {
          "type": "string",
          "description": "この文字が書かれた玉を箱の中から検索（オプション）。"
        },
        "count_only": {
          "type": "boolean",
          "description": "箱の中の玉の数を取得するか（オプション）。"
        }
      }
    }
  }
]
```


上記のように、このMCPサーバーが提供する全ツール分の”仕様”をMCPクライアントに返します。
いわば、**このMCPサーバーが持つ全ツールの「仕様書」「APIリファレンス」のようなものです[(MCP形式)](https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure)。**
これを、MCPクライアントに返します。

MCPクライアントでは、ユーザーから自然言語での依頼があった際、その依頼と一緒にこの全ツールの「仕様書」もLLMに投入して、LLMに適切なツールを選択させ、そのツールの引数も生成させます。
LLMは、この全ツールの「仕様書」のうち、**各ツールの「description」を見てどのツールが適切か判断し、各ツールの各引数の「description」などを見て引数を生成**します。[（詳細）](#３llmにユーザーの依頼を実現するツールの選択と引数の生成をさせる)

だからこそ、
**この2種類のdescriptionは、LLMがわかるように、ちゃんと書く**
ということになるのです。

※本稿のAIエージェント機能はシンプルなので、descriptionの工夫の余地はあまりありませんでした。なのであまり良いお手本ではないです。

### その他、個々の実装 ###

#### @server.call_tool()のasync def call_tool(・・) ####

```python:該当箇所　@server.call_tool()
# 特定のツールの呼び出し
@server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> list[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:
    """
    指定されたツールを引数を使って実行する
    LLMが選択したツールと生成した引数に基づいて処理を行う
    """

    # 各ツールを呼び出し
    result = None

    if name == "add_ball":
        text = arguments.get("text")
        result = await add_ball(text)
    elif name == "get_balls_status":
        search_text = arguments.get("search_text")
        count_only = arguments.get("count_only", False)
        result = await get_balls_status(search_text, count_only)
    else:
        result = {"status": "error", "data": {"error": "不明なツール"}}

    # 結果を返す
    return [types.TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
```
この関数は「`@server.`」付きなので、別プロセスにいるMCPクライアントから標準入出力経由で呼ぶことができます。

MCPクライアントから指示された特定のツールを実行します。
MCPクライアントから、ツール名とそのツールの引数を受け取って、そのツール名のツールを実行します。

**このMCPサーバーでは**、戻り値は
`return [mcp.types.TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]`
にしています。どんなツールの実行を要求されても、その実行結果は必ずJSON文字列を含む`mcp.types.TextContent`オブジェクト1個のみのlistとなります。

このcall_toolのシグネチャの最後の方
` -> list[Union[mcp.types.TextContent, mcp.types.ImageContent, mcp.types.EmbeddedResource]]:`
は規定のもので変えてはいけませんが、これを見ると、MCP汎用的には、
`mcp.types.TextContent`オブジェクト、 `mcp.types.ImageContent`オブジェクト、`mcp.types.EmbeddedResource`オブジェクトが、**いくつでも入り乱れて**listに入って返ってくる可能性もある、ということがわかります。
**このMCPサーバーでは**、`mcp.types.TextContent`オブジェクト1個のみのlistを返します。

#### @server.list_resources()のasync def list_resources_info() ####

```python:該当箇所　@server.list_resources()
# 全リソースの付属情報のリスト
@server.list_resources()
async def list_resources_info() -> list[Resource]:
    return [
        Resource(
            uri="mcp://resources/balls",
            name="箱の状態",
            description="現在、箱に入っている玉の情報（JSON）",
            mimeType="application/json"
        ),
        Resource(
            uri="mcp://resources/box_appear",
            name="箱の外観",
            description="箱のサイズと色（JSON）",
            mimeType="application/json"
        )
    ]
```
この関数は「`@server.`」付きなので、別プロセスにいるMCPクライアントから標準入出力経由で呼ぶことができます。

MCPサーバーが持つ全リソース分の付属情報（メタデータ）を、MCPクライアントに返します。
各リソースの個々の付属情報（メタデータ）を、全リソース分、束ねてリストにしたものをMCPクライアントに返す、という意味です。
デコレーター名「`@server.list_resources()`」がとても誤解を招くのですが、リソース本体は含まれないので注意してください。「`@server.list_resources_info()`」と**脳内**変換した方がいいかもしれない。

#### @server.read_resource()のasync def read_resource(・・) ####

```python:該当箇所@server.read_resource()を含めた関連個所
# リソース定義 - URIによって識別されるリソース。ここでは空の辞書で生成しておく。
resources = {}

async def get_balls_resource() -> str:
    """リソース関数　箱のデータリソースを取得してJSON文字列を返す"""
    items = read_file(BALLS_FILE)
    data = {
        "items": items,
        "count": len(items)
    }
    return json.dumps(data, ensure_ascii=False)

async def get_box_appearance_resource() -> str:
    """リソース関数　箱の外観のJSON文字列を返す"""
    data = {
        "length": "50cm",
        "width": "60cm",
        "height": "70cm",
        "color": "transparent"
    }
    return json.dumps(data, ensure_ascii=False)
    
# リソース関数を登録
resources["mcp://resources/balls"] = get_balls_resource
resources["mcp://resources/box_appear"] = get_box_appearance_resource

# 特定のリソースの取得
@server.read_resource()
async def read_resource(resource_uri: AnyUrl) -> str:
   
    str_resource_uri = str(resource_uri)
    
    if str_resource_uri in resources:        
        resource_getter = resources[str_resource_uri]        
        if resource_getter is None:
            logger.error("登録されているリソース関数無し")
            raise ValueError(f"このURIで登録されているリソース関数がありません。: {str_resource_uri}")             
        # resource_getterが直接シリアライズした文字列を返すのでそのまま返す
        resource_ret = await resource_getter()       
        return resource_ret
    else:
        raise ValueError(f"リソースが見つかりません: {str_resource_uri}")
```
この関数は「`@server.`」付きなので、別プロセスにいるMCPクライアントから標準入出力経由で呼ぶことができます。

MCPクライアントからリソースの一意識別子であるURIを受け取って、そのリソースをMCPクライアントに返します。

**このMCPサーバーでは**、リソース追加時の拡張性とか、URI文字列埋め込みでの条件分岐を避ける、などの理由で「ディスパッチテーブル方式」を取っています。
が、こんな工夫は大してメリットは無いので、URI埋め込みでベタに条件分岐でもいいと思います。

そもそも、MCPサーバーによって、保持するリソースは千差万別なので、実装方法に一律の正解や模範はありません。

<BR>以上で、
メイン機能「AIエージェントが玉の箱を管理」のMCPサーバーの実装の説明は終わりです。
次は、同じメイン機能のMCPクライアントの実装です。

</details>

## メイン機能「AIエージェントが玉の箱を管理」のMCPクライアントの実装 ##

<details><summary>クリックして、メイン機能（AIエージェント）のMCPクライアントの実装を見る</summary>

AIエージェント機能におけるMCPクライアントの主な仕事は
・LLMと協働してFunction callingを行う
・LLMの指示通りにMCPサーバーにツールを実行させる
の2点です。
これらを中心に実装していきます。

### MCPクライアント「mcp_client_ball.py」　コード全部 ###

```python:メイン機能「AIエージェントが玉の箱を管理」のMCPクライアント（mcp_client_ball.py）
import asyncio
import json
import os
import logging
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
from openai import AsyncOpenAI
from dotenv import load_dotenv
from pydantic import AnyUrl, parse_obj_as

# 環境変数の読み込み
load_dotenv()
env_vars = os.environ.copy()

# ロギング設定
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [MCP-CLIENT-BALL] %(message)s"
)
logger = logging.getLogger("mcp-client-ball")

# MCPサーバーの設定（グローバル変数として定義）
SERVER_COMMAND = os.getenv("PYTHON_PATH", "python")  # デフォルトは "python"
SERVER_SCRIPT = "./server_ball/mcp_server_ball.py"
SERVER_PARAMS = StdioServerParameters(
    command=SERVER_COMMAND,
    args=[SERVER_SCRIPT],
    env=env_vars
)

LLM_MODEL_NAME ="gpt-4o"

# LLMに投入するシステムメッセージ
LLM_SYSTEM_MESSAGE = "あなたは、ユーザーの依頼を受けて、玉が入った箱の管理をするAIです。ユーザーの依頼に応じて、玉に文字を書いて箱に入れたり、その箱に特定の文字が書かれた玉が入っているか確認したり、玉の数を確認したりします。玉を箱に入れたら、何をしたのか報告してください。玉が入った箱の管理とは関係ないプロンプトにはツールを適用しないで、無視してください。"

# グローバル変数で状態を保持（各関数は自己完結するが、状態は共有）
available_tools = [] # MCP形式でのツール一覧
available_tools_gpt = [] # OpenAI形式でのツール一覧
available_resources = [] # リソース付属情報一覧
conversation_history = [] # LLMとMCPクライアントとのやりとりの履歴
is_initialized = False

def initialize_client():
    """
    MCPクライアントの初期化、MCPサーバーからツール一覧と各リソースの付属情報一覧を取得する
    """
    global available_tools, available_tools_gpt, available_resources, is_initialized

    if is_initialized:
        logger.info("既に初期化済みのため、再初期化をスキップします")
        return True

    # MCPクライアント初期化処理
    async def async_init():
        global available_tools, available_tools_gpt, available_resources

        # MCPサーバーとMCPで通信
        # MCPサーバーのためのプロセスを開始、MCPサーバーをスクリプト実行、MCPサーバーとの標準入出力通信ルートを構築
        async with stdio_client(SERVER_PARAMS) as (read, write): 
            # MCPサーバーとのMCP通信セッションを構築
            async with ClientSession(read, write) as session:
                # MCPサーバーとの初期化ハンドシェイク
                await session.initialize()
                
                # ツール一覧取得                
                list_tool_result = await session.list_tools()
                # 戻り値の型は、mcp.types.ListToolsResult
                available_tools = list_tool_result.tools
                # MCPサーバーから取得したMCP形式のツール一覧をOpenAI形式に変換
                available_tools_gpt = [
                    {
                        "type": "function",
                        "name": tool.name,
                        "description": tool.description,
                        "parameters": tool.inputSchema
                    }
                    for tool in available_tools
                ]

                # 各リソースの付属情報の一覧取得
                # 本機能では、MCPサーバーのリソースの増減が無く常に一定なので、ここで取得
                # MCPサーバーのリソースの増減がある場合は、ここではなく、必要に応じて取得
                list_resource_result = await session.list_resources()
                # 戻り値の型は、mcp.types.ListResourcesResult
                available_resources = list_resource_result.resources                          

            # async with ClientSessionを抜け、MCPサーバーとのMCP通信セッションが自動的に遮断される
        # async with stdio_clientを抜け、MCPサーバーとの標準入出力通信ルートの遮断とサーバープロセス終了が自動で行われる
    
        return True

    try:
        result = False
        result = asyncio.run(async_init())
        is_initialized = result
        return result
    except Exception as e:
        logger.error(f"MCPクライアント（玉の箱の管理）初期化処理中にエラー: {type(e).__name__}: {e}")
        return False

def process_user_prompt(user_input: str) -> str:
    """
    ユーザー依頼を処理し、GPTにツールを選択させ、
    必要なツール呼び出し・最終応答までを行う。
    """
    
    if not is_initialized:
        logger.info("MCPクライアント（玉の箱の管理）が未初期化のため、初期化を実行")
        initialize_client()
        
    # ユーザーの依頼を会話履歴に追加
    conversation_history.append({"role": "user", "content": user_input}) 
    
    async def async_process():
        try:
            # clientインスタンスを生成
            client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
            
            # GPTが、ユーザー依頼に対応するのに適切なツールをツール一覧から選択、引数も生成
            response = await client.responses.create(
                model=LLM_MODEL_NAME,
                tools=available_tools_gpt,
                input=[{"role": "system", "content": LLM_SYSTEM_MESSAGE}] + conversation_history
            )
            # response.outputはlist
            
            # GPTからの返答responseは、「自然言語でのメッセージ」or「選択したツール」のどちらか
            response_out_type = response.output[0].type
            # response_out_typeが"message"なら「自然言語でのメッセージ」　→　ツールは選択せず
            # response_out_typeが"function_call"なら「選択したツール」　→　以下の、MCPサーバーにツールを実行させる処理に入る

            # MCPサーバーとMCPで通信
            # MCPサーバーのためのプロセスを開始、MCPサーバーをスクリプト実行、MCPサーバーとの標準入出力通信ルートを構築
            async with stdio_client(SERVER_PARAMS) as (read, write):

                # MCPサーバーとのMCP通信セッションを構築
                async with ClientSession(read, write) as session:
                    # MCPサーバーとの初期化ハンドシェイク
                    await session.initialize()

                    while response_out_type == "function_call":
                        # GPTが何らかのツールを選択
                        tool_calls = response.output                

                        # GPTが選択したツールを順次実行
                        for call in tool_calls:
                            
                            if call.type != "function_call":                                
                                continue
                                
                            # 会話履歴にツール選択結果を追加
                            # callの型は、openai.types.responses.response_function_tool_call.ResponseFunctionToolCall
                            conversation_history.append(call)
                            # GPTが選択したツールの名前を取得
                            tool_name = call.name
                            # GPTが生成したツールの引数を取得
                            arguments = json.loads(call.arguments)
                            # GPTより採番されたcall_idを取得
                            call_id = call.call_id
                            # MCPサーバーにツールを実行させる
                            result_content = await session.call_tool(tool_name, arguments)

                            # MCPサーバーでのツール実行結果のjson文字列（欲しい答えはこれ）を戻り値result_contentから抽出
                            # 戻り値result_contentはmcp.types.CallToolResult型
                            # result_content.contentはlistで、MCPサーバーのcall_toolのシグネチャの戻り値の型list[Union[mcp.types.TextContent, mcp.types.ImageContent, mcp.types.EmbeddedResource]]そのもの
                            # MCPサーバー「mcp_server_ball」のcall_toolの実行結果はmcp.types.TextContentオブジェクトが1個のみのlistであることがわかっているので、以下のような簡素な抽出法でよい。
                            result_text = result_content.content[0].text

                            # JSONパースできる場合、status確認（ツール実行時にエラーが起こったかどうか）
                            # しかし、is_errorフラグそのものには使いみちは無いので、やらなくてもいいかも。
                            # ツール実行時のエラーには、玉に書く文字列（玉のキー文字列）の重複、というのがあり、必ずしもこのユーザー依頼対応の順次処理を止めるべきものとは限らない。
                            try:
                                result_json = json.loads(result_text)
                                is_error = (result_json.get("status") == "error")
                                e_msg_tool = result_json.get("data").get("error") if is_error else ""
                            except Exception:
                                logger.warning("結果文字列をJSONに変換できず")
                                # ツール実行結果文字列のJSON変換に失敗しているので、ツール実行時にエラー発生とみなす
                                is_error = True
                                e_msg_tool = "ツール実行時に不明なエラー"
                                
                            if is_error:
                                # ツール実行時にエラーが起こっても処理は止めない。
                                # ツール実行のこのターン（forループの）でエラーが起こっても、次のターンに影響を及ぼさない。
                                logger.warning(f"ツール実行時のエラー:{e_msg_tool}")
                                
                            # 会話履歴にツール実行結果を追加
                            conversation_history.append({
                                "type": "function_call_output",
                                "call_id": call_id,
                                "output": result_text
                            })
                        # for　ループを抜けた。GPTにより選択されていた全ツールの実行を完了した。

                        # 最終応答の生成 or 次のFunction callのループ
                        # ＜最終応答の生成＞
                        # ユーザー依頼が、実行すべき全ツールを確定させるものの場合
                        # （「玉に鈴木と書いて箱に入れて。箱に今玉は何個ある？」）
                        # このresponseのcreateでは、全ツールの実行が完了したので、最終応答の生成となる。
                        # response.output[0].typeは"message"になる。
                        # ＜次のFunction callのループ＞
                        # ユーザー依頼が、実行すべきツールを全確定させない（あるツールの実行で、次のツールが決まる）ものの場合
                        # （「箱に鈴木と書いてある玉が無ければ追加して」）
                        # もし、そのツールの実行結果が、さらなるツールの実行を要求する時（箱に鈴木と書いてある玉が無かった）、
                        # このresponseのcreateでは、最終応答の生成ではなく、次のFunction callのループとなる。
                        # response.output[0].typeは"function_call"になる。
                        response = await client.responses.create(
                            model=LLM_MODEL_NAME,
                            tools=available_tools_gpt,
                            input=[{"role": "system", "content": LLM_SYSTEM_MESSAGE}] + conversation_history
                            )
                            
                        response_out_type = response.output[0].type
                        # response_out_typeが"message"なら、responseには最終応答が入っている。whileを抜ける。
                        # response_out_typeが"function_call"なら、responseには次に実行すべきツールが入っている。whileループをもう一回行う。

                    # while response_out_type == "function_call"　ループを抜けた。
            
                # async with ClientSessionを抜け、MCPサーバーとのMCP通信セッションが自動的に遮断される
            # async with stdio_clientを抜け、MCPサーバーとの標準入出力通信ルートの遮断とサーバープロセス終了が自動で行われる
            
            #最終応答の抽出
            final_text = response.output_text
            
            # 会話履歴に最終応答を追加
            conversation_history.append({"role": "assistant", "content": final_text})
            
            return final_text
            
        except Exception as e:
            logger.error(f"ユーザー依頼の処理中にエラー発生: {type(e).__name__}: {e}")
            return f"[MCP] エラーが発生しました: {str(e)}"
            
    try:
        return asyncio.run(async_process())
    except Exception as e:
        logger.error(f"asyncio実行エラー: {type(e).__name__}: {e}")
        return f"[MCP] 実行エラー: {str(e)}"


def read_resource_by_uri(uri: str) -> str:
    """
    MCPサーバーから指定URIのリソース1個を取得する
    """
    
    if not is_initialized:
        logger.info("MCPクライアント（玉の箱の管理）が未初期化のため、初期化を実行")
        initialize_client()

    async def async_read():
        try:
            # MCPサーバーとMCPで通信
            async with stdio_client(SERVER_PARAMS) as (read, write):
                async with ClientSession(read, write) as session:
                    await session.initialize()
                    
                    # MCPサーバーからリソースを取得
                    uri_ob = parse_obj_as(AnyUrl, uri)
                    resource_data = await session.read_resource(uri_ob)
                    # resource_dataはmcp.types.ReadResourceResult型
                    # 欲しいリソースは文字列、resource_dataから抽出
                    return resource_data.contents[0].text
                    
        except Exception as e:
            logger.error(f"リソース取得エラー: {type(e).__name__}: {e}")
            return f"[MCP] リソース取得エラー: {str(e)}"

    try:
        return asyncio.run(async_read())
    except Exception as e:
        logger.error(f"asyncio実行エラー: {type(e).__name__}: {e}")
        return f"[MCP] 実行エラー: {str(e)}"
```

### AIエージェントとしての機能～ユーザーの依頼を捌く仕組み ###

ユーザーの自然言語での依頼から、AIエージェントからユーザーへの返答までは、以下の流れになります。
**主要部分は、OpenAIの「Function calling」です。**
LLMの判断材料になるツール一覧の取得と、実際のツールの実行、の2ヶ所で、MCPサーバーが絡みます。

#### １．事前にMCPサーバーからツール一覧を取得し、保持 ####
MCPクライアント初期化処理の`initialize_client`関数において、MCPサーバーからツール一覧（全ツールの仕様書のようなもの）を取得し、OpenAI形式に変換します。
くわしくは[こちら](#その3mcpクライアント起動時にmcpサーバーからツール一覧を取得)。
ここで取得して[保持するツール一覧（OpenAI形式）](#mcpクライアントが保持するツール一覧openai形式)は、後にユーザーから自然言語での依頼が投入された際、LLMのGPTに、その自然言語での依頼と一緒に投入し、どのツールが適切か選択させるのに使います。

～～～～～～
#### ２．ユーザーがUIに依頼を投入し、MCPクライアントへとその依頼が渡される ####
UIからMCPクライアントのprocess_user_prompt関数に、ユーザーの依頼が送られます。

依頼は、<font color="limegreen">「玉に岸田と書いて箱に入れて。玉は全部で何個になった？」</FONT>とします。

#### ３．LLMに、ユーザーの依頼を実現するツールの選択と引数の生成をさせる ####
ユーザーの依頼を会話履歴に追加します。

そして、保持してあるツール一覧（jsonのlist）とその会話履歴をGPTに投入し、以下の2つをさせます。
・ツールの選択
・選択したツールの引数の生成
**GPTは、「ユーザーの自然言語での依頼」と「ツール一覧」を照合し、ツールの選択と引数の生成**をします。

GPTがツール一覧（jsonのlist）で依拠するのは、以下の2つです。
・各ツールの要素「description」（そのツールの機能や果たす役割が書いてある）
&emsp;⇒　ツールを選択するのに使用
・各ツールの入れ子オブジェクト「parameters」（そのツールの全引数について、その意味や型が書いてある）
&emsp;⇒　選択したツールの引数を生成するのに使用

ここでは、GPTは、以下のようにツールを選択し、それらツールの引数を生成したとします。
|GPTが選択したツール | GPTが生成した引数 |
|:-----------:|:------------|
|add_ball|text="岸田"|
|get_balls_status|count_only=True|

ユーザーの依頼は、<font color="limegreen">「玉に岸田と書いて箱に入れて。玉は全部で何個になった？」</FONT>で、1つの依頼文の中に2つの依頼が含まれています。
なので、GPTは、ツールを2回分選択したわけです。

:::note info
この３を、OpenAI用語では「Function calling」、Anthropic用語では「Tool use」と言います。どちらも同じ意味です。
LLMが、プロンプトとともに提供された関数（Function）情報一覧から、適切な関数を選択し、その関数の引数も生成することを言います。
:::

#### ４．MCPサーバーに、ツールを順次実行させる ####

以下のサイクルを、選択されたツール全てについて、繰り返します。
↓↓↓　ツール1回分のサイクル　はじめ　↓↓↓
選択されたツールとその引数を会話履歴に追加
↓
選択されたツールを、引数を添えて、MCPサーバーに実行させる
↓
そのツールの実行結果を会話履歴に追加
↑↑↑　ツール1回分のサイクル　終わり　↑↑↑

今の例では、以下のようになります。

ツール「add_ball」が選択され、引数は「count_only=True」であることを会話履歴に追加
↓
MCPサーバーに、ツール「add_ball」を引数（text="岸田"）を添えて実行させる
↓
ツール「add_ball」の実行結果「成功」を会話履歴に追加
↓
ツール「get_balls_status」が選択され、引数は「count_only=True」であることを会話履歴に追加
↓
MCPサーバーに、ツール「get_balls_status」を引数（count_only=True）を添えて実行させる
↓
ツール「get_balls_status」の実行結果「4個」を会話履歴に追加

#### ５．LLMに、ユーザーへの最終応答を生成させる ####

GPTに、会話履歴をもとに、自然言語での最終応答を生成させます。

この時点での会話履歴は以下の通りです（上に行くほど新しい）。
5）ツール「get_balls_status」の実行結果「4個」
4）ツール「get_balls_status」が選択され、引数は「count_only=True」
3）ツール「add_ball」の実行結果「成功」
2）ツール「add_ball」が選択され、引数は「text="岸田"」
1）ユーザーの依頼<font color="limegreen">「玉に岸田と書いて箱に入れて。玉は全部で何個になった？」</FONT>

この会話履歴を踏まえたGPTの最終応答は、<font color="orangered">「玉に「岸田」と書いて箱に入れました。箱の中には全部で4個の玉があります。」</font>となりました。

このGPTの最終応答を会話履歴に追加します。
結果、この時点での会話履歴は以下の通りとなります（上に行くほど新しい）。
6）GPTの最終応答<font color="orangered">「玉に「岸田」と書いて箱に入れました。箱の中には全部で4個の玉があります。」</font>
5）ツール「get_balls_status」の実行結果「4個」
4）ツール「get_balls_status」が選択され、引数は「count_only=True」
3）ツール「add_ball」の実行結果「成功」
2）ツール「add_ball」が選択され、引数は「text="岸田"」
1）ユーザーの依頼<font color="limegreen">「玉に岸田と書いて箱に入れて。玉は全部で何個になった？」</FONT>

この会話履歴の状態で、次の新たなユーザー依頼に対処していきます。

#### ６．UIに最終応答を返す ####

MCPクライアントからUIに最終応答を返します。
MCPクライアントのprocess_user_prompt関数の戻り値が最終応答です。


### MCPクライアント実装のポイント ###

MCPクライアント実装のポイントを列挙していきます。
MCPクライアント実装の際の参考にしてください。

[その1：MCPサーバーとのsessionは、定型文で都度構築](#その1mcpサーバーとのsessionは定型文で都度構築)
[その2：MCPサーバー4関数の戻り値のトリセツ](#その2mcpサーバー4関数の戻り値のトリセツ)
[その3：MCPクライアント起動時に、MCPサーバーからツール一覧を取得](#その3mcpクライアント起動時にmcpサーバーからツール一覧を取得)

#### その1：MCPサーバーとのsessionは、定型文で都度構築 ####

MCPサーバーと何かやり取りする時は、**その都度、MCPサーバーとのsessionを構築**します（そしてやり取りが終わったら**自動的にsessionが破棄**されます）。

**実装においては単純**です。
<font color="red">**MCPサーバーと何かやり取りする場面毎に、以下の「定型文」を作ってください。**</font>

「定型文」とは別に、まずは以下を1ヶ所だけ記載してください。
MCPサーバーを独立プロセスでスクリプト実行するためのパラメーター設定にすぎません。
環境変数「PYTHON_PATH」は、心配なら設定してください。
```python:これは1ヶ所だけ。MCPサーバーを独立プロセスでスクリプト実行するためのパラメーター設定。
# 環境変数の読み込み
load_dotenv()
env_vars = os.environ.copy()

# MCPサーバーの設定（グローバル変数として定義）
SERVER_COMMAND = os.getenv("PYTHON_PATH", "python")  # デフォルトは "python"
SERVER_SCRIPT = "./server_ball/mcp_server_ball.py"  # MCPサーバーの.pyの場所
SERVER_PARAMS = StdioServerParameters(
    command=SERVER_COMMAND,
    args=[SERVER_SCRIPT],
    env=env_vars
)
```
（環境変数をコピーして、StdioServerParametersのenv引数に渡しているのは、MCPサーバー.pyの場所がこのMCPクライアント.pyの場所と異なり（./server_ball/mcp_server_ball.py）、MCPサーバー.py側で確実に.envファイルを読むことができないからです。）

MCPサーバーとやり取りする場面では、以下の「定型文」にしてください。
```python:MCPサーバーとやり取りする場面は以下の「定型文」にする。try～exceptまで含めて全部が「定型文」。
async def some_process(): #これが「定型文」。関数名「some_process」を適宜変更されたい。
    
    # MCPサーバーとMCPで通信
    # MCPサーバーのためのプロセスを開始、MCPサーバーをスクリプト実行、MCPサーバーとの標準入出力通信ルートを構築
    async with stdio_client(SERVER_PARAMS) as (read, write):
        # MCPサーバーとのMCP通信セッションを構築
        async with ClientSession(read, write) as session:
            # MCPサーバーとの初期化ハンドシェイク
            await session.initialize()

            # ここに、MCPサーバーとのやり取りを記述する。
            # ここでは一例として、リソース関連のやり取りをMCPサーバーと行う、とする。
            # MCPサーバーからリソース付属情報一覧を取得。MCPサーバーの@server.list_resources()を呼ぶ。
            res_info_list = session.list_resources()
            # 欲しいリソースのURIをres_info_listから抽出。ここら辺の細かいことは省略。
            # MCPサーバーから欲しいリソースを取得。MCPサーバーの@server.read_resource()を呼ぶ。
            res = session.read_resource(uri)
            some_result = resを使った何かの演算          

        # async with ClientSessionを抜け、MCPサーバーとのMCP通信セッションが自動的に遮断される
    # async with stdio_clientを抜け、MCPサーバーとの標準入出力通信ルートの遮断とサーバープロセス終了が自動で行われる

    return some_result

try:
    result = asyncio.run(some_process())    
    return result
except Exception as e:
    logger.error("エラー")
```
実際には、（非同期ではなく）同期関数の中にこの「定型文」が丸々置かれるはずです（非同期関数`async def`を`asyncio.run`で実行しているから）。

**MCPサーバーに何か用事があるたびに、０からMCPサーバーとのsessionを構築し直し、その用事が終わったら、sessionが自動的に破棄される（sessionの使い捨て）**、ということになります。
そしてそれを、**上記の「定型文」で行う**、ということです。

本稿の実装例では、以下3ヶ所で、同じ「定型文」で、上記の「MCPサーバーとのsessionの構築＆使い捨て」をしています。
・同期関数`def initialize_client`内の`async def async_init`
・同期関数`def process_user_prompt`内の`async def async_process`
・同期関数`def read_resource_by_uri`内の`async def async_read`

```python:（簡略コード）MCPサーバーとやり取りをしている3ヶ所で、同じ「定型文」を使用
def initialize_client():
    """
    MCPクライアントの初期化、MCPサーバーからツール一覧と各リソースの付属情報一覧を取得する
    """    
    # MCPクライアント初期化処理
    async def async_init():        
        # MCPサーバーとMCPで通信
        # MCPサーバーのためのプロセスを開始、MCPサーバーをスクリプト実行、MCPサーバーとの標準入出力通信ルートを構築
        async with stdio_client(SERVER_PARAMS) as (read, write):  
            # MCPサーバーとのMCP通信セッションを構築
            async with ClientSession(read, write) as session:
                # MCPサーバーとの初期化ハンドシェイク
                await session.initialize()
                （MCPサーバーとやり取り）
            # async with ClientSessionを抜け、MCPサーバーとのMCP通信セッションが自動的に遮断される
        # async with stdio_clientを抜け、MCPサーバーとの標準入出力通信ルートの遮断とサーバープロセス終了が自動で行われる
        return True

    try:
        result = False
        result = asyncio.run(async_init())
        is_initialized = result
        return result
    except Exception as e:
        logger.error(f"MCPクライアント（玉の箱の管理）初期化処理中にエラー: {type(e).__name__}: {e}")
        return False


def process_user_prompt(user_input: str) -> str:
    """
    ユーザー依頼を処理し、GPTにツールを選択させ、
    必要なツール呼び出し・最終応答までを行う。
    """    
    async def async_process():
        try:
            # MCPサーバーとMCPで通信
            async with stdio_client(SERVER_PARAMS) as (read, write):
                async with ClientSession(read, write) as session:
                    await session.initialize()
                    （MCPサーバーとやり取り）
        except Exception as e:
            logger.error(f"ユーザー依頼の処理中にエラー発生: {type(e).__name__}: {e}")
            return f"[MCP] エラーが発生しました: {str(e)}"

    try:
        return asyncio.run(async_process())
    except Exception as e:
        logger.error(f"asyncio実行エラー: {type(e).__name__}: {e}")
        return f"[MCP] 実行エラー: {str(e)}"


def read_resource_by_uri(uri: str) -> str:
    """
    MCPサーバーから指定URIのリソース1個を取得する
    """
    async def async_read():
        try:
            # MCPサーバーとMCPで通信
            async with stdio_client(SERVER_PARAMS) as (read, write):
                async with ClientSession(read, write) as session:
                    await session.initialize()
                    （MCPサーバーとやり取り）
        except Exception as e:
            logger.error(f"リソース取得エラー: {type(e).__name__}: {e}")
            return f"[MCP] リソース取得エラー: {str(e)}"

    try:
        return asyncio.run(async_read())
    except Exception as e:
        logger.error(f"asyncio実行エラー: {type(e).__name__}: {e}")
        return f"[MCP] 実行エラー: {str(e)}"
```


`async with stdio_client(SERVER_PARAMS) as (read, write):`で、MCPサーバーのためのプロセスを立てて、そのMCPサーバーをスクリプト実行しています。
MCPサーバーの[ここ](#その1if-__name__--__main__でサーバーを起動mcpクライアントからの要求待ち受け状態を作る)で書いた通り、
このMCPクライアントの起動方法に関わらず、**MCPサーバー側の`if __name__ == "__main__":`が必ず実行され、MCPサーバーは標準入出力を介した待ち受け状態になります。**

※sessionの使い捨てについて
以下は、興味のある人だけ。
せっかくMCPサーバーのプロセスを立てて、MCPサーバーとの標準入出力を介したsessionを確立したのだから、使い捨てにせずにこれを使い回したい、と思うのが自然です。
筆者は色々試しましたが、「SDKでは、それはやれないようになっている」と結論付けました。
SDKの（標準入出力の）設計思想は、どうも「都度確立＆使い捨て」のようです。
例えば、stdio_client。
```python:以下は必ずエラーになる。stdio_clientはasync with stdio_clientと呼ぶことがSDKで決まっている。
generator = stdio_client(SERVER_PARAMS)
(read_stream, write_stream) = await anext(generator)
```
stdio_clientは`async with stdio_client`と呼ぶことがSDKで決まっています。
`async with stdio_client`ブロックを抜けると、自動的にMCPクライアントとの標準入出力通信も閉じられます。おそらくSDKは、この「自動で閉じる」を常にしたいのだと思います。
しかし、`async with stdio_client`ブロックの中で、「任意のタイミングで」MCPサーバーとやり取りを行う「シンプルな」方法は、おそらく無いと思います。

SDKのみを使用する場合、sessionを使い回したいのなら、標準入出力ではなくhttpを使用するしかないのでは、と思います（そっちは未経験なのでよくわかりません）。

#### その2：MCPサーバー4関数の戻り値のトリセツ ####

MCPサーバー4関数とは[このこと](#その2mcpサーバーに必須のserver付き4関数を必ず実装)で、別プロセスにいるMCPクライアントから標準入出力経由で呼べるMCPサーバーの関数4つです。

MCPサーバーの関数の戻り値の扱いで<FONT COLOR="RED">**よくある間違い**</FONT>に、以下のようなものがあります。
```python:よくある間違い
# MCPサーバー上の
# @server.read_resource()
# async def read_resource(resource_uri: AnyUrl) -> str:
# を呼ぶ

ret = session.read_resource(uri) # 有効なuriとする
# 「async def read_resource(resource_uri: AnyUrl) -> str:」なんだから、retは文字列だろう！
# retを小文字変換
ret_lower = ret.lower() # エラー！retは文字列ではない！
```
**「MCPクライアントとMCPサーバーは異なるプロセス上にあり、標準入出力を介してやり取りしている」**
ということと、
**「MCPのSDKが、そのやりとりのプロトコルの低階層部分を裏でやっておいてくれている」**
ということを思い出してください。

MCPのSDKは、MCPサーバーでのこの`@server.read_resource()`関数の実行結果の文字列を、MCPのある型でラップします。その**ラップしたオブジェクトがret**になります。なのでretは文字列ではないです。
標準入出力という道を、MCPサーバーでの関数の実行結果の文字列を乗せて、サーバープロセスからクライアントプロセスに向けて走るコンテナ車「ret」を想像してください。

今、例で挙げた`@server.read_resource()`以外の3関数にも、同じことが言えます。
<font color="red">**MCPクライアントが受け取る戻り値は、MCPサーバーでの関数実行結果そのものではない**</font>
<font color="red">**関数のシグネチャでの戻り値の型と、MCPクライアントが受け取る戻り値の型は異なる**</font>
ということです。

以下に、MCPサーバーの4関数について、MCPクライアントが受け取る戻り値「ret」の型と、その戻り値「ret」からMCPサーバーでの関数実行結果を取り出す方法についてまとめました。
| MCPサーバーの関数のデコレーター名 | MCPサーバーでの関数実行結果の型<br>（シグネチャでの戻り値の型） | MCPクライアントが受け取る戻り値「ret」の型 | retの中から、MCPサーバーでの関数実行結果を取り出す方法|
|:-----------|:------------|:------------|:------------|
|`@server.list_tools()`|`list[mcp.types.Tool]`|`mcp.types.ListToolsResult`|`ret.tools`|
|`@server.call_tool()`|`list[Union[mcp.types.TextContent, mcp.types.ImageContent, mcp.types.EmbeddedResource]]`|`mcp.types.CallToolResult`|※後述|
|`@server.list_resources()`|`list[mcp.types.Resource]`|`mcp.types.ListResourcesResult`|`ret.resources`|
|`@server.read_resource()`|`str`|`mcp.types.ReadResourceResult`|`ret.contents[0].text`|

`@server.call_tool()`の「後述」について。

この関数のシグネチャでの戻り値の型は、
`list[Union[mcp.types.TextContent, mcp.types.ImageContent, mcp.types.EmbeddedResource]]`
です。
これは規定のもので、変えてはいけないです。
つまり、MCP汎用的には、
`mcp.types.TextContent`オブジェクト、 `mcp.types.ImageContent`オブジェクト、`mcp.types.EmbeddedResource`オブジェクトが、**いくつでも入り乱れて**listに入っている可能性があり、そのlistが`mcp.types.CallToolResult`オブジェクトに乗っけられてMCPクライアントに返ってくる、ということになります。

実際に何がlistに入ってきて、MCPクライアントがその中から何を求めるか、というのは、**個々のアプリケーション固有の話**になります。
なので、**ここで統一的に「常にこうします」と記すことはできません。**

本稿の実装例ではどうしているか、を書きましょう。

MCPサーバー側の`@server.call_tool()`では、実際に以下のlistを返しています。
`[mcp.types.TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]`
listは固定で1要素のみ、その1要素は常に`mcp.types.TextContent`オブジェクト、ということです。
これが、**このMCPサーバーでの**関数の実行結果になります。
MCPクライアント側で、関数の戻り値`mcp.types.CallToolResult`オブジェクト「ret」からこの**実行結果のlistを取り出すには、
`ret.content`
をします。**

さらにここから、MCPクライアントが本当に欲しい値（json文字列）を取り出すには、
`ret.content[0].text`
をします。
「listは固定で1要素のみ、その1要素は常に`mcp.types.TextContent`オブジェクト」で、`mcp.types.TextContent`オブジェクトのtext属性に、MCPクライアントが本当に欲しい値（json文字列）があるからです。

#### その3：MCPクライアント起動時に、MCPサーバーからツール一覧を取得 ####

MCPサーバーが提供するツール一覧は、アプリケーション動作中に変わることはまずありません。
ですので、MCPクライアント起動時にMCPサーバーから取得してしまい、ずっと保持しておいたほうがいいです。

ただ、本実装例のAIエージェント機能では、LLMにOpenAIのGPTを使っており、[MCP形式](https://modelcontextprotocol.io/docs/concepts/tools#tool-definition-structure)でのツール一覧と、[OpenAI形式](https://platform.openai.com/docs/guides/function-calling?api-mode=responses#defining-functions)のツール一覧では、その形式に差分があるため、OpenAI形式でのツール一覧に変換してから保持しています。

MCPクライアントの初期化関数`initialize_client`において、MCPサーバーからツール一覧を取得し、それをOpenAI形式に変換します。

```python:（簡略コード）initialize_client　該当箇所
def initialize_client():
    """
    MCPクライアントの初期化、ツール一覧と各リソースの付属情報一覧を取得する
    """
    # MCPクライアント初期化処理
    async def async_init():        

        # MCPサーバーとMCPで通信        
        async with stdio_client(SERVER_PARAMS) as (read, write):            
            async with ClientSession(read, write) as session:
                await session.initialize()
                
                # ツール一覧取得                
                list_tool_result = await session.list_tools()
                available_tools = list_tool_result.tools
                # MCPサーバーから取得したMCP形式のツール一覧をOpenAI形式に変換
                available_tools_gpt = [
                    {
                        "type": "function",
                        "name": tool.name,
                        "description": tool.description,
                        "parameters": tool.inputSchema
                    }
                    for tool in available_tools
                ]

                # 各リソースの付属情報の一覧取得
                # 本機能では、MCPサーバーのリソースの増減が無く常に一定なので、ここで取得
                # MCPサーバーのリソースの増減がある場合は、ここではなく、必要に応じて取得
                list_resource_result = await session.list_resources()
                available_resources = list_resource_result.resources                          

        return True

    try:
        result = False
        result = asyncio.run(async_init())
        is_initialized = result
        return result
    except Exception as e:
        logger.error(f"MCPクライアント（玉の箱の管理）初期化処理中にエラー: {type(e).__name__}: {e}")
        return False
```

OpenAI形式に変換しているところは、具体的に以下のことをしています。
[MCP形式のツール一覧](#serverlist_tools-の戻り値ツール一覧のlistmcp形式)に対して、
・個々のツールに、キーと値のペア「"type": "function"」を挿入
・その中の入れ子オブジェクトである、そのツールの引数情報の要素名を「inputSchema」→「parameters」に差し替え

##### MCPクライアントが保持するツール一覧（OpenAI形式） #####
```python:MCPクライアントが保持するツール一覧（OpenAI形式）
[
  {
    "type": "function",
    "name": "add_ball",
    "description": "玉に文字を書いて箱に入れる。（プロンプト例：玉に鈴木と書いて箱に入れて。）",
    "parameters": {
      "type": "object",
      "properties": {
        "text": {
          "type": "string",
          "description": "箱に入れる玉に書く文字。"
        }
      },
      "required": [
        "text"
      ]
    }
  },
  {
    "type": "function",
    "name": "get_balls_status",
    "description": "箱に入っている玉や箱の状態を取得する。（プロンプト例：鈴木と書いてある玉はある？）（プロンプト例：今、玉は箱にいくつある？）",
    "parameters": {
      "type": "object",
      "properties": {
        "search_text": {
          "type": "string",
          "description": "この文字が書かれた玉を箱の中から検索（オプション）。"
        },
        "count_only": {
          "type": "boolean",
          "description": "箱の中の玉の数を取得するか（オプション）。"
        }
      }
    }
  }
]
```

ちなみに、**このMCPクライアントでは**、ツール一覧だけではなく、リソース付属情報一覧も同じ場所で事前に取得しています。
本実装例のAIエージェント機能では、MCPサーバー側のリソースに増減が無く常に一定であることが明確なので、ここでしています。
MCPサーバー側のリソースに増減がある場合は、ここではなく必要に応じて行ってください。

### その他、個々の実装 ###

#### `process_user_prompt`関数でのFunction callingの補足説明 ####

`process_user_prompt`関数は、ユーザーの依頼を処理する、まさにこのMCPクライアントの核となる部分です。
ここで、LLMにGPTを使用してFunction callingをしています。
これについて簡単に補足説明します。

##### 理解のポイントは、`response.output[0].type` #####

この関数では、2ヶ所で、GPTに対応`response = await client.responses.create`をさせています。
1つ目の冒頭のものには、**ユーザー依頼に対して**対応させています。
2つ目のwhileループの終端あたりのには、**ユーザー依頼と全ツールの実行結果に基づく**対応をさせています。
「対応」とは、「自然言語でのメッセージを生成した」or「ツールを選択した」のどちらかです。
`response.output[0].type`が文字列「message」なら、対応「自然言語でのメッセージを生成した」。
`response.output[0].type`が文字列「function_call」なら、対応「ツールを選択した」。

まとめます。
| GPT対応箇所 |何に対する対応か| `response.output[0].type`の値 | どういう状況か |
|:-----------:|:------------:|:------------:|:------------|
|1つ目<br>関数冒頭|ユーザー依頼|message|玉の箱の管理とは無関係な依頼（天気とか）。→最終応答を生成した。|
|1つ目<br>関数冒頭|ユーザー依頼|function_call|玉の箱の管理に関係がある依頼なので、何らかのツールを選択した。|
|2つ目<br>whileループ終端|ユーザー依頼と全ツールの実行結果|message|もう実行すべきツールが残っていないので、何もツールを選択しなかった→最終応答を生成した。|
|2つ目<br>whileループ終端|ユーザー依頼と全ツールの実行結果|function_call|実行すべきツールが「発生した」ので、そのツールを選択した。<BR>ユーザー依頼が、**あるツールの実行結果により次に実行すべきツールが決まる**ようなもの。<BR>例）ユーザー依頼「鈴木と書いてある玉が無ければ追加して。」<BR>鈴木と書いてある玉の有無をツールを実行して確認したところ、無いことが分かった（これがforループから出てきた状態）。なので次は、玉を箱に追加するツールの実行が必要、とGPTが判断して、玉を箱に追加するツールを選択。|

最後の4パターン目が気付きにくいです。
whileループでやっていることは、こういうことです。

</details>

### 玉の箱の管理をするAIエージェントで遊んでみる！ ###

せっかく作ったので、遊んでみました。
フツーにやっても面白くないので、ちょいと**変化球**を投げてみました。

#### 第1の依頼）条件分岐で次にやることが決まる依頼、しかも全く別の依頼が同時にされる
AIエージェントは、まず「橋本」の玉の検索をし、その玉が無かったので、「橋本」の玉の追加をそのタイミングで決定、そして実際に「橋本」の玉の追加をした。
そして、全く別の依頼「箱の中の玉の名前はどうなった？」にも対処し、箱の中の全部の玉の名前を検索して列挙した。
これらの応答を1回にまとめて行った。
![AIエージェント画面_会話07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/311afd52-3db7-4b4d-ab02-b1d73235b36e.png)

#### 第2の依頼）機械的に収集したデータから判断させる
AIエージェントは、箱の中の全ての玉に書いてある名前（森、中曽根、鳩山（由）、小泉、小渕、岸田、橋本）を収集し、それをもとに、日本の歴代首相の名前だ、と推測・回答した。
![AIエージェント画面_会話04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/0a3e0aac-6ee0-4cbf-8a79-f8b00ab0b8f2.png)

#### 第3の依頼）一般的知識と組み合わせる
AIエージェントは、日本の初代首相の名前は伊藤博文である、という一般的知識を使用して、依頼に応えた。
![AIエージェント画面_会話06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/9b4bc94a-637c-4bfa-a7f7-b7cbc164ecda.png)

#### 第4の依頼）1度に3個の依頼、しかも真ん中の依頼はAIエージェントの対応範囲外
AIエージェントは、無関係な真ん中の依頼以外の2個の依頼に一括して対応し、最終応答においてのみ、無関係な真ん中の依頼に応答した。
まあ、「感情を持たない」と言ってるのに「大好き」とも言ってること自体が矛盾に満ちているが・・。
![AIエージェント画面_会話01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/0ac5299e-d12a-46ba-bc9e-d0c58d954b22.png)

<BR>これで、メイン機能「AIエージェントが玉の箱を管理」は全部終わりました。
次は、サブ機能「別AIがユーザーとAIエージェントとの会話を評論」を見てみましょう。

##  サブ機能「別AIがユーザーとAIエージェントとの会話を評論」について ##

<img width="600" alt="アプリ画面_02_サブ機能（別AIによる評論）部分_注釈.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/ca57a0dc-849e-4d5d-a00d-f3fb7230bf76.png">

このサブ機能は、**AIエージェントではなく、MCP準拠のただのC/S**です。

「AIエージェント」という大きな雑音が取り払われ、**剥き出しの鉄骨となったMCP準拠のC/Sを眺めることで、逆に、いろんな”創造的想像”がはたらくのでは**ないでしょうか。
「この作りはあれに使える」とか、「これをこう追加すれば生まれ変わる」など。

「会話をLLMに評論させる」という機能から、「AI臭」が漂っていますが、あくまでもC/Sのプロトコルに過ぎないMCPとしては、別にAIなんか無くてもいいのです。
例えば、「サーバー側で、会話を何かの分析に使用する」「サーバー側で、会話を何かの暗号に置き換える」でもいいのです。AIはどこにもありません。
筆者が、このサブ機能のネタとして「LLMによる会話の評論」を選んだのは、単にそれが面白いと思ったからで、AIを使ってやろう、なんて意識は毛頭ありませんでした。

Anthropicが
>MCP is an open protocol that standardizes how applications provide context to **LLMs**.
>The Model Context Protocol (MCP) is built on a flexible, extensible architecture that enables seamless communication between **LLM applications** and integrations.

と言っているのは、単にMCPを開発した目的がこれ、ということか、LLM方面からのMCPへの需要が大きい、ということでしょう。しかしそれらはMCPの機能とは関係がないです。
そういうものを無視して、MCPを**冷徹に「プロトコル」という機能的側面で見る**と、本機能は「MCP準拠のただのC/S」と捉えられるし、眺めれば、いろんな”創造的想像”もはたらくかもしれません。

話が横道に逸れてしまいました。元に戻します。

### 挙動 ###

「あなたとAIエージェントとの会話」を、別AIが評論します。
ただそれだけのシンプルな機能です。
AIエージェントではなく、MCP準拠のただのC/Sです。

評論は、辛口ではあるが深い哲学的な、しかも蘊蓄に富んだもので、関西弁で行われます。
故・竹村健一さんをイメージしました。パイプ片手に「大体やねぇ、」とか「これからはデリーシャスの時代や。」とは言いませんが・・。

<img width="200" alt="GPT_別AIによる評論_イメージ加工_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/3ff8245e-4267-4475-9580-e32079710794.png">

### MCPクライアントとMCPサーバー ###

MCPクライアントとMCPサーバーは、同一マシン内の別プロセスで走ります。
両者の通信は、**標準入出力**を介して行われます。

![サブ機能（別AIによる評論）のモジュール構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/6f487bfb-aede-4191-8c1d-93c0af5f0ac4.png)

<BR>まずは、MCPサーバー（mcp_server_review.py）から実装を見て行きましょう。

## サブ機能「別AIがユーザーとAIエージェントとの会話を評論」のMCPサーバーの実装 ##

<details><summary>クリックして、サブ機能のMCPサーバーの実装を見る</summary>

MCPサーバーは、MCPクライアントから、玉の箱の管理をするAIエージェントとユーザーの会話を渡され、それを別AIに評論させます（ツールの実行）。
別AIとは、gpt-4.1です。

### MCPサーバーが持つツール ###

会話をGPTに評論させる1個のみです。

| ツール名 | 機能 |
|:-----------:|:------------|
|review_text|渡された会話をgpt-4.1に評論させ、その評論結果を文字列で返します。|

### MCPサーバー「mcp_server_review.py」　コード全部 ###

```python:サブ機能「別AIがユーザーとAIエージェントとの会話を評論」のMCPサーバー（mcp_server_review.py）
import os
import json
import logging
import asyncio
from datetime import datetime
from typing import Dict, List, Optional, Any, Union
from mcp.server import Server
from mcp.server.lowlevel.server import NotificationOptions
import mcp.types as types
from pydantic import AnyUrl
from mcp.types import Resource
from dotenv import load_dotenv
from openai import OpenAI

# 環境変数の読み込み
# これが意味があるかどうかは、MCPクライアントがこのMCPサーバーをどう起動するかに依存
load_dotenv()

# ロギング設定（出力フォーマット追加）
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [MCP-SERVER-REVIIEW] %(message)s"
)
logger = logging.getLogger("mcp-server-review")

# サーバーの初期化
server = Server("mcp-review-server")

LLM_MODEL_NAME ="gpt-4.1"

# LLMに投入するシステムメッセージ
LLM_SYSTEM_MESSAGE = "あなたは、AIと人間との会話を評論するAIです。主に、玉が入った仮想的な箱の管理をするAIと、そのAIに（箱に玉を入れさせたり箱の中の玉の数を数えさせたりといった）依頼をする人間との間の会話です。辛口ではあるが深い哲学的な、しかも蘊蓄に富んだ評論を、関西弁で行います。評論は150字程度で濃密なものを。"

# リソース定義 - URIによって識別されるリソース
# このMCPサーバーではリソースは無い。空の辞書のまま。
resources = {}

async def review_text(text: str) -> Dict[str, Any]:
    """渡された会話をLLMが評論する"""

    # LLMの準備。
    # このMCPサーバーは、MCPクライアントとのセッションごとに刷新される。
    # LLMの使用も、このreview_text内でのみ。
    # 従って、事前にLLMの準備をすることに意味は無い。
    # load_dotenv()が意味のある結果を生むかわからないので、APIキーを取り出す処理を切り出し、キーが取れなかったらその旨エラーで通知。
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        logger.error("環境変数 OPENAI_API_KEY が読めませんでした。")
        return {"status": "error", "data": {"text": "[レビュー失敗] OPENAI_API_KEY が読めませんでした"}}
        
    # clientインスタンスを生成
    client = OpenAI(api_key=api_key)
    
    try:
        # 評論をさせる
        response = client.responses.create(
            model=LLM_MODEL_NAME,
            input=[
                {"role": "system", "content": LLM_SYSTEM_MESSAGE},
                {"role": "user", "content": text}
            ],
            temperature=0.7
        )

        # LLMの評論本文を抽出
        res_review_str = response.output_text

        return {"status": "success", "data": {"text": res_review_str}}

    except Exception as e:
        logger.error(f"OpenAI API呼び出しでエラー: {type(e).__name__}: {e}")
        return {"status": "error", "data": {"text": f"[レビュー失敗] {str(e)}"}}

@server.list_tools()
async def list_tools() -> List[types.Tool]:
    """
    利用可能なツールの仕様のリストを返す
    """
    return [
        types.Tool(
            name="review_text",
            description="渡された会話をLLMが評論して、返す",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {
                        "type": "string",
                        "description": "評論の対象になる文章"
                    }
                },
                "required": ["text"]
            }
        )
    ]

# ツール呼び出し
@server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:
    """
    指定されたツールを引数を使って実行する
    LLMが選択したツールと生成した引数に基づいて処理を行う
    """
    # 各ツールを呼び出し
    result = None

    if name == "review_text":
        text = arguments.get("text")
        result = await review_text(text)
    else:
        result = {"status": "error", "data": {"error": "不明なツール"}}

    # 結果を返す
    return [types.TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]

# 各リソースの付属情報のリスト
@server.list_resources()
async def list_resources_info() -> list[Resource]:
    # MCPサーバーにリソースそのものが無い場合、空のリストを返す
    return []

# 特定のリソースの取得
@server.read_resource()
async def read_resource(resource_uri: AnyUrl) -> str:
    # MCPサーバーにリソースそのものが無い場合、すべてのURIは無効
    raise ValueError(f"リソースが見つかりません: {resource_uri}")

# メイン関数 - MCPサーバーが独立プロセスとしてスクリプト実行された時の事実上のエントリーポイント
async def main():
    """
    MCPサーバーを実行するメイン関数
    独立したプロセスとして実行された際のエントリーポイント
    """
    # MCPサーバー実行
    from mcp.server.stdio import stdio_server
    try:
        # MCPクライアントと標準入出力を介したやり取りができる状態にする
        async with stdio_server() as (read_stream, write_stream):
            from mcp.server.models import InitializationOptions
            # MCPサーバーを実際に動かして、標準入出力を介した待ち受け状態を作る
            await server.run(
                read_stream,
                write_stream,
                InitializationOptions(
                    server_name="mcp-review-server",
                    server_version="1.0.0",
                    capabilities=server.get_capabilities(
                        notification_options=NotificationOptions(),
                        experimental_capabilities={}
                    )
                )
            )
    except Exception as e:
        logger.error(f"MCPサーバー（会話の評論）の起動中にエラーが発生しました: {e}")
        raise

# MCPサーバーが独立プロセスとしてスクリプト実行された時のエントリーポイント
if __name__ == "__main__":
    asyncio.run(main())
```

### MCPサーバー実装のポイント ###

MCPサーバー実装のポイントを列挙していきます。
MCPサーバー実装の際の参考にしてください。
**※ほとんど、メイン機能のAIエージェントのMCPサーバーの方で既に説明しています。
多くはそちらにリンクで飛ばすだけになります。**

[その1：`if __name__ == "__main__"`でサーバーを起動、MCPクライアントからの要求待ち受け状態を作る](#その1if-__name__--__main__でサーバーを起動mcpクライアントからの要求待ち受け状態を作る)
[その2：MCPサーバーに必須の`@server.`付き4関数を必ず実装](#その2mcpサーバーに必須のserver付き4関数を必ず実装)

#### その1：`if __name__ == "__main__`"でサーバーを起動、MCPクライアントからの要求待ち受け状態を作る ####
[詳細説明はこちら](#その1if-__name__--__main__でサーバーを起動mcpクライアントからの要求待ち受け状態を作る) 

#### その2：MCPサーバーに必須の`@server.`付き4関数を必ず実装 ####
[詳細説明はこちら](#その2mcpサーバーに必須のserver付き4関数を必ず実装)

### その他、個々の実装 ###

#### @server.call_tool()のasync def call_tool(・・) #### 
[詳細説明はこちら](#servercall_toolのasync-def-call_tool)


#### @server.list_resources()のasync def list_resources_info() ####

```python:該当箇所　@server.list_resources()
# 各リソースの付属情報のリスト
@server.list_resources()
async def list_resources_info() -> list[Resource]:
    # MCPサーバーにリソースそのものが無い場合、空のリストを返す
    return []
```

**このMCPサーバーには**、リソースそのものがありません。
**リソースは、必ず定義しなければならないものではないです。**
このMCPサーバーのように「ただ処理をして返すだけ」の場合は、リソースは定義のしようが無いです。

MCPサーバーにリソースそのものが無い場合は、`@server.list_resources()`は、常に空のリストを返します。

#### @server.read_resource()のasync def read_resource(・・) ####

```python:該当箇所　@server.read_resource()
# 特定のリソースの取得
@server.read_resource()
async def read_resource(resource_uri: AnyUrl) -> str:
    # MCPサーバーにリソースそのものが無い場合、すべてのURIは無効
    raise ValueError(f"リソースが見つかりません: {resource_uri}")
```

**このMCPサーバーには**、リソースそのものがありません。
**リソースは、必ず定義しなければならないものではないです。**
このMCPサーバーのように「ただ処理をして返すだけ」の場合は、リソースは定義のしようが無いです。

MCPサーバーにリソースそのものが無い場合は、`@server.read_resource()`は、常にエラー（該当するURIが無い）を返します。

<BR>以上で、
サブ機能「別AIがユーザーとAIエージェントとの会話を評論」のMCPサーバーの実装の説明は終わりです。
次は、同じサブ機能のMCPクライアントの実装です。

</details>

## サブ機能「別AIがユーザーとAIエージェントとの会話を評論」のMCPクライアントの実装 ##

<details><summary>クリックして、サブ機能のMCPクライアントの実装を見る</summary>

**本機能はAIエージェントではない＝特定の機能を果たすことが初めから決まっている**ものです。
なので、このMCPクライアントも単純で、ユーザーとAIエージェントとの会話文をMCPサーバーに渡し、評論させる、ということしかしません。

### MCPクライアント「mcp_client_review.py」　コード全部 ###

```python:サブ機能「別AIがユーザーとAIエージェントとの会話を評論」のMCPクライアント（mcp_client_ball.py）
import asyncio
import json
import os
import logging
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
from dotenv import load_dotenv

# 環境変数の読み込み
load_dotenv()
env_vars = os.environ.copy()

# ロギング設定
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [MCP-CLIENT-REVIEW] %(message)s"
)
logger = logging.getLogger("mcp-client-review")

# MCPサーバーの設定（グローバル変数として定義）
SERVER_COMMAND = os.getenv("PYTHON_PATH", "python")  # デフォルトは "python"
SERVER_SCRIPT = "./server_review/mcp_server_review.py"
SERVER_PARAMS = StdioServerParameters(
    command=SERVER_COMMAND,
    args=[SERVER_SCRIPT],
    env=env_vars
)

# グローバル変数で状態を保持（各関数は自己完結するが、状態は共有）
available_resources = [] # リソース付属情報一覧
is_initialized = False

def initialize_client():
    """
    MCPクライアントを初期化し、リソース付属情報一覧を取得する
    """
    global available_resources, is_initialized

    if is_initialized:
        logger.info("既に初期化済みのため、再初期化をスキップします")
        return True

    async def async_init():
        global available_resources

        # MCPサーバーとMCPで通信
        # MCPサーバーのためのプロセスを開始、MCPサーバーをスクリプト実行、MCPサーバーとの標準入出力通信ルートを構築
        async with stdio_client(SERVER_PARAMS) as (read, write):        
            # MCPサーバーとのMCP通信セッションを構築
            async with ClientSession(read, write) as session:
                # MCPサーバーとの初期化ハンドシェイク
                await session.initialize()

                # ツール一覧取得は行わない
                # 本機能はAIエージェントではないので、ツール一覧を取得しても使いみちは無く、不要である。
                
                # 各リソースの付属情報の一覧取得
                # 本機能では、そもそもMCPサーバー側にリソースとして定義されるものが無いが、一応やっておく。空のListを含んだmcp.types.ListResourcesResultが返ってくる。
                # 各リソースの付属情報が固定ではない（リソースそのものが増減したりする）場合は、ここではなく、必要に応じて取得
                list_resource_result = await session.list_resources()
                # 戻り値の型は、mcp.types.ListResourcesResult
                available_resources = list_resource_result.resources

            # async with ClientSessionを抜け、MCPサーバーとのMCP通信セッションが自動的に遮断される
        # async with stdio_clientを抜け、MCPサーバーとの標準入出力通信ルートの遮断とサーバープロセス終了が自動で行われる
                
        return True

    try:
        result = False
        result = asyncio.run(async_init())
        is_initialized = result
        return result
    except Exception as e:
        logger.error(f"MCPクライアント（会話の評論）初期化処理中にエラー: {type(e).__name__}: {e}")
        return False

def get_review_llm(conversation: str) -> str:
    """
    渡された会話をMCPサーバーに投げて評論させる。
    """
    
    if not is_initialized:
        logger.info("MCPクライアント（会話の評論）が未初期化のため、初期化を実行")
        initialize_client()

    async def async_process():

        # MCPサーバーとMCPで通信
        # MCPサーバーのためのプロセスを開始、MCPサーバーをスクリプト実行、MCPサーバーとの標準入出力通信ルートを構築
        async with stdio_client(SERVER_PARAMS) as (read, write):
            # MCPサーバーとのMCP通信セッションを構築
            async with ClientSession(read, write) as session:
                # MCPサーバーとの初期化ハンドシェイク
                await session.initialize()
                
                # MCPサーバーのcall_tool()を使用し、会話の評論をさせてその結果を得る。
                # 本機能はAIエージェントではない。やりたいこと（実行すべきツール）は既に決まっているので、ツール名と引数は埋め込みとする。
                tool_name = "review_text"
                arguments = {"text": conversation}
                result_content = await session.call_tool(tool_name, arguments)                
                # 戻り値result_contentはmcp.types.CallToolResult型
                # result_content.contentはlistで、MCPサーバーのcall_toolのシグネチャの戻り値の型list[Union[mcp.types.TextContent, mcp.types.ImageContent, mcp.types.EmbeddedResource]]そのもの
                # MCPサーバー「mcp_server_review」のcall_toolの実行結果はmcp.types.TextContentオブジェクトが1個のみのlistであることがわかっているので、以下のような簡素な抽出法でよい。                
                result_text = result_content.content[0].text                

                # JSON文字列であるresult_text中の["data"]["text"]に、評論本文がある
                # JSONパースして中のtextを取り出し、statusも確認（ツール実行時にエラーが起こったかどうか）
                try:
                    result_json = json.loads(result_text)
                    is_error = (result_json.get("status") == "error")
                    review_text_result = result_json["data"]["text"]
                except Exception as e:
                    logger.warning("結果文字列をJSONに変換できず")
                    review_text_result = "LLM評論処理結果文字列のJSON変換失敗。"
                    # ツール実行結果文字列のJSON変換に失敗しているので、ツール実行時にエラー発生とみなす
                    is_error = True

                if is_error:
                    # review_text_resultに、エラーの内容が入っているはず。
                    review_text_result = "LLM評論処理中にエラー発生。" + review_text_result

        return review_text_result

    try:
        return asyncio.run(async_process())
    except Exception as e:
        logger.error(f"asyncio実行エラー: {type(e).__name__}: {e}")
        return f"[MCP] 実行エラー: {str(e)}"
```

### ユーザーとAIエージェントとの会話の評論を得るまで ###

UIから、ユーザーとAIエージェントとの会話の評論を得るように指示
↓
MCPクライアントは、MCPサーバーで評論を行うツール「review_text」の名前を、その会話文を添えて、MCPサーバーの関数`@server.call_tool()`に入れて実行させる
↓
評論文がMCPサーバーから返ってくると、それをUIに返す

これらは、MCPクライアントの関数`get_review_llm`で行われます。

### MCPクライアント実装のポイント ###

MCPクライアント実装のポイントを列挙していきます。
MCPクライアント実装の際の参考にしてください。
**※ほとんど、メイン機能のAIエージェントのMCPクライアントの方で既に説明しています。
多くはそちらにリンクで飛ばすだけになります。**

[その1：MCPサーバーとのsessionは、定型文で都度構築](#その1mcpサーバーとのsessionは定型文で都度構築)
[その2：MCPサーバー4関数の戻り値のトリセツ](#その2mcpサーバー4関数の戻り値のトリセツ)

#### その1：MCPサーバーとのsessionは、定型文で都度構築 ####
[詳細説明はこちら](#その1mcpサーバーとのsessionは定型文で都度構築)

#### その2：MCPサーバー4関数の戻り値のトリセツ ####
[詳細説明はこちら](#その2mcpサーバー4関数の戻り値のトリセツ)

### その他、個々の実装 ###

#### MCPクライアント自体の初期化処理の要不要 ####

AIエージェントではない場合、一般的には、ツール一覧の使いみちが無く、MCPクライアントの初期化処理では、[AIエージェント機能のMCPクライアントがやったように](#その3mcpクライアント起動時にmcpサーバーからツール一覧を取得)MCPサーバーからツール一覧を取得する、ということを行いません。

従って、要不要は、MCPサーバーからリソース付属情報一覧を取得するかどうか、になります。
MCPサーバーのリソースが増減しない＝リソース付属情報一覧が固定、であるならば、初期化処理を作って、そこでMCPサーバーからリソース付属情報一覧を取得すればいいと思います。
本実装例の**このMCPクライアントでは**初期化処理`initialize_client`を設けています。
しかし本来はこの初期化処理は要らないです。MCPサーバーにはリソースそのものがありません。
敢えてやるとどういうものか、示しておきました。

</details>

### ユーザーとAIエージェントとの会話を評論させて遊んでみる！ ###

こっちも、せっかく作ったので、遊んでみました。

#### ＜評論の対象＞ユーザーとAIエージェントとの会話 ####

![会話の評論画面_会話01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/d69e8bbf-522a-460f-9e11-dc89e448eb60.png)


何回か、この会話を評論させてみます。

#### 評論バージョン1 ####

![会話の評論画面_会話01_評論07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/6f98f7f8-6d75-404e-9196-a0821a5f5492.png)

#### 評論バージョン2 ####

![会話の評論画面_会話01_評論01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/1c14ba64-cdfc-4e34-94e2-01eec48d8a9e.png)

#### 評論バージョン3 ####

![会話の評論画面_会話01_評論06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/75422b56-35cc-4c59-a4a2-6e5354da67d4.png)

#### 評論バージョン4 ####

![会話の評論画面_会話01_評論03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/768ce4d5-1255-41ec-a1a7-a1d2ae9778c1.png)

#### 評論バージョン5 ####

![会話の評論画面_会話01_評論08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613026/bb3270ab-603a-4fec-8860-611c78d96389.png)

これで、ミニアプリ「玉の箱を管理するAIエージェント」のMCP関連部分は、全部見終わりました！
おつかれさまでした。

**<FONT COLOR="red">次からは、FastMCPを使おう！</font>**