add: hermes plugin and update openclaw logo

This commit is contained in:
Zoltán Szőgyényi
2026-07-02 13:14:14 +03:00
parent 2e49119f8d
commit 9f313dad87
16 changed files with 742 additions and 27 deletions
+1 -1
View File
@@ -238,7 +238,7 @@ Install TypeUI where you build. Connect your AI coding tool to TypeUI MCP, then
<b>Hermes</b>
</a>
<br />
<sub>Add the hosted TypeUI MCP server to Hermes Agent.</sub>
<sub>Install the TypeUI MCP plugin for Hermes Agent.</sub>
<br /><br />
<a href="https://www.typeui.sh/docs/guides/hermes"><b>Open Hermes guide →</b></a>
</td>
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 446 B

After

Width:  |  Height:  |  Size: 19 KiB

+21 -7
View File
@@ -1,8 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" role="img" aria-labelledby="title desc">
<title id="title">OpenClaw</title>
<desc id="desc">OpenClaw mark for TypeUI integration listings.</desc>
<rect width="48" height="48" rx="12" fill="#FF5A36"/>
<path fill="#fff" d="M24 12c-7.18 0-13 5.37-13 12s5.82 12 13 12 13-5.37 13-12-5.82-12-13-12Zm0 5c4.31 0 8 3.1 8 7s-3.69 7-8 7-8-3.1-8-7 3.69-7 8-7Z"/>
<path fill="#fff" d="M24 20a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z"/>
<path fill="#fff" fill-opacity=".72" d="M9 15h6l-3 4-3-4Zm24 0h6l-3 4-3-4ZM9 33l3-4 3 4H9Zm24 0 3-4 3 4h-6Z"/>
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff4d4d"/>
<stop offset="100%" stop-color="#991b1b"/>
</linearGradient>
</defs>
<!-- Body -->
<path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#lobster-gradient)"/>
<!-- Left Claw -->
<path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#lobster-gradient)"/>
<!-- Right Claw -->
<path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#lobster-gradient)"/>
<!-- Antenna -->
<path d="M45 15 Q35 5 30 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
<path d="M75 15 Q85 5 90 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
<!-- Eyes -->
<circle cx="45" cy="35" r="6" fill="#050810"/>
<circle cx="75" cy="35" r="6" fill="#050810"/>
<circle cx="46" cy="34" r="2.5" fill="#00e5cc"/>
<circle cx="76" cy="34" r="2.5" fill="#00e5cc"/>
</svg>

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 1.2 KiB

+1 -1
View File
@@ -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.
+4
View File
@@ -0,0 +1,4 @@
include README.md
include plugin.yaml
include config.yaml
recursive-include typeui_hermes/skills *.md
+80 -11
View File
@@ -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
+8
View File
@@ -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"]
+8
View File
@@ -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
+43
View File
@@ -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"]
@@ -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 <install-mcp|mcp-status>")
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)
@@ -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.",
},
},
},
}
@@ -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.
@@ -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)})