ドローンシミュレータ（箱庭）をMCPサーバー/Claude連携した！

# 🛫 ドローン操縦の未来を、"自然言語"で

「ドローンを飛ばして！」とAI（Claude）に話しかけるだけで、
仮想空間上でリアルなドローンを自在に操縦できたら──。

そんな夢を、箱庭ドローンシミュレータ × MCPサーバー × Claude連携で実現しました！

本記事では、
[箱庭ドローンシミュレータ](https://github.com/toppers/hakoniwa-drone-core)をMCPサーバー化し、Claude（AI）から自然言語コマンドで操縦できる仕組みを紹介します。

# 論よりRUN!!

まずは、動く様子をご覧ください！

📺 デモ動画はこちら →
<iframe width="1294" height="480" src="https://www.youtube.com/embed/hbdJr-2zLBs" title="Claude.ai を使って、箱庭ドローンシミュレータを操作！" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 仕組み

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/87d1b6d2-bac9-49e8-ad4a-98ab6e6ffce8.png)

全体アーキテクチャ上図の通りです。

この連携で重要なキーとなる技術要素は、たった2つだけ。

- Claude.ai は、MCPプロトコルで MCPサーバーと通信できる
- 箱庭ドローンシミュレータは、Python API経由でドローンを操作できる

つまり、
「自然言語 → MCP経由コマンド → Python API → ドローン操作」
というシンプルな連携構成が実現できた、というわけです！

## 箱庭MCPサーバー

箱庭はPythonと相性が良いので、今回作成したMCPサーバーはPythonで作っています。

こちらの記事を参考にして作成しました（感謝）。

https://note.com/npaka/n/n8348d59b6ef3

今回やりたいことは、ツールの外部起動になりますので、list_toolsとcall_toolのみを追加することで対応しました。

コード断片：
```python

@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="describe_hakoniwa_drone",
            description="Provides an overview of the Hakoniwa Drone Simulator",
            inputSchema={
                "type": "object",
                "properties": {},
                "required": []
            },
        ),
        types.Tool(
            name="hakoniwa_drone_simulator_start",
            description="Starts the Hakoniwa Drone Simulator",
            inputSchema={
                "type": "object",
                "properties": {},
                "required": []
            }
        ),
        types.Tool(
            name="hakoniwa_drone_simulator_stop",
            description="Stops the Hakoniwa Drone Simulator",
            inputSchema={
                "type": "object",
                "properties": {},
                "required": []
            }
        ),
        types.Tool(
            name="provide_hakoniwa_drone_manual",
            description="Provides the Hakoniwa Drone Simulator operation manual (PDF)",
            inputSchema={
                "type": "object",
                "properties": {},
                "required": []
            }
        ),
        types.Tool(
            name="drone_command_takeoff",
            description="Executes a takeoff command on the drone simulator",
            inputSchema={
                "type": "object",
                "properties": {
                    "height": {
                        "type": "number",
                        "description": "The target height for takeoff (default 1.0m)"
                    }
                },
                "required": []
            }
        ),
        types.Tool(
            name="drone_command_move_to_position",
            description="Move drone to specified (x, y, z) position (ROS coordinate system, meters)",
            inputSchema={
                "type": "object",
                "properties": {
                    "x": {"type": "number", "description": "Forward direction, meters"},
                    "y": {"type": "number", "description": "Left direction, meters"},
                    "z": {"type": "number", "description": "Upward direction, meters"},
                    "speed": {"type": "number", "description": "Movement speed, meters/second"}
                },
                "required": ["x", "y", "z", "speed"]
            }
        ),
        types.Tool(
            name="drone_command_land",
            description="Land the drone",
            inputSchema={
                "type": "object",
                "properties": {},
                "required": []
            }
        )    

    ]

@server.call_tool()
async def handle_call_tool(name: str, arguments: dict | None) -> list[types.TextContent]:
    if name == "describe_hakoniwa_drone":
        return [
            types.TextContent(
                type="text",
                text=(
                    "Hakoniwa Drone Simulatorは、仮想環境上でドローンの自律飛行や荷物輸送タスクを模擬できる高機能シミュレータです。"
                    "ROS座標系およびUnity座標系の両方に対応し、リアルタイムなカメラ画像取得、Lidarデータ取得、API制御による移動・離着陸・物体把持が可能です。"
                    "訓練用途、研究用途、またはAI開発向けのプロトタイピングに適しています。"
                )
            ),
        ]
    elif name == "hakoniwa_drone_simulator_start":
        return [
            types.TextContent(
                type="text",
                text=hakoniwa_drone_simulator_start()
            )
        ]
    elif name == "hakoniwa_drone_simulator_stop":
        return [
            types.TextContent(
                type="text",
                text=hakoniwa_drone_simulator_stop()
            )
        ]
    elif name == "provide_hakoniwa_drone_manual":
        return [
            types.TextContent(
                type="text",
                text=(
                    "箱庭ドローンシミュレータの公式マニュアルはこちらです！\n"
                    "👉 [Hakoniwa Drone Manual PDF](https://www.jasa.or.jp/dl/tech/simulator_operation_edition.pdf)"
                )
            )
        ]
    elif name == "drone_command_takeoff":
        height = 1.0
        if arguments and "height" in arguments:
            height = float(arguments["height"])
        return [
            types.TextContent(
                type="text",
                text=drone_command("takeoff", [str(height)])
            )
        ]

    elif name == "drone_command_move_to_position":
        if arguments is None:
            raise ValueError("Arguments required for moveToPosition")
        x = str(arguments["x"])
        y = str(arguments["y"])
        z = str(arguments["z"])
        speed = str(arguments["speed"])
        return [
            types.TextContent(
                type="text",
                text=drone_command("moveToPosition", [x, y, z, speed])
            )
        ]

    elif name == "drone_command_land":
        return [
            types.TextContent(
                type="text",
                text=drone_command("land")
            )
        ]
    else:
        raise ValueError(f"Unknown tool: {name}")

```

