【MCP】大規模言語モデルでLチカ【IoT】

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/bde885cf-0cc2-4857-b950-3f8fd09dc709.png)


Claude（実際にはデスクトップアプリのClaude Desktop）でMCP(Model Context Protocol)という仕組みを利用すると、チャット中に外部関数を呼び出すことができます。この外部関数を提供するプログラムをMCPサーバーと呼びます。MCPサーバーとリモートのRaspberry Piで通信して、Raspberry PiにつながったLEDを制御します。すると、Claudeのチャット画面で「ライトを付けて」「ライトを消して」の様に指示すると、リモートのLEDの点灯消灯を行うことができます。

# 環境構築

Windows環境を想定しています。

以下のものをインストールします。

- [Claude Desktop](https://claude.ai/download)
- [Python](https://www.python.org/downloads/windows/)

# とりあえずClaude Desktopを使ってみる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/5cf54df5-884d-4a87-ac0b-3ded90b32f9d.png)



メニューから次の項目を選択：`ファイル` → `設定...`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/731265eb-1f20-43a4-9806-808b4ea014fe.png)

`開発者`カテゴリを選択して`構成を編集`ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/95117e66-c65f-43df-8ebe-63432457c22d.png)

`claude_desktop_config.json`というファイルのあるフォルダが開かれます。MCPサーバーをインストールすると、このファイルが更新されます。後述しますが、本記事では`uv`コマンドによって自動的に更新されます。（MCPサーバーによっては、手作業でインストールする場合に、このファイルを自分で編集しなければならないことがあります）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/daa34fe2-cc53-4d12-9ff1-02eff9e599bb.png)

MCPサーバーを追加した後、Claude Desktopを終了して起動し直す必要があります。ここで注意ですが、Claude Desktopの閉じ方には2種類あります。メニューから`Close`を選ぶと常駐状態でウィンドウが閉じるだけで終了はしません。Alt+F4で閉じた時もこの挙動です。`claude_desktop_config.json`を再読込するには、メニューの`終了`を選択して完全に終了する必要がありますので注意してください。

# チュートリアル

まず、Claude DesktopとMCPサーバー間でやりとりする基本的な手順を確認しておきます。最初に必要なことは公式サイトに書いてありますので、そのとおりに進めてみます。

https://github.com/modelcontextprotocol/python-sdk

必要なパッケージをインストールします。

``` .sh
pip install uv
uv add "mcp[cli]"
```
（`python`と`pip`コマンドに`PATH`を通しておく必要があるかもしれません）


適当な作業ディレクトリを作って、その中で`mcp_server.py`を作成します。

``` mcp_server.py
# mcp_server.py
from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("Demo")

# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
    """Get a personalized greeting"""
    return f"Hello, {name}!"
```

Claude Desktopにインストール（登録）します。

``` .sh
mcp install mcp_server.py
```

`claude_desktop_config.json`が更新されたことを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/e24229e8-26e8-4f5f-8120-a1b21b5eaf67.png)

Claude Desktopを起動します。

ClaudeはMCPサーバーを「ツール」として認識します。この例では「Demoツール」です。利用可能な関数をみてみましょう。`mcp_server.py`で実装されている`add`関数が見えていれば正常です。無い場合は`claude_desktop_config.json`が正しく読み込まれていないかもしれませんので、Claude Desktopを完全に終了してから起動し直し、再度確認してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/c81f2d65-4ac3-4b7c-98f2-24e85aca9115.png)

`add`関数を実行するような指示をすると、ツールを実行して良いか聞かれますので、許可します。すると、計算結果が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/09d8a024-d525-46c6-92e8-362f983dfdee.png)

# リモートの Raspberry Pi を構築

次のような回路を用意します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/50801b91-6bb4-4351-811c-471a3fb9bc29.png)

まずは試しにシェルから点灯確認します。

GPIO3を出力用として設定
```
sudo echo "3" > /sys/class/gpio/export
sudo echo "out" > /sys/class/gpio/gpio3/direction
```

