MCPサーバーを自分で作るでは、AIに自分の道具を差し出す側を作りました。今回はその道具が3Dソフトそのものだったら、という話です。AIにBlenderを操作させます。

やってみたら、いちばん面白かったのは成功や失敗そのものではなく、「実行が通ったこと」と「正しくできたこと」がまったく別だったことでした😳 同じ指示を30回与えると、コードは30回とも通るのに、意図どおりだったのは4回だけです。

この記事は下調べが2つ、実験が4つあります。

先に検証した環境を書いておきます。★この記事の数字はこの環境のものです。とくにBlenderの版と、使ったモデルの大きさで結果が変わります。

項目内容
パソコンApple M4 / メモリ32GB / macOS 26.6.2
Blender5.2.1 LTS(Python 3.13.13)。⚠検証の途中で 5.1.1 から更新したので、全項目を測り直しています
アドオンBlender Lab MCP Server 1.0.0(公式)
MCPのSDKMCP Python SDK 1.27.0(FastMCP)
生成に使ったモデルQwen2.5-Coder-14B-Instruct Q4_K_M(llama.cpp)/ qwen2.5:3b(Ollama)
生成の設定いずれも temperature 0.2
スポンサーリンク

用語集

用語平たく言うとなぜ効くのか
MCPAIに「使ってよい道具」を差し出すための共通の約束ごとこの記事の土台。詳しくはp992
bpyBlenderをPythonから操作するための窓口AIが書くのはこれを使ったコード
アドオンBlenderに機能を足す拡張Blenderの中で動く。外のプログラムからbpyは直接触れない
Principled BSDFBlenderの標準的な材質のノード。色や粗さをここで決める★今回の主役。ここを設定しないと本当の色にならない
diffuse_colorマテリアルが持つ「画面に表示するときの色」★★今回の落とし穴。レンダリングには効かないのに、画面では色が変わる
EEVEEBlenderの速いほうのレンダリング方式色を確かめるのに使った(p976で詳しく測っています)
stdio標準入出力。プログラム同士が文字をやりとりする最も素朴な経路MCPサーバーの標準的な繋ぎ方
llama.cpp手元のパソコンでLLMを動かすしくみ★公式のドキュメントもこれを例に説明している=ローカルLLM前提

【下調べ】前提が変わっていた ── 公式のMCPサーバーが出ていた

この回を計画したのは2026年8月で、そのときは「bpyを叩くMCPサーバーを自分で書く」つもりでした。ところが調べてみると、Blender財団自身が公式のMCPサーバーを出していました。

公式コミュニティ版
名前Blender Lab MCP Server(v1.0.0)blender-mcp
配布projects.blender.org の Blender LabGitHub
必要なBlender5.1 以上4.x から

⚠この2つは別のプロジェクトです。 導入手順もアドオンも違います。ネットの情報を読むときは、どちらの話なのかを先に確かめる必要があります。

そして公式のページには、こういう警告が載っています。

★公式の警告(要約)
このMCPサーバーは、LLMが生成したコードを何のガードも無しにBlenderで実行します。データが消されたり外部へ送られたりすることから守るしくみは入っていません。データを守りたいなら仮想マシンか、機微な情報の無いシステムを使ってください。

公式が自分でここまで書くのは珍しいと思います。本当にそこまで無防備なのかを、あとで実際に測ります。

なお、ソースの置き場(projects.blender.org)で配布されているzipを開いてみると、中身は9ファイル・16KBしかありませんでした。全部読める量なので、一通り目を通してみます。

【下調べ】アドオンは「コードを実行するだけ」だった

中身を読んで、構造がはっきりしました。

AIがBlenderを触るまでの経路 LLMクライアント llama.cpp など MCPサーバー 道具を見せる係 アドオン(Blenderの中) TCPソケット :9876 bpy exec(code) MCP TCP・ヌルバイト区切りのJSON ★アドオンがやっているのは「受け取ったPythonコードを exec する」だけ =道具の粒度は「任意のコードを実行する」という1つしかない。ここが結果を決めていた

やりとりの形も単純です。

