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:
2026-02-18 23:27:00 -05:00
parent b8b3f44d52
commit 34dea65a07
3 changed files with 339 additions and 1 deletions

215
main.py
View File

@@ -242,6 +242,12 @@ def ai_handler(msg: IncomingMessage) -> str:
if cmd.startswith("usage"):
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
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:
"""
Called by the scheduler when a job fires.
@@ -1089,6 +1291,17 @@ def _format_help() -> str:
"• `cron remove <id>` — Remove a scheduled job\n"
"• `subagents` — List active background tasks\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"
"• Send any other message and the AI will respond\n"
"• Each thread maintains its own conversation\n"