mirror of
https://github.com/bergside/typeui.git
synced 2026-09-19 07:24:37 +08:00
add: hermes plugin and update openclaw logo
This commit is contained in:
@@ -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 |
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
include README.md
|
||||
include plugin.yaml
|
||||
include config.yaml
|
||||
recursive-include typeui_hermes/skills *.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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)})
|
||||
Reference in New Issue
Block a user