はじめに
本記事では、MCP(Model Context Protocol)サーバを経由して、雑に(すなわち自然言語で)モータを動作させるデモを紹介します。MCPサーバやさまざまなコンポーネントの連動によって、現実世界のモータを自然言語でかんたんに駆動することができます。
↓こんな動きをします。
なお本記事の内容は、下記のMCPサーバ経由でラジコン用サーボを動かすものを、Roller485をI2C接続で動かすものに改変したものです。
実行環境
- Win10/11
- VSCode
- GitHub Copilot
- Node.js 22.x
- Arduino UNO 互換ボード
- Roller485モータユニット(位置制御モードを使用)
主要なコンポーネントとその接続
-
index.js
: MCPサーバ本体。roller485-mcp
サーバのmoveRoller485
ツールで角度指定(-3600〜3600度)によるモータ制御を行う - MCPツール:
moveRoller485
(引数 degrees: number) - Roller485: STM32マイコンが内蔵された、ブラシレスDCモータユニット
- Johnny-Five: MCPサーバ(JSコード)にて、ArduinoボードとのIFとして機能するAPI
- Firmata: Arduinoボードに書き込んで、MCPサーバ(JSコード)とのIFとして機能するファームウェア
- I2C通信: ArduinoボードとRoller485を接続
セットアップ~動かすまでの手順
-
Arduino互換ボードをUSB接続、Roller485をI2C接続(激しく動かす・負荷を繋ぐ場合には5Vでは心許ないので、HT3.96 コネクタで6~16Vを供給)
-
Arduino側をNode.js経由で動かすために、Firmataを書き込む。 Arduino IDEを起動して、ファイル>スケッチの例>Firmata>StandardFirmata を選択し、Arduinoに書き込む。
-
依存パッケージをPCにインストール
npm install
-
MCPサーバを起動
npm index.js
もしくは、VS Codeの場合は下記の手順でもMCPサーバが起動できる。
4.1. .vscode/mcp.jsonに以下を設定する。mcp.json{ "servers": { "roller485-mcp": { "type": "stdio", "command": "node", "args": [ "yourworkspace\\index.js" ] } } }
4.2. コマンドパレット(Ctrl+Shift+P)で「MCP: Start Server」を実行し、
roller485-mcp
を選択する。歯車マークからサーバーの起動、を選択。
正常な場合、MCPサーバの起動時のログは下記のようになる。ツールのdescriptionが見当たらない…とワーニングが出るが、無視してOK。
[info] サーバー roller485-mcp を起動しています [info] 接続状態: 開始しています [info] Starting server from LocalProcess extension host [info] 接続状態: 開始しています [info] 接続状態: 実行中 [warning] [server stderr] [mcp] Board ready [warning] [server stderr] [mcp] MCP server started (stdio) [info] Discovered 1 tools [warning] Tool moveRoller485 does not have a description. Tools must be accurately described to be called
-
チャット欄に「moveRoller485でモータを〇〇度回して」、「適当な角度でモータを回して」などを入力する。問題なくMCPサーバが起動していれば、詳しい使い方自体もチャットで聞ける。
-
ツールの実行前に確認ウインドウが表示されるので、続行を押すと指定した回転角度でRoller485が回転する。応答がイマイチ要領を得ない場合は、VSCodeの再起動、ウインドウの再起動(コマンドパレットを開き、>Reload Windowを選択)、新しいチャットを開く…などを試す。
MCPツールの使い方
-
moveRoller485
は degrees(-3600〜3600)で回転角度を指定 - 1度 = 100counts で内部変換
- 目標位置到達判定は150ms経過後、誤差50counts以内に達したとき行う
- タイムアウトは10秒
- 結果は
OK pos=xxxx (deg=xxx)
またはTimeout pos=xxxx targetCounts=xxxx (deg=xxx)
MCPサーバのコード
このコードでは、先述のとおりツールのdescriptionが見当たらない…とワーニングが出ます。最近の記法ではserver.registerTool…を使うのが普通みたいで、このあたりの詳細はあまり気にしないこととします。
// index.js — MCP + Johnny-Five + Roller485
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import five from 'johnny-five';
// ===== Roller485固定パラメータ =====
const I2C_ADDR = 0x64; // Roller485 I2C アドレス
const REG_ENABLE = 0x00; // モータイネーブルレジスタアドレス
const REG_MODE = 0x01; // モード設定レジスタアドレス
const MODE_POS = 0x02; // 位置モード
const REG_POS_TGT = 0x80; // 目標位置設定レジスタアドレス s32 LE
const REG_POS_NOW = 0x90; // 現在位置読み出しレジスタアドレス s32 LE
const DEG_TO_CNT = 100; // 1degあたり100counts
// 到達判定のパラメータ
const WAIT_ENABLE_MS_STABLE = 150; // 誤差内に留まる最短時間[ms]
const WAIT_POLL_MS = 20; // ポーリング周期[ms]
const WAIT_EPS_COUNTS = 50; // 許容誤差[counts]
const WAIT_TIMEOUT_MS = 10000;// タイムアウト[ms]
// ===== ユーティリティ =====
const s32le = (v) => {
const n = (v | 0) >>> 0;
return [n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF];
};
const fromS32le = (b) => (b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)) | 0;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// 角度[deg] → 絶対カウント
const degToCounts = (deg) => Math.round(deg * DEG_TO_CNT); // Roller485の場合、1degあたり100counts
// ===== メイン =====
async function main() {
// Johnny-Five 初期化
const board = new five.Board({ repl: false, debug: false });
await new Promise((res) => {
board.on('ready', () => {
board.i2cConfig(); // 既定100kHz
console.error('[mcp] Board ready');
res();
});
});
const writeBytes = (reg, bytes) => board.i2cWrite(I2C_ADDR, [reg, ...bytes]);
const readS32Once = (reg) =>
new Promise((resolve, reject) => {
// レジスタ指定で4バイト読む
board.i2cReadOnce(I2C_ADDR, reg, 4, (data) => {
if (!data || data.length !== 4) return reject(new Error('I2C read length'));
resolve(fromS32le(data));
});
});
// MCP サーバ
const server = new McpServer({
name: 'roller485-mcp',
version: '1.1.0',
description: 'Roller485 を I2C で角度指定で回す。指定可能範囲は-3600〜3600度',
});
server.tool(
'moveRoller485',
{
degrees: z.number().min(-3600).max(3600),// -3600〜3600度
},
async ({ degrees }) => {
try {
// 角度 -> カウント
const targetCounts = degToCounts(degrees);
// 位置モード+Enable(冪等)
writeBytes(REG_MODE, [MODE_POS]);
writeBytes(REG_ENABLE, [1]);
// 目標位置書き込み
writeBytes(REG_POS_TGT, s32le(targetCounts));
// 固定条件で到達待ち
const t0 = Date.now();
let stableStart = null;
let lastPos = 0;
while (true) {
const pos = await readS32Once(REG_POS_NOW);
lastPos = pos;
const err = Math.abs(targetCounts - pos);
if (err <= WAIT_EPS_COUNTS) {
if (stableStart === null) stableStart = Date.now();
if (Date.now() - stableStart >= WAIT_ENABLE_MS_STABLE) {
return {
content: [{ type: 'text', text: `OK pos=${pos} (deg=${degrees})` }],
};
}
} else {
stableStart = null;
}
if (Date.now() - t0 > WAIT_TIMEOUT_MS) {
return {
content: [{ type: 'text', text: `Timeout pos=${lastPos} targetCounts=${targetCounts} (deg=${degrees})` }],
isError: true,
};
}
await sleep(WAIT_POLL_MS);
}
} catch (e) {
console.error('[mcp] setRollerAngleDeg error:', e);
return {
content: [{ type: 'text', text: `Error: ${String(e)}` }],
isError: true,
};
}
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[mcp] MCP server started (stdio)');
}
main().catch((e) => console.error('[mcp] fatal:', e));
依存パッケージ
-
@modelcontextprotocol/sdk
: MCPサーバ -
johnny-five
: Arduino制御 -
zod
: パラメータバリデーション
おわりに
本記事では、MCPサーバなどによって、自然言語で簡単にモータが動かせることを紹介しました。自然言語で簡単に現実環境に働きかけできると、今後さまざまな応用が考えられます。
ただし、例えば「ちょっとだけ動け」と言っても、その意図を正確にCopilot Chatへ伝えるのはやはり難しいです。動画内でも「現在角度から相対的に、少しだけ回転してほしい」という意図が伝わらず、「絶対角度として小さな角度(例えば15°)にする」ために、相対的にはけっこう大きな回転をしたりします。また、チャット一つでモノが動くことは便利な一方、安全面が非常に重要なことは想像に難くないです。そのあたりの技術も今後に期待ですね。
間違い、ご指摘等あればぜひお知らせください。閲覧いただきありがとうございました。