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)})