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
+14
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":