From 9f313dad87165df5c2a34a244e3adad5d2a66fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Sz=C5=91gy=C3=A9nyi?= Date: Thu, 2 Jul 2026 13:14:14 +0300 Subject: [PATCH] add: hermes plugin and update openclaw logo --- README.md | 2 +- ai-tools-logos/light/hermes.svg | 8 +- ai-tools-logos/light/openclaw.svg | 28 +- plugins/README.md | 2 +- plugins/hermes/typeui/MANIFEST.in | 4 + plugins/hermes/typeui/README.md | 91 +++++- plugins/hermes/typeui/__init__.py | 8 + plugins/hermes/typeui/plugin.yaml | 8 + plugins/hermes/typeui/pyproject.toml | 43 +++ .../hermes/typeui/typeui_hermes/__init__.py | 197 ++++++++++++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 7032 bytes .../__pycache__/schemas.cpython-313.pyc | Bin 0 -> 1157 bytes .../__pycache__/tools.cpython-313.pyc | Bin 0 -> 7199 bytes .../hermes/typeui/typeui_hermes/schemas.py | 40 +++ .../typeui_hermes/skills/typeui/SKILL.md | 51 ++++ plugins/hermes/typeui/typeui_hermes/tools.py | 287 ++++++++++++++++++ 16 files changed, 742 insertions(+), 27 deletions(-) create mode 100644 plugins/hermes/typeui/MANIFEST.in create mode 100644 plugins/hermes/typeui/__init__.py create mode 100644 plugins/hermes/typeui/plugin.yaml create mode 100644 plugins/hermes/typeui/pyproject.toml create mode 100644 plugins/hermes/typeui/typeui_hermes/__init__.py create mode 100644 plugins/hermes/typeui/typeui_hermes/__pycache__/__init__.cpython-313.pyc create mode 100644 plugins/hermes/typeui/typeui_hermes/__pycache__/schemas.cpython-313.pyc create mode 100644 plugins/hermes/typeui/typeui_hermes/__pycache__/tools.cpython-313.pyc create mode 100644 plugins/hermes/typeui/typeui_hermes/schemas.py create mode 100644 plugins/hermes/typeui/typeui_hermes/skills/typeui/SKILL.md create mode 100644 plugins/hermes/typeui/typeui_hermes/tools.py 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 0000000000000000000000000000000000000000..14aac3a3c8919437858cd496a1c02a3b5fe7b04c GIT binary patch literal 7032 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a8f5388b21a5a3d59bcd58c27e066082ced50438 GIT binary patch literal 1157 zcmah|O>Wab6t$Dl{IlD@s!DoH8W}IEVu$I z&VaZF2Y_Ai3W*gvSG@qwu~P+vge52M|9$U$&$o?6-GJxQuY-%Ll41NzmBmxZh1)|A zJ{qne4cBxnx8N4tl3TVdx8hdk|22^7@UGYeX}YU!17g+|-d*#~XN}g|_h!$#LNKKK zfQ+$1Lm`n425=^dd%BF99bkWEIE+i_Eac|twPmBx&DV?UgIIm#3SnUaK#7Bjyeqp;XRHFpasIiBUv& zM4fXf(&JE0pU9D-1L6#snD*f|oNOt!3T&TIC;Lv6lV4co%wxRVn15+@7{%2WV=NuiXcR3W9Y{=oWXY{({b F{|zT;dawWh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fa643322dac2de8c1b68ca841d90d7693f6b144e GIT binary patch literal 7199 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 literal 0 HcmV?d00001 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)})