要求: {"type": "execute", "code": "<Pythonのコード>", "strict_json": false} + ヌルバイト
応答: {"status": "ok", "result": {}, "stdout": "..."}             + ヌルバイト

既定の宛先: localhost:9876   要求は最大10MiB   クライアントの待ち時間は10秒

これが分かれば、MCPサーバーを通さずにアドオンのソケットへ直接しゃべれます。公式のMCPサーバーと同じ経路で、間に何も挟まずに測れるので、以降の実験は全部この方法でやりました。

【実験1・Mac】公式の「弱いサンドボックス」は何を止めているのか

配布物には weak_sandbox.py というファイルが入っています。名前からして弱そうですが、中のコメントはもっと正直でした。

Note that this isn't really a sandbox,
more guidance that some things should not be done.

If the LLM (or its user) is motivated these can be worked around.
This is more of a slap on the wrist not to try some things.

「これは本当のサンドボックスではない」「その気になれば回避できる。軽く手を叩く程度のもの」。書いてある内容を鵜呑みにせず、10通り投げて確かめました。

試したこと結果
sys.exit()✓ 止まった
bpy.ops.wm.quit_blender()✓ 止まった
工場出荷設定に戻す✓ 止まった
ユーザー設定を読み直す✓ 止まった
ファイルを作る⚠ 通った
ファイルを消す⚠ 通った
ホームの中身を列挙する⚠ 通った(85件返ってきた)
外へHTTPで出る⚠ 通った(200が返り、559バイト読めた)
別プロセスを起動する⚠ 通った
環境変数を読む⚠ 通った

止まったのは10通り中4つ。しかもその4つは、ソースを読むとこう選ばれていました。

The rule of thumb for inclusion is:
   The operator is guaranteed to cause problems and/or failure.

★★止めている4つは、安全のためではなかった
選定の基準は「確実に問題や失敗を起こすもの」。つまりBlenderのセッションが壊れて作業が続けられなくなるのを防ぐためのものです。

現に「ユーザー設定を読み直す」を止めている理由は、ソースにこう書いてあります——「このアドオン自身が無効化されるかもしれないから」。

★データを守るしくみは、名前どおり一切入っていません。 公式の警告は正確でした。

⚠このブログでは、この先の実験を検証用のディレクトリの中だけで完結させています。 外へのHTTPも、経路が開いていることを確かめるための1回のGETだけです。実際に使うなら、公式が書いているとおり仮想マシンか、消えて困るものが無い環境でやるべきものです。

【実験2・Mac】同じ指示を30回与えて、成功率を数える

ここから本題です。AIにどこまで任せられるのかを数えます。

課題は、機械で判定できるように条件を具体的にしました。

1. 既定の立方体(名前 Cube)を削除する
2. 原点に半径1のUV球を作る
3. その球の名前を Ball にする
4. Ball に赤いマテリアル(Base Color が赤)を割り当てる

判定は目視ではなく、実行したあとにシーンへ問い合わせて4条件を個別に確かめます。そして——ここが今回いちばん大事な設計です——「実行がエラーにならなかったか」と「意図どおりにできたか」を別々に数えます。

試したのは2つのモデルです。どちらも手元で動く無料のものです。

qwen2.5:3b(10回)Qwen2.5-Coder-14B(30回)
生成の時間(中央値)3.5秒18.0秒
実行がエラーにならなかった0/1030/30(100%)
★意図どおりにできた0/104/30(13%)
立方体を消せた10/1030/30
球になっている9/1030/30
赤くなっている0/104/30
bpy.ops を使った10/1030/30

⚠10回では足りませんでした
14Bの「意図どおり」は、回すたびに 1/10(10%)→ 2/10(20%)→ 2/20(10%) と振れました。

★10回だけ見ていたら「10%」とも「20%」とも書けてしまいます。 2倍の差が、ただのばらつきで出ます。30回まわして、ようやく13%と言えました。

【続き】3Bは全部落ちた。ただし「何も起きなかった」ではない

3Bのモデルは10回とも実行時にエラーになりました。中身はほぼ1種類です。

ValueError: bpy_struct: item.attr = val: sequences of dimension 0
            should contain 4 items, not 3

