M5StackのModule-LLMはNPUを搭載し、ローカルでLLMを実行できる組み込みモジュールです。LLMでAIと対話をしたり、TTSのテキストの音声合成など、様々なモデルを使用することで手元でAIを体験することができます。今回はこのローカルで動作するAIを、MCPという仕組みを使って、別のAIエージェントと連携してみました。

MCPに触れて1週間程度の人間が書いてる内容なので、間違いや説明不足があるかもしれませんが大目に見てください。

2025/05/04 追記「MCPライブラリをFastMCPからgradioに変更してみた」

MCPサーバーとは?

MCPサーバーとは、AIが外部のツールと連携してデータのやりとりを行ったり、操作をするためのものです。例えばPCのファイルの内容を取得したり、ソフトの操作するというような作業をAIができるようにするためのインターフェースがMCPで、それを外部から接続できる機能を提供するのがMCPサーバーです。

APIと似てますが、APIはサーバー側の仕様に合わせてクライアント側のプログラムも修正しなくてはなりません。しかしMCPクライアントは、サーバー側の仕様を理解していい感じにやってくれるのが特徴です。ユーザーはいちいちMCPの使い方を説明する必要はありません。AIはMCPサーバーにどんな機能があるのかを知り、必要に応じて実行してくれます。ただし、たまに間違えます。

Module-LLMをはじめるには

とりあえず流行に乗ろうとModule-LLMを買ってみたのですが、何をすればいいのかわかりませんでした。何ができるのかもわからなかったので、とりあえずはModule-LLMの同人誌を買って、書いてあることをただ進めました。

Module-LLM MAniaX【電子書籍版】
著者 aNo研 A5サイズ・156ページ

こちらの内容は古く、現在のものとはちょっと違ったりしますが、しくみの解説やサンプルプログラムもあってModule-LLMの理解が進みます。最新情報は公式ドキュメントを参考にするといいと思います。
特にパッケージのインストール方法が新しくなってるのでWebの情報と合わせてみてください。

ほかには Module LLM Advent Calendar も情報の宝庫です。

LEDを光らせるMCPサーバー

まずは練習として、LEDを光らせるMCPサーバーを作ってみました。LLMとは何も関係ありませんね。でもまずは動くことを確認することが大切。

プログラムは GitHub にアップしました。(led.py)

#!/usr/bin/env python3
import asyncio
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP

# Initialize FastMCP server
mcp = FastMCP(
    "led",
    host="0.0.0.0",
    port=8000,
)

# LEDの明るさを設定する
def set_led_brightness(color: str, value: int) -> None:
    with open(f"/sys/class/leds/{color}/brightness", "w") as f:
        f.write(str(value))

# MCP Tool: LEDの明るさを設定する
@mcp.tool()
async def set_led_colors(red: int, green: int, blue: int) -> str:
    """Change the LED colors. (0-255)"""
    if (red < 0 or red > 255 or green < 0 or green > 255 or blue < 0 or blue > 255):
        return "Invalid color values. Please provide values between 0 and 255."
    
    try:
        set_led_brightness("R", red)
        set_led_brightness("G", green)
        set_led_brightness("B", blue)
        return "\n---\nsuccess"
    except Exception as e:
        return f"\n---\nerror: {str(e)}"


if __name__ == "__main__":
    # Run SSE over HTTP on port 8000
    asyncio.run(
        mcp.run_sse_async()
    )

MCPサーバーの作り方は こちら のページを参考にしました。真似て書いただけなのでよくわかってませんが、set_led_colors() というのがMCPのツールになります。これにはred, green, blueという3つ整数型の引数があり、ここに0~255の値を与えると、本体のLEDが光るというわけです。

このMCPツールの関数のコメントに “Change the LED colors. (0-255)” という説明を入れてあり、AIには red, green, blue に0~255の数字を入れてくれるだろう、という期待をしています。APIではないのでうまくいくとは限りません。実際「LEDを赤くして」と頼んだところredだけの値を送信してエラーになることもありました。その後3つ与えないといけないと気付いて再実行してくれました。やっぱAIってすげーな、と思った瞬間です。

クライアント側の設定

