feat(mcp): add Blender to the MCP catalog with a curated 4-tool default

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.
This commit is contained in:
Teknium 2026-07-14 07:18:41 -07:00
parent 7af59f474d
commit 9be941dac1
3 changed files with 123 additions and 0 deletions

View file

@ -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":

View file

@ -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.

View file

@ -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