material.diffuse_color = (1, 0, 0) と3つの数字で渡していました。ここはRGBAの4つが必要です。

ところが表をもう一度見てください。立方体は10/10で消えていて、球は9/10でできています。

★「失敗した」は「何も起きていない」ではない
例外はそこで止まるだけで、それまでに実行した操作は取り消されません。立方体は消えたまま、球は置かれたまま、色だけが付いていない状態でシーンに残ります。

AIに何かを任せて「エラーになったからやり直そう」と思ったとき、前回の途中結果が残っていることを忘れると、2回目はまったく違う状況から始まります。

【続き】14Bは全部通った。それでも正しかったのは13%

コーディング向けの14Bモデルは、30回ともエラーなく実行できました。立方体も消え、球もできています。それなのに、赤くなっていたのは4回だけでした。

26回はこう書いていました。

material = bpy.data.materials.new(name="RedMaterial")
material.diffuse_color = (1, 0, 0, 1)   # RGBA     ← ★ここ
ob.data.materials.append(material)

通った4回だけが、こう書いていました。

material = bpy.data.materials.new(name="RedMaterial")
material.use_nodes = True
bsdf = material.node_tree.nodes.get('Principled BSDF')
bsdf.inputs['Base Color'].default_value = (1, 0, 0, 1)
ob.data.materials.append(material)

diffuse_color はビューポート(作業画面)の表示色であって、レンダリングの色ではありません。書いてもエラーは出ませんし、警告も出ません。

そして厄介なのは、作業画面では本当に赤く見えることです。目で見て確かめても気づけません。

【続き・確かめ】本当に赤くないのか、描いて確かめる

推測で終わらせず、同じ条件で2つの球を並べてレンダリングし、画素の色を読みました。

同じ「赤いマテリアルを割り当てる」コードで作った2つの球。左のdiffuse_colorだけのものは灰色、右のBase Colorを設定したものは赤くレンダリングされている
球書き方レンダリングの画素
左diffuse_color だけR=0.557 G=0.561 B=0.569 = 灰色
右Principled の Base ColorR=0.616 G=0.078 B=0.016 = 赤

左はRGBがほぼ同じ値で、完全な灰色です。どちらも「赤いマテリアルを割り当てる」コードは通っています。 片方だけが本当に赤いのです。

★★「動いた」と「意図した経路で動いた」は別
このブログでは何度も同じ顔に出会っています。p967ではベンチマークが動いてしまい、危うく別の経路の数字を載せるところでした。p992では道具の失敗がisError で返ることを知らず、失敗を成功として扱っていました。

今回は3Dで、しかも見た目では絶対に気づけない形で出ました。

【続き】ほかにも作らせてみる ── 6つのうち仕様どおりは3つ

球を1つ作らせただけでは、どこまで作れるのかが分かりません。形の違う課題を6つ出して、出てきたコードを実行した結果をそのまま描きました。うまくいったものも、そうでないものも、加工せずに並べます。

AIに作らせた6つの結果。階段・タワー・らせんは仕様どおり、円柱は半径が違い、机は脚が外れ、アーチは平らな円になっている

実行がエラーにならなかったのは6つ中5つでした。★何かができたのは6つ全部です。アーチは途中で落ちましたが、それまでに直方体を100個置いていました。

ここで正直に書いておくことがあります。eightは最初この絵を見て、「まともなのはタワーだけだ」と思いました。階段が、平たい板を横に並べただけに見えたからです。

ところが測ってみると、階段のz座標は 0, 0.25, 0.5 … 2.25 ときれいに0.25刻みで上がっていました。仕様どおりです。平らに見えたのは、こちらが出した寸法(1段の幅2に対して高さ0.25)だと7度ほどの緩い坂になるからでした。⚠課題の出し方が悪かったのであって、AIは間違えていません。

★★見た目では、どちらにも間違える
赤い球のときは、画面では赤いのに、実際は灰色でした。=できていないのに、できて見える。
階段のときは、平らに見えるのに、仕様どおりでした。=できているのに、できていなく見える。

★目で見て判断すると、両方向に間違えます。 測るしかありません。

測った結果はこうなりました。