Cursorの場合は歯車マーク→MCPでMCPサーバーの追加ができます。+Add new global MCP Serverを押すとJSONの編集画面が開きます。以下はCursorの設定です。

{
  "mcpServers":{
    "modulellm":{
      "url":"http://192.168.xx.xx:8000/sse"
    }
  }
}

URLは module-llm.local でもいけますが、時々DNSの解決ができないことがあるので、IPアドレスで指定した方が安定して使えます。以下はModule-LLMのIPアドレスを固定する方法です。設定に失敗してアクセスできなくなったらシリアルからアクセスして直しましょう。

source /etc/network/interfaces.d/*
auto eth0
allow-hotplug eth0
iface eth0 inet static
  address 192.168.xx.xxx
  netmask 255.255.255.0
  gateway 192.168.xx.x
  dns-nameservers 8.8.8.8 1.1.1.1

MCPサーバーを起動して、Cursorの画面のスイッチをONにします。正常に接続されればスイッチが緑色になります。

Claude Desktopの場合は、直接リモートのホストに接続することはできないそうなので mcp-proxy を使って中継させて使いました。以下はClaude Desktopの設定です。

{
  "mcpServers":{
    "modulellm":{
      "command":"C:your_opath_to\\mcp-proxy.exe",
      "args":[
        "http://192.168.xx.xxx:8000/sse"
      ]
    }
  }
}

実行してみた結果

ちゃんと動いてますね。データの与え方は何も指示してないのに、MCPツールの仕様を理解して実行してくれました。

LLMとTTSのMCPサーバーを作る

ここからがいよいよ本番。Module-LLMのLLM(AIエージェント)と対話したり、テキストの音声合成(TTS)を試してみたいと思います。プログラムは Module-LLM MAniaXサンプルコードを参考にさせていただきました。
Module-LLMにはStackFlowというシステムが稼働しいて、StackFlowのサーバーにデータを送信するとLLMやTTSなどを制御してくれます。

今回はデフォルトでインストールされているLLMモデルを使用しました。
・LLM: qwen2.5-0.5B-prefill-20e
・TTS: melotts_zh-cn
現時点では日本語が喋れるTTSはリリースされていないので、喋れるのは英語か中国語のみになります。

作成したMCPサーバーのプログラムは GitHub にアップしました。(mcp_llmtts.py)

デバッグ用MCPクライアント

MCPサーバーを操作しようとしてうまく動かない場合は、まずは問題切り分けのために、デモ用のMCPクライアントからMCPサーバーにアクセスしてみるといいでしょう。(サンプルMCPクライアント client-http.py)このサンプルプログラムは、MCPサーバーで使えるツールの一覧表示と、send_message、speak_text、set_led_colorsを実行します。

root@m5stack-LLM:# uv run client-http.py
MCP Server URL: http://127.0.0.1:8000/sse
=== Available tools ===
- send_message: Send a message to the LLM and get the response.
  Parameters:
    - message (string): No description

- speak_text: It will speak when you give it a message. English only.
  Parameters:
    - message (string): No description

- set_led_colors: Change the LED colors. (0-255)
  Parameters:
    - red (integer): No description
    - green (integer): No description
    - blue (integer): No description


=== Send Message ===
[TextContent(type='text', text='\n---\n私はスタックチャンという名前のAIアシスタントで、親切で礼儀正しく正直な人間です。あなたの質問に答えたり、情報を提供したりするためのサポートを提供します。何かお手伝いできることがありますか?\n\n', annotations=None)]

=== SET LED COLORS ===
[TextContent(type='text', text='\n---\nsuccess', annotations=None)]

=== Seak Text ===
[TextContent(type='text', text='\n---\nsuccess', annotations=None)]

=== SET LED COLORS ===
[TextContent(type='text', text='\n---\nsuccess', annotations=None)]

それぞれ結果が返ってくれば準備完了です。もしエラーが出る場合は、MCPサーバーの方の出力にエラーが出てないか確認してみてください。Module-LLMは結構動作が不安定でよくに止まります。llm-sysを再起動したり、OS事態を再起動すると改善します。

systemctl restart llm-sys

AIエージェントに使ってもらう

それでは実際にAIにMCPサーバーのツールを操作してもらいましょう。クライアント側で正しくMCPサーバーが認識されているか確認します。Claude Desktopの場合は、+の横のツールボタンを押すと使用するツールのオンオフができます。

Cursorの場合は歯車アイコン→MCPでオンオフできます。

それではAIエージェントに、MCPツールを使ってもらいましょう。こちらからの指示は「メッセージを送って相手の名前を聞いてみてください。」とだけです。

ちゃんと自己紹介をしてくれましたね。名前はスタックチャンと言うようです。かわいいですね。

それでは次は「スタックチャンに喋ってもらいましょう。英語でスタックチャンに簡単な挨拶の英語を喋らせてください。」と指示してみます。

スタックチャンに英語で挨拶を…と伝えたものの日本語で返ってきましが、それをAIエージェントが英訳して、TTSで音声合成してくれました。AIとAIの競業ですね!

こんな感じでざっくりとMCPを体験してみましたが、想像以上にAIが賢くうまい感じに連携してくれたのが面白かったです。

余談 Module-LLMは普通にLinuxとして遊べる

Module-LLMにはUbuntuがインストールされているので、普通にLinuxサーバーとして遊べます。パッケージも普通にaptコマンドなどでインストールできます。メモリは1GB(4GB中3GBはLLM用)、ストレージは32GBあり、2コアのARM Cortex-A53があり、シングルコア性能ではRaspberry PI 4くらい、マルチコア性能ではRaspberry PI 2くらいあります。以下はUnixbenchの結果です。

   BYTE UNIX Benchmarks (Version 5.1.3)

   System: m5stack-LLM: GNU/Linux
   OS: GNU/Linux -- 4.19.125 -- #1 SMP PREEMPT Wed Nov 20 14:43:36 CST 2024
   Machine: aarch64 (aarch64)
   Language: en_US.utf8 (charmap="UTF-8", collate="UTF-8")
   CPU 0: ARM Cortex-A53 (48.0 bogomips)
          CPU Features: fp asimd evtstrm crc32 cpuid
   CPU 1: ARM Cortex-A53 (48.0 bogomips)
          CPU Features: fp asimd evtstrm crc32 cpuid
   14:46:39 up 42 min,  1 user,  load average: 1.62, 1.57, 1.54; runlevel 2025-02-20

------------------------------------------------------------------------
Benchmark Run: Fri Apr 25 2025 14:46:39 - 15:14:38
2 CPUs in system; running 1 parallel copy of tests

Dhrystone 2 using register variables        6726783.3 lps   (10.0 s, 7 samples)
Double-Precision Whetstone                     1646.8 MWIPS (10.0 s, 7 samples)
Execl Throughput                                695.7 lps   (30.0 s, 2 samples)
File Copy 1024 bufsize 2000 maxblocks        180539.3 KBps  (30.0 s, 2 samples)
File Copy 256 bufsize 500 maxblocks           54000.8 KBps  (30.0 s, 2 samples)
File Copy 4096 bufsize 8000 maxblocks        437487.0 KBps  (30.0 s, 2 samples)
Pipe Throughput                              433941.2 lps   (10.0 s, 7 samples)
Pipe-based Context Switching                  85813.8 lps   (10.0 s, 7 samples)
Process Creation                               2170.7 lps   (30.0 s, 2 samples)
Shell Scripts (1 concurrent)                   1571.6 lpm   (60.0 s, 2 samples)
Shell Scripts (8 concurrent)                    320.3 lpm   (60.1 s, 2 samples)
System Call Overhead                         625569.8 lps   (10.0 s, 7 samples)

System Benchmarks Index Values               BASELINE       RESULT    INDEX
Dhrystone 2 using register variables         116700.0    6726783.3    576.4
Double-Precision Whetstone                       55.0       1646.8    299.4
Execl Throughput                                 43.0        695.7    161.8
File Copy 1024 bufsize 2000 maxblocks          3960.0     180539.3    455.9
File Copy 256 bufsize 500 maxblocks            1655.0      54000.8    326.3
File Copy 4096 bufsize 8000 maxblocks          5800.0     437487.0    754.3
Pipe Throughput                               12440.0     433941.2    348.8
Pipe-based Context Switching                   4000.0      85813.8    214.5
Process Creation                                126.0       2170.7    172.3
Shell Scripts (1 concurrent)                     42.4       1571.6    370.6
Shell Scripts (8 concurrent)                      6.0        320.3    533.8
System Call Overhead                          15000.0     625569.8    417.0
                                                                   ========
System Benchmarks Index Score                                         349.6

------------------------------------------------------------------------
Benchmark Run: Fri Apr 25 2025 15:14:38 - 15:42:40
2 CPUs in system; running 2 parallel copies of tests

Dhrystone 2 using register variables       13359822.9 lps   (10.0 s, 7 samples)
Double-Precision Whetstone                     3267.9 MWIPS (10.0 s, 7 samples)
Execl Throughput                               1226.9 lps   (29.8 s, 2 samples)
File Copy 1024 bufsize 2000 maxblocks        339262.9 KBps  (30.0 s, 2 samples)
File Copy 256 bufsize 500 maxblocks          102300.4 KBps  (30.0 s, 2 samples)
File Copy 4096 bufsize 8000 maxblocks        837516.9 KBps  (30.0 s, 2 samples)
Pipe Throughput                              861233.8 lps   (10.0 s, 7 samples)
Pipe-based Context Switching                 121443.2 lps   (10.0 s, 7 samples)
Process Creation                               3015.9 lps   (30.0 s, 2 samples)
Shell Scripts (1 concurrent)                   2335.4 lpm   (60.1 s, 2 samples)
Shell Scripts (8 concurrent)                    324.3 lpm   (60.1 s, 2 samples)
System Call Overhead                        1239842.1 lps   (10.0 s, 7 samples)

System Benchmarks Index Values               BASELINE       RESULT    INDEX
Dhrystone 2 using register variables         116700.0   13359822.9   1144.8
Double-Precision Whetstone                       55.0       3267.9    594.2
Execl Throughput                                 43.0       1226.9    285.3
File Copy 1024 bufsize 2000 maxblocks          3960.0     339262.9    856.7
File Copy 256 bufsize 500 maxblocks            1655.0     102300.4    618.1
File Copy 4096 bufsize 8000 maxblocks          5800.0     837516.9   1444.0
Pipe Throughput                               12440.0     861233.8    692.3
Pipe-based Context Switching                   4000.0     121443.2    303.6
Process Creation                                126.0       3015.9    239.4
Shell Scripts (1 concurrent)                     42.4       2335.4    550.8
Shell Scripts (8 concurrent)                      6.0        324.3    540.5
System Call Overhead                          15000.0    1239842.1    826.6
                                                                   ========
System Benchmarks Index Score                                         591.6

驚くのはその消費電力です。

LLMで推論を行っている最中の測定値ですが、5V 0.25Aと1.25Wしかありません。たったこれだけの消費電力でLLMが動いてるなんてすごいですね。

余談 ストレージをバックアップすべし!

Module-LLMはストレージが壊れやすいという印象です。実際に壊れてファームウェアのインストールからやり直したこともあります。せっかく作成したプログラムや設定が消えると悲しいので、SDカードにバックアップを取っておくと安心です。Module-LLMにSDカードを挿入すると自動的にマウントします。

dfコマンドで/dev/mmcblk1p1というデバイスが/mnt/mmcblk1p1にマウントしていることを確認

root@m5stack-LLM:# df -Th
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/root      ext4    28G  6.0G   22G  22% /
tmpfs          tmpfs  480M     0  480M   0% /dev/shm
tmpfs          tmpfs  192M  896K  191M   1% /run
tmpfs          tmpfs  5.0M     0  5.0M   0% /run/lock
tmpfs          tmpfs  480M     0  480M   0% /tmp
/dev/mmcblk1p1 ext2    30G   13G   15G  47% /mnt/mmcblk1p1
tmpfs          tmpfs   96M     0   96M   0% /run/user/0

vfatからext4に変更してフォーマット

# umount /mnt/mmcblk1p1
# mkfs.ext2 /dev/mmcblk1p1
# mount -o noatime,nodiratime /dev/mmcblk1p1 /mnt/mmcblk1p1

バックアップスクリプト

#!/bin/bash
BACKUP_TO=/mnt/mmcblk1p1/backup

cd /
mkdir -p ${BACKUP_TO}
rsync -aH --delete \
  --exclude=proc \
  --exclude=sys \
  --exclude=dev \
  --exclude=run \
  --exclude=tmp \
  --exclude=mnt \
  --exclude=media \
  --exclude=lost+found \
  --exclude=etc/configfs \
  --exclude=opt/swapfile \
  ./ \
  ${BACKUP_TO}/

rsyncのオプションや–excludeは適宜修正してください。初回バックアップ時はフルバックアップするので時間がかかりますが、2回目以降は差分バックアップなので早く終わるはずです。

余談 その他のモデル

標準でインストールされているモデル以外にも、他のモデルが入手可能です。

root@m5stack-LLM:# apt list|grep llm-model
llm-model-audio-en-us/stable 0.2 arm64
llm-model-audio-zh-cn/stable 0.2 arm64
llm-model-deepseek-r1-1.5b-ax630c/stable 0.3 arm64
llm-model-deepseek-r1-1.5b-p256-ax630c/stable 0.4 arm64
llm-model-depth-anything-ax630c/stable 0.3 arm64
llm-model-internvl2.5-1b-ax630c/stable 0.4 arm64
llm-model-llama3.2-1b-p256-ax630c/stable 0.4 arm64
llm-model-llama3.2-1b-prefill-ax630c/stable 0.2 arm64
llm-model-melotts-zh-cn/stable 0.4 arm64
llm-model-openbuddy-llama3.2-1b-ax630c/stable 0.2 arm64
llm-model-qwen2.5-0.5b-int4-ax630c/stable 0.4 arm64
llm-model-qwen2.5-0.5b-p256-ax630c/stable 0.4 arm64
llm-model-qwen2.5-0.5b-prefill-20e/stable 0.2 arm64
llm-model-qwen2.5-1.5b-ax630c/stable 0.3 arm64
llm-model-qwen2.5-1.5b-int4-ax630c/stable 0.4 arm64
llm-model-qwen2.5-1.5b-p256-ax630c/stable 0.4 arm64
llm-model-qwen2.5-coder-0.5b-ax630c/stable 0.2 arm64
llm-model-sherpa-ncnn-streaming-zipformer-20m-2023-02-17/stable 0.2 arm64
llm-model-sherpa-ncnn-streaming-zipformer-zh-14m-2023-02-23/stable 0.2 arm64
llm-model-sherpa-onnx-kws-zipformer-gigaspeech-3.3m-2024-01-01/stable 0.3 arm64
llm-model-sherpa-onnx-kws-zipformer-wenetspeech-3.3m-2024-01-01/stable 0.3 arm64
llm-model-silero-vad/stable 0.3 arm64
llm-model-single-speaker-english-fast/stable 0.2 arm64
llm-model-single-speaker-fast/stable 0.2 arm64
llm-model-whisper-base/stable 0.3 arm64
llm-model-whisper-tiny/stable 0.3 arm64
llm-model-yolo11n-hand-pose/stable 0.3 arm64
llm-model-yolo11n-pose/stable 0.3 arm64
llm-model-yolo11n-seg/stable 0.3 arm64
llm-model-yolo11n/stable 0.2 arm64

例えば標準でインストールされているモデルは qwen2.5-0.5b-prefill-20e ですが、qwen2.5-1.5b-p256-ax630c を使うとかなり賢くなります。その代わり応答はとても遅くなります。性能か、スピードか、トレードオフですね。

追記 MCPライブラリをFastMCPからgradioに変更してみた

gradioというUIを提供するライブラリでMCPサーバーを構築できることを知りました。調べてみるととても簡単そうだったので、FastMCPからgradioに変更してみました。

修正したプログラムはこちら→ mcp_llmtts_gradio.py

これがgradioによるWebのUIです。初めて使ったんですが、ブラウザでツールのデバッグができて便利ですね。これは素晴らしい。
URLは http://192.168.x.xxx:7860/ のようになります。

MCPサーバーのURLは http://192.168.x.xxx:7860/gradio_api/mcp/sse のようになります。

LINEで送る
Pocket