1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

おもちゃのロボットアームでフィジカルAIを網羅的に勉強したい 〜 その3:MCPサーバ編

1
Last updated at Posted at 2026-01-22

はじめに:MCPサーバーの実装とフィジカルAIの可能性

前回は、自作コントローラによるロボットの物理制御を実現しました。今回は、この「体」を生成AIという「脳」につなぐため、MCP(Model Context Protocol)サーバーの実装を行います。

あえてハードウェアからクラウド連携までを自作する理由は、単なる技術的興味ではありません。ボトムアップで全レイヤーの仕組みを深く知ることで初めて、手触り感のある真のビジネス案が浮かんでくるからです。

過去、技術レイヤー間の断絶を放置したために、有望な事業が失脚する場面を幾度も見てきました。物理世界をAIで制御する「フィジカルAI」の領域において、この垂直統合の理解こそが、商機を嗅ぎ分ける力になると確信しています。

本稿では、MCPサーバーの実装詳解に加え、付録としてICT企業・ネットワークエンジニアの視点から見たビジネスの可能性についても考察します。

機能分担と全体構成

過去のネットワークシステム構築の経験から、コンポーネント間の機能分担の重要性を痛感しています。かつて、多数の機器で構成されるシステムのリアルタイム処理を中央サーバーに集約しようとした結果、スケーラビリティや品質の問題に直面したことがありました。

その教訓を活かし、今回の設計では「リアルタイム処理」と「高度な論理処理」を明確に分離します。

  • ロボットコントローラ(Arduino UNO): 与えられたコマンドシーケンスに従い、ミリ秒単位のリアルタイム性を持ってサーボを駆動する。
  • MCPサーバー(Python): コントローラへのコマンド送信を仲介するほか、作業対象物(ワーク)の仕様情報をMCPクライアントへ提供する。
  • MCPクライアント(Gemini CLI): ユーザーの自然言語入力を受け、クラウド上のGeminiと連携して実行手順や最適なコマンド列を生成する。

今回は、下図の実線部分であるMCPサーバーおよびGemini CLIによる連携部分を実装します。

Screenshot 2026-01-22 at 15.24.08.png

MCPサーバーの実装

例えば「ピック&プレイス」という一連の動作をロボットに実行させるには、コントローラに対して以下のようなコマンドシーケンスを送信する必要があります。

grip close; move x=160 y=0 z=20; grip open; delay t=1000; move x=160 y=140 z=60; ...

本実装の目標は、Gemini CLIに入力されたユーザーの曖昧な指示を、このような厳密なシーケンスへと落とし込み、コントローラへ確実に届けることです。MCPサーバーの主要な構成要素のうち、今回は以下の Tools を中心に実装しました。

実装した主な機能(MCP Tools)

  1. execute_sequence(commands): ロボットコントローラへ一括コマンドを送信。
  2. get_robot_status(): 現在の座標や関節角度を取得。
  3. get_workpiece_catalog(): ワーク(耳栓ケース等)の形状・サイズのデータを取得。

動作フローの例

  1. ユーザーがGemini CLIで「耳栓ケースを移動して」と指示。
  2. Geminiが get_workpiece_catalog を参照し、耳栓ケースの物理情報を把握。
  3. Geminiが安全な経路を計算し、execute_sequence を介してロボットを駆動。

Geminiによるコード生成とチューニング

実装にあたっては、Gemini(FastMCPライブラリを使用)を活用してコードの雛形を生成しました。

特に工夫したのは、各関数の docstring(説明文) です。MCPではこのdocstringがそのままAIへの「取扱説明書」として渡されます。動作を確認しながら「いつこのツールを呼ぶべきか」「安全高度はどう計算すべきか」といった指示をdocstringに書き込み、手作業でチューニングを繰り返すことで、意図した通りの自律的なプランニングを実現できました。

from fastmcp import FastMCP
import serial
import time
import json

# --- 設定 ---
SERIAL_PORT = '/dev/cu.usbmodem101'
BAUD_RATE = 9600
TIMEOUT = 45

mcp = FastMCP(
    "RobotArmController"
)

# --- 内部用データソース ---
def _fetch_workpiece_data():
    return {
        "earplug_case": {
            "name": "耳栓ケース",
            "height": 43.0,
            "approach_z_offset": 50.0,
            "description": "円筒形のケース。Z=20で把持し、移動は安全高度 Z=93 (43+50) を経由してください。"
        },
        "base_tray": {
            "name": "配置トレイ",
            "height": 5.0,
            "approach_z_offset": 60.0,
            "description": "配置用の平坦な面。Z=5で解放、Z=65で移動してください。"
        }
    }

_serial_conn = None

def get_serial():
    global _serial_conn
    if _serial_conn and _serial_conn.is_open:
        return _serial_conn
    try:
        _serial_conn = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=TIMEOUT)
        time.sleep(2)
        _serial_conn.reset_input_buffer()
        return _serial_conn
    except Exception as e:
        return None

def send_command(cmd: str) -> str:
    conn = get_serial()
    if not conn: return "Error: ロボットに接続できません。"
    try:
        full_cmd = cmd.strip() + "\n"
        conn.write(full_cmd.encode('utf-8'))
        response = []
        while True:
            line = conn.readline().decode('utf-8', errors='replace').strip()
            if line == '%': break
            if not line and conn.in_waiting == 0: break
            if line: response.append(line)
        return "\n".join(response) if response else "Success"
    except Exception as e:
        return f"Error: {e}"

# =================================================================
# MCP Tools (ここにあるdocstringがAIの「脳」になります)
# =================================================================

@mcp.tool()
def get_workpiece_catalog() -> str:
    """
    登録されているすべてのワーク(作業対象物)の物理特性リストを取得します。
    
    【使用上の義務】
    ピック&プレイスの計画を立てる際、対象物の正確な高さ(height)や
    安全高度(approach_z_offset)を把握するために、必ず最初にこのツールを実行してください。

    【重要】
    作業対象(ワーク)の物理特性と移動計画用の計算式です。
    - height: 対象物自体の高さ。把持位置(Z値)の基準。
    - approach_z_offset: 安全のために加算する「逃げ」の距離。
    
    水平移動の際は、必ず『height + approach_z_offset』の高度まで上昇してください。
    """
    return json.dumps(_fetch_workpiece_data(), ensure_ascii=False, indent=2)

@mcp.tool()
def execute_sequence(commands: str) -> str:
    """
    ロボットアームに一連の動作(コマンドシーケンス)をセミコロン ';' 区切りで送信します。
    
    【内部コマンドの文法】
    1. move x=<値> y=<値> z=<値> s=<速度> : TCP(Tool Center Point)を指定座標へ移動(sは0-100)。
    2. grip <open|close> : グリッパーを全開、または全閉。
    3. delay t=<ミリ秒> : 物理的な安定を待つために必須。

    【AI管制官への絶対ルール:経路プランニング】
    - 把持(grip close)や解放(grip open)の直後には、必ず 'delay t=1000' を挿入せよ。
    - 事前に `get_workpiece_catalog` を参照し、把持位置を把握するとともに、以下の式で安全高度を算出せよ。
      『安全高度 = height + approach_z_offset』
    - 水平移動を行う際は、必ずこの安全高度を経由すること。対象物をなぎ倒す直線移動は厳禁。
    """
    return send_command(commands)

@mcp.tool()
def get_robot_status() -> str:
    """
    現在のロボット座標(TCP: X, Y, Z)を取得します。
    動作計画を立てる前に、ロボットが現在どこにいるかを確認するために使用してください。
    """
    return send_command("dump")


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8888)

Gemini CLIでの動作確認

Gemini CLIをMCPクライアントとして、実際に動作確認を行います。

最初に、Gemini CLIへこのMCPサーバーを認識させるため、以下の設定ファイル(.gemini/settings.json)を作成します。

.gemini/settings.json

{
  "mcpServers": {
    "robot": {
      "httpUrl": "http://localhost:8888/mcp"
    }
  }
}

Gemini CLIを起動します。

$ gemini

MCPサーバが提供するToolsのリストを表示します。

> /mcp list

Configured MCP servers:

🟢 robot - Ready (3 tools)
  Tools:
  - execute_sequence
  - get_robot_status
  - get_workpiece_catalog

ロボットのステータスを表示するよう指示してみます。

> ロボットのステータスについて知りたい

╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓  get_robot_status (robot MCP Server) {}                                    │
│                                                                              │
│ --- CONFIG DUMP ---                                                          │
│ Ch0: [P0=1650, A0=0.0] [P1=2300, A1=48.4] | CUR=1373                         │
│ Ch1: [P0=1600, A0=61.0] [P1=2300, A1=15.3] | CUR=1560                        │
│ Ch2: [P0=1900, A0=90.2] [P1=2400, A1=132.4] | CUR=2465                       │
│ Grip: Open=2750, Close=2250 | CUR=2750                                       │
│ Current Logic TCP: X=160.00 Y=-60.00 Z=70.00                                 │
│ -------------------                                                          │
╰──────────────────────────────────────────────────────────────────────────────╯
✦ Here is the current status of the robot:

   1 --- CONFIG DUMP ---
   2 Ch0: [P0=1650, A0=0.0] [P1=2300, A1=48.4] | CUR=1373
   3 Ch1: [P0=1600, A0=61.0] [P1=2300, A1=15.3] | CUR=1560
   4 Ch2: [P0=1900, A0=90.2] [P1=2400, A1=132.4] | CUR=2465
   5 Grip: Open=2750, Close=2250 | CUR=2750
   6 Current Logic TCP: X=160.00 Y=-60.00 Z=70.00
   7 -------------------

次は、ピック&プレイスの動作確認です。結構複雑なことを指示してみます。

> x=130, y=100に置かれた 耳栓ケースをx=160
  y=-60へピック&プレイスしてください。アーム移動時は常に安全高度を維持してくだ
  さい。耳栓ケースをピック時は把持位置まで降下、プレイス時はTCP高さ25mmでリリー
  スしてください。

✦ はい、承知いたしました。耳栓ケースのピック&プレイス操作を再度開始します。
  まず、対象物の物理データとロボットの現在位置を再確認します。
╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓  get_workpiece_catalog (robot MCP Server) {}                               │
│                                                                              │
│ {                                                                            │
│   "earplug_case": {                                                          │
│     "name": "耳栓ケース",                                                    │
│     "height": 43.0,                                                          │
│     "approach_z_offset": 50.0,                                               │
│     "description": "円筒形のケース。Z=20で把持し、移動は安全高度 Z=93        │
│ (43+50) を経由してください。"                                                │
│   },                                                                         │
│   "base_tray": {                                                             │
│     "name": "配置トレイ",                                                    │
│     "height": 5.0,                                                           │
│     "approach_z_offset": 60.0,                                               │
│     "description": "配置用の平坦な面。Z=5で解放、Z=65で移動してください。"   │
│   }                                                                          │
│ }                                                                            │
│                                                                              │
│ ✓  get_robot_status (robot MCP Server) {}                                    │
│                                                                              │
│ --- CONFIG DUMP ---                                                          │
│ Ch0: [P0=1650, A0=0.0] [P1=2300, A1=48.4] | CUR=1373                         │
│ Ch1: [P0=1600, A0=61.0] [P1=2300, A1=15.3] | CUR=1415                        │
│ Ch2: [P0=1900, A0=90.2] [P1=2400, A1=132.4] | CUR=2651                       │
│ Grip: Open=2750, Close=2250 | CUR=2750                                       │
│ Current Logic TCP: X=160.00 Y=-60.00 Z=93.00                                 │
│ -------------------                                                          │
╰──────────────────────────────────────────────────────────────────────────────╯
✦ はい、情報を再確認しました。
  耳栓ケースの定義に基づき、把持高さを20mm、安全高度を93mmとしてピック&プレイス
  操作を実行します。
╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓  execute_sequence (robot MCP Server) {"commands":"grip open;move x=130 y=… │
│                                                                              │
│ Success                                                                      │
╰──────────────────────────────────────────────────────────────────────────────╯
✦ 操作が完了しました。

最終的にロボットコントローラへ送信されたコマンド列は以下となりました。

grip open;move x=130 y=100 z=93;move x=130 y=100 z=20;grip close;delay
t=1000;move x=130 y=100 z=93;move x=160 y=-60 z=93;move x=160 y=-60 z=25;grip open;delay t=1000;move x=160 y=-60 z=93

Geminiは自発的に get_workpiece_catalog を呼び出し、「耳栓ケースの高さは43mm、安全オフセットは50mmだから、移動はZ=93mmで行うべき」と判断しました。その後、適切なコマンドシーケンスを生成し、一括で送信することに成功しました。

ここまで、Geminiの力を借りてMCPサーバーを超高速で構築してきました。機械制御のコードまでオンデマンドで生成し、物理的な制約を自律的に解釈して動くその挙動には、フィジカルAIの確かな可能性を感じます。

今回登場したMCPですが、ハードとソフト、あるいはマルチベンダー・マルチレイヤーの複雑な境界をシームレスに繋ぎ合わせる「共通の橋渡し(コネクタ)」として機能することが期待されます。この垂直統合的なアプローチが、実際の社会インフラやネットワークビジネスにおいてどのような変革をもたらすのか。私なりの考察を巻末の付録にまとめました。

次のステップ

今後は、広角USBカメラとOpenCVを組み合わせた「ロボットの目」を実装する予定です。この視覚情報とコントローラを統合し、Gemini Robotics-ERとの連携を通じて、より高度なVLA(Vision-Language-Action)モデルの実現を目指します。

付録

社会インフラ視点でMCPを考える

以前、国際空港でテレワークをしていた際、多様な言語、事業者、データが交錯する大規模インフラにおけるMCPの重要性に気づきました。「国際空港におけるMCP仕様」をGeminiに生成させてみたところ、搭乗案内(Resources)から警備ロボットの巡回(Tools)まで、多岐にわたるサービスが定義されました。

これらのサイロ化されたシステムがMCPという共通インターフェースで統合され、一つの端末からシームレスに利用可能になれば、ICT企業にとってとてつもなく巨大なビジネスチャンスになるはずです。国内企業が得意とする顔認証技術や物理的な制御システムを、生成AIで再編する試みには、産業用ロボットの枠を超えた可能性が秘められています。

ネットワークエンジニア視点でMCPを考える

長らくネットワークエンジニアとして活動してきた視点で見ると、現在のMCPの盛り上がりは、10年ほど前の SDN (Software Defined Networking) の波を彷彿とさせます。

マルチベンダー環境の複雑さをAPIで隠蔽し、全体をプログラマブルにするSDNの思想は、物理世界をAIで抽象化するMCPの思想と驚くほど共通しています。MCPは、まさに「AIが物理世界を操作するための現代版MIB/YANG」と言えるかもしれません。

追記(2026/1/24):フィジカルAIの動向とSDNの歴史に学ぶ課題解決

某大手ICTベンダーの共創施設で開催されたフィジカルAIベンチャー各社によるワークショップに参加してきました。最新技術の紹介を通じて、本記事で扱ってきた内容と重なる点が多くありましたので、備忘録としてまとめます。

ハードウェアからクラウドへのシフトと「人材」

国内の多くのICT企業がハードウェア中心の事業からクラウド構築へとシフトした現在、フィジカルAIの現場では「ハードとソフトの両面を理解している人材」の不足が深刻化しています。 ICTエンジニアがクラウド側の知見を保持しつつ、フィジカル領域までを網羅的に学ぶことは、今後の開発において非常に重要な意味を持つと改めて確信しました。

社会実装における現実的な課題

会場での議論は、人型ロボットといった形態そのものよりも、「いかにビジネスとして成立させるか」という実利的な側面に集中していました。特に警備や清掃といった、一見自動化が進んでいるようでいて、現場の要求水準にはまだ達していない「目の前の課題」をどう突破するかが焦点となっています。

ネットワークグラフによる空間理解

技術面で注目したのは、主催ベンダーが研究されている「世界空間をネットワークグラフで構造化する技術」です。 本記事で挙げた国際空港向けのMCPサービス(警備ロボット等)を例に取ると、ロボットがカメラやLiDARで巡回しながら「何がどこにあり、互いにどう関連しているか」を自動でグラフ化する技術が考えられます。こうした空間の構造化が進めば、フィジカルAIによる新たなサービス形態が見えてくるはずです。

私見:SDNが解決してきた課題からの学び

ここからは私自身の主観になりますが、今後のフィジカルAIの発展を考える上で、かつて SDN(Software Defined Networking) がネットワークの世界で解決してきたアプローチが非常に参考になるのではないかと感じています。

具体的には、以下のキーワードがフィジカルAIの顕在化した課題を解く鍵になると考えています。

  • マルチベンダー・マルチレイヤーの統合
  • デジタルツインとネットワークグラフの活用
  • (非技術面を含めた)サイロ化の解消

かつてSDNが物理的な制約をソフトウェアによって抽象化し、運用の柔軟性を高めたように、フィジカルAIにおいても「物理空間をいかにソフトウェアで定義し、制御するか」という視点が重要です。

また、これはフィジカルAIに限った話ではありません。生成AIを用いて業務の自動化を検討する場合においても、常に当てはまる本質的な課題であると言えます。複雑なレイヤーをいかに繋ぎ、情報のサイロ化を解消して、一貫した制御構造(グラフ)を構築できるか。過去のICTの歴史から得られたこれらの視点を持ち続けることが、着実な社会実装への近道ではないでしょうか。

ソースコードの公開

以下に最新版を公開します。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?