From 9be941dac1f8fad51a0320202d73fec423fbf3e4 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 14 Jul 2026 07:18:41 -0700 Subject: [PATCH] feat(mcp): add Blender to the MCP catalog with a curated 4-tool default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds optional-mcps/blender (ahujasid/blender-mcp, stdio via uvx). The server advertises 22 tools; 18 front optional asset services with no upstream trim mechanism, so tools.default_enabled pins the install to the core surface (scene/object info, viewport screenshot, code exec) and the rest stay opt-in through 'hermes mcp configure blender'. Manifests can now declare transport.env (static, non-secret subprocess env vars), parsed/validated in _parse_manifest and written by _build_server_config — used here to ship DISABLE_TELEMETRY=true per the no-telemetry-without-opt-in policy. Runtime already honored per-server env; manifests just couldn't declare it. --- hermes_cli/mcp_catalog.py | 14 +++++ optional-mcps/blender/manifest.yaml | 83 ++++++++++++++++++++++++++++ tests/hermes_cli/test_mcp_catalog.py | 26 +++++++++ 3 files changed, 123 insertions(+) create mode 100644 optional-mcps/blender/manifest.yaml diff --git a/hermes_cli/mcp_catalog.py b/hermes_cli/mcp_catalog.py index aab35394964a..a75a0d2ca6e9 100644 --- a/hermes_cli/mcp_catalog.py +++ b/hermes_cli/mcp_catalog.py @@ -77,6 +77,10 @@ class TransportSpec: args: List[str] = field(default_factory=list) url: Optional[str] = None version: Optional[str] = None # informational, pinned + # Static environment variables for the stdio subprocess (e.g. telemetry + # opt-outs, mode flags). NOT for secrets — credentials go through + # auth.env so they are prompted for and land in ~/.hermes/.env. + env: Dict[str, str] = field(default_factory=dict) @dataclass @@ -184,12 +188,20 @@ def _parse_manifest(path: Path) -> CatalogEntry: args = transport_raw.get("args") or [] if not isinstance(args, list): raise CatalogError(f"{path}: transport.args must be a list") + env_raw = transport_raw.get("env") or {} + if not isinstance(env_raw, dict) or not all( + isinstance(k, str) and isinstance(v, str) for k, v in env_raw.items() + ): + raise CatalogError( + f"{path}: transport.env must be a mapping of string to string" + ) transport = TransportSpec( type=t_type, command=transport_raw.get("command"), args=[str(a) for a in args], url=transport_raw.get("url"), version=transport_raw.get("version"), + env=dict(env_raw), ) if t_type == "stdio" and not transport.command: raise CatalogError(f"{path}: stdio transport requires 'command'") @@ -468,6 +480,8 @@ def _build_server_config( cfg["command"] = _expand_install_dir(t.command or "", install_dir) if t.args: cfg["args"] = [_expand_install_dir(a, install_dir) for a in t.args] + if t.env: + cfg["env"] = dict(t.env) elif t.type == "http": cfg["url"] = t.url if entry.auth.type == "oauth": diff --git a/optional-mcps/blender/manifest.yaml b/optional-mcps/blender/manifest.yaml new file mode 100644 index 000000000000..e897a0778b5a --- /dev/null +++ b/optional-mcps/blender/manifest.yaml @@ -0,0 +1,83 @@ +# Nous-approved MCP catalog entry. +# Presence in this directory = approval. Merged via PR review. +manifest_version: 1 + +name: blender +description: Drive a live Blender session — modeling, scenes, and renders. +source: https://github.com/ahujasid/blender-mcp + +# ahujasid/blender-mcp (MIT, PyPI: blender-mcp) is the de-facto standard +# Blender MCP. It is a bridge with two halves: +# 1. This stdio server (uvx blender-mcp), which Hermes launches. +# 2. An addon (addon.py) running inside Blender that opens a local command +# socket on 127.0.0.1:9876. The stdio server relays to it. +# There is nothing to git-clone on the Hermes side — uvx resolves the package — +# so no install block. The in-Blender addon setup is covered in post_install. +# +# Why not Blender's official MCP (blender.org/lab): it requires Blender 5.1+ +# and targets scene analysis/documentation rather than asset creation. This +# server works across Blender 3.x/4.x and exposes full modeling control. +transport: + type: stdio + command: "uvx" + args: + - "blender-mcp" + env: + # Upstream ships anonymous telemetry, on by default. Hermes policy is + # no outbound telemetry without explicit opt-in, so the catalog entry + # disables it. Remove this line only if you deliberately want it on. + DISABLE_TELEMETRY: "true" + +# The Blender addon binds 127.0.0.1 only and has no auth of its own; the +# stdio server needs no credentials. Optional integrations (Sketchfab, +# Hyper3D Rodin, Hunyuan3D) take API keys entered in the addon's N-panel +# inside Blender, not via Hermes env. +auth: + type: none + +# Tool selection at install time: +# The server advertises 22 tools. 18 of them front optional third-party asset +# services (PolyHaven, Sketchfab, Hyper3D Rodin, Hunyuan3D) that are dead +# weight unless the matching integration is enabled in the addon panel — and +# upstream has no server-side trim mechanism, so without a filter every one +# of them costs schema tokens on every API call. Default to the core surface: +# scene/object inspection, viewport screenshots, and code execution, which is +# the complete modeling/render capability. Users who enable an asset service +# in the addon can opt into its tools with `hermes mcp configure blender`. +tools: + default_enabled: + - get_scene_info + - get_object_info + - get_viewport_screenshot + - execute_blender_code + +post_install: | + This entry launches the bridge server, but the bridge talks to an addon + INSIDE a running Blender (3.0+). One-time setup: + + 1. Download addon.py from https://github.com/ahujasid/blender-mcp + (raw file: https://raw.githubusercontent.com/ahujasid/blender-mcp/main/addon.py) + 2. Blender > Edit > Preferences > Add-ons > Install... > select addon.py, + then enable "Interface: Blender MCP". + 3. In the 3D viewport press N, open the "BlenderMCP" tab, click + "Connect to Claude" (starts the local socket on 127.0.0.1:9876). + + Blender must be RUNNING with the addon connected before the tools work — + start Blender first, then your Hermes session. The addon refuses to start + under `blender -b` (background mode): its command queue runs on UI timers. + On a machine without a display, run Blender under a virtual one: + xvfb-run blender (or Xvfb :99 & DISPLAY=:99 blender) + GPU rendering (Cycles/OptiX) works fine under Xvfb. + + SECURITY: execute_blender_code runs arbitrary Python inside Blender with no + sandbox — same trust level as the terminal tool. Upstream telemetry is + disabled via DISABLE_TELEMETRY in this entry's env block. + + The 18 asset-service tools (PolyHaven/Sketchfab/Hyper3D/Hunyuan3D) are off + by default. To use one: enable the service in the addon's BlenderMCP panel + (API key there if required), then `hermes mcp configure blender` and tick + its tools. PolyHaven is free and keyless; the others need accounts. + + If Hermes and Blender run on different machines, note that file paths in + execute_blender_code (texture loads, exports) resolve on the BLENDER host's + filesystem, not where Hermes runs. diff --git a/tests/hermes_cli/test_mcp_catalog.py b/tests/hermes_cli/test_mcp_catalog.py index b86cb5ea14e2..a1679e38f5b9 100644 --- a/tests/hermes_cli/test_mcp_catalog.py +++ b/tests/hermes_cli/test_mcp_catalog.py @@ -197,6 +197,32 @@ class TestManifestParsing: assert get_entry("official/demo") is not None assert get_entry("missing") is None + def test_transport_env_parsed_and_written_to_server_config(self, catalog_dir): + body = _basic_manifest() + body["transport"]["env"] = {"DISABLE_TELEMETRY": "true"} + _write_manifest(catalog_dir, "demo", body) + from hermes_cli.mcp_catalog import _build_server_config + + e = _entry("demo") + assert e.transport.env == {"DISABLE_TELEMETRY": "true"} + cfg = _build_server_config(e, None) + assert cfg["env"] == {"DISABLE_TELEMETRY": "true"} + + def test_transport_env_absent_leaves_config_without_env_key(self, catalog_dir): + _write_manifest(catalog_dir, "demo", _basic_manifest()) + from hermes_cli.mcp_catalog import _build_server_config + + cfg = _build_server_config(_entry("demo"), None) + assert "env" not in cfg + + def test_transport_env_bad_shape_rejected(self, catalog_dir): + body = _basic_manifest() + body["transport"]["env"] = ["DISABLE_TELEMETRY=true"] # list, not mapping + _write_manifest(catalog_dir, "demo", body) + from hermes_cli.mcp_catalog import list_catalog + + assert list_catalog() == [] + # --------------------------------------------------------------------------- # Install flow