#!/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