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:
215
main.py
215
main.py
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user