feat(blender): agent control via mcp-for-blender (in-container, ssh stdio)

The MCP server (mcp-for-blender 2.1.1, frozen requirements) runs inside the Blender
container. Its add-on is vendored at upstream 41a18432 (MIT) and started by a
startup hook. scripts/blender-mcp carries the stdio over ssh + docker exec, so the
add-on socket, which runs arbitrary Python with no auth, stays on the container's
localhost with no published port. It also runs there because viewport screenshots
need a filesystem shared by server and Blender. Telemetry is off and safe mode is
on. The hook also defaults Cycles to OptiX on GPU 3, because safe mode forbids
agents from touching preferences.

Verified end to end from nh3-dev: 36 tools; a GPU render of an agent-built scene;
a viewport screenshot; and safe mode refusing 'import os'. Blender left down
(on demand).
This commit is contained in:
vh
2026-09-27 14:34:07 -07:00
parent 7ae7193c21
commit ac1cd29afa
8 changed files with 6442 additions and 6 deletions
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# blender-mcp — the agent side of fv-ml1's Blender (stacks/blender). Prime, 2026-09-27.
#
# scripts/blender-mcp serve MCP over stdio (what an MCP client launches)
# scripts/blender-mcp up|down start / stop the Blender container (GPU 3 is borrowed: on demand)
# scripts/blender-mcp status is the container up, and does the add-on answer a ping?
# scripts/blender-mcp setup (re)build the in-container MCP venv from the pinned requirements
#
# Shape: the MCP server (mcp-for-blender 2.1.1, pinned in stacks/blender/conf/mcp-requirements.txt)
# runs INSIDE the Blender container, and this script carries its stdio over
# `ssh … docker exec -i`. Why it runs there rather than here:
# - The add-on socket executes arbitrary Python with NO auth. Beside the server it stays on the
# container's localhost, with no port published anywhere. Access = ssh to fv-ml1 + docker.
# - get_viewport_screenshot has Blender write a file that the MCP server then reads, so the two
# must share a filesystem. With the server on nh3-dev it failed: "Screenshot file was not created".
# Telemetry is off. SAFE MODE is on: model-written code must clear upstream's AST allowlist before it
# reaches Blender. It guards against prompt injection from third-party asset text; it is not a sandbox.
#
# ⚠ `serve` does NOT start Blender. An MCP client launches this at session start, and auto-starting
# would put Blender on the reserve card for every session. If Blender is down, `serve` exits with
# a message: run `up`, wait for `status` to answer, then reconnect the MCP server.
set -euo pipefail
HOST=${BLENDER_SSH_HOST:-infra-ops@10.251.50.54}
COMPOSE_DIR=/opt/docker/compose/blender
VENV=/work/.mcp-venv
REQS=/opt/docker/conf/blender/mcp-requirements.txt
SSH=(ssh -o BatchMode=yes -o ConnectTimeout=10 "$HOST")
# ⚠ -n on every ssh except the serving one: without it, this pre-flight ssh swallows the MCP
# client's `initialize` from stdin, and the session hangs at init (found 2026-09-27).
running() { [ "$(ssh -n -o BatchMode=yes -o ConnectTimeout=10 "$HOST" "docker inspect -f '{{.State.Running}}' blender 2>/dev/null")" = true ]; }
case "${1:-serve}" in
up)
"${SSH[@]}" "cd $COMPOSE_DIR && docker compose up -d" >&2
echo "blender: starting; the add-on answers once the desktop has loaded (~20-40 s). Check: $0 status" >&2 ;;
down)
"${SSH[@]}" "cd $COMPOSE_DIR && docker compose down" >&2 ;;
status)
# ⚠ Not a TCP probe: a port can accept while Blender is still loading. Ask the add-on to ping.
"${SSH[@]}" "docker ps -a --filter name=^blender\$ --format 'container: {{.Status}}'; \
docker exec -u abc blender python3 -c '
import json, socket
try:
s = socket.create_connection((\"localhost\", 9876), 3); s.settimeout(5)
s.sendall(json.dumps({\"type\": \"ping\"}).encode())
ok = json.loads(s.recv(4096)).get(\"status\") == \"success\"
except Exception:
ok = False
print(\"mcp add-on: answering\" if ok else \"mcp add-on: not answering\")
' 2>/dev/null || echo 'mcp add-on: container not running'" ;;
setup)
running || { echo "blender-mcp: container is down; run '$0 up' first" >&2; exit 1; }
"${SSH[@]}" "docker exec -u abc blender sh -c 'rm -rf $VENV.new && python3 -m venv $VENV.new \
&& $VENV.new/bin/pip install -q -r $REQS && rm -rf $VENV && mv $VENV.new $VENV && echo setup: ok'" ;;
serve)
if ! running; then
echo "blender-mcp: Blender is not running on fv-ml1 (GPU 3 is on-demand). Run '$0 up', wait for '$0 status', then reconnect this MCP server." >&2
exit 1
fi
exec "${SSH[@]}" docker exec -i -u abc \
-e DISABLE_TELEMETRY=true -e BLENDER_MCP_SAFE_MODE=1 \
-e BLENDER_HOST=localhost -e BLENDER_PORT=9876 \
blender "$VENV/bin/mcp-for-blender" ;;
*)
echo "usage: $0 [serve|up|down|status|setup]" >&2; exit 2 ;;
esac