手を動かして学ぶ！MCPステップバイステップ実践ガイド for Beginners - Vol.5 ねらいうち！URLで指定したMCPモデル情報だけを取得する

## はじめに

皆さん、こんにちは！「手を動かして学ぶ！MCPステップバイステップ実践ガイド for Beginners」へようこそ。このシリーズでは、初心者の皆さんが簡単なコードを通じてMCP（Model Context Protocol）を実際に体験し、理解を深めていくことを目指しています。

前回までのあらすじ：
- [Vol.1](https://qiita.com/QueryPie/items/0004908e90a8796c3da1) 「MCPってどんなもの？ Pythonプログラミングの準備を始めよう！」では、MCPの概要と開発環境の準備について学びました。
- [Vol.2](https://qiita.com/QueryPie/items/19e87985394db096562a) 「リクエストを待つ窓口！Flaskで動かす初めてのMCPサーバー」では、MCPサーバーの役割を理解し、Flaskを使って簡単なサーバー (```app.py```) を立ち上げました。
- [Vol.3](https://qiita.com/QueryPie/items/66042580e43b102e0400) 「サーバーに「お願い」！requestsで作るMCPクライアントの基本」では、MCPクライアント (```client.py```) の役割と、requestsライブラリを使ってサーバーにアクセスする方法を学びました。
 - [Vol.4](https://qiita.com/QueryPie/items/4e3b30176b1522e107df) 「情報を分かりやすく！JSON形式でMCPデータをやり取りする」では、```app.py``` と ```client.py``` を改良し、コンピュータにも人間にも分かりやすいJSON形式でMCPデータをやり取りする方法を実践しました。具体的には、サーバーはスマートサーモスタットのモデル情報をJSONで返し、クライアントはそれを受け取って表示しました。

前回は、サーバーからスマートサーモスタットの全情報をJSON形式で取得する方法を学びましたね。しかし、もしこのサーバーが複数のデバイスの情報を持っていて、特定のデバイスの情報だけが欲しい場合はどうでしょうか？全ての情報を取得してから絞り込むのは少し非効率です。

今回のVol.5では、まさにその「ねらいうち」を実現する方法を学びます。具体的には、URLを使って特定のMCPモデル情報（今回は特定のデバイスIDに対応する情報）だけを指定して取得する方法です。インターネットで特定の商品ページを見たり、特定の記事を読んだりするのと同じように、MCPでも特定のモデル情報をピンポイントで取得できるようになります。

今回学ぶ内容は以下の通りです。
- **URLパスパラメータとは？：** URLの一部を使って情報を指定する方法を、身近な例えで解説します。
- **サーバーで特定情報を返す：** クライアントからの指定に応じて、該当するMCPモデル情報だけを返すように前回作成した ```app.py``` を改良します。
- **クライアントから指定して取得：** 前回作成した ```client.py``` から、欲しいMCPモデル情報をURLで指定して取得する方法を実践します。

この記事を読み終える頃には、あなたはMCPサーバーに対して「このデバイスIDの情報だけちょうだい！」とお願いし、的確な情報を手に入れられるようになっているはずです。それでは、さっそく始めていきましょう！
 
## 1. URLパスパラメータとは？ - まるでお店の「商品番号」

皆さんはインターネットでウェブサイトを見るとき、ブラウザのアドレスバーに表示される文字列を意識したことがありますか？あれが **URL（Uniform Resource Locator）** と呼ばれるもので、インターネット上のどこに情報があるかを示す「住所」のようなものです。

例えば、あるオンラインショップで特定の商品を見たいとき、URLは次のようになっているかもしれません。

```https://example-shop.com/products/12345```

このURLの中で、```/products/``` の後にある ```12345``` という数字、これが今回注目する **パスパラメータ (Path Parameter)** の一例です。

パスパラメータとは、簡単に言うと、**URLの「パス」部分に含まれる、動的に変わる値のこと** で、特定の情報やリソースを指定するために使われます。

例え話で考えてみましょう。

- **大きなデパートの各階案内:** デパートのURLが ```https://department-store.com/floor/``` だとします。もし3階の婦人服売り場の情報が見たければ、```https://department-store.com/floor/3F-womens-fashion``` のように、パスの最後に具体的な階数や売り場名を指定するかもしれません。この ```3F-womens-fashion``` がパスパラメータの役割を果たします。
- **図書館の本の背番号:** 図書館の蔵書検索システムのURLが ```https://library-system.com/books/``` だとします。特定の背番号「98765」の本の情報を知りたければ、```https://library-system.com/books/98765``` のようにアクセスするでしょう。この ```98765``` がパスパラメータです。
- **今回のお題、スマートデバイスのID:** これから作る仕組みでは、例えば ```http://127.0.0.1:5000/devices/THERMO-001-A``` のようにアクセスすると、```THERMO-001-A``` というIDを持つデバイスの情報だけを取得できるようになります。この ```THERMO-001-A``` がパスパラメータにあたります。

**なぜパスパラメータが必要なのでしょうか？**

もしパスパラメータがなければ、例えば ```/devices``` というURLにアクセスすると、サーバーが管理する全てのデバイス情報が一度に送られてくるかもしれません。それはそれで便利な場合もありますが、特定の情報だけが欲しい場合には非効率です。たくさんの情報の中から目的のものを探し出す手間がかかりますし、データ量も無駄に多くなってしまいます。

パスパラメータを使うことで、クライアント（情報を要求する側）はサーバー（情報を提供する側）に対して、「たくさんのデバイス情報はいらないよ、このデバイスIDのデバイス情報だけをちょうだい！」と、的確に伝えることができるのです。

MCPの世界でも同様です。私たちが扱うMCPモデルは、たくさんの種類やバージョン、あるいは個別のインスタンス（今回のデバイスのようなもの）が存在する可能性があります。その中から特定の情報だけを取得したい場合に、このパスパラメータが非常に役立ちます。

まとめると、パスパラメータは、URLをより具体的で意味のあるものにし、必要な情報へ効率的にアクセスするための重要な仕組みなのです。
 
## 2. サーバー側：指定された情報だけを返す準備 (```app.py``` の改良)

それでは、クライアントから特定のデバイスIDを指定されたときに、そのデバイスの情報だけを的確に返せるように、前回作成した ```app.py``` を改良していきましょう。今回も、PythonのWebフレームワークである **Flask** を使用します。

**環境の確認**
- **Python:** コンピュータにPythonがインストールされている必要があります。まだの方は、[Vol.1](https://qiita.com/QueryPie/items/0004908e90a8796c3da1) を参考に準備してください。Pythonのバージョンによっては python コマンドではなく python3 コマンドを使用する場合があります。
- **Flaskライブラリ:** Flaskライブラリが必要です。インストールがまだの場合は、ターミナル（WindowsならコマンドプロンプトやPowerShell、macOSやLinuxならターミナル.appなど）で以下のコマンドを実行してください。 

```
pip install Flask
```

もし ```pip``` コマンドでパーミッションエラーなどが出る場合や、Python 3系を明示的に使いたい場合は、```pip3 install Flask``` を試してみてください。 すでに[Vol.2](https://qiita.com/QueryPie/items/19e87985394db096562a)や[Vol.4](https://qiita.com/QueryPie/items/4e3b30176b1522e107df)で ```app.py``` を動かせていれば、この手順は不要です。

**前回 (Vol.4) の ```app.py``` （おさらい）**

前回の ```app.py``` は以下のようになっていました。これは1つのスマートサーモスタットの情報を返すものでした。

```Python
# app.py (Vol.4)
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def get_model_data():
    model_data = {
        "modelName": "Smart Thermostat X1000",
        "deviceId": "THERMO-001-A",
        "location": "Living Room",
        "status": {
            "currentTemperature": 23.5,
            "targetTemperature": 24.0,
            "unit": "Celsius",
            "isActive": True,
            "mode": "auto"
        },
        "supportedModes": ["auto", "cool", "heat", "off"]
    }
    return jsonify(model_data)

if __name__ == '__main__':
    app.run(debug=True) # デフォルトのポート5000で動作
```

このサーバーは、ルートURL (```/```) にアクセスすると、固定のモデル情報を返します。

**複数のデバイス情報を扱うための準備**

今回は複数のデバイス情報を扱えるようにし、その中から特定のIDのデバイス情報を返せるようにします。まず、サーバー側で複数のデバイス情報を持つようにデータを変更しましょう。リストの中に各デバイスの情報を辞書として格納する形にします。

```Python
# app_v5.py の冒頭部分
from flask import Flask, jsonify

app = Flask(__name__)

# サンプルのMCPデバイスデータ (リスト形式)
# 各デバイスはユニークな "deviceId" を持つ想定
all_devices_data = [
    {
        "modelName": "Smart Thermostat X1000",
        "deviceId": "THERMO-001-A",
        "location": "Living Room",
        "status": {"currentTemperature": 23.5, "targetTemperature": 24.0, "unit": "Celsius", "isActive": True, "mode": "auto"},
        "supportedModes": ["auto", "cool", "heat", "off"]
    },
    {
        "modelName": "Smart Light L200",
        "deviceId": "LIGHT-002-B",
        "location": "Bedroom",
        "status": {"brightness": 80, "color": "warm_white", "unit": "percent", "isActive": True},
        "supportedModes": ["on", "off", "dim"]
    },
    {
        "modelName": "Security Camera C300",
        "deviceId": "CAM-003-C",
        "location": "Entrance",
        "status": {"isRecording": False, "detectionMode": "motion", "isActive": True},
        "supportedEvents": ["motion_detected", "sound_detected"]
    }
]
```

**パスパラメータを受け取るためのルート定義**

次に、特定のデバイスIDをURLで受け取れるようにルートを定義します。例えば、```/devices/THERMO-001-A``` のようにアクセスされたら、```THERMO-001-A``` の部分をパラメータとして受け取ります。Flaskでは、ルート定義に ```<variable_name>``` のように記述します。

```Python
@app.route('/devices/<device_id>', methods=['GET'])
```

この ```<device_id>``` が、クライアントからURLで指定された実際のデバイスIDに置き換えられます。

**特定のデバイス情報を検索して返す処理**

このルートに対応する関数では、受け取った ```device_id``` を使って ```all_devices_data``` リストの中から該当するデバイスを探します。

```Python
# ... (app と all_devices_data の定義は上記参照) ...

@app.route('/devices/<device_id>', methods=['GET'])
def get_specific_device(device_id):
    found_device = None
    for device in all_devices_data:
        if device["deviceId"] == device_id:
            found_device = device
            break # 見つかったらループを抜ける

    if found_device:
        return jsonify(found_device) # 見つかったデバイス情報をJSONで返す
    else:
        # 見つからなかった場合は、エラーメッセージとステータスコード404を返す
        return jsonify({"error": "Device not found", "requested_id": device_id}), 404
```

この ```get_specific_device``` 関数では、

1.引数 ```device_id``` でURLから渡されたIDを受け取ります。
1. all_devices_data リストを反復処理し、各デバイスの "deviceId" と引数の ```device_id``` を比較します。
1. 一致するデバイスが見つかれば、そのデバイス情報を ```found_device``` に格納し、ループを終了します。
1. ```found_device``` に情報があれば（つまり見つかれば）、その情報をJSON形式で返します。HTTPステータスコードは ```200 OK``` になります。
1. 見つからなければ、エラーメッセージとリクエストされたIDを含むJSONを、HTTPステータスコード ```404 Not Found``` と共に返します。

**おまけ：全デバイス情報を返すエンドポイントも作成**

比較のために、全てのデバイス情報を一覧で返すエンドポイントも用意しておくと便利です。これは[Vol.4](https://qiita.com/QueryPie/items/4e3b30176b1522e107df)で行ったJSON形式でのデータ送信に近い形です。

```Python
@app.route('/devices', methods=['GET'])
def get_all_devices():
    return jsonify({"devices": all_devices_data})
```

ここでは ```/devices``` というURLにアクセスすると、```all_devices_data``` の全デバイス情報が ```"devices"``` というキーのリストとして返されます。

**```app.py``` の全体コード (Vol.5 版)**

これまでの変更を反映した ```app.py``` の全体像は以下のようになります。これを ```app.py``` という名前で保存するか、既存の ```app.py``` を上書きしてください。

```Python
# app.py (Vol.5 版)
from flask import Flask, jsonify

app = Flask(__name__)

# サンプルのMCPデバイスデータ (リスト形式)
all_devices_data = [
    {
        "modelName": "Smart Thermostat X1000",
        "deviceId": "THERMO-001-A",
        "location": "Living Room",
        "status": {"currentTemperature": 23.5, "targetTemperature": 24.0, "unit": "Celsius", "isActive": True, "mode": "auto"},
        "supportedModes": ["auto", "cool", "heat", "off"]
    },
    {
        "modelName": "Smart Light L200",
        "deviceId": "LIGHT-002-B",
        "location": "Bedroom",
        "status": {"brightness": 80, "color": "warm_white", "unit": "percent", "isActive": True},
        "supportedModes": ["on", "off", "dim"]
    },
    {
        "modelName": "Security Camera C300",
        "deviceId": "CAM-003-C",
        "location": "Entrance",
        "status": {"isRecording": False, "detectionMode": "motion", "isActive": True},
        "supportedEvents": ["motion_detected", "sound_detected"]
    }
]

# 全デバイス情報を返すエンドポイント
@app.route('/devices', methods=['GET'])
def get_all_devices():
    return jsonify({"devices": all_devices_data})

# 特定のデバイスIDに基づいて情報を返すエンドポイント
@app.route('/devices/<device_id>', methods=['GET'])
def get_specific_device(device_id):
    found_device = None
    for device in all_devices_data:
        if device.get("deviceId") == device_id: # .get() を使うとキーが存在しなくてもエラーにならない
            found_device = device
            break

    if found_device:
        return jsonify(found_device)
    else:
        return jsonify({"error": "Device not found", "requested_id": device_id}), 404

if __name__ == '__main__':
    print("MCP Server (Vol.5) is running on http://127.0.0.1:5000")
    print("Access all devices at: http://127.0.0.1:5000/devices")
    print("Access a specific device, e.g., THERMO-001-A at: http://127.0.0.1:5000/devices/THERMO-001-A")
    print("To stop the server, press CTRL+C")
    # macOSやLinuxで python3 を使っている場合は、python3 app.py で実行してください。
    app.run(debug=True, port=5000) # ポート番号を明示 (デフォルトは5000)
```

```.get("deviceId")``` のように ```.get()``` を使うことで、万が一 ```deviceId``` キーが存在しないデータがあってもエラーにならず、安全に比較できます。

**実行方法:**

ターミナルで ```app.py``` があるディレクトリに移動し、以下のコマンドで実行します。

Windows: ```python app.py```
macOS/Linux: ```python3 app.py``` (または ```python app.py```)

これで、サーバー側の準備は完了です。
 
## 3. クライアント側：欲しい情報を指定して取得 (client.py の改良)

サーバー側で、特定のデバイス情報をURLで指定されたら返せる準備が整いました。次は、クライアント側 (```client.py```) から実際に「このデバイスの情報が欲しい！」と指定して、情報を取得してみましょう。クライアント側のプログラムには、引き続きPythonの**requests**ライブラリを使用します。

**環境の確認**

- **Python:** こちらもPythonがインストールされている必要があります。
- **requestsライブラリ:** requestsライブラリが必要です。インストールがまだの場合は、ターミナルで以下のコマンドを実行してください。 

```
pip install requests
```

もし ```pip``` コマンドでパーミッションエラーなどが出る場合や、Python 3系を明示的に使いたい場合は、```pip3 install requests``` を試してみてください。 すでに[Vol.3](https://qiita.com/QueryPie/items/66042580e43b102e0400)や[Vol.4](https://qiita.com/QueryPie/items/4e3b30176b1522e107df)で ```client.py``` を動かせていれば、この手順は不要です。

**前回 (Vol.4) の ```client.py``` （おさらい）**

前回提供いただいた ```client.py``` は、固定のURL (```http://127.0.0.1:5000/```) にアクセスして、1つのスマートサーモスタットの情報を取得・表示するものでした。

```Python
# client.py (Vol.4 の想定)
import requests
from requests.exceptions import JSONDecodeError

url = "http://127.0.0.1:5000/" # 固定のURL

# ... (try-exceptブロックで情報を取得・表示する処理) ...
```

**パスパラメータを含むURLへのアクセス**

特定のデバイスID（例えば ```"LIGHT-002-B"```）を指定して情報を取得したいので、アクセスするURLを動的に生成する必要があります。サーバー側のエンドポイントは ```/devices/<device_id>``` としたので、例えば基本のURLが ```base_url = "http://127.0.0.1:5000/devices"``` で、取得したいデバイスIDが ```target_device_id = "LIGHT-002-B"``` だとすると、アクセスしたいURLはPythonの **f-string (フォーマット済み文字列リテラル)** を使って以下のように作れます。

```Python
target_url = f"{base_url}/{target_device_id}"
# target_url の中身は "http://127.0.0.1:5000/devices/LIGHT-002-B" になります
```

**特定のデバイス情報を取得し表示する処理**

では、特定のデバイスIDを指定して情報を取得し、その結果（成功した場合のデバイス情報、または失敗した場合のエラーメッセージ）を表示する関数を作成してみましょう。

```Python
# client_v5.py の一部
import requests
import json # JSONを整形して表示するためにインポート
from requests.exceptions import JSONDecodeError # 既にあれば不要

BASE_SERVER_URL = "http://127.0.0.1:5000/devices" # 基本のURL (末尾に /devices を追加)

def get_specific_device_info(device_id):
    target_url = f"{BASE_SERVER_URL}/{device_id}" # f-stringでターゲットURLを生成
    print(f"\n--- MCPデバイスID '{device_id}' の情報を取得します ---")
    print(f"アクセスするURL: {target_url}")

    try:
        response = requests.get(target_url, timeout=5)

        # サーバーからの応答ステータスコードを確認
        if response.status_code == 200: # 成功 (OK)
            device_data = response.json()
            print("--- 取得成功 ---")
            # 取得した情報を汎用的に表示する関数を呼ぶ (後で定義)
            display_device_info(device_data)
        elif response.status_code == 404: # 見つからない (Not Found)
            error_data = response.json() # サーバーが返すエラー情報もJSON
            print("--- 取得失敗 (404 Not Found) ---")
            print(f"  エラーメッセージ: {error_data.get('error', '不明なエラー')}")
            print(f"  リクエストしたID: {error_data.get('requested_id', device_id)}")
        else:
            # その他のエラー
            print(f"--- エラー (ステータスコード: {response.status_code}) ---")
            try:
                error_details = response.json()
                print("  サーバーからの詳細:")
                print(json.dumps(error_details, indent=2, ensure_ascii=False))
            except JSONDecodeError:
                print(f"  サーバーからの応答はJSON形式ではありませんでした。内容: {response.text[:200]}...")
            # response.raise_for_status() # 詳細なエラー情報を表示したい場合に有効化

    except requests.exceptions.ConnectionError:
        print(f"エラー: サーバー ({target_url}) への接続に失敗しました。サーバーが起動しているか確認してください。")
    except requests.exceptions.Timeout:
        print(f"エラー: サーバー ({target_url}) への接続がタイムアウトしました。")
    except JSONDecodeError: # response.json() でエラーが発生した場合
        print(f"エラー: サーバーからの応答 ({target_url}) が正しいJSON形式ではありません。")
        if 'response' in locals() and response is not None: # responseオブジェクトが存在するか確認
             print(f"  応答内容(先頭200文字): {response.text[:200]}...")
    except requests.exceptions.RequestException as e: # その他のrequests関連エラー
        print(f"エラー: リクエスト中に問題が発生しました ({target_url}): {e}")

# デバイス情報を整形して表示するヘルパー関数
def display_device_info(device_info):
    print(f"  モデル名: {device_info.get('modelName', '情報なし')}")
    print(f"  デバイスID: {device_info.get('deviceId', '情報なし')}")
    print(f"  設置場所: {device_info.get('location', '情報なし')}")

    status_info = device_info.get('status', {})
    print("  ステータス:")
    for key, value in status_info.items():
        if isinstance(value, bool): # 真偽値は分かりやすく表示
            print(f"    {key}: {'オン' if value else 'オフ'}")
        else:
            print(f"    {key}: {value}")

    if "supportedModes" in device_info:
        print(f"  対応モード: {', '.join(device_info.get('supportedModes', []))}")
    if "supportedEvents" in device_info:
        print(f"  対応イベント: {', '.join(device_info.get('supportedEvents', []))}")
    print("-" * 30)

# 全デバイス情報を取得する関数も用意
def get_all_device_infos():
    target_url = BASE_SERVER_URL # /devices エンドポイント
    print(f"\n--- 全てのMCPデバイス情報を取得します ---")
    print(f"アクセスするURL: {target_url}")
    # (エラー処理は get_specific_device_info と同様に実装)
    try:
        response = requests.get(target_url, timeout=5)
        if response.status_code == 200:
            all_data = response.json()
            devices = all_data.get("devices", [])
            if devices:
                print("--- 取得成功 (全デバイス) ---")
                for device in devices:
                    display_device_info(device)
            else:
                print("デバイス情報が見つかりませんでした。")
        else:
            print(f"--- エラー (ステータスコード: {response.status_code}) ---")
            # ... (エラー詳細表示処理) ...
    except Exception as e: # ここでは簡略化
        print(f"全デバイス取得中にエラー: {e}")
```

```display_device_info``` というヘルパー関数を作り、デバイス情報の表示を共通化しました。これにより、様々な種類のデバイス情報をある程度汎用的に表示できます。

**```client.py``` の全体コード (Vol.5 版)**

それでは、上記の関数を使って、いくつかのデバイスIDを指定して情報を取得してみる ```client.py``` の全体像を以下に示します。これを ```client.py``` という名前で保存するか、既存の ```client.py``` を上書きしてください。

```Python
# client.py (Vol.5 版)
import requests
import json # JSONを整形して表示するためにインポート
from requests.exceptions import JSONDecodeError

BASE_SERVER_URL = "http://127.0.0.1:5000/devices" # サーバーの基本URL (末尾に /devices を追加)

def display_device_info(device_info):
    """デバイス情報を整形して表示するヘルパー関数"""
    print(f"  モデル名: {device_info.get('modelName', '情報なし')}")
    print(f"  デバイスID: {device_info.get('deviceId', '情報なし')}")
    print(f"  設置場所: {device_info.get('location', '情報なし')}")

    status_info = device_info.get('status', {})
    if status_info:
        print("  ステータス:")
        for key, value in status_info.items():
            if isinstance(value, bool):
                print(f"    {key.replace('is', '').capitalize() if 'is' in key else key.capitalize()}: {'オン' if value else 'オフ'}")
            else:
                print(f"    {key.capitalize()}: {value} {device_info.get('status', {}).get('unit', '') if key == 'currentTemperature' or key == 'targetTemperature' or key == 'brightness' else ''}")
    else:
        print("  ステータス: 情報なし")

    if "supportedModes" in device_info and device_info["supportedModes"]:
        print(f"  対応モード: {', '.join(device_info['supportedModes'])}")
    if "supportedEvents" in device_info and device_info["supportedEvents"]:
        print(f"  対応イベント: {', '.join(device_info['supportedEvents'])}")
    print("-" * 30)


def get_specific_device_info(device_id):
    target_url = f"{BASE_SERVER_URL}/{device_id}"
    print(f"\n--- MCPデバイスID '{device_id}' の情報を取得します ---")
    print(f"アクセスするURL: {target_url}")

    try:
        response = requests.get(target_url, timeout=5)

        if response.status_code == 200:
            device_data = response.json()
            print("--- 取得成功 ---")
            display_device_info(device_data)
        elif response.status_code == 404:
            try:
                error_data = response.json()
                print("--- 取得失敗 (404 Not Found) ---")
                print(f"  エラーメッセージ: {error_data.get('error', 'サーバーからのエラー詳細なし')}")
                print(f"  リクエストしたID: {error_data.get('requested_id', device_id)}")
            except JSONDecodeError:
                print("--- 取得失敗 (404 Not Found) ---")
                print(f"  サーバーからのエラーメッセージはJSON形式ではありませんでした。応答: {response.text[:200]}...")
        else:
            print(f"--- エラー (ステータスコード: {response.status_code}) ---")
            try:
                error_details = response.json()
                print("  サーバーからの詳細:")
                print(json.dumps(error_details, indent=2, ensure_ascii=False))
            except JSONDecodeError:
                print(f"  サーバーからの応答はJSON形式ではありませんでした。内容: {response.text[:200]}...")

    except requests.exceptions.ConnectionError:
        print(f"エラー: サーバー ({target_url}) への接続に失敗しました。サーバーが起動しているか確認してください。")
    except requests.exceptions.Timeout:
        print(f"エラー: サーバー ({target_url}) への接続がタイムアウトしました。")
    except JSONDecodeError:
        print(f"エラー: サーバーからの応答 ({target_url}) が正しいJSON形式ではありません。")
        if 'response' in locals() and response is not None:
             print(f"  応答内容(先頭200文字): {response.text[:200]}...")
    except requests.exceptions.RequestException as e:
        print(f"エラー: リクエスト中に問題が発生しました ({target_url}): {e}")


def get_all_device_infos():
    target_url = BASE_SERVER_URL # これは /devices エンドポイント
    print(f"\n--- 全てのMCPデバイス情報を取得します ---")
    print(f"アクセスするURL: {target_url}")
    try:
        response = requests.get(target_url, timeout=5)
        response.raise_for_status() # 200番台以外ならエラーを発生

        data = response.json()
        devices = data.get("devices", [])
        if devices:
            print(f"--- 取得成功 (全 {len(devices)} デバイス) ---")
            for device in devices:
                display_device_info(device)
        else:
            print("デバイス情報が見つかりませんでした。")

    except requests.exceptions.ConnectionError:
        print(f"エラー: サーバー ({target_url}) への接続に失敗しました。")
    except requests.exceptions.Timeout:
        print(f"エラー: サーバー ({target_url}) からの応答がタイムアウトしました。")
    except requests.exceptions.HTTPError as e:
        print(f"エラー: HTTPエラーが発生しました。ステータスコード: {e.response.status_code}")
        try:
            error_details = e.response.json()
            print(f"  サーバーからの詳細: {json.dumps(error_details, indent=2, ensure_ascii=False)}")
        except JSONDecodeError:
            print(f"  サーバーからの応答はJSON形式ではありませんでした。内容: {e.response.text[:200]}...")
    except JSONDecodeError:
        print(f"エラー: サーバーからの応答 ({target_url}) が正しいJSON形式ではありません。")
        if 'response' in locals() and response is not None:
             print(f"  応答内容(先頭200文字): {response.text[:200]}...")
    except requests.exceptions.RequestException as e:
        print(f"エラー: リクエスト中に予期せぬ問題が発生しました ({target_url}): {e}")


if __name__ == '__main__':
    print("MCPクライアント (Vol.5) を実行します。")

    # 全デバイス情報を取得
    get_all_device_infos()

    # 存在するデバイスIDを指定して取得
    get_specific_device_info("THERMO-001-A")
    get_specific_device_info("LIGHT-002-B")

    # 存在しないデバイスIDを指定して取得
    get_specific_device_info("FAKE-DEVICE-007")
```

```display_device_info``` 関数を少し改良して、より多くの情報を表示できるようにしました。

**実行方法:**

ターミナルで ```client.py``` があるディレクトリに移動し、以下のコマンドで実行します。

Windows: ```python client.py```
macOS/Linux: ```python3 client.py``` (または ```python client.py```)

これで、クライアント側の準備もできました。
 
## 4. 実際に動かしてみよう！

さあ、いよいよ改良した ```app.py``` と ```client.py``` を実際に動かして、特定のMCPデバイス情報を「ねらいうち」で取得できるか試してみましょう！

**ステップ1：サーバー (```app.py```) の起動**

まず、ターミナル（またはコマンドプロンプト）を開き、```app.py``` ファイルを保存したディレクトリに移動します。そして、以下のコマンドでMCPサーバーを起動してください。

Windowsの場合:
```
python app.py
```

macOSやLinuxで python3 を使用している場合:
```
python3 app.py
```

起動に成功すると、ターミナルに次のようなメッセージが表示されるはずです。

```
MCP Server (Vol.5) is running on http://127.0.0.1:5000
Access all devices at: http://127.0.0.1:5000/devices
Access a specific device, e.g., THERMO-001-A at: http://127.0.0.1:5000/devices/THERMO-001-A
To stop the server, press CTRL+C
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: xxx-xxx-xxx
```

これで、サーバーはポート ```5000``` でクライアントからのリクエストを待ち受ける状態になりました。

**ステップ2：クライアント (```client.py```) の実行**

次に、**別のターミナル** を開きます（サーバーを起動したターミナルはそのままにしておいてください）。そして、```client.py``` ファイルを保存したディレクトリに移動し、以下のコマンドでクライアントプログラムを実行します。

Windowsの場合:
```
python client.py
```

macOSやLinuxで ```python3``` を使用している場合:
```
python3 client.py
```

**実行結果の確認**
クライアントプログラムを実行すると、ターミナルに以下のような出力が表示されるはずです（表示の細部は ```display_device_info``` 関数の実装によります）。

まず、全デバイス情報が取得・表示されます。

```
MCPクライアント (Vol.5) を実行します。

--- 全てのMCPデバイス情報を取得します ---
アクセスするURL: http://127.0.0.1:5000/devices
--- 取得成功 (全 3 デバイス) ---
  モデル名: Smart Thermostat X1000
  デバイスID: THERMO-001-A
  設置場所: Living Room
  ステータス:
    Currenttemperature: 23.5 Celsius
    Targettemperature: 24.0 Celsius
    Unit: Celsius
    Active: オン
    Mode: auto
  対応モード: auto, cool, heat, off
------------------------------
  モデル名: Smart Light L200
  デバイスID: LIGHT-002-B
  設置場所: Bedroom
  ステータス:
    Brightness: 80 percent
    Color: warm_white
    Unit: percent
    Active: オン
  対応モード: on, off, dim
------------------------------
  モデル名: Security Camera C300
  デバイスID: CAM-003-C
  設置場所: Entrance
  ステータス:
    Recording: オフ
    Detectionmode: motion
    Active: オン
  対応イベント: motion_detected, sound_detected
------------------------------
```

続いて、特定のデバイスの情報が取得・表示されます。

```
--- MCPデバイスID 'THERMO-001-A' の情報を取得します ---
アクセスするURL: http://127.0.0.1:5000/devices/THERMO-001-A
--- 取得成功 ---
  モデル名: Smart Thermostat X1000
  デバイスID: THERMO-001-A
  設置場所: Living Room
  ステータス:
    Currenttemperature: 23.5 Celsius
    Targettemperature: 24.0 Celsius
    Unit: Celsius
    Active: オン
    Mode: auto
  対応モード: auto, cool, heat, off
------------------------------

--- MCPデバイスID 'LIGHT-002-B' の情報を取得します ---
アクセスするURL: http://127.0.0.1:5000/devices/LIGHT-002-B
--- 取得成功 ---
  モデル名: Smart Light L200
  デバイスID: LIGHT-002-B
  設置場所: Bedroom
  ステータス:
    Brightness: 80 percent
    Color: warm_white
    Unit: percent
    Active: オン
  対応モード: on, off, dim
------------------------------
```

最後に、存在しないデバイスIDを指定した場合の結果です。

```
--- MCPデバイスID 'FAKE-DEVICE-007' の情報を取得します ---
アクセスするURL: http://127.0.0.1:5000/devices/FAKE-DEVICE-007
--- 取得失敗 (404 Not Found) ---
  エラーメッセージ: Device not found
  リクエストしたID: FAKE-DEVICE-007
```

期待通り、存在するデバイスの情報は正しく取得でき、存在しないIDについては ```404 Not Found``` となり、サーバーからのエラーメッセージが表示されています。

**サーバー側のログの確認**

サーバーを実行しているターミナルにも、クライアントからのアクセスログが表示されているはずです。

```
127.0.0.1 - - [日付 時刻] "GET /devices HTTP/1.1" 200 -
127.0.0.1 - - [日付 時刻] "GET /devices/THERMO-001-A HTTP/1.1" 200 -
127.0.0.1 - - [日付 時刻] "GET /devices/LIGHT-002-B HTTP/1.1" 200 -
127.0.0.1 - - [日付 時刻] "GET /devices/FAKE-DEVICE-007 HTTP/1.1" 404 -
```

ログの最後に表示される数字 ```200``` は成功、```404``` は見つからなかったことを示しており、クライアントの実行結果と一致しています。

**実験してみよう**

- ```client.py``` の ```if __name__ == '__main__':``` ブロック内で、```get_specific_device_info("CAM-003-C")``` を追加して実行してみてください。カメラの情報が正しく取得できるはずです。
- サーバーの ```app.py``` の ```all_devices_data``` リストに、ご自身で考えた新しいデバイス情報（例：スマートスピーカーなど）を追加し、サーバーを再起動（Ctrl+Cで一度止めてから再度起動）した後、クライアントからその新しいデバイスIDを指定して取得できるか試してみてください。

これらの手順を通じて、URLのパスパラメータを使って特定の情報を指定し、サーバーがそれに応じて的確なデータを返し、クライアントがそれを受け取るという一連の流れを実際に体験できたことと思います。
 
## 5. まとめ

今回は「Vol.5 ねらいうち！URLで指定したMCPモデル情報だけを取得する」と題して、特定のMCPデバイス情報をURLで指定して取得する方法を学びました。既存の ```app.py``` と ```client.py``` を拡張する形で、より実践的な機能を追加しました。

今回のポイントを振り返ってみましょう。

**1\. URLパスパラメータ:**
- URLの一部を使って、特定の情報（リソース）を指定するための仕組みであることを学びました。
- ```http://example.com/items/101``` の ```101``` のような部分がパスパラメータであり、これが「商品番号」や「ID」のように機能することを理解しました。今回の例ではデバイスIDとして使用しました。

**2\. サーバー側の対応 (Flask - ```app.py``` の改良):**
- Flaskのルート定義で ```<variable_name>``` のように書くことで、URLからパスパラメータを受け取れるようになりました。 (```@app.route('/devices/<device_id>')```)
- 受け取ったパラメータ（```device_id```）を使って、サーバーが保持する複数のデバイス情報の中から該当するものを検索し、見つかればその情報だけを、見つからなければ「見つからない」という情報（404エラー）を返す処理を実装しました。
- 全デバイス情報を返すエンドポイント (```/devices```) も作成しました。

**3\. クライアント側の対応 (requests - ```client.py``` の改良):**
- Pythonのf-stringなどを使って、取得したい情報を示すパスパラメータを含んだURLを動的に生成しました。
- 生成したURLに対して ```requests.get()``` でアクセスし、サーバーからの応答（特定のデバイス情報やエラー情報）を適切に処理して表示する方法を学びました。
- サーバーからのHTTPステータスコード（```200 OK``` や ```404 Not Found```）を確認し、それに応じて処理を分岐させることの重要性も確認しました。
- 取得した情報を整形して表示するヘルパー関数を作成し、コードの再利用性を高めました。

パスパラメータを使うことで、クライアントは大量のデータの中から必要なものだけを効率的にピンポイントで要求できるようになります。これは、ウェブサービスやAPIを設計・利用する上で非常に基本的ながら強力な機能です。MCPにおいても、多くのモデルやデバイス情報の中から特定の詳細を知りたい場合に、この仕組みが不可欠となります。

今回の学習で、皆さんのMCPクライアントとサーバーは、より多くの情報を扱え、より賢く、より具体的にコミュニケーションを取れるようになりました。

さて、次回はいよいよ、クライアントからサーバーへ新しい情報を送る方法、つまりMCPモデル（デバイス）情報を「登録」したり「更新」したりする方法について学びます。これまではサーバーから情報を「もらう(GET)」だけでしたが、次回は情報を「渡す」ことに挑戦します。

**次回予告: Vol.6 データをサーバーへ！MCPモデル情報の登録(POST)＆更新(PUT)**

どうぞお楽しみに！今回も最後までお読みいただき、ありがとうございました。