diff --git a/README.md b/README.md
index 6483caa..04ed011 100644
--- a/README.md
+++ b/README.md
@@ -238,7 +238,7 @@ Install TypeUI where you build. Connect your AI coding tool to TypeUI MCP, then
Hermes
- Add the hosted TypeUI MCP server to Hermes Agent.
+ Install the TypeUI MCP plugin for Hermes Agent.
Open Hermes guide →
diff --git a/ai-tools-logos/light/hermes.svg b/ai-tools-logos/light/hermes.svg
index 8ade343..2521a10 100644
--- a/ai-tools-logos/light/hermes.svg
+++ b/ai-tools-logos/light/hermes.svg
@@ -1,7 +1 @@
-
- Hermes
- Hermes Agent mark for TypeUI integration listings.
-
-
-
-
+Hermes Agent
\ No newline at end of file
diff --git a/ai-tools-logos/light/openclaw.svg b/ai-tools-logos/light/openclaw.svg
index 6e6c37d..bcbc1e1 100644
--- a/ai-tools-logos/light/openclaw.svg
+++ b/ai-tools-logos/light/openclaw.svg
@@ -1,8 +1,22 @@
-
- OpenClaw
- OpenClaw mark for TypeUI integration listings.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/README.md b/plugins/README.md
index bc2813a..6147842 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -13,7 +13,7 @@ Tool-specific TypeUI plugins live in provider namespaces:
- `cline/typeui` for the Cline plugin and MCP setup helper.
- `opencode/typeui` for the OpenCode helper plugin and MCP configuration.
- `antigravity/typeui` for the Antigravity CLI plugin and MCP configuration.
-- `hermes/typeui` for the Hermes Agent MCP configuration guide.
+- `hermes/typeui` for the Hermes Agent plugin and MCP setup helper.
- `openclaw/typeui` for the OpenClaw MCP configuration guide.
- `zed/typeui` for the Zed MCP server extension package.
diff --git a/plugins/hermes/typeui/MANIFEST.in b/plugins/hermes/typeui/MANIFEST.in
new file mode 100644
index 0000000..ce0b2d4
--- /dev/null
+++ b/plugins/hermes/typeui/MANIFEST.in
@@ -0,0 +1,4 @@
+include README.md
+include plugin.yaml
+include config.yaml
+recursive-include typeui_hermes/skills *.md
diff --git a/plugins/hermes/typeui/README.md b/plugins/hermes/typeui/README.md
index e8bc390..a54ba3b 100644
--- a/plugins/hermes/typeui/README.md
+++ b/plugins/hermes/typeui/README.md
@@ -2,9 +2,55 @@
TypeUI connects Hermes Agent to the hosted TypeUI MCP server so it can use project design skills, brand kits, UI prompts, and layout variations while building interfaces.
-## Configure TypeUI MCP
+This folder is a Hermes plugin package. It includes:
-Merge this server into your Hermes configuration at `~/.hermes/config.yaml`:
+- `plugin.yaml` for Hermes directory-plugin discovery.
+- `typeui_hermes/` with the plugin registration, tool schemas, and MCP install handlers.
+- `pyproject.toml` with a `hermes_agent.plugins` entry point for pip installs.
+- `typeui_hermes/skills/typeui/SKILL.md` with TypeUI usage guidance for Hermes.
+- `config.yaml` with the Hermes MCP server definition for TypeUI.
+
+## Install as a Hermes directory plugin
+
+Copy this folder into Hermes' user plugin directory:
+
+```bash
+mkdir -p ~/.hermes/plugins/typeui
+cp -R plugins/hermes/typeui/. ~/.hermes/plugins/typeui/
+```
+
+Enable the plugin:
+
+```bash
+hermes plugins enable typeui
+```
+
+## Install as a pip entry-point plugin
+
+From this repository:
+
+```bash
+cd plugins/hermes/typeui
+pip install .
+hermes plugins enable typeui
+```
+
+The package exposes this Hermes entry point:
+
+```toml
+[project.entry-points."hermes_agent.plugins"]
+typeui = "typeui_hermes"
+```
+
+## Install TypeUI MCP
+
+After installing and enabling the plugin, run:
+
+```bash
+hermes typeui install-mcp
+```
+
+The plugin writes this server into your Hermes configuration at `~/.hermes/config.yaml`:
```yaml
mcp_servers:
@@ -15,19 +61,13 @@ mcp_servers:
This folder includes the same snippet in `config.yaml`.
-After updating the file, start or reload Hermes Agent:
-
-```bash
-hermes chat
-```
-
-Then run this command inside Hermes Agent:
+You can also ask a Hermes agent to call the `typeui_install_mcp` tool, or run the slash command inside a session:
```text
-/reload-mcp
+/typeui install-mcp
```
-If Hermes does not start the OAuth flow automatically, run:
+Start the TypeUI OAuth flow:
```bash
hermes mcp login typeui
@@ -35,6 +75,35 @@ hermes mcp login typeui
Sign in with TypeUI when Hermes asks you to authorize the connection.
+If Hermes Agent is already running, reload MCP servers inside the session:
+
+```text
+/reload-mcp
+```
+
+Check the saved server:
+
+```bash
+hermes typeui mcp-status
+```
+
+## Package for distribution
+
+Build a local wheel or source distribution from this folder:
+
+```bash
+cd plugins/hermes/typeui
+python -m build
+```
+
+Or create a ZIP artifact from `plugins/hermes/typeui` for users who want the directory-plugin install path.
+
+For plugin discovery debugging, Hermes supports:
+
+```bash
+HERMES_PLUGINS_DEBUG=1 hermes plugins list
+```
+
## Links
- Website: https://www.typeui.sh
diff --git a/plugins/hermes/typeui/__init__.py b/plugins/hermes/typeui/__init__.py
new file mode 100644
index 0000000..e68c7ab
--- /dev/null
+++ b/plugins/hermes/typeui/__init__.py
@@ -0,0 +1,8 @@
+"""Directory-plugin entrypoint for TypeUI Hermes integration."""
+
+try:
+ from .typeui_hermes import register
+except ImportError:
+ from typeui_hermes import register
+
+__all__ = ["register"]
diff --git a/plugins/hermes/typeui/plugin.yaml b/plugins/hermes/typeui/plugin.yaml
new file mode 100644
index 0000000..53e17da
--- /dev/null
+++ b/plugins/hermes/typeui/plugin.yaml
@@ -0,0 +1,8 @@
+name: typeui
+version: 1.0.0
+description: Install and inspect the hosted TypeUI MCP server for Hermes Agent.
+author: Bergside LLC
+homepage: https://www.typeui.sh/docs/guides/hermes
+provides_tools:
+ - typeui_install_mcp
+ - typeui_mcp_status
diff --git a/plugins/hermes/typeui/pyproject.toml b/plugins/hermes/typeui/pyproject.toml
new file mode 100644
index 0000000..f41488b
--- /dev/null
+++ b/plugins/hermes/typeui/pyproject.toml
@@ -0,0 +1,43 @@
+[build-system]
+requires = ["setuptools>=69", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "typeui-hermes"
+version = "1.0.0"
+description = "Hermes Agent plugin for installing and inspecting the TypeUI MCP server."
+readme = "README.md"
+requires-python = ">=3.10"
+license = { text = "MIT" }
+authors = [
+ { name = "Bergside LLC", email = "zoltan@bergside.com" }
+]
+keywords = ["hermes-agent", "typeui", "mcp", "design-skills", "frontend"]
+dependencies = [
+ "PyYAML>=6.0"
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Software Development :: User Interfaces",
+]
+
+[project.urls]
+Homepage = "https://www.typeui.sh"
+Documentation = "https://www.typeui.sh/docs/guides/hermes"
+Repository = "https://github.com/bergside/typeui"
+
+[project.entry-points."hermes_agent.plugins"]
+typeui = "typeui_hermes"
+
+[tool.setuptools.packages.find]
+include = ["typeui_hermes*"]
+
+[tool.setuptools.package-data]
+typeui_hermes = ["skills/typeui/SKILL.md"]
diff --git a/plugins/hermes/typeui/typeui_hermes/__init__.py b/plugins/hermes/typeui/typeui_hermes/__init__.py
new file mode 100644
index 0000000..51658c3
--- /dev/null
+++ b/plugins/hermes/typeui/typeui_hermes/__init__.py
@@ -0,0 +1,197 @@
+"""TypeUI plugin registration for Hermes Agent."""
+
+from __future__ import annotations
+
+import json
+import shlex
+from pathlib import Path
+
+from . import schemas, tools
+
+
+def _format_json_result(payload: str, *, status: bool = False) -> str:
+ try:
+ data = json.loads(payload)
+ except json.JSONDecodeError:
+ return payload
+
+ if data.get("error"):
+ return f"TypeUI MCP error: {data['error']}"
+
+ if status:
+ if data.get("configured"):
+ return "\n".join(
+ [
+ "TypeUI MCP is configured in Hermes.",
+ f"Config: {data.get('config_path')}",
+ "",
+ json.dumps(data.get("server", {}), indent=2),
+ ]
+ )
+
+ return "\n".join(
+ [
+ "TypeUI MCP is not configured in Hermes yet.",
+ f"Config: {data.get('config_path')}",
+ "",
+ "Install it with:",
+ "hermes typeui install-mcp",
+ ]
+ )
+
+ changed = bool(data.get("changed"))
+ first_line = (
+ "TypeUI MCP has been installed in Hermes config."
+ if changed
+ else "TypeUI MCP is already installed in Hermes config."
+ )
+ lines = [
+ first_line,
+ f"Config: {data.get('config_path')}",
+ ]
+
+ if data.get("backup_path"):
+ lines.append(f"Backup: {data['backup_path']}")
+
+ lines.extend(
+ [
+ "",
+ "Next, authorize TypeUI OAuth:",
+ "hermes mcp login typeui",
+ "",
+ "If Hermes is already running, reload MCP servers in the session:",
+ "/reload-mcp",
+ ]
+ )
+
+ return "\n".join(lines)
+
+
+def _cli_command(args) -> None:
+ subcommand = getattr(args, "typeui_command", None)
+ config_path = getattr(args, "config_path", None)
+
+ if subcommand == "install-mcp":
+ result = tools.install_mcp(
+ {
+ "force": bool(getattr(args, "force", False)),
+ "config_path": config_path,
+ }
+ )
+ print(_format_json_result(result))
+ return
+
+ if subcommand == "mcp-status":
+ result = tools.mcp_status({"config_path": config_path})
+ print(_format_json_result(result, status=True))
+ return
+
+ print("Usage: hermes typeui ")
+
+
+def _setup_argparse(subparser) -> None:
+ subcommands = subparser.add_subparsers(dest="typeui_command")
+
+ install = subcommands.add_parser(
+ "install-mcp",
+ help="Install the hosted TypeUI MCP server into Hermes config.",
+ )
+ install.add_argument(
+ "--force",
+ action="store_true",
+ help="Overwrite the existing typeui MCP server entry.",
+ )
+ install.add_argument(
+ "--config",
+ dest="config_path",
+ help="Path to Hermes config.yaml. Defaults to $HERMES_HOME/config.yaml or ~/.hermes/config.yaml.",
+ )
+
+ status = subcommands.add_parser(
+ "mcp-status",
+ help="Show whether TypeUI MCP is installed in Hermes config.",
+ )
+ status.add_argument(
+ "--config",
+ dest="config_path",
+ help="Path to Hermes config.yaml. Defaults to $HERMES_HOME/config.yaml or ~/.hermes/config.yaml.",
+ )
+
+ subparser.set_defaults(func=_cli_command)
+
+
+def _slash_command(raw_args: str) -> str:
+ try:
+ parts = shlex.split(raw_args or "")
+ except ValueError as exc:
+ return f"Invalid /typeui arguments: {exc}"
+
+ subcommand = parts[0] if parts else "mcp-status"
+ force = "--force" in parts
+
+ if subcommand in {"help", "-h", "--help"}:
+ return "\n".join(
+ [
+ "Usage:",
+ "/typeui install-mcp [--force]",
+ "/typeui mcp-status",
+ ]
+ )
+
+ if subcommand in {"install", "install-mcp"}:
+ result = tools.install_mcp({"force": force})
+ return _format_json_result(result)
+
+ if subcommand in {"status", "mcp-status"}:
+ result = tools.mcp_status({})
+ return _format_json_result(result, status=True)
+
+ return "Unknown /typeui command. Use /typeui help."
+
+
+def _register_skills(ctx) -> None:
+ if not hasattr(ctx, "register_skill"):
+ return
+
+ skills_dir = Path(__file__).parent / "skills"
+ if not skills_dir.exists():
+ return
+
+ for child in sorted(skills_dir.iterdir()):
+ skill_md = child / "SKILL.md"
+ if child.is_dir() and skill_md.exists():
+ ctx.register_skill(child.name, skill_md)
+
+
+def register(ctx) -> None:
+ """Register TypeUI tools, commands, and bundled skills with Hermes."""
+ ctx.register_tool(
+ name="typeui_install_mcp",
+ toolset="typeui",
+ schema=schemas.TYPEUI_INSTALL_MCP,
+ handler=tools.install_mcp,
+ )
+ ctx.register_tool(
+ name="typeui_mcp_status",
+ toolset="typeui",
+ schema=schemas.TYPEUI_MCP_STATUS,
+ handler=tools.mcp_status,
+ )
+
+ if hasattr(ctx, "register_cli_command"):
+ ctx.register_cli_command(
+ name="typeui",
+ help="Install and inspect TypeUI MCP for Hermes.",
+ setup_fn=_setup_argparse,
+ handler_fn=_cli_command,
+ )
+
+ if hasattr(ctx, "register_command"):
+ ctx.register_command(
+ "typeui",
+ handler=_slash_command,
+ description="Install and inspect TypeUI MCP.",
+ args_hint="install-mcp|mcp-status",
+ )
+
+ _register_skills(ctx)
diff --git a/plugins/hermes/typeui/typeui_hermes/__pycache__/__init__.cpython-313.pyc b/plugins/hermes/typeui/typeui_hermes/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..14aac3a
Binary files /dev/null and b/plugins/hermes/typeui/typeui_hermes/__pycache__/__init__.cpython-313.pyc differ
diff --git a/plugins/hermes/typeui/typeui_hermes/__pycache__/schemas.cpython-313.pyc b/plugins/hermes/typeui/typeui_hermes/__pycache__/schemas.cpython-313.pyc
new file mode 100644
index 0000000..a8f5388
Binary files /dev/null and b/plugins/hermes/typeui/typeui_hermes/__pycache__/schemas.cpython-313.pyc differ
diff --git a/plugins/hermes/typeui/typeui_hermes/__pycache__/tools.cpython-313.pyc b/plugins/hermes/typeui/typeui_hermes/__pycache__/tools.cpython-313.pyc
new file mode 100644
index 0000000..fa64332
Binary files /dev/null and b/plugins/hermes/typeui/typeui_hermes/__pycache__/tools.cpython-313.pyc differ
diff --git a/plugins/hermes/typeui/typeui_hermes/schemas.py b/plugins/hermes/typeui/typeui_hermes/schemas.py
new file mode 100644
index 0000000..87946ba
--- /dev/null
+++ b/plugins/hermes/typeui/typeui_hermes/schemas.py
@@ -0,0 +1,40 @@
+"""Tool schemas for the TypeUI Hermes plugin."""
+
+TYPEUI_INSTALL_MCP = {
+ "name": "typeui_install_mcp",
+ "description": (
+ "Install or repair the hosted TypeUI MCP server in Hermes Agent config. "
+ "Use when the user wants Hermes to connect to TypeUI, enable TypeUI design "
+ "skills, or add TypeUI MCP to ~/.hermes/config.yaml."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "force": {
+ "type": "boolean",
+ "description": "Overwrite the existing typeui MCP server entry if it is already configured.",
+ },
+ "config_path": {
+ "type": "string",
+ "description": "Optional path to Hermes config.yaml. Defaults to $HERMES_HOME/config.yaml or ~/.hermes/config.yaml.",
+ },
+ },
+ },
+}
+
+TYPEUI_MCP_STATUS = {
+ "name": "typeui_mcp_status",
+ "description": (
+ "Show whether the hosted TypeUI MCP server is configured in Hermes Agent. "
+ "Use before installing or when troubleshooting TypeUI MCP availability."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "config_path": {
+ "type": "string",
+ "description": "Optional path to Hermes config.yaml. Defaults to $HERMES_HOME/config.yaml or ~/.hermes/config.yaml.",
+ },
+ },
+ },
+}
diff --git a/plugins/hermes/typeui/typeui_hermes/skills/typeui/SKILL.md b/plugins/hermes/typeui/typeui_hermes/skills/typeui/SKILL.md
new file mode 100644
index 0000000..22710c1
--- /dev/null
+++ b/plugins/hermes/typeui/typeui_hermes/skills/typeui/SKILL.md
@@ -0,0 +1,51 @@
+---
+name: typeui
+description: Use TypeUI with Hermes Agent to access project design skills, brand kits, UI prompts, and layout variations through the TypeUI MCP server when creating or refining user interfaces.
+license: MIT
+metadata:
+ author: Bergside LLC
+ homepage: https://www.typeui.sh
+---
+
+# TypeUI for Hermes Agent
+
+Use this skill when the user wants Hermes Agent to build, redesign, or refine UI with TypeUI context.
+
+## What TypeUI Provides
+
+- Project design skills for consistent visual direction.
+- Brand kits with project name, logo, typography, and brand color tokens.
+- UI prompt libraries for common interface sections and full-product flows.
+- Layout variation guidance for producing better options before implementation.
+
+## How to Use TypeUI
+
+1. Prefer TypeUI MCP tools when they are available in the session.
+2. Ask the user which TypeUI project or design skill to use if the request does not specify one.
+3. Apply the selected project context before choosing colors, type, spacing, component tone, or page composition.
+4. Treat brand kit values as brand parameters only: logo, project name, typography, and brand colors.
+5. Treat the design skill as the source of UI style, component behavior, visual hierarchy, spacing, and interaction patterns.
+
+## MCP Server
+
+This plugin package installs the TypeUI remote MCP server:
+
+```text
+https://mcp.typeui.sh
+```
+
+If TypeUI tools are not available in the current Hermes session, install the MCP server with the plugin command:
+
+```bash
+hermes typeui install-mcp
+hermes mcp login typeui
+```
+
+Hermes agents can also call `typeui_install_mcp` to write the TypeUI MCP server entry.
+
+## Prompt Examples
+
+- Use my TypeUI project and build a landing page.
+- Generate three pricing section variations with TypeUI.
+- Redesign this dashboard using my TypeUI design skill.
+- Pull the TypeUI context for this project before changing the UI.
diff --git a/plugins/hermes/typeui/typeui_hermes/tools.py b/plugins/hermes/typeui/typeui_hermes/tools.py
new file mode 100644
index 0000000..94e9b40
--- /dev/null
+++ b/plugins/hermes/typeui/typeui_hermes/tools.py
@@ -0,0 +1,287 @@
+"""Tool handlers for installing TypeUI MCP in Hermes Agent."""
+
+from __future__ import annotations
+
+import json
+import os
+import shutil
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any
+
+try:
+ import yaml
+except ImportError: # pragma: no cover - Hermes ships YAML support.
+ yaml = None
+
+
+TYPEUI_MCP_URL = "https://mcp.typeui.sh"
+TYPEUI_MCP_SERVER = {
+ "url": TYPEUI_MCP_URL,
+ "auth": "oauth",
+}
+_RAW_CONFIG_TEXT = "__typeui_raw_config_text"
+
+
+def _json(data: dict[str, Any]) -> str:
+ return json.dumps(data, indent=2, sort_keys=True)
+
+
+def _default_config_path() -> Path:
+ hermes_home = os.environ.get("HERMES_HOME")
+ if hermes_home:
+ return Path(hermes_home).expanduser() / "config.yaml"
+ return Path.home() / ".hermes" / "config.yaml"
+
+
+def _resolve_config_path(value: Any) -> Path:
+ if isinstance(value, str) and value.strip():
+ return Path(value).expanduser()
+ return _default_config_path()
+
+
+def _unquote_yaml_scalar(value: str) -> str:
+ value = value.strip()
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
+ return value[1:-1]
+ return value
+
+
+def _load_config_without_yaml(path: Path) -> dict[str, Any]:
+ if not path.exists():
+ return {}
+
+ raw_text = path.read_text(encoding="utf-8")
+ config: dict[str, Any] = {_RAW_CONFIG_TEXT: raw_text}
+
+ if not raw_text.strip():
+ return config
+
+ lines = raw_text.splitlines()
+ mcp_index = None
+ for index, line in enumerate(lines):
+ if line.startswith("mcp_servers:"):
+ mcp_index = index
+ break
+
+ if mcp_index is None:
+ config["mcp_servers"] = {}
+ return config
+
+ typeui_server: dict[str, Any] | None = None
+ in_typeui = False
+ in_auth = False
+
+ for line in lines[mcp_index + 1 :]:
+ if not line.strip() or line.lstrip().startswith("#"):
+ continue
+
+ indent = len(line) - len(line.lstrip(" "))
+ stripped = line.strip()
+
+ if indent == 0:
+ break
+
+ if indent == 2 and stripped.startswith("typeui:"):
+ typeui_server = {}
+ in_typeui = True
+ in_auth = False
+ continue
+
+ if in_typeui and indent <= 2:
+ break
+
+ if not in_typeui or typeui_server is None:
+ continue
+
+ if indent == 4 and ":" in stripped:
+ key, value = stripped.split(":", 1)
+ key = key.strip()
+ value = value.split(" #", 1)[0].strip()
+
+ if key == "url":
+ typeui_server["url"] = _unquote_yaml_scalar(value)
+ in_auth = False
+ elif key == "auth" and value:
+ typeui_server["auth"] = _unquote_yaml_scalar(value)
+ in_auth = False
+ elif key == "auth":
+ typeui_server["auth"] = {}
+ in_auth = True
+ else:
+ in_auth = False
+ continue
+
+ if in_auth and indent == 6 and ":" in stripped:
+ key, value = stripped.split(":", 1)
+ if key.strip() == "type":
+ typeui_server["auth"]["type"] = _unquote_yaml_scalar(
+ value.split(" #", 1)[0].strip()
+ )
+
+ config["mcp_servers"] = {"typeui": typeui_server} if typeui_server else {}
+ return config
+
+
+def _load_config(path: Path) -> dict[str, Any]:
+ if yaml is None:
+ return _load_config_without_yaml(path)
+
+ if not path.exists():
+ return {}
+
+ with path.open("r", encoding="utf-8") as handle:
+ loaded = yaml.safe_load(handle) or {}
+
+ if not isinstance(loaded, dict):
+ raise ValueError(f"{path} must contain a YAML mapping at the top level.")
+
+ return loaded
+
+
+def _typeui_server_yaml() -> str:
+ return "\n".join(
+ [
+ "mcp_servers:",
+ " typeui:",
+ f' url: "{TYPEUI_MCP_URL}"',
+ " auth: oauth",
+ "",
+ ]
+ )
+
+
+def _write_config_without_yaml(path: Path, config: dict[str, Any]) -> str | None:
+ raw_text = config.get(_RAW_CONFIG_TEXT)
+ server = (config.get("mcp_servers") or {}).get("typeui")
+
+ if not _is_typeui_mcp_configured(server):
+ raise RuntimeError("Cannot write TypeUI MCP config without a valid typeui server entry.")
+
+ if raw_text and raw_text.strip():
+ if "\nmcp_servers:" in f"\n{raw_text}" and "typeui:" not in raw_text:
+ raise RuntimeError(
+ "PyYAML is required to update an existing Hermes config that already contains mcp_servers."
+ )
+ next_text = raw_text.rstrip() + "\n\n" + _typeui_server_yaml()
+ else:
+ next_text = _typeui_server_yaml()
+
+ path.parent.mkdir(parents=True, exist_ok=True)
+ backup_path = _backup_config(path)
+ path.write_text(next_text, encoding="utf-8")
+ return backup_path
+
+
+def _backup_config(path: Path) -> str | None:
+ if not path.exists():
+ return None
+
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
+ backup_path = path.with_name(f"{path.name}.typeui.{stamp}.bak")
+ shutil.copy2(path, backup_path)
+ return str(backup_path)
+
+
+def _write_config(path: Path, config: dict[str, Any]) -> str | None:
+ if yaml is None:
+ return _write_config_without_yaml(path, config)
+
+ path.parent.mkdir(parents=True, exist_ok=True)
+ backup_path = _backup_config(path)
+
+ config = {key: value for key, value in config.items() if key != _RAW_CONFIG_TEXT}
+
+ with path.open("w", encoding="utf-8") as handle:
+ yaml.safe_dump(config, handle, sort_keys=False, default_flow_style=False)
+
+ return backup_path
+
+
+def _has_oauth_auth(auth: Any) -> bool:
+ if auth == "oauth":
+ return True
+
+ if isinstance(auth, dict):
+ return auth.get("type") == "oauth"
+
+ return False
+
+
+def _is_typeui_mcp_configured(server: Any) -> bool:
+ return (
+ isinstance(server, dict)
+ and server.get("url") == TYPEUI_MCP_URL
+ and _has_oauth_auth(server.get("auth"))
+ )
+
+
+def _server_payload(current: Any) -> dict[str, Any]:
+ server = dict(current) if isinstance(current, dict) else {}
+ server.update(TYPEUI_MCP_SERVER)
+ return server
+
+
+def install_mcp(args: dict[str, Any], **kwargs: Any) -> str:
+ """Install TypeUI MCP into Hermes config.yaml."""
+ try:
+ config_path = _resolve_config_path(args.get("config_path"))
+ force = bool(args.get("force", False))
+ config = _load_config(config_path)
+
+ mcp_servers = config.get("mcp_servers")
+ if mcp_servers is None:
+ mcp_servers = {}
+ config["mcp_servers"] = mcp_servers
+ elif not isinstance(mcp_servers, dict):
+ raise ValueError("mcp_servers must be a YAML mapping.")
+
+ current = mcp_servers.get("typeui")
+ was_configured = _is_typeui_mcp_configured(current)
+ changed = False
+ backup_path = None
+
+ if force or not was_configured:
+ mcp_servers["typeui"] = _server_payload(current)
+ backup_path = _write_config(config_path, config)
+ changed = True
+
+ return _json(
+ {
+ "changed": changed,
+ "was_configured": was_configured,
+ "config_path": str(config_path),
+ "backup_path": backup_path,
+ "server": mcp_servers.get("typeui"),
+ "next_steps": [
+ "hermes mcp login typeui",
+ "Restart Hermes or run /reload-mcp in an active Hermes session.",
+ ],
+ }
+ )
+ except Exception as exc:
+ return _json({"error": str(exc)})
+
+
+def mcp_status(args: dict[str, Any], **kwargs: Any) -> str:
+ """Return TypeUI MCP status for Hermes config.yaml."""
+ try:
+ config_path = _resolve_config_path(args.get("config_path"))
+ config = _load_config(config_path)
+ mcp_servers = config.get("mcp_servers") or {}
+
+ if not isinstance(mcp_servers, dict):
+ raise ValueError("mcp_servers must be a YAML mapping.")
+
+ server = mcp_servers.get("typeui")
+
+ return _json(
+ {
+ "configured": _is_typeui_mcp_configured(server),
+ "config_path": str(config_path),
+ "server": server,
+ "expected": TYPEUI_MCP_SERVER,
+ }
+ )
+ except Exception as exc:
+ return _json({"error": str(exc)})