課題実測判定
10段の階段10個・z が0.25刻みで上昇✓ 仕様どおり
立方体10個のタワー10個・z が2.1刻みで上昇✓ 仕様どおり
球20個をらせん状に20個・z が0.5刻みで上昇✓ 仕様どおり
半径5の円周に円柱12本12本・実際は半径6△ 半径が違う
机(天板1枚と脚4本)脚が天板の外と中央に付いた✘ 形になっていない
半円状のアーチ100個・z が全部0=平らな円✘ 実行も途中で失敗

★「並べる・積む・繰り返す」は通り、「部品どうしの位置関係」で崩れます。 階段もタワーもらせんも、やっていることは数を数えながら座標をずらすことです。一方で机は、天板の大きさに合わせて脚を置く必要があり、そこで外れました。アーチも、半円という形を座標に落とすところで平らになりました。

【実験3・Mac】道具の粒度を変える ── 13%が100%になった

ここまでの13%という数字は、渡している道具が「任意のPythonコードを実行する」1つしかないことの結果でもあります。そのぶん、Blenderの流儀をモデルが全部知っている前提になっています。

そこで同じ課題・同じモデル・同じ判定のまま、渡す道具だけを変えました。

delete_object(name)
add_uv_sphere(name, radius, x, y, z)
set_base_color(object, r, g, b)      ← ★落とし穴を道具の中に閉じ込める

モデルがすることは、呼ぶ順にJSONで並べるだけです。

[
  {"tool": "delete_object",  "args": {"name": "Cube"}},
  {"tool": "add_uv_sphere",  "args": {"name": "Ball", "radius": 1.0}},
  {"tool": "set_base_color", "args": {"object": "Ball", "r": 1.0, "g": 0.0, "b": 0.0}}
]
同じモデル・同じ課題で、渡し方を変えたときの「意図どおりにできた」割合 作らせる(生のコード) 13% 4/30 調べさせる(生のコード) 45% 9/20 作らせる(意味のある道具) 100% 30/30 モデルは同じ。変えたのは「何を渡すか」だけ
渡し方意図どおり生成の中央値
生のPythonコード(公式と同じ形)4/30(13%)18.0秒
意味のある道具3つ30/30(100%)9.5秒

13%が100%になり、生成の時間も半分になりました。出力がJSONの3行で済むからです。

⚠モデルが賢くなったのではありません
正解(Principled BSDF の Base Color を設定する)を道具の中に書いておいただけです。つまりAIに任せる範囲を狭めたぶんだけ、成功率が上がった——それが正確な言い方です。

⚠代償もあります。道具ごしにできるのは道具にしたことだけ。生のコードなら何でもできます。「どちらが良い」ではなく、どこまでを人が先に決めておくかの線引きの話です。

【実験4・Mac】「作らせる」と「調べさせる」は別の難しさだった

公式のページが載せている使用例は、実はモデリングではありませんでした。シーンの分析です——「ポリゴン数が多いのに、画面上では小さく映るオブジェクト」を洗い出して最適化の候補を探す、という例が載っています。

ならば「調べさせる」ほうが向いているのでしょうか。答えが1つに決まるシーンを自分で組んで、同じモデル・同じ生コードの経路で聞きました。

Floor   面4つ・画面では大きい
Hero    中くらいの面数・大きい
Detail  32,768面・画面では小さい(scale 0.22)   ← ★正解
Post    中くらい・中くらい
渡し方実行がエラーにならなかった意図どおり/正解
作らせる(生コード・30回)30/30(100%)4/30(13%)
調べさせる(生コード・20回)12/20(60%)9/20(45%)

正解率は3倍以上になりました。公式が分析の例を載せているのは理にかなっています。

そして順番が逆転しているのが面白いところです。

★落ちるほうが、静かに間違うよりマシ
作らせるとき、コードは全部通ります。そのかわり中身が間違っています。
調べさせるとき、コードはよく落ちます(NameError: math や SyntaxError で8回)。そのかわり通れば正解にたどり着きます。

答えの内訳を見ると、間違った名前を答えたのは20回中1回だけ。残りはそもそも答えを出せなかったものでした。

