feat: MCP server management and skills commands from chat, future-changes doc with UI/skills/MCP plans
- mcp list/add/remove commands to manage MCP servers from any channel - skill list/show/create/remove/reload commands for skill management - skill create generates a SKILL.md template, or ask the AI naturally - Updated help text, commands.md with new commands - future-changes.md: WebChat UI overhaul plan (OpenClaw-inspired), MCP management UI, skill creator system, ClawHub/GitHub skill import plans
This commit is contained in:
@@ -79,6 +79,35 @@ After `config set`, run `reload` to apply changes that don't auto-apply (adapter
|
|||||||
| `cron list` | List all scheduled jobs |
|
| `cron list` | List all scheduled jobs |
|
||||||
| `cron remove <id>` | Remove a scheduled job by ID |
|
| `cron remove <id>` | Remove a scheduled job by ID |
|
||||||
|
|
||||||
|
### MCP Servers
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `mcp list` | List configured MCP servers |
|
||||||
|
| `mcp add <name> <command> [args]` | Add a new MCP server |
|
||||||
|
| `mcp remove <name>` | Remove an MCP server |
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp add brave-search uvx brave-search-mcp@latest
|
||||||
|
mcp add filesystem npx @anthropic-ai/mcp-filesystem /home/user/projects
|
||||||
|
mcp add github uvx github-mcp-server
|
||||||
|
mcp remove brave-search
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skills
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `skill list` | List all loaded skills |
|
||||||
|
| `skill show <name>` | View a skill's content |
|
||||||
|
| `skill create <name>` | Create a new skill template |
|
||||||
|
| `skill remove <name>` | Remove a skill |
|
||||||
|
| `skill reload` | Reload all skills |
|
||||||
|
|
||||||
|
You can also create skills via natural language — just ask: "Create a skill for checking stock prices" and the AI will write the SKILL.md for you.
|
||||||
|
|
||||||
### AI Chat
|
### AI Chat
|
||||||
|
|
||||||
Any message that isn't a command is sent to the AI. The AI can also trigger actions by including tags in its response:
|
Any message that isn't a command is sent to the AI. The AI can also trigger actions by including tags in its response:
|
||||||
|
|||||||
@@ -188,6 +188,102 @@ Key files to study in `inspirations/nanoclaw/`:
|
|||||||
|
|
||||||
## Other Planned Changes
|
## Other Planned Changes
|
||||||
|
|
||||||
|
### WebChat UI Overhaul
|
||||||
|
|
||||||
|
The current chat.html is a minimal single-file UI. Upgrade to match OpenClaw's design language:
|
||||||
|
|
||||||
|
**Design elements from OpenClaw to adopt:**
|
||||||
|
- Color palette: `--bg: #12141a`, `--card: #181b22`, `--accent: #ff5c5c` (or Aetheel's own brand color)
|
||||||
|
- Typography: Space Grotesk for body, JetBrains Mono for code
|
||||||
|
- Chat bubbles: user messages right-aligned with accent background, AI messages left-aligned with card background
|
||||||
|
- Streaming indicator: pulsing border on AI bubble while generating
|
||||||
|
- Tool cards: collapsible cards showing tool usage (web search, file ops, etc.)
|
||||||
|
- Compose area: multi-line textarea with send button, sticky at bottom
|
||||||
|
- Status bar: connection status, current engine/model display
|
||||||
|
|
||||||
|
**New panels (sidebar or tabs):**
|
||||||
|
- Settings panel: engine/model/provider switching, timeout config
|
||||||
|
- MCP Servers panel: add/remove/enable MCP servers
|
||||||
|
- Skills panel: view loaded skills, create new ones
|
||||||
|
- Usage panel: cost tracking, request history, rate limit status
|
||||||
|
- Sessions panel: active sessions, session history
|
||||||
|
|
||||||
|
### MCP Server Management
|
||||||
|
|
||||||
|
Add the ability to manage MCP servers from chat, CLI, and the WebChat UI.
|
||||||
|
|
||||||
|
**Chat commands:**
|
||||||
|
```
|
||||||
|
mcp list # List configured MCP servers
|
||||||
|
mcp add <name> <command> [args...] # Add a new MCP server
|
||||||
|
mcp remove <name> # Remove an MCP server
|
||||||
|
mcp enable <name> # Enable a disabled server
|
||||||
|
mcp disable <name> # Disable without removing
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
mcp add brave-search uvx brave-search-mcp@latest
|
||||||
|
mcp add filesystem npx @anthropic-ai/mcp-filesystem /home/user/projects
|
||||||
|
mcp add github uvx github-mcp-server
|
||||||
|
mcp remove brave-search
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- Chat commands write to `config.json` → `mcp.servers` section
|
||||||
|
- `write_mcp_config()` already generates `.mcp.json` (Claude) or `opencode.json` (OpenCode)
|
||||||
|
- After adding/removing, auto-call `write_mcp_config()` and notify user to reload
|
||||||
|
- WebChat UI: form with name, command, args fields + env vars
|
||||||
|
- CLI: `aetheel mcp add/remove/list` subcommands
|
||||||
|
|
||||||
|
**Popular MCP servers to suggest during setup:**
|
||||||
|
- `brave-search` — Web search
|
||||||
|
- `filesystem` — File system access
|
||||||
|
- `github` — GitHub API
|
||||||
|
- `postgres` / `sqlite` — Database access
|
||||||
|
- `puppeteer` — Browser automation
|
||||||
|
- `memory` — Persistent memory server
|
||||||
|
|
||||||
|
### Skills System Enhancement
|
||||||
|
|
||||||
|
#### Skill Creator (via chat)
|
||||||
|
|
||||||
|
The AI should be able to create new skills when asked. This works by having the AI write a `SKILL.md` file to the workspace.
|
||||||
|
|
||||||
|
**How it works today:**
|
||||||
|
- The system prompt already tells the AI: "You can create new skills by writing SKILL.md files to ~/.aetheel/workspace/skills/<name>/SKILL.md"
|
||||||
|
- The AI has file write access via its runtime tools (Bash, Write, Edit)
|
||||||
|
- So skill creation via natural language already works — just ask: "Create a skill for checking weather"
|
||||||
|
|
||||||
|
**What to add:**
|
||||||
|
- A `skill` chat command for explicit management:
|
||||||
|
```
|
||||||
|
skill list # List all loaded skills
|
||||||
|
skill create <name> # Interactive skill creation wizard
|
||||||
|
skill remove <name> # Remove a skill
|
||||||
|
skill reload # Reload all skills (same as /reload)
|
||||||
|
skill show <name> # Show a skill's SKILL.md content
|
||||||
|
```
|
||||||
|
- Skill templates: pre-built SKILL.md templates for common use cases
|
||||||
|
- Skill import from URL: `skill import https://github.com/user/repo/SKILL.md`
|
||||||
|
|
||||||
|
#### ClawhHub / Community Skills
|
||||||
|
|
||||||
|
Integration with external skill sources:
|
||||||
|
|
||||||
|
- **ClawHub.ai**: If they provide an API or registry, add `skill search <query>` and `skill install <name>` commands
|
||||||
|
- **GitHub Anthropic skills**: Import from `github.com/anthropics/claude-code/tree/main/skills/`
|
||||||
|
- **User-defined skill repos**: `skill import <git-url>` clones a repo's skills into the workspace
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
```
|
||||||
|
skill search weather # Search ClawHub/GitHub for skills
|
||||||
|
skill install clawhub/weather # Install from ClawHub
|
||||||
|
skill import https://github.com/user/skills-repo # Import from git
|
||||||
|
```
|
||||||
|
|
||||||
|
Each imported skill gets its own folder under `~/.aetheel/workspace/skills/<name>/` with a `SKILL.md` and optionally a `handler.py`.
|
||||||
|
|
||||||
### Security Fixes (from security-audit.md)
|
### Security Fixes (from security-audit.md)
|
||||||
|
|
||||||
- Path containment check in `memory/manager.py` `read_file()`
|
- Path containment check in `memory/manager.py` `read_file()`
|
||||||
|
|||||||
215
main.py
215
main.py
@@ -242,6 +242,12 @@ def ai_handler(msg: IncomingMessage) -> str:
|
|||||||
if cmd.startswith("usage"):
|
if cmd.startswith("usage"):
|
||||||
return _handle_usage_command()
|
return _handle_usage_command()
|
||||||
|
|
||||||
|
if cmd.startswith("mcp"):
|
||||||
|
return _handle_mcp_command(msg.text.strip().lstrip("/"))
|
||||||
|
|
||||||
|
if cmd.startswith("skill"):
|
||||||
|
return _handle_skill_command(msg.text.strip().lstrip("/"))
|
||||||
|
|
||||||
# Build context from memory + skills
|
# Build context from memory + skills
|
||||||
context = _build_context(msg)
|
context = _build_context(msg)
|
||||||
|
|
||||||
@@ -894,10 +900,206 @@ def _handle_usage_command() -> str:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Scheduler Callback
|
# MCP Server Management Commands
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_mcp_command(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Handle mcp commands — manage MCP servers from chat.
|
||||||
|
|
||||||
|
mcp list List configured servers
|
||||||
|
mcp add <name> <command> [args...] Add a server
|
||||||
|
mcp remove <name> Remove a server
|
||||||
|
mcp enable <name> Enable a disabled server
|
||||||
|
mcp disable <name> Disable a server
|
||||||
|
"""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
parts = text.strip().split(maxsplit=3)
|
||||||
|
subcommand = parts[1] if len(parts) > 1 else "list"
|
||||||
|
|
||||||
|
cfg = load_config()
|
||||||
|
|
||||||
|
if subcommand == "list":
|
||||||
|
if not cfg.mcp.servers:
|
||||||
|
return "📦 No MCP servers configured.\n\nAdd one: `mcp add <name> <command> [args]`"
|
||||||
|
lines = ["📦 *MCP Servers:*\n"]
|
||||||
|
for name, srv in cfg.mcp.servers.items():
|
||||||
|
args_str = " ".join(srv.args) if srv.args else ""
|
||||||
|
lines.append(f"• `{name}` — `{srv.command} {args_str}`")
|
||||||
|
lines.append(f"\nManage: `mcp add`, `mcp remove <name>`")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
elif subcommand == "add" and len(parts) >= 3:
|
||||||
|
rest = text.strip().split(maxsplit=2)[2] # everything after "mcp add"
|
||||||
|
add_parts = rest.split()
|
||||||
|
name = add_parts[0]
|
||||||
|
command = add_parts[1] if len(add_parts) > 1 else "uvx"
|
||||||
|
args = add_parts[2:] if len(add_parts) > 2 else []
|
||||||
|
|
||||||
|
# Update config
|
||||||
|
server_data = {"command": command, "args": args, "env": {}}
|
||||||
|
_update_config_file({"mcp": {"servers": {name: server_data}}})
|
||||||
|
|
||||||
|
# Rewrite MCP config for the runtime
|
||||||
|
new_cfg = load_config()
|
||||||
|
write_mcp_config(new_cfg.mcp, new_cfg.memory.workspace, _use_claude)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"✅ MCP server `{name}` added\n"
|
||||||
|
f"• Command: `{command} {' '.join(args)}`\n"
|
||||||
|
f"Run `reload` to apply."
|
||||||
|
)
|
||||||
|
|
||||||
|
elif subcommand == "remove" and len(parts) >= 3:
|
||||||
|
name = parts[2].strip()
|
||||||
|
try:
|
||||||
|
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||||
|
data = _json.load(f)
|
||||||
|
servers = data.get("mcp", {}).get("servers", {})
|
||||||
|
if name not in servers:
|
||||||
|
return f"⚠️ MCP server `{name}` not found."
|
||||||
|
del servers[name]
|
||||||
|
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||||
|
_json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
new_cfg = load_config()
|
||||||
|
write_mcp_config(new_cfg.mcp, new_cfg.memory.workspace, _use_claude)
|
||||||
|
return f"✅ MCP server `{name}` removed."
|
||||||
|
except Exception as e:
|
||||||
|
return f"⚠️ Failed to remove: {e}"
|
||||||
|
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
"Usage:\n"
|
||||||
|
"• `mcp list` — List servers\n"
|
||||||
|
"• `mcp add <name> <command> [args]` — Add a server\n"
|
||||||
|
"• `mcp remove <name>` — Remove a server\n"
|
||||||
|
"\nExamples:\n"
|
||||||
|
"• `mcp add brave-search uvx brave-search-mcp@latest`\n"
|
||||||
|
"• `mcp add filesystem npx @anthropic-ai/mcp-filesystem /home/user`\n"
|
||||||
|
"• `mcp add github uvx github-mcp-server`"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Skills Management Commands
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_skill_command(text: str) -> str:
|
||||||
|
"""
|
||||||
|
Handle skill commands — manage skills from chat.
|
||||||
|
|
||||||
|
skill list List loaded skills
|
||||||
|
skill show <name> Show a skill's content
|
||||||
|
skill create <name> Create a new skill interactively
|
||||||
|
skill remove <name> Remove a skill
|
||||||
|
skill reload Reload all skills
|
||||||
|
"""
|
||||||
|
global _skills
|
||||||
|
|
||||||
|
parts = text.strip().split(maxsplit=2)
|
||||||
|
subcommand = parts[1] if len(parts) > 1 else "list"
|
||||||
|
|
||||||
|
if subcommand == "list":
|
||||||
|
if not _skills or not _skills.skills:
|
||||||
|
return (
|
||||||
|
"🎯 No skills loaded.\n\n"
|
||||||
|
"Create one: `skill create <name>`\n"
|
||||||
|
"Or ask me naturally: \"Create a skill for checking weather\""
|
||||||
|
)
|
||||||
|
lines = ["🎯 *Skills:*\n"]
|
||||||
|
for s in _skills.skills:
|
||||||
|
triggers = ", ".join(s.triggers[:4])
|
||||||
|
lines.append(f"• `{s.name}` — {s.description}\n Triggers: {triggers}")
|
||||||
|
lines.append(f"\nManage: `skill show <name>`, `skill create <name>`, `skill remove <name>`")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
elif subcommand == "show" and len(parts) >= 3:
|
||||||
|
name = parts[2].strip()
|
||||||
|
if not _skills:
|
||||||
|
return "⚠️ Skills system not initialized."
|
||||||
|
for s in _skills.skills:
|
||||||
|
if s.name.lower() == name.lower():
|
||||||
|
content = s.body[:2000]
|
||||||
|
return (
|
||||||
|
f"🎯 *{s.name}*\n"
|
||||||
|
f"_{s.description}_\n"
|
||||||
|
f"Triggers: {', '.join(s.triggers)}\n\n"
|
||||||
|
f"```\n{content}\n```"
|
||||||
|
)
|
||||||
|
return f"⚠️ Skill `{name}` not found. Run `skill list` to see available skills."
|
||||||
|
|
||||||
|
elif subcommand == "create" and len(parts) >= 3:
|
||||||
|
name = parts[2].strip().lower().replace(" ", "-")
|
||||||
|
cfg = load_config()
|
||||||
|
workspace = os.path.expanduser(cfg.memory.workspace)
|
||||||
|
skill_dir = os.path.join(workspace, "skills", name)
|
||||||
|
skill_path = os.path.join(skill_dir, "SKILL.md")
|
||||||
|
|
||||||
|
if os.path.exists(skill_path):
|
||||||
|
return f"⚠️ Skill `{name}` already exists at `{skill_path}`"
|
||||||
|
|
||||||
|
os.makedirs(skill_dir, exist_ok=True)
|
||||||
|
template = (
|
||||||
|
f"---\n"
|
||||||
|
f"name: {name}\n"
|
||||||
|
f"description: TODO — describe what this skill does\n"
|
||||||
|
f"triggers: [{name}]\n"
|
||||||
|
f"---\n\n"
|
||||||
|
f"# {name.title()} Skill\n\n"
|
||||||
|
f"<!-- Instructions for the AI when this skill is triggered -->\n\n"
|
||||||
|
f"When the user asks about {name}:\n"
|
||||||
|
f"1. ...\n"
|
||||||
|
f"2. ...\n"
|
||||||
|
f"3. ...\n"
|
||||||
|
)
|
||||||
|
with open(skill_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(template)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"✅ Skill `{name}` created at:\n"
|
||||||
|
f"`{skill_path}`\n\n"
|
||||||
|
f"Edit the SKILL.md to add your instructions, then run `skill reload`.\n"
|
||||||
|
f"Or just tell me what the skill should do and I'll write it for you."
|
||||||
|
)
|
||||||
|
|
||||||
|
elif subcommand == "remove" and len(parts) >= 3:
|
||||||
|
name = parts[2].strip()
|
||||||
|
cfg = load_config()
|
||||||
|
workspace = os.path.expanduser(cfg.memory.workspace)
|
||||||
|
skill_dir = os.path.join(workspace, "skills", name)
|
||||||
|
|
||||||
|
if not os.path.isdir(skill_dir):
|
||||||
|
return f"⚠️ Skill `{name}` not found."
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(skill_dir)
|
||||||
|
if _skills:
|
||||||
|
_skills.reload()
|
||||||
|
return f"✅ Skill `{name}` removed."
|
||||||
|
|
||||||
|
elif subcommand == "reload":
|
||||||
|
if _skills:
|
||||||
|
loaded = _skills.reload()
|
||||||
|
return f"🔄 Reloaded {len(loaded)} skill(s)."
|
||||||
|
return "⚠️ Skills system not initialized."
|
||||||
|
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
"Usage:\n"
|
||||||
|
"• `skill list` — List loaded skills\n"
|
||||||
|
"• `skill show <name>` — Show skill content\n"
|
||||||
|
"• `skill create <name>` — Create a new skill template\n"
|
||||||
|
"• `skill remove <name>` — Remove a skill\n"
|
||||||
|
"• `skill reload` — Reload all skills\n"
|
||||||
|
"\nYou can also create skills naturally:\n"
|
||||||
|
"\"Create a skill for checking stock prices\""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _on_scheduled_job(job: ScheduledJob) -> None:
|
def _on_scheduled_job(job: ScheduledJob) -> None:
|
||||||
"""
|
"""
|
||||||
Called by the scheduler when a job fires.
|
Called by the scheduler when a job fires.
|
||||||
@@ -1089,6 +1291,17 @@ def _format_help() -> str:
|
|||||||
"• `cron remove <id>` — Remove a scheduled job\n"
|
"• `cron remove <id>` — Remove a scheduled job\n"
|
||||||
"• `subagents` — List active background tasks\n"
|
"• `subagents` — List active background tasks\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
"*MCP Servers:*\n"
|
||||||
|
"• `mcp list` — List configured MCP servers\n"
|
||||||
|
"• `mcp add <name> <cmd> [args]` — Add a server\n"
|
||||||
|
"• `mcp remove <name>` — Remove a server\n"
|
||||||
|
"\n"
|
||||||
|
"*Skills:*\n"
|
||||||
|
"• `skill list` — List loaded skills\n"
|
||||||
|
"• `skill create <name>` — Create a new skill\n"
|
||||||
|
"• `skill show <name>` — View a skill\n"
|
||||||
|
"• `skill remove <name>` — Remove a skill\n"
|
||||||
|
"\n"
|
||||||
"*AI Chat:*\n"
|
"*AI Chat:*\n"
|
||||||
"• Send any other message and the AI will respond\n"
|
"• Send any other message and the AI will respond\n"
|
||||||
"• Each thread maintains its own conversation\n"
|
"• Each thread maintains its own conversation\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user