# MCPサーバーを作ってみて気づいたこと

今回、初めてMCPサーバーを作ってみて、いくつかハマりポイントがありましたので、共有しておきます！

## 標準出力にメッセージを出してはいけない

Claude側（MCPクライアント）は、標準出力を純粋なJSON通信に使っています。

そのため、print() などでログを出すと、JSONパースエラーになり通信が途絶えます。

➔ ログ出ししたい場合は、ファイルに書き出すか、サーバー側だけで完結させる必要あり。

ちなみに、今回は、箱庭のPython APIを外部プロセスとして外出して、subprocess.Popen()で実行かける対応をしました。

## 環境変数（PYTHONPATH / DYLD_LIBRARY_PATH）に注意

サーバープロセスを起動する際、Unityや箱庭ライブラリにパスを通しておかないと、モジュールインポートや動的ライブラリ読込みに失敗します。

➔ subprocess.Popen()のときに、環境変数を適切に引き継ぐ必要あり。

## MCPサーバーからUnityプロセスの起動で落ちる...

Unityプロセスをバックグラウンドで起動しても、バックグラウンド実行コマンドが終了すると、一緒に落ちてしまうことがわかりました。

そのため、Unityを起動するためのデーモンプロセスを用意して、MCPサーバーと通信させて今回のデモを実現させています。


# まとめ

ドローンシミュレータ（箱庭）をMCPサーバー化し、
Claudeと自然言語で連携できる未来を手に入れました！

まだまだ機能追加や改善の余地はたくさんありますが、
まずは 「論よりRUN」 をモットーに、小さな一歩を形にすることができました！

次は…

# 今後の展望（TODO）

🛫 自律飛行・WayPoint巡回モードを追加したい

🎒 荷物輸送ミッション（グラブ＆リリース）に挑戦したい

🎥 飛行中のカメラ画像を取得して、AI解析に使いたい

🤖 Claudeともっと高度な「ドローン対話」シナリオを作りたい