点灯
```
sudo echo 1 > /sys/class/gpio/gpio3/value
```

消灯
```
sudo echo 0 > /sys/class/gpio/gpio3/value
```

Raspberry Pi 用 API サーバーを用意します。

``` api_server.py
from flask import Flask, request, jsonify
import RPi.GPIO as GPIO
import atexit

app = Flask(__name__)

LED_PIN = 3
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)

def cleanup_gpio():
    GPIO.cleanup()

atexit.register(cleanup_gpio)

@app.route('/led/on', methods=['GET'])
def turn_on_led():
    GPIO.output(LED_PIN, GPIO.HIGH)
    return jsonify({'status': 'on'}), 200

@app.route('/led/off', methods=['GET'])
def turn_off_led():
    GPIO.output(LED_PIN, GPIO.LOW)
    return jsonify({'status': 'off'}), 200

@app.route('/cpu/temperature', methods=['GET'])
def get_cpu_temperature():
    try:
        with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
            temp_str = f.readline()
            temp_c = int(temp_str) / 1000.0
            return jsonify({'status': 'success', 'unit': 'degree celsius', 'value': temp_c}), 200
    except:
        return jsonify({'status': 'error', 'message': 'thermal sensor is not available'}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

Raspberry Piで`ip a`を実行して、IPアドレスを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/af2b8246-438f-4512-8939-7e71ddc707be.png)

API サーバーを実行します。

``` .sh
python api_server.py
```

このAPIに接続してみましょう。（IPアドレスは上で確認した値に適宜書き換えてください）

`http://192.168.1.7:5000/led/on`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/19a20856-fa4c-4ef5-baab-66ec85d458f5.png)

LEDが点灯すれば成功です。`on`を`off`に変えて、消灯することも確認しておきます。

# MCPサーバーに関数を実装

`remote_server`の値は実際のアドレスに書き換えてください。

``` mcp_server.py
# mcp_server.py
from mcp.server.fastmcp import FastMCP
import urllib.request

remote_server = "192.168.1.7:5000"

# Create an MCP server
mcp = FastMCP("Demo")

# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

@mcp.tool()
def light_switch(on: bool):
    """Turn on or turn off the light"""
    try:
        if on:
            urllib.request.urlopen("http://" + remote_server + "/led/on")
            return {"status": "The light is turned on"}
        else:
            urllib.request.urlopen("http://" + remote_server + "/led/off")
            return {"status": "The light is turned off"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

@mcp.tool()
def query_cpu_temperature():
    """Query temperature of the CPU in degree celsius"""
    try:
        with urllib.request.urlopen("http://" + remote_server + "/cpu/temperature") as res:
            return res.read()
    except Exception as e:
        return {"status": "error", "message": str(e)}

# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
    """Get a personalized greeting"""
    return f"Hello, {name}!"
```

MCPクライアント（Claude Desktop）から関数を使用できるようにするには、関数定義の前の行に`@mcp.tool()`と書きます。次の行に`"""関数の説明"""`を書きます。

引数の`on`の値に応じて、リモートの LED API のアドレスを呼び分けています。`/led/on`にアクセスすると点灯、`/led/off`にアクセスすると消灯となります。

Claude Desktopを起動して、もしエラーがあった場合は、通知が表示され、`設定`の`開発者`の画面に三角の！マークが表示されます。

# Claude Desktop で動作確認

問題なければ、`設定`の`開発者`の画面で、`Demo Running`の様に表示されます。チャット欄に「Demoツールは使用可能ですか？ 可能なら提供されている関数を列挙してください。」と指示をしてみます。`light_switch`関数が認識されていれば、「ライトを付けてください」「ライトを消してください」の様な指示が実行されるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/3287dacd-f66f-4e4a-a078-561f01f0f7ae.png)

`query_cpu_temperature`という関数も用意しておきました。「CPUの温度を取得して」の様に訊くと答えてくれます。

デバッグの参考になる情報として、「Demoツールで使用可能な関数に関する情報をJSONSchemaで出力してください」という指示をすると、関数の詳細を見ることができます。