★分からないときに黙って間違えるより、落ちてくれるほうが扱いやすい——AIに任せる範囲を決めるときの、実際的な基準になりそうです。

【続き】自分でMCPサーバーを書く

ここまでで分かったことを踏まえて、意味のある道具を公開するMCPサーバーを書きました。p992で作ったものと同じ形(FastMCP・stdio)です。

@mcp.tool()
def blender_set_base_color(object_name: str, r: float, g: float, b: float) -> str:
    """オブジェクトの色を決める(0〜1)。"""
    return _run(f"""
import bpy
ob = bpy.data.objects.get({object_name!r})
if ob is None:
    raise KeyError("そのオブジェクトは無い: " + {object_name!r})
m = bpy.data.materials.new(name={object_name!r} + "_mat")
m.use_nodes = True
m.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = ({r}, {g}, {b}, 1.0)
m.diffuse_color = ({r}, {g}, {b}, 1.0)
ob.data.materials.clear()
ob.data.materials.append(m)
""")

繋いで確かめた結果です。

確かめたこと結果
見えた道具5個(名前+説明+スキーマで1,559文字)
実際に呼んでシーンが変わるか✓ 変わった
検算(Base Colorの値)[1.0, 0.0, 0.0, 1.0]
⚠わざと失敗させたときisError = True で返った

最後の行が大事です。p992では、道具の失敗がJSON-RPCの error ではなく isError で返ることを知らず、失敗を成功として扱っていました。同じ轍を踏まないよう、今回は最初から確かめています。

⚠道具の名前の付け方

道具名は全部 blender_ で始めました。MCPクライアントは普通 mcp__サーバ名__道具名 のように名前空間を付けますが、付けないクライアントもあります。

p949では、AIエージェント内蔵の read_file とMCPサーバーの read_file が衝突し、黙って別のほうが呼ばれました。delete_object のような一般的すぎる名前は付けないほうが安全です。

公式(Blender Lab)今回書いたもの
道具の数実質1つ(コードを実行)5つ(意味で切った)
モデルに要る知識bpyの書き方を全部何をさせたいかだけ
できること何でも道具にしたことだけ
実測の成功率13%100%

⚠【続き】Blenderを5.2.1に上げても、結果は変わらなかった

検証の途中でBlenderを 5.1.1 から 5.2.1 LTS に更新したので、同じ検査を全部かけ直しました。

検査5.1.15.2.1 LTS
弱いサンドボックス止まったのは4/10同じ 4/10
レンダリングの画素灰 0.557/0.561/0.569 ・ 赤 0.616/0.082/0.020ほぼ完全に同じ
3Bの成功率0/100/10
14Bの成功率1/102/10・2/20(ばらつきの範囲)

★何も変わりませんでした。 これは当たり前ではありません。p970では、Wine 11.13 と 11.14 という10日差で結果が逆転しています。「新しいから違うはず」とも「同じはず」とも決めつけず、測ってから言うべきところでした。

【手順】自分で試すには

手元で動かすまでの最短経路です。⚠その前に、実験1で測ったとおりLLMが書いたコードは無制限に実行されます。消えて困るものが無い環境で試してください。

1. アドオンを入れる

公式ページからアドオンのzipを取得します。⚠普段のBlender設定を汚したくない場合は、専用の設定ディレクトリを指定すると丸ごと分けられます(消せば元通りです)。

export BLENDER_USER_RESOURCES=~/blender_mcp_test
blender --command extension install-file -r user_default -e mcp-1.0.0.zip

2. サーバーを起動する

blender --background --online-mode --command blender_mcp

⚠--online-mode が要ります。localhost のソケットを開くだけでも、Blenderの「オンラインアクセス」が有効でないと起動しません。

3. 動いているか確かめる

MCPクライアントを繋ぐ前に、ソケットへ直接投げると切り分けが楽です。⚠要求の終わりはヌルバイトです。

import json, socket

payload = json.dumps({"type": "execute",
                      "code": "import bpy; print(bpy.app.version_string)",
                      "strict_json": False}).encode() + b"\x00"   # ★ヌルバイトが要る

