"""Interactive setup utility for Ludus MCP configuration."""
import os
import sys
from pathlib import Path
from typing import Any
from .config import get_settings
from .logging import get_logger
logger = get_logger(__name__)
def prompt_input(prompt: str, default: str | None = None, secret: bool = False) -> str:
"""Prompt user for input with optional default value."""
if default:
prompt_text = f"{prompt} [{default}]: "
else:
prompt_text = f"{prompt}: "
if secret:
import getpass
value = getpass.getpass(prompt_text)
else:
value = input(prompt_text).strip()
return value if value else (default or "")
def prompt_yes_no(prompt: str, default: bool = True) -> bool:
"""Prompt user for yes/no input."""
default_text = "Y/n" if default else "y/N"
response = input(f"{prompt} [{default_text}]: ").strip().lower()
if not response:
return default
return response in ("y", "yes")
def validate_url(url: str) -> bool:
"""Validate URL format."""
return url.startswith(("http://", "https://"))
def validate_file_path(path: str) -> bool:
"""Validate that a file path exists."""
return Path(path).exists()
def interactive_setup() -> int:
"""Interactive setup wizard for Ludus MCP configuration."""
print("=" * 60)
print("Ludus MCP Configuration Setup")
print("=" * 60)
print()
settings = get_settings()
config: dict[str, Any] = {}
# Ludus API Configuration
print("Ludus API Configuration")
print("-" * 60)
api_url = prompt_input(
"Ludus API URL",
default=settings.ludus_api_url or "http://localhost:8080"
)
if not validate_url(api_url):
print("ERROR: URL must start with http:// or https://", file=sys.stderr)
return 1
config["LUDUS_API_URL"] = api_url
api_key = prompt_input(
"Ludus API Key",
default=settings.ludus_api_key or "",
secret=True
)
if not api_key:
print("WARNING: API key is required for most operations", file=sys.stderr)
config["LUDUS_API_KEY"] = api_key
print()
# LLM Configuration (Optional)
print("LLM Configuration (Optional)")
print("-" * 60)
print("Press Enter to skip LLM configuration (fallback parser will be used)")
print()
use_llm = prompt_yes_no("Do you want to configure an LLM?", default=False)
if use_llm:
print("\nSupported providers:")
print(" 1. llama-cpp (Local: Mistral 7B, Llama, etc.) - Auto-install available")
print(" 2. gpt4all (Local alternative) - Auto-install available")
print(" 3. ollama (Local Ollama server)")
print(" 4. openai (ChatGPT: GPT-4o, GPT-4, GPT-3.5)")
print(" 5. anthropic (Claude: Claude 3.5 Sonnet, Claude 3 Opus)")
print(" 6. google (Gemini: Gemini 1.5 Pro, Gemini Pro)")
print()
provider_choice = prompt_input(
"LLM Provider (llama-cpp, gpt4all, ollama, openai, anthropic, google)",
default=getattr(settings, "llm_provider_type", None) or "llama-cpp"
)
valid_providers = ["llama-cpp", "gpt4all", "ollama", "openai", "anthropic", "google"]
if provider_choice not in valid_providers:
print(f"WARNING: Invalid provider, defaulting to llama-cpp", file=sys.stderr)
provider_choice = "llama-cpp"
config["LLM_PROVIDER_TYPE"] = provider_choice
# Cloud providers need API key
if provider_choice in ["openai", "anthropic", "google"]:
api_key = prompt_input(
f"{provider_choice.title()} API Key",
default=getattr(settings, "llm_api_key", None) or "",
secret=True
)
if not api_key:
print(f"WARNING: API key is required for {provider_choice}", file=sys.stderr)
config["LLM_API_KEY"] = api_key
# Model selection for cloud providers (latest models)
default_models = {
"openai": "gpt-4o",
"anthropic": "claude-3-5-sonnet-20241022",
"google": "gemini-1.5-pro",
}
model = prompt_input(
"Model name",
default=getattr(settings, "llm_model", None) or default_models.get(provider_choice, "")
)
config["LLM_MODEL"] = model
# Local providers need model path or Ollama config
elif provider_choice == "ollama":
base_url = prompt_input(
"Ollama base URL",
default=getattr(settings, "llm_ollama_base_url", None) or "http://localhost:11434"
)
config["LLM_OLLAMA_BASE_URL"] = base_url
model = prompt_input(
"Ollama model name",
default=getattr(settings, "llm_model", None) or "mistral"
)
config["LLM_MODEL"] = model
else: # llama-cpp or gpt4all
model_path = prompt_input(
"LLM Model Path",
default=getattr(settings, "llm_model_path", None) or ""
)
if model_path and not validate_file_path(model_path):
print(f"\n[WARN] Model file not found: {model_path}")
print()
# Offer to install the model
try:
from .model_installer import ModelInstaller
installer = ModelInstaller(model_path, interactive=True)
results = installer.run_checks()
print("Running model installation checks...")
print("-" * 60)
for message in results["messages"]:
print(f" {message}")
print("-" * 60)
print()
if results["checks_passed"]:
if results.get("model_info"):
print("Model Information:")
print(f" Name: {results['model_info']['name']}")
print(f" Size: ~{results['model_info']['size_gb']} GB")
print(f" Description: {results['model_info']['description']}")
print()
install_model = prompt_yes_no(
"Would you like to download and install this model now?",
default=True
)
if install_model:
print()
if installer.install_model_interactive():
print()
print("[OK] Model installed successfully!")
# Model path is already set, continue
else:
print()
print("[WARN] Model installation failed or cancelled")
if not prompt_yes_no("Continue without model?", default=False):
model_path = ""
else:
print("[INFO] Skipping model installation. You can install it later with:")
print(f" python3 scripts/install_model.py {model_path}")
if not prompt_yes_no("Continue without model?", default=False):
model_path = ""
else:
print("[WARN] Pre-installation checks failed. Cannot install model automatically.")
print()
print("Please fix the following issues:")
if not results.get("has_disk_space", True):
print(" - Free up disk space")
if not results.get("has_download_tool", False):
print(" - Install wget or curl: sudo apt-get install wget")
if not results.get("has_internet", False):
print(" - Check your internet connection")
print()
print("You can install the model manually later with:")
print(f" python3 scripts/install_model.py {model_path}")
print()
if not prompt_yes_no("Continue without model?", default=False):
model_path = ""
except ImportError:
print("[WARN] Model installer not available")
if not prompt_yes_no("Continue anyway?", default=False):
model_path = ""
except Exception as e:
logger.warning(f"Error during model installation check: {e}")
if not prompt_yes_no("Continue anyway?", default=False):
model_path = ""
config["LLM_MODEL_PATH"] = model_path
if model_path:
context_size = prompt_input(
"Context Size",
default=str(getattr(settings, "llm_context_size", None) or 4096)
)
try:
config["LLM_CONTEXT_SIZE"] = int(context_size)
except ValueError:
config["LLM_CONTEXT_SIZE"] = 4096
# Common settings
temperature = prompt_input(
"Temperature (0.0-1.0)",
default=str(getattr(settings, "llm_temperature", None) or 0.7)
)
try:
config["LLM_TEMPERATURE"] = float(temperature)
except ValueError:
config["LLM_TEMPERATURE"] = 0.7
print()
# Logging Configuration
print("Logging Configuration")
print("-" * 60)
log_level = prompt_input(
"Log Level (DEBUG, INFO, WARNING, ERROR)",
default=settings.log_level or "INFO"
)
if log_level.upper() not in ("DEBUG", "INFO", "WARNING", "ERROR"):
log_level = "INFO"
config["LOG_LEVEL"] = log_level.upper()
print()
# Save Configuration
print("=" * 60)
save_to_file = prompt_yes_no("Save configuration to .env file?", default=True)
if save_to_file:
env_file = Path(".env")
if env_file.exists():
overwrite = prompt_yes_no(".env file already exists. Overwrite?", default=False)
if not overwrite:
print("Configuration not saved.")
return 0
try:
with open(env_file, "w") as f:
f.write("# Ludus MCP Configuration\n")
f.write("# Generated by interactive setup\n\n")
for key, value in config.items():
if value: # Only write non-empty values
# Escape special characters in values
if "\n" in str(value) or '"' in str(value):
value = repr(str(value))
f.write(f"{key}={value}\n")
print(f"[OK] Configuration saved to {env_file.absolute()}")
except Exception as e:
print(f"ERROR: Failed to save configuration: {e}", file=sys.stderr)
return 1
else:
# Show environment variables to set
print("\nSet these environment variables:")
print("-" * 60)
for key, value in config.items():
if value:
print(f"export {key}={value}")
print()
# Test Configuration
print("=" * 60)
test_config = prompt_yes_no("Test configuration now?", default=True)
if test_config:
print("\nTesting configuration...")
# Test API connection
if config.get("LUDUS_API_URL") and config.get("LUDUS_API_KEY"):
import subprocess
import shutil
# Prefer using ludus CLI if available (more reliable)
ludus_cli = shutil.which("ludus")
if ludus_cli:
try:
# Set API key in environment and test with ludus CLI
env = os.environ.copy()
env["LUDUS_API_KEY"] = config["LUDUS_API_KEY"]
# Use 'ludus version' - lightweight command that verifies connectivity
result = subprocess.run(
["ludus", "version", "--url", config["LUDUS_API_URL"]],
capture_output=True,
text=True,
timeout=15,
env=env
)
if result.returncode == 0:
output = (result.stdout + result.stderr).strip()
# Parse server version from output
server_version = None
for line in output.split('\n'):
if 'Ludus Server' in line:
parts = line.split('Ludus Server')[-1].strip()
server_version = parts.split()[0] if parts else None
break
print(f"[OK] API connection successful")
if server_version:
print(f" Ludus Server: {server_version}")
else:
error_msg = result.stderr.strip() or result.stdout.strip()
if "401" in error_msg or "unauthorized" in error_msg.lower() or "No API key" in error_msg:
print("[ERR] API connection failed: Invalid API key")
elif "connection" in error_msg.lower() or "connect" in error_msg.lower():
print(f"[ERR] Could not connect to Ludus server")
else:
print(f"[ERR] API test failed: {error_msg[:100]}")
except subprocess.TimeoutExpired:
print("[ERR] Connection timed out - check if Ludus server is reachable")
except Exception as e:
print(f"[ERR] Error testing with ludus CLI: {e}")
else:
# Fallback to httpx if ludus CLI not available
try:
import httpx
import asyncio
async def test_connection():
# Ludus typically uses self-signed certs - disable SSL verify by default
ssl_verify_str = config.get("LUDUS_SSL_VERIFY", "false")
ssl_verify = ssl_verify_str.lower() not in ("false", "0", "no", "")
async with httpx.AsyncClient(timeout=10.0, verify=ssl_verify) as client:
try:
# Use /user endpoint (not /api/user/whoami - Ludus doesn't use /api/ prefix)
response = await client.get(
f"{config['LUDUS_API_URL']}/user",
headers={"X-API-KEY": config['LUDUS_API_KEY']}
)
if response.status_code == 200:
print("[OK] API connection successful")
return True
elif response.status_code == 401:
print("[ERR] API connection failed: Invalid API key")
return False
else:
print(f"[WARN] API returned status {response.status_code}")
return False
except httpx.ConnectError as e:
print(f"[ERR] Could not connect to API: {e}")
return False
except httpx.ConnectTimeout:
print("[ERR] Connection timed out - check if Ludus server is reachable")
return False
except Exception as e:
print(f"[ERR] Connection error: {type(e).__name__}: {e}")
return False
asyncio.run(test_connection())
except ImportError:
print("[WARN] Cannot test API connection (ludus CLI and httpx not available)")
except Exception as e:
print(f"[ERR] Error testing API connection: {e}")
else:
print("[WARN] Skipping API test (URL or key not provided)")
# Test LLM
if config.get("LLM_MODEL_PATH"):
model_path = Path(config["LLM_MODEL_PATH"])
if model_path.exists():
print("[OK] LLM model file found")
# Verify model integrity
try:
from .model_installer import ModelInstaller
installer = ModelInstaller(str(model_path), interactive=False)
is_valid, msg = installer.verify_model_file()
if is_valid:
print(f"[OK] {msg}")
else:
print(f"[WARN] {msg}")
except Exception:
pass # Skip verification if installer not available
else:
print("[WARN] LLM model file not found")
print(" You can install it later with:")
print(f" python3 scripts/install_model.py {config['LLM_MODEL_PATH']}")
elif config.get("LLM_PROVIDER_TYPE") in ["openai", "anthropic", "google", "ollama"]:
print("[OK] LLM configured (cloud provider, no local model needed)")
else:
print("[INFO] LLM not configured (will use fallback parser)")
print()
print("=" * 60)
print("Setup complete!")
print("=" * 60)
if save_to_file:
local_env = Path('.env').absolute()
print(f"\nConfiguration saved to: {local_env}")
# Also copy to ~/.ludus-fastmcp/.env for global access
global_config_dir = Path.home() / ".ludus-fastmcp"
global_env = global_config_dir / ".env"
try:
global_config_dir.mkdir(parents=True, exist_ok=True)
# Copy the .env content to global location
import shutil
shutil.copy2(local_env, global_env)
print(f"Configuration also saved to: {global_env}")
print("\nThe MCP server will now find your configuration from any directory.")
except Exception as e:
print(f"\n[WARN] Could not copy to {global_env}: {e}")
print("You may need to manually copy the .env file or set environment variables.")
print("\nYou can now run: ludus-fastmcp")
else:
print("\nRemember to set the environment variables before running ludus-fastmcp")
return 0