"""Fleet startup hook for the mcp-for-blender add-on (runs at every Blender launch, from the user scripts/startup dir). Part of eshpfi-management stacks/blender; see its README. The add-on (scripts/addons/blender_mcp.py, pinned upstream commit 41a18432) serves a socket that runs arbitrary Python with NO authentication. The MCP server that talks to it runs INSIDE this same container (/work/.mcp-venv), so the socket stays on the container's localhost and no port is published. That same-container arrangement is also what makes viewport screenshots work: the MCP server reads the image file Blender writes, which needs a shared filesystem. This hook exists because the add-on is not listed in any saved user preferences. It enables the add-on after the prefs have loaded, and makes sure its server is running. Blender's stdout goes to /dev/null in this image, so every step is logged to /config/.local/state/fleet_mcp.log. """ import os import time import traceback import addon_utils import bpy MODULE, HOST, PORT = "blender_mcp", "localhost", 9876 LOG = "/config/.local/state/fleet_mcp.log" def log(msg): os.makedirs(os.path.dirname(LOG), exist_ok=True) with open(LOG, "a") as f: f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {msg}\n") def _ensure_serving(): """Timer: once Blender is fully up, enable the add-on and make sure its server is running on HOST:PORT. Retries every second until it is. ⚠ The add-on is enabled HERE, not in register(). Startup scripts run before Blender loads the user preferences, and loading them disables any add-on the prefs do not list: an early enable was undone, taking the add-on's Scene properties with it, while the server object kept answering "'Scene' object has no attribute 'blendermcp_use_polyhaven'" (found 2026-09-27).""" try: server = getattr(bpy.types, "blendermcp_server", None) if not addon_utils.check(MODULE)[1]: import blender_mcp # our object first, so the add-on's auto-start adopts it if server is None: bpy.types.blendermcp_server = blender_mcp.BlenderMCPServer(host=HOST, port=PORT) addon_utils.enable(MODULE, default_set=True) log(f"add-on enabled: {MODULE}") return 1.0 import blender_mcp if server is not None and server.host != HOST: if server.running: server.stop() server = None if server is None: server = blender_mcp.BlenderMCPServer(host=HOST, port=PORT) bpy.types.blendermcp_server = server if not server.running: server.start() if server.running and hasattr(bpy.context.scene, "blendermcp_use_polyhaven"): log(f"serving on {server.host}:{server.port}") _gpu_by_default() return None log("not serving yet; retrying") return 1.0 except Exception: log("ensure_serving failed:\n" + traceback.format_exc()) return 2.0 def _gpu_by_default(): """Point Cycles at GPU 3 through OptiX. Safe mode forbids agent code from touching bpy.context.preferences, so a model cannot select the GPU itself: "'preferences' reached through an unresolvable receiver" (found 2026-09-27). It CAN set scene.cycles.device, so the current scene is set to GPU here too, and agents set it on scenes they create.""" try: prefs = bpy.context.preferences.addons["cycles"].preferences prefs.compute_device_type = "OPTIX" prefs.refresh_devices() for d in prefs.devices: d.use = d.type == "OPTIX" bpy.context.scene.cycles.device = "GPU" log("cycles: OPTIX on " + ", ".join(d.name for d in prefs.devices if d.use)) except Exception: log("cycles GPU default failed:\n" + traceback.format_exc()) def register(): if bpy.app.background: # headless renders never need the socket return bpy.app.timers.register(_ensure_serving, first_interval=2.0, persistent=True) def unregister(): pass