with socket.create_connection(("localhost", 9876), timeout=30) as s:
    s.sendall(payload)
    buf = b""
    while b"\x00" not in buf:
        buf += s.recv(4096)
print(json.loads(buf.split(b"\x00", 1)[0]))

4. 任せる範囲を決める

ここからが今回の本題です。いきなり全部を任せないほうが結果が良くなります。

  • 調べさせる用途から始める(正解率が3倍以上違いました)
  • 繰り返す作業は道具にする。落とし穴を道具の中に閉じ込めれば、呼ぶ側は間違えようがありません
  • ★できたものを機械で確かめる。「エラーが出なかった」は「正しくできた」ではありません

つまずき集

症状原因と対処
★サーバーが起動せず Online access must be enabledlocalhostのソケットでもオンラインアクセスが要る。--online-mode を付ける。⚠ソースに「localhostは許してよいかもしれない、ここはグレーゾーン」という開発者のコメントがある
★★要求を送っても黙って何も返らない終端はヌルバイト。接続を閉じて送信終了を伝えると「切断」と見なされて閉じられる
BLENDER_EEVEE_NEXT が enum に無い⚠4.2で入った名前は5.xでは BLENDER_EEVEE に戻っている
エラー本文が空になるmessage の末尾は改行。最後の行ではなく空でない最後の行を取る
画素の色が絵と合わない⚠Blenderの image.pixels は下から上に並ぶ。上基準の座標は反転させる

まとめ

  • ★Blender財団が公式のMCPサーバーを出していた(Blender Lab・v1.0.0・Blender 5.1以上)。よく話題になるコミュニティ版とは別プロジェクト
  • 構造は3段。★アドオンがやっているのは「受け取ったPythonコードを exec する」だけで、道具の粒度は事実上1つしかない
  • ★★公式の weak_sandbox が実際に止めたのは10通り中4つ。ファイル削除も、ホームの列挙(85件)も、外部へのHTTP(200が返る)も通った。⚠止めている4つも安全のためではなく、Blenderのセッションが壊れないためだった
  • ★★同じ指示を30回。コードは30回とも通ったのに、意図どおりは4回(13%)だけ
  • ★★26回は material.diffuse_color に赤を入れていた。これはビューポートの表示色で、レンダリングすると灰色(R=G=B=0.56)。⚠作業画面では赤く見えるので、目視では気づけない
  • ★小さい3Bは10回とも実行時に落ちた。⚠それでも立方体の削除は10/10、球の作成は9/10まで進んでいた。「失敗した」は「何も起きていない」ではない
  • ★形の違う課題を6つ出すと、仕様どおりは3つ・惜しいが1つ・違うものが2つ。★「並べる・積む・繰り返す」は通り、「部品どうしの位置関係」で崩れた(机の脚、半円のアーチ)
  • ⚠★絵だけ見ると、今度は逆に間違えた。平らに見えた階段は、測ると0.25刻みで上がっていて仕様どおりだった。★目視は「できている」も「できていない」も外す
  • ★★★渡す道具を「意味のある3つ」に切り直したら 13% → 100%。生成時間も18.0秒→9.5秒。⚠モデルが賢くなったのではなく、任せる範囲を狭めたぶんだけ上がった
  • ★「調べさせる」は「作らせる」の3倍以上正解する(45% 対 13%)。そして順番が逆転する——作らせるとコードは通るが中身が間違い、調べさせるとコードは落ちやすいが通れば当たる。⚠落ちるほうが、静かに間違うよりマシ
  • ⚠自作のMCPサーバーでは失敗が isError = True で返ることを最初に確かめた(p992で踏んだ轍)。道具名は一般名を避ける(p949で衝突した)
  • ★Blenderを5.1.1から5.2.1 LTSに上げても結果は何も変わらなかった。p970のように10日差で逆転することもあるので、決めつけずに測り直した

「AIに3Dソフトを任せられるか」という問いに、今回いちばん近い答えは「任せる範囲を決めてから渡せば任せられる」でした。範囲を決めずに渡すと、エラーは出ないのに違うものができあがります。そしてそれは、画面を見ていても気づけません😳

それでは、今回はここまで。最後までありがとうございました😊

参考サイト