kivantium活動日記

プログラムを使っていろいろやります

MCPを使ってLLMにLチカしてもらう

Model Context Protocol (MCP) のビッグウェーブが来ていますが、まだ乗れていなかったので乗ってみます。

今回やること

  • PythonでPCからArduinoにシリアル通信を行い、内蔵LEDを点灯・消灯するMCPサーバーを構築する
  • MCPサーバーをMCPクライアントから直接叩いてLチカする
  • Claude Desktopを使ってMCP経由でLEDを操作する
  • OpenAI Agents SDKを使ってAIエージェントにMCP経由でLEDを操作してもらってLチカする

以下手順の紹介です。

環境構築

Windows上に適当なPython環境が構築されていることを前提としますが、他のOSでもだいたい同じだと思います。

uvのインストール

この頃はやりのPythonパッケージマネージャーです。MCP関係の記事だとuvが使われている場合が多いように思います。 GitHubのドキュメントに従ってインストールします。

pipを使う場合は以下の通り。

pip install uv

必要なライブラリのインストール

今回はfastmcppyserialを使います。

適当なディレクトリを作って以下を実行します。 この記事では C:\Users\kivantium\mcp_arduino 内で作業するものとします。

cd C:\Users\kivantium\mcp_arduino
uv init
uv add fastmcp pyserial

FastMCPに関する注意

現在FastMCPと呼ばれているものには2種類あります。

二つはimport文で見分けることができます。

  • import mcp.server.fastmcp でインポートしていたら公式SDK
  • import fastmcp でインポートしていたらv2系列

両者にはある程度の互換性があるものの、細かい部分には違いがあるのでどちらのFastMCPを使っているかには注意する必要があります。この記事はv2系列のFastMCPを利用しています。

Arduinoの準備

適当なArduinoを買って、PCにはArduino IDEをインストールしておきます。

Arduinoスケッチ

以下のコードをArduinoに書き込みます。シリアル通信で 1 を送信したら内蔵LEDが点灯し、0 を送信したら消灯します。

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  char key;
  if (Serial.available()) {
    key = Serial.read();
    switch(key) {
      case '1':
        digitalWrite(LED_BUILTIN, HIGH);
        break;
      case '0':
        digitalWrite(LED_BUILTIN, LOW);
        break;
    }
  }
}

MCP サーバー

Pythonからシリアル通信で1または0を送信するだけのプログラムserver.pyを作ります。

import serial

from fastmcp import FastMCP

# Arduinoとのシリアル通信を行うクラス
class ArduinoLEDController:
    def __init__(self, port: str):
        try:
            self.ser = serial.Serial(
                port=port,
                baudrate=9600,
                parity=serial.PARITY_NONE,
                timeout=1
            )
        except serial.SerialException as e:
            raise RuntimeError(f"Failed to open serial port {port}: {e}") from e
    
    def write_str(self, data: str) -> None:
        try:
            self.ser.write(data.encode('utf-8'))
            self.ser.flush()
        except serial.SerialException as e:
            print(f"Serial write error: {e}")
    
    def led_on(self) -> None:
        """Turn on LED"""
        self.write_str('1')
    
    def led_off(self) -> None:
        """Turn off LED"""
        self.write_str('0')
    
    def close(self):
        if self.ser.is_open:
            self.ser.close()

ctrl = ArduinoLEDController("COM3")

# クラスのメンバ関数をMCPサーバー化
# https://gofastmcp.com/v2/patterns/decorating-methods
mcp = FastMCP("Arduino LED controller")
mcp.tool(ctrl.led_on)
mcp.tool(ctrl.led_off)

if __name__ == "__main__":
    try:
        mcp.run()
    finally:
        ctrl.close()

以下を実行して正常に起動することを確認します。

uv run server.py

MCPクライアント

動作確認として、LチカするためのMCPクライアントの例を示します。

import asyncio

from fastmcp import Client
from fastmcp.client.transports import StdioTransport

async def main():
    # 標準入出力でMCPサーバーと通信するときの書き方
    # https://gofastmcp.com/clients/transports
    transport = StdioTransport(
        command="uv",
        args=["run", "server.py"]
    )

    async with Client(transport) as client:
        # ツール一覧を表示
        tools = await client.list_tools()
        print(tools)
        # LEDを1秒間隔で点滅
        print("Start blinking LED... (Ctrl+C to stop)")
        while True:
            await client.call_tool("led_on")
            await asyncio.sleep(1)
            await client.call_tool("led_off")
            await asyncio.sleep(1)

asyncio.run(main())

これを uv run client.py のように実行すると、以下のツール一覧が表示された後にLEDが点滅します。

[Tool(name='led_on', title=None, description='Turn on LED', inputSchema={'properties': {}, 'type': 'object'}, outputSchema=None, icons=None, annotations=None, meta={'_fastmcp': {'tags': []}}, execution=None), Tool(name='led_off', title=None, description='Turn off LED', inputSchema={'properties': {}, 'type': 'object'}, outputSchema=None, icons=None, annotations=None, meta={'_fastmcp': {'tags': []}}, execution=None)]

Claude Desktopとの連携

Claude Desktopをダウンロードして適宜アカウントを設定したら、「設定 (Ctrl+,)」→「開発者」→「設定を編集」を選んでMCPの設定ファイルを開きます。

今回のケースでは claude_desktop_config.json に以下の内容を書き込みます。 パスは環境に応じて適宜変更してください。

{
  "mcpServers": {
    "led_control": {
      "command": "C:\\Users\\kivantium\\.local\\bin\\uv.exe",
      "args": [
        "--directory",
        "C:\\Users\\kivantium\\mcp_arduino",
        "run",
        "server.py"
      ]
    }
  }
}

設定を終えたらClaude Desktopを再起動(重要:単にウィンドウを閉じるだけでなくシステムトレイのアイコンを右クリックして「終了」を選んで完全に終了すること)すると、MCPサーバーがClaude Desktopに認識されるはずです。

認識されたMCPサーバー

この状態でClaude DesktopにLEDの点灯を依頼すると、MCPサーバー経由でツールを実行してくれます。

MCPツールの実行許可を求めている様子

OpenAI Agents SDKからのMCPサーバー呼び出し

AIエージェントからのMCPサーバー呼び出しの例として、OpenAI Agents SDKを使うコードを示します。

インストール

uv add openai-agents

コード例

import asyncio

from agents import Agent, Runner
from agents.mcp import MCPServerStdio

async def main():
    async with MCPServerStdio(
        name="led_control",
        params={
            "command": "uv",
            "args": ["run", "server.py"],
        }
    ) as server:

        agent = Agent(
            name="Assistant",
            instructions="You are a helpful assistant",
            model="gpt-4o-mini",
            mcp_servers=[server],
        )
        
        while True:
            result = await Runner.run(agent, "Please turn on the LED.")
            print(result.final_output)
            result = await Runner.run(agent, "Please turn off the LED.")
            print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())

上のコードをclient_agent.pyという名前で保存して、環境変数 OPENAI_API_KEY にOpenAIのAPIキーを設定してから実行します。

uv run client_agent.py

ArduinoのLEDが点滅し、ターミナルには次のようなメッセージが表示されます。

The LED has been turned on.
The LED has been turned off.
The LED has been turned on. If you need anything else, just let me know!
The LED has been turned off. If you need anything else, just let me know!
The LED has been turned on. If you need anything else, just let me know!
The LED is now turned off.

点滅させるためのロジックを自分で書いてしまってはエージェントの意味がないので、AIが自分で考えてLチカできるようにします。

まず、MCPサーバーに遅延関数を加えます。diffを示します。

@@ -31,6 +31,10 @@
         """Turn off LED"""
         self.write_str('0')

+    def wait_1s(self) -> None:
+        """Wait for 1 second"""
+        time.sleep(1)
+
     def close(self):
         if self.ser.is_open:
             self.ser.close()
@@ -42,6 +46,7 @@
 mcp = FastMCP("Arduino LED controller")
 mcp.tool(ctrl.led_on)
 mcp.tool(ctrl.led_off)
+mcp.tool(ctrl.wait_1s)

 if __name__ == "__main__":
     try:

次にエージェントに指示を与えるループを以下の単一の指示に書き換えます。

result = await Runner.run(agent, "Please blink the LED 3 times at 1 second intervals.")

すると、以下の動画に示すように無事にLチカが実行されました。

youtu.be

LEDを点滅させるためだけにOpenAI社のサーバーでGPUがうなる富豪的Lチカが完成しました。めでたしめでたし。

解決していない問題点

上で示したコードを実行すると以下のようなエラーが表示されます。

Error invoking MCP tool led_off: Timed out while waiting for response to ClientRequest. Waited 5.0 seconds.
Error invoking MCP tool led_off: Timed out while waiting for response to ClientRequest. Waited 5.0 seconds.
Error invoking MCP tool wait_1s: Timed out while waiting for response to ClientRequest. Waited 5.0 seconds.

検索するとタイムアウト時間を延ばせば良いという解決策が出てきますが、長いタイムアウト時間を設定するとLEDの点滅が3回で終わらなくなりました。

何が起きているのか調べる必要があります。

参考文献