"""Unified launcher for Ludus MCP client with multiple UI modes."""
import asyncio
import os
import subprocess
import sys
from pathlib import Path
from typing import Optional
try:
import click
except ImportError:
print("ERROR: click library not installed. Install with: pip install click")
sys.exit(1)
@click.group(invoke_without_command=True, context_settings={"max_content_width": 120})
@click.option('--version', is_flag=True, help='Show version information')
@click.pass_context
def cli(ctx, version):
"""Ludus FastMCP Client - AI-Powered Cyber Range Management
\b
Features (v1.0):
- 157 FastMCP tools across 15 modules
- Skeleton templates for VMs and ranges
- Ansible Galaxy & custom role management
- Enhanced AI-assisted range building
\b
Setup:
ludus-ai setup-llm # Setup local LLM with Ollama (interactive)
\b
Chat Interfaces:
ludus-ai install anythingllm # Install AnythingLLM (Recommended)
ludus-ai install openwebui # Install Open WebUI
ludus-ai install opencode # Install OpenCode AI
\b
Maintenance:
ludus-ai clear-cache --opencode # Clear OpenCode cache
ludus-ai clear-cache --all # Clear all caches
ludus-ai uninstall opencode # Uninstall OpenCode AI
ludus-ai uninstall anythingllm # Uninstall AnythingLLM
ludus-ai uninstall openwebui # Uninstall Open WebUI
\b
Direct Tools:
ludus-ai tool list-tools # List all 157 FastMCP tools
ludus-ai tool list-ranges # List all ranges with details
ludus-ai tool get-inventory # Get range inventory
ludus-ai tool call-tool <name> # Call any FastMCP tool directly
"""
if version:
click.echo("Ludus FastMCP Client v1.0")
click.echo("FastMCP Server: 157 tools")
ctx.exit(0)
# Store context for subcommands
ctx.ensure_object(dict)
# If no subcommand is provided, show help
if ctx.invoked_subcommand is None:
click.echo(ctx.get_help())
ctx.exit(0)
# Web, GUI, and Chat commands removed - use AnythingLLM or Open WebUI instead
@cli.group(name='install')
def install_group():
"""
Install professional chat interfaces for Ludus MCP.
Choose between three community-maintained chat interfaces with beautiful
dark themes and modern UIs.
Examples:
ludus-ai install anythingllm # Install AnythingLLM (Recommended)
ludus-ai install openwebui # Install Open WebUI
ludus-ai install opencode # Install OpenCode AI
"""
pass
@cli.group(name='uninstall')
def uninstall_group():
"""
Uninstall chat interfaces and clean up configurations.
Examples:
ludus-ai uninstall opencode # Uninstall OpenCode AI
ludus-ai uninstall anythingllm # Uninstall AnythingLLM
ludus-ai uninstall openwebui # Uninstall Open WebUI
"""
pass
@cli.command('clear-cache')
@click.option('--all', 'clear_all', is_flag=True, help='Clear all caches (OpenCode, pipx, Python)')
@click.option('--opencode', is_flag=True, help='Clear OpenCode cache only')
@click.option('--pipx', is_flag=True, help='Clear pipx cache only')
def clear_cache(clear_all, opencode, pipx):
"""
Clear caches for installed components.
This clears cached data that might cause issues with updated versions.
Examples:
ludus-ai clear-cache --all # Clear all caches
ludus-ai clear-cache --opencode # Clear OpenCode cache
ludus-ai clear-cache --pipx # Clear pipx cache
"""
import shutil
if not any([clear_all, opencode, pipx]):
click.echo("[ERROR] Please specify which cache to clear", err=True)
click.echo(" Use --all, --opencode, or --pipx", err=True)
click.echo(" Run: ludus-ai clear-cache --help", err=True)
sys.exit(1)
cleared = []
errors = []
# Clear OpenCode cache
if clear_all or opencode:
opencode_cache = Path.home() / ".cache" / "opencode"
if opencode_cache.exists():
try:
shutil.rmtree(opencode_cache)
cleared.append(f"[OK] OpenCode cache ({opencode_cache})")
except Exception as e:
errors.append(f"[ERROR] OpenCode cache: {e}")
else:
cleared.append("[OK] OpenCode cache (already clean)")
# Clear pipx cache
if clear_all or pipx:
pipx_cache = Path.home() / ".local" / "pipx" / ".cache"
if pipx_cache.exists():
try:
shutil.rmtree(pipx_cache)
cleared.append(f"[OK] Pipx cache ({pipx_cache})")
except Exception as e:
errors.append(f"[ERROR] Pipx cache: {e}")
else:
cleared.append("[OK] Pipx cache (already clean)")
# Clear Python __pycache__
if clear_all:
project_root = Path.cwd()
pycache_count = 0
for pycache_dir in project_root.rglob("__pycache__"):
try:
shutil.rmtree(pycache_dir)
pycache_count += 1
except Exception as e:
errors.append(f"[ERROR] {pycache_dir}: {e}")
if pycache_count > 0:
cleared.append(f"[OK] Removed {pycache_count} __pycache__ directories")
# Print results
click.echo("\n🧹 Cache Cleanup Results:\n")
for msg in cleared:
click.echo(f" {msg}")
if errors:
click.echo("\n[WARNING] Errors:\n")
for msg in errors:
click.echo(f" {msg}", err=True)
click.echo("\n[OK] Cache cleanup complete!")
if opencode or clear_all:
click.echo("\n[TIP] Restart OpenCode to use the updated MCP server:")
click.echo(" opencode")
@install_group.command('anythingllm')
def install_anythingllm():
"""
Install AnythingLLM chat interface (Recommended).
AnythingLLM provides a beautiful dark theme, professional UI, and native
MCP support. Best for most users.
Features:
- Beautiful dark theme UI
- Desktop app or Docker
- Native MCP support
- Built-in RAG capabilities
- ChatGPT-like experience
After installation, access at: http://localhost:3001
"""
# Find the script relative to the package installation
script_path = Path(__file__).parent.parent / "scripts" / "install-anythingllm.sh"
# If not found, try relative to current working directory
if not script_path.exists():
script_path = Path.cwd() / "scripts" / "install-anythingllm.sh"
if not script_path.exists():
click.echo("[ERROR] Installation script not found.", err=True)
click.echo(f" Expected location: {script_path}", err=True)
click.echo("\nPlease ensure you're running this from the ludus-fastmcp directory,", err=True)
click.echo("or run the script manually:", err=True)
click.echo(" ./scripts/install-anythingllm.sh", err=True)
sys.exit(1)
click.echo("[INFO] Installing AnythingLLM...")
click.echo(f"[INFO] Script: {script_path}\n")
try:
result = subprocess.run(
["bash", str(script_path)],
check=False,
env=os.environ.copy()
)
if result.returncode == 0:
click.echo("\n[OK] AnythingLLM installation complete!")
click.echo(" Access at: http://localhost:3001")
# Configure MCP server
click.echo("\n[INFO] Configuring Ludus FastMCP server...")
try:
from ludus_mcp.utils.mcp_config import configure_anythingllm
success, message = configure_anythingllm(overwrite=False)
if success:
click.echo(f"[OK] {message}")
else:
click.echo(f"[WARNING] {message}", err=True)
click.echo(" You can configure it manually later.", err=True)
except Exception as e:
click.echo(f"[WARNING] Failed to auto-configure MCP: {e}", err=True)
click.echo(" You can configure it manually later.", err=True)
else:
click.echo(f"\n[ERROR] Installation failed with exit code {result.returncode}", err=True)
sys.exit(result.returncode)
except Exception as e:
click.echo(f"\n[ERROR] Error during installation: {e}", err=True)
sys.exit(1)
@install_group.command('openwebui')
def install_openwebui():
"""
Install Open WebUI chat interface.
Open WebUI provides a gorgeous modern UI with extensive customization
options. Best for teams and production environments.
Features:
- Gorgeous modern dark theme
- Multi-user support
- Extensive customization
- Production-ready
- Web-only interface
After installation, access at: http://localhost:3000
"""
# Find the script relative to the package installation
script_path = Path(__file__).parent.parent / "scripts" / "install-openwebui.sh"
# If not found, try relative to current working directory
if not script_path.exists():
script_path = Path.cwd() / "scripts" / "install-openwebui.sh"
if not script_path.exists():
click.echo("[ERROR] Installation script not found.", err=True)
click.echo(f" Expected location: {script_path}", err=True)
click.echo("\nPlease ensure you're running this from the ludus-fastmcp directory,", err=True)
click.echo("or run the script manually:", err=True)
click.echo(" ./scripts/install-openwebui.sh", err=True)
sys.exit(1)
click.echo("[INFO] Installing Open WebUI...")
click.echo(f"[INFO] Script: {script_path}\n")
try:
result = subprocess.run(
["bash", str(script_path)],
check=False,
env=os.environ.copy()
)
if result.returncode == 0:
click.echo("\n[OK] Open WebUI installation complete!")
click.echo(" Access at: http://localhost:3000")
# Configure MCP server
click.echo("\n[INFO] Configuring Ludus FastMCP server...")
try:
from ludus_mcp.utils.mcp_config import configure_openwebui
success, message = configure_openwebui(overwrite=False)
if success:
click.echo(f"[OK] {message}")
else:
click.echo(f"[WARNING] {message}", err=True)
click.echo(" You can configure it manually later.", err=True)
except Exception as e:
click.echo(f"[WARNING] Failed to auto-configure MCP: {e}", err=True)
click.echo(" You can configure it manually later.", err=True)
else:
click.echo(f"\n[ERROR] Installation failed with exit code {result.returncode}", err=True)
sys.exit(result.returncode)
except Exception as e:
click.echo(f"\n[ERROR] Error during installation: {e}", err=True)
sys.exit(1)
@install_group.command('opencode')
def install_opencode():
"""
Install OpenCode AI terminal agent.
OpenCode AI is a powerful terminal-native AI coding agent with native
MCP support. Best for developers who prefer terminal-based workflows.
Features:
- Terminal-native AI agent
- Native MCP support
- LSP integration
- Multi-session support
- 75+ LLM providers
- Privacy-first, local execution
After installation, run: opencode
"""
# Find the script relative to the package installation
script_path = Path(__file__).parent.parent / "scripts" / "install-opencode.sh"
# If not found, try relative to current working directory
if not script_path.exists():
script_path = Path.cwd() / "scripts" / "install-opencode.sh"
if not script_path.exists():
click.echo("[ERROR] Installation script not found.", err=True)
click.echo(f" Expected location: {script_path}", err=True)
click.echo("\nPlease ensure you're running this from the ludus-fastmcp directory,", err=True)
click.echo("or run the script manually:", err=True)
click.echo(" ./scripts/install-opencode.sh", err=True)
sys.exit(1)
click.echo("[INFO] Installing OpenCode AI...")
click.echo(f"[INFO] Script: {script_path}\n")
try:
result = subprocess.run(
["bash", str(script_path)],
check=False,
env=os.environ.copy()
)
if result.returncode == 0:
click.echo("\n[OK] OpenCode AI installation complete!")
click.echo(" Run: opencode")
# Configure MCP server
click.echo("\n[INFO] Configuring Ludus FastMCP server...")
try:
from ludus_mcp.utils.mcp_config import configure_opencode
success, message = configure_opencode(overwrite=False)
if success:
click.echo(f"[OK] {message}")
else:
click.echo(f"[WARNING] {message}", err=True)
click.echo(" You can configure it manually later.", err=True)
except Exception as e:
click.echo(f"[WARNING] Failed to auto-configure MCP: {e}", err=True)
click.echo(" You can configure it manually later.", err=True)
else:
click.echo(f"\n[ERROR] Installation failed with exit code {result.returncode}", err=True)
sys.exit(result.returncode)
except Exception as e:
click.echo(f"\n[ERROR] Error during installation: {e}", err=True)
sys.exit(1)
@uninstall_group.command('opencode')
@click.option('--keep-config', is_flag=True, help='Keep OpenCode configuration files')
def uninstall_opencode(keep_config):
"""
Uninstall OpenCode AI and optionally remove configuration.
This will:
- Uninstall OpenCode AI binary
- Optionally remove configuration files (unless --keep-config is used)
- Clear OpenCode cache
Examples:
ludus-ai uninstall opencode # Full uninstall
ludus-ai uninstall opencode --keep-config # Keep configs
"""
import shutil
click.echo("🗑️ Uninstalling OpenCode AI...\n")
removed = []
errors = []
# Uninstall OpenCode binary
opencode_bin = Path.home() / ".opencode"
if opencode_bin.exists():
try:
shutil.rmtree(opencode_bin)
removed.append(f"[OK] OpenCode binary ({opencode_bin})")
except Exception as e:
errors.append(f"[ERROR] OpenCode binary: {e}")
else:
removed.append("[OK] OpenCode binary (not found)")
# Remove configuration (unless --keep-config)
if not keep_config:
opencode_config = Path.home() / ".config" / "opencode"
if opencode_config.exists():
try:
shutil.rmtree(opencode_config)
removed.append(f"[OK] OpenCode config ({opencode_config})")
except Exception as e:
errors.append(f"[ERROR] OpenCode config: {e}")
else:
removed.append("[OK] OpenCode config (not found)")
else:
click.echo("ℹ️ Keeping OpenCode configuration files")
# Clear cache
opencode_cache = Path.home() / ".cache" / "opencode"
if opencode_cache.exists():
try:
shutil.rmtree(opencode_cache)
removed.append(f"[OK] OpenCode cache ({opencode_cache})")
except Exception as e:
errors.append(f"[ERROR] OpenCode cache: {e}")
# Print results
click.echo("\n[INFO] Uninstall Results:\n")
for msg in removed:
click.echo(f" {msg}")
if errors:
click.echo("\n[WARNING] Errors:\n")
for msg in errors:
click.echo(f" {msg}", err=True)
if errors:
click.echo("\n[WARNING] Uninstall completed with errors")
sys.exit(1)
else:
click.echo("\n[OK] OpenCode AI uninstalled successfully!")
@uninstall_group.command('anythingllm')
@click.option('--keep-data', is_flag=True, help='Keep AnythingLLM data and configurations')
def uninstall_anythingllm(keep_data):
"""
Uninstall AnythingLLM and optionally remove data.
This will:
- Stop and remove AnythingLLM Docker container
- Optionally remove data directory (unless --keep-data is used)
Examples:
ludus-ai uninstall anythingllm # Full uninstall
ludus-ai uninstall anythingllm --keep-data # Keep data
"""
click.echo("🗑️ Uninstalling AnythingLLM...\n")
removed = []
errors = []
# Stop and remove Docker container
try:
result = subprocess.run(
["docker", "stop", "anythingllm"],
capture_output=True,
text=True
)
if result.returncode == 0:
removed.append("[OK] Stopped AnythingLLM container")
except Exception as e:
errors.append(f"[ERROR] Stop container: {e}")
try:
result = subprocess.run(
["docker", "rm", "anythingllm"],
capture_output=True,
text=True
)
if result.returncode == 0:
removed.append("[OK] Removed AnythingLLM container")
except Exception as e:
errors.append(f"[ERROR] Remove container: {e}")
# Remove data (unless --keep-data)
if not keep_data:
import shutil
anythingllm_data = Path.home() / ".anythingllm"
if anythingllm_data.exists():
try:
shutil.rmtree(anythingllm_data)
removed.append(f"[OK] AnythingLLM data ({anythingllm_data})")
except Exception as e:
errors.append(f"[ERROR] AnythingLLM data: {e}")
else:
click.echo("ℹ️ Keeping AnythingLLM data directory")
# Print results
click.echo("\n[INFO] Uninstall Results:\n")
for msg in removed:
click.echo(f" {msg}")
if errors:
click.echo("\n[WARNING] Errors:\n")
for msg in errors:
click.echo(f" {msg}", err=True)
if errors:
click.echo("\n[WARNING] Uninstall completed with errors")
sys.exit(1)
else:
click.echo("\n[OK] AnythingLLM uninstalled successfully!")
@uninstall_group.command('openwebui')
@click.option('--keep-data', is_flag=True, help='Keep Open WebUI data and configurations')
def uninstall_openwebui(keep_data):
"""
Uninstall Open WebUI and optionally remove data.
This will:
- Stop and remove Open WebUI Docker container
- Optionally remove data directory (unless --keep-data is used)
Examples:
ludus-ai uninstall openwebui # Full uninstall
ludus-ai uninstall openwebui --keep-data # Keep data
"""
click.echo("🗑️ Uninstalling Open WebUI...\n")
removed = []
errors = []
# Stop and remove Docker container
try:
result = subprocess.run(
["docker", "stop", "open-webui"],
capture_output=True,
text=True
)
if result.returncode == 0:
removed.append("[OK] Stopped Open WebUI container")
except Exception as e:
errors.append(f"[ERROR] Stop container: {e}")
try:
result = subprocess.run(
["docker", "rm", "open-webui"],
capture_output=True,
text=True
)
if result.returncode == 0:
removed.append("[OK] Removed Open WebUI container")
except Exception as e:
errors.append(f"[ERROR] Remove container: {e}")
# Remove data (unless --keep-data)
if not keep_data:
import shutil
openwebui_data = Path.home() / ".open-webui"
if openwebui_data.exists():
try:
shutil.rmtree(openwebui_data)
removed.append(f"[OK] Open WebUI data ({openwebui_data})")
except Exception as e:
errors.append(f"[ERROR] Open WebUI data: {e}")
else:
click.echo("ℹ️ Keeping Open WebUI data directory")
# Print results
click.echo("\n[INFO] Uninstall Results:\n")
for msg in removed:
click.echo(f" {msg}")
if errors:
click.echo("\n[WARNING] Errors:\n")
for msg in errors:
click.echo(f" {msg}", err=True)
if errors:
click.echo("\n[WARNING] Uninstall completed with errors")
sys.exit(1)
else:
click.echo("\n[OK] Open WebUI uninstalled successfully!")
@cli.command('setup-llm')
def setup_llm():
"""
Setup local LLM with Ollama (interactive).
This interactive script will:
- Check if Ollama is installed and running
- Show available models
- Help you download a recommended model
- Verify your configuration
- Provide next steps for each chat interface
Recommended models:
- qwen2.5-coder:7b (Best for coding/Ludus tasks)
- llama3.2:3b (Fast, low RAM)
- deepseek-coder-v2:16b (Most capable, high RAM)
After setup, all three interfaces (OpenCode, AnythingLLM, Open WebUI)
can use your local LLM via Ollama.
"""
# Find the script relative to the package installation
script_path = Path(__file__).parent.parent / "scripts" / "setup-local-llm.sh"
# If not found, try relative to current working directory
if not script_path.exists():
script_path = Path.cwd() / "scripts" / "setup-local-llm.sh"
if not script_path.exists():
click.echo("[ERROR] setup-local-llm.sh script not found", err=True)
click.echo(" Expected locations:", err=True)
click.echo(f" - {Path(__file__).parent.parent / 'scripts' / 'setup-local-llm.sh'}", err=True)
click.echo(" - ./scripts/setup-local-llm.sh", err=True)
sys.exit(1)
click.echo("[INFO] Setting up local LLM with Ollama...")
click.echo(f"[INFO] Script: {script_path}\n")
try:
result = subprocess.run(
["bash", str(script_path)],
check=False,
env=os.environ.copy()
)
if result.returncode != 0:
click.echo(f"\n[ERROR] Setup failed with exit code {result.returncode}", err=True)
sys.exit(result.returncode)
except Exception as e:
click.echo(f"\n[ERROR] Error during setup: {e}", err=True)
sys.exit(1)
@cli.group(name='tool')
def tool_group():
"""
Execute Ludus MCP tools directly (for scripting).
This provides direct tool execution without interactive chat.
Perfect for scripts and automation.
Examples:
ludus-ai tool list-tools
ludus-ai tool list-scenarios
ludus-ai tool list-ranges
"""
pass
@tool_group.command('list-tools')
@click.option('--command', default='ludus-fastmcp', help='FastMCP server command (default: ludus-fastmcp)')
@click.option('--env', multiple=True, help='Environment variables (KEY=VALUE)')
def list_tools(command, env):
"""List all available FastMCP tools (125 total)."""
async def _run():
from ludus_mcp.mcp_client.unified_client import UnifiedMCPClient
env_dict = {}
for e in env:
if '=' in e:
key, value = e.split('=', 1)
env_dict[key] = value
async with UnifiedMCPClient(command=command, env=env_dict) as client:
tools = await client.list_tools()
click.echo("\nAvailable Ludus MCP Tools:\n")
for tool in tools:
click.echo(f" - {tool['name']}")
if 'description' in tool:
click.echo(f" {tool['description']}")
click.echo()
try:
asyncio.run(_run())
except Exception as e:
click.echo(f"[ERROR] Error: {e}", err=True)
sys.exit(1)
@tool_group.command('list-scenarios')
@click.option('--command', default='ludus-fastmcp', help='FastMCP server command (default: ludus-fastmcp)')
@click.option('--env', multiple=True, help='Environment variables (KEY=VALUE)')
def list_scenarios(command, env):
"""List all available scenarios."""
async def _run():
from ludus_mcp.mcp_client.unified_client import UnifiedMCPClient
import json
env_dict = {}
for e in env:
if '=' in e:
key, value = e.split('=', 1)
env_dict[key] = value
async with UnifiedMCPClient(command=command, env=env_dict) as client:
result = await client.call_tool("ludus.list_scenarios", {})
click.echo("\nAvailable Scenarios:\n")
# Parse the result dict from UnifiedMCPClient
if isinstance(result, dict) and 'result' in result:
# Parse the JSON string result
try:
result_data = json.loads(result['result'])
if 'error' in result_data:
click.echo(f"[ERROR] Error: {result_data['error']}", err=True)
else:
click.echo(json.dumps(result_data, indent=2))
except json.JSONDecodeError:
click.echo(result['result'])
else:
click.echo(json.dumps(result, indent=2))
try:
asyncio.run(_run())
except Exception as e:
click.echo(f"[ERROR] Error: {e}", err=True)
sys.exit(1)
@tool_group.command('get-inventory')
@click.option('--command', default='ludus-fastmcp', help='FastMCP server command (default: ludus-fastmcp)')
@click.option('--env', multiple=True, help='Environment variables (KEY=VALUE)')
def get_inventory(command, env):
"""Get Ansible inventory for your range."""
async def _run():
from ludus_mcp.mcp_client.unified_client import UnifiedMCPClient
import json
env_dict = {}
for e in env:
if '=' in e:
key, value = e.split('=', 1)
env_dict[key] = value
async with UnifiedMCPClient(command=command, env=env_dict) as client:
result = await client.call_tool("ludus.get_range_ansible_inventory", {})
click.echo("\nAnsible Inventory:\n")
# Parse the result dict from UnifiedMCPClient
if isinstance(result, dict) and 'result' in result:
# Parse the JSON string result
try:
result_data = json.loads(result['result'])
if 'error' in result_data:
click.echo(f"[ERROR] Error: {result_data['error']}", err=True)
else:
click.echo(json.dumps(result_data, indent=2))
except json.JSONDecodeError:
click.echo(result['result'])
else:
click.echo(json.dumps(result, indent=2))
try:
asyncio.run(_run())
except Exception as e:
click.echo(f"[ERROR] Error: {e}", err=True)
sys.exit(1)
@tool_group.command('list-ranges')
@click.option('--command', default='ludus-fastmcp', help='FastMCP server command (default: ludus-fastmcp)')
@click.option('--env', multiple=True, help='Environment variables (KEY=VALUE)')
def list_ranges(command, env):
"""List all ranges (uses new range management tools)."""
async def _run():
from ludus_mcp.mcp_client.unified_client import UnifiedMCPClient
import json
env_dict = {}
for e in env:
if '=' in e:
key, value = e.split('=', 1)
env_dict[key] = value
async with UnifiedMCPClient(command=command, env=env_dict) as client:
result = await client.call_tool("ludus.list_ranges", {})
click.echo("\nAvailable Ranges:\n")
# Parse the result dict from UnifiedMCPClient
if isinstance(result, dict) and 'result' in result:
# Parse the JSON string result
try:
result_data = json.loads(result['result'])
if 'error' in result_data:
click.echo(f"[ERROR] Error: {result_data['error']}", err=True)
else:
click.echo(json.dumps(result_data, indent=2))
except json.JSONDecodeError:
click.echo(result['result'])
else:
click.echo(json.dumps(result, indent=2))
try:
asyncio.run(_run())
except Exception as e:
click.echo(f"[ERROR] Error: {e}", err=True)
sys.exit(1)
@tool_group.command('call-tool')
@click.argument('tool_name')
@click.option('--args', default='{}', help='Tool arguments as JSON')
@click.option('--command', default='ludus-fastmcp', help='FastMCP server command (default: ludus-fastmcp)')
@click.option('--env', multiple=True, help='Environment variables (KEY=VALUE)')
def call_tool(tool_name, args, command, env):
"""Call a specific FastMCP tool with arguments (125 tools available).
Example:
ludus-ai tool call-tool list_all_ranges_detailed
ludus-ai tool call-tool create_custom_os_template --args '{"name": "kali-custom", "os_type": "debian"}'
"""
async def _run():
from ludus_mcp.mcp_client.unified_client import UnifiedMCPClient
import json
env_dict = {}
for e in env:
if '=' in e:
key, value = e.split('=', 1)
env_dict[key] = value
# Parse the JSON arguments
try:
tool_args = json.loads(args)
except json.JSONDecodeError as e:
click.echo(f"[ERROR] Invalid JSON arguments: {e}", err=True)
sys.exit(1)
async with UnifiedMCPClient(command=command, env=env_dict) as client:
click.echo(f"\nCalling tool: {tool_name}")
if tool_args:
click.echo(f"Arguments: {json.dumps(tool_args, indent=2)}\n")
result = await client.call_tool(tool_name, tool_args)
# Parse the result dict from UnifiedMCPClient
if isinstance(result, dict) and 'result' in result:
# Parse the JSON string result
try:
result_data = json.loads(result['result'])
if 'error' in result_data:
click.echo(f"\n[ERROR] Error: {result_data['error']}", err=True)
else:
click.echo(f"\nResult:\n{json.dumps(result_data, indent=2)}")
except json.JSONDecodeError:
click.echo(f"\nResult:\n{result['result']}")
else:
click.echo(f"\nResult:\n{json.dumps(result, indent=2)}")
try:
asyncio.run(_run())
except Exception as e:
click.echo(f"[ERROR] Error: {e}", err=True)
sys.exit(1)
# Sessions command removed - session management is handled by AnythingLLM or Open WebUI
def main():
"""Main entry point."""
cli()
if __name__ == "__main__":
main()