UNPKG

ludus-mcp

Version:

MCP server for managing Ludus cybersecurity training environments through natural language commands

922 lines (801 loc) 32.9 kB
import { execSync, spawn } from 'child_process'; import { existsSync } from 'fs'; import { join, basename } from 'path'; import { homedir } from 'os'; import { createInterface } from 'readline'; import * as net from 'net'; import { Logger } from '../utils/logger.js'; import { storeCredentials, getCredentials, CREDENTIAL_KEYS, isKeyringSupportAvailable, getCredentialSummary } from '../utils/keyring.js'; export interface LudusConfig { adminUser: string; connectionMethod: 'wireguard' | 'ssh-tunnel'; wireguardConfig?: string | undefined; // Optional for SSH tunnel mode apiKey: string; sshHost: string; sshUser: string; sshAuthMethod: 'password' | 'key'; sshPassword?: string | undefined; // Optional for key-based auth sshKeyPath?: string | undefined; // Optional for password auth sshKeyPassphrase?: string | undefined; // Optional passphrase for password-protected keys ludusUrl?: string; verifySSL?: boolean; } export interface SetupResult { success: boolean; message: string; config?: LudusConfig; requiresRestart?: boolean; } export class InteractiveSetup { private logger: Logger; private rl: any; constructor(logger: Logger) { this.logger = logger; this.rl = createInterface({ input: process.stdin, output: process.stderr // Use stderr to avoid interfering with MCP protocol stdout }); } /** * Secure prompt for sensitive data (hides input) */ private async securePrompt(question: string): Promise<string> { // Simple fallback - just use regular prompt for now // The security benefit of hiding input is minimal for this use case return this.prompt(question); } private async prompt(question: string): Promise<string> { return new Promise((resolve) => { this.rl.question(question, (answer: string) => { resolve(answer.trim()); }); }); } private checkWireGuardAvailable(): boolean { try { execSync('wg --version', { stdio: 'ignore' }); return true; } catch { return false; } } private checkWireGuardConnectivity(): boolean { try { const result = execSync('wg show', { encoding: 'utf8' }); return result.trim().length > 0; } catch { return false; } } private async connectWireGuard(configPath: string): Promise<boolean> { try { if (!existsSync(configPath)) { console.error(`WireGuard config file not found: ${configPath}`); return false; } // First check if WireGuard is already connected if (this.checkWireGuardConnectivity()) { console.error('WireGuard VPN already connected'); return true; } // Check if interface already exists try { const interfaces = execSync('wg show interfaces', { encoding: 'utf8' }); if (interfaces.trim().length > 0) { console.error('WireGuard interface already active'); return true; } } catch (e) { // wg command might not be available, continue with connection attempt } // Try to connect to WireGuard - platform-specific approach const isWindows = process.platform === 'win32'; if (isWindows) { // Windows: Use WireGuard GUI or service approach return await this.connectWireGuardWindows(configPath); } else { // Linux/Mac: Use wg-quick return await this.connectWireGuardUnix(configPath); } } catch (error: any) { console.error(`WireGuard connection failed: ${error.message}`); return false; } } // Helper function to execute WireGuard commands securely private async executeWireGuardCommand(command: string, args: string[]): Promise<string> { return new Promise((resolve, reject) => { const process = spawn(command, args, { stdio: 'pipe', shell: false }); let output = ''; let error = ''; process.stdout.on('data', (data) => output += data.toString()); process.stderr.on('data', (data) => error += data.toString()); process.on('close', (code) => { if (code === 0) { resolve(output); } else { reject(new Error(`Command failed with code ${code}: ${error}`)); } }); process.on('error', (err) => reject(err)); }); } private async connectWireGuardWindows(configPath: string): Promise<boolean> { try { console.error('Setting up WireGuard tunnel on Windows...'); // Find WireGuard installation const wireguardPaths = [ 'C:\\Program Files\\WireGuard\\wireguard.exe', 'C:\\Program Files (x86)\\WireGuard\\wireguard.exe' ]; let wireguardPath = ''; for (const path of wireguardPaths) { if (existsSync(path)) { wireguardPath = path; break; } } if (!wireguardPath) { console.error('WireGuard not found. Please install it first:'); console.error(' Download from: https://www.wireguard.com/install/'); console.error(' Or run: winget install WireGuard.WireGuard'); return false; } // Extract tunnel name from config file path const tunnelName = basename(configPath, '.conf'); // Stop existing tunnel if running try { console.error('Stopping existing WireGuard tunnel...'); // Use spawn to prevent command injection const result = spawn(wireguardPath, ['/uninstalltunnelservice', tunnelName], { stdio: 'pipe', shell: false }); await new Promise<void>((resolve) => { result.on('close', () => resolve()); result.on('error', () => resolve()); // Ignore errors for cleanup }); console.error('Existing tunnel stopped'); } catch (error) { // Tunnel might not exist, continue console.error(' No existing tunnel to stop'); } // Install and start new tunnel try { console.error('Installing WireGuard tunnel service...'); const result = await this.executeWireGuardCommand(wireguardPath, ['/installtunnelservice', configPath]); console.error('WireGuard tunnel service installed and started'); // Wait a moment for tunnel to establish await new Promise(resolve => setTimeout(resolve, 3000)); // Verify connection if (this.checkWireGuardConnectivity()) { console.error('WireGuard VPN connected successfully'); return true; } else { console.error('WireGuard tunnel started but connectivity verification failed'); console.error('This might be normal - the tunnel may need more time to establish'); return true; // Return true since the service started successfully } } catch (error: any) { console.error(`Failed to install WireGuard tunnel service: ${error.message}`); return false; } } catch (error: any) { console.error(`Windows WireGuard connection failed: ${error.message}`); return false; } } /** * Stop WireGuard tunnel on Windows */ private async stopWireGuardWindows(configPath: string): Promise<void> { try { const wireguardPaths = [ 'C:\\Program Files\\WireGuard\\wireguard.exe', 'C:\\Program Files (x86)\\WireGuard\\wireguard.exe' ]; let wireguardPath = ''; for (const path of wireguardPaths) { if (existsSync(path)) { wireguardPath = path; break; } } if (wireguardPath) { const tunnelName = basename(configPath, '.conf'); console.error('Stopping WireGuard tunnel...'); try { await this.executeWireGuardCommand(wireguardPath, ['/uninstalltunnelservice', tunnelName]); console.error('WireGuard tunnel stopped'); } catch (error) { console.error(' WireGuard tunnel was not running'); } } } catch (error: any) { console.error(` Error stopping WireGuard tunnel: ${error.message}`); } } private async connectWireGuardUnix(configPath: string): Promise<boolean> { try { const result = await this.executeWireGuardCommand('wg-quick', ['up', configPath]); console.error('WireGuard VPN connected successfully'); return true; } catch (error: any) { // Handle "already exists" error gracefully if (error.message.includes('already exists')) { console.error('WireGuard interface already active'); return true; } throw error; // Re-throw other errors } } private async validateApiKey(apiKey: string, ludusUrl: string = 'https://198.51.100.1:8080'): Promise<boolean> { try { // Set the API key in environment for the command const env = { ...process.env, LUDUS_API_KEY: apiKey }; // Test the API key by getting server info const result = spawn('ludus', ['--url', ludusUrl, 'version'], { env, stdio: 'pipe' }); let stdout = ''; let stderr = ''; result.stdout.on('data', (data) => { stdout += data.toString(); }); result.stderr.on('data', (data) => { stderr += data.toString(); }); return new Promise((resolve) => { result.on('close', (code) => { // Check both stdout and stderr for success indicators const output = stdout + stderr; const success = code === 0 || output.includes('version') || output.includes('Ludus') || output.includes('server'); if (success) { console.error(`API key validation successful`); resolve(true); } else { console.error(`API key validation failed. Code: ${code}`); console.error(`Output: ${output}`); resolve(false); } }); result.on('error', (error) => { console.error(`API key validation error: ${error.message}`); resolve(false); }); }); } catch (error: any) { console.error(`API key validation failed: ${error.message}`); return false; } } private async testSshConnectivity(config: LudusConfig): Promise<boolean> { try { // Use ssh2 library (consistent with tunnel manager) const { Client } = await import('ssh2'); const ssh = new Client(); const sshConfig: any = { host: config.sshHost, username: config.sshUser, readyTimeout: 10000, // 10 second timeout }; if (config.sshAuthMethod === 'key') { // Key-based authentication if (!config.sshKeyPath) { console.error('SSH key path is required for key authentication'); return false; } const fs = await import('fs/promises'); try { const privateKey = await fs.readFile(config.sshKeyPath); sshConfig.privateKey = privateKey; if (config.sshKeyPassphrase) { sshConfig.passphrase = config.sshKeyPassphrase; } } catch (keyError: any) { console.error(`Failed to read SSH private key: ${keyError.message}`); return false; } } else { // Password authentication sshConfig.password = config.sshPassword; } return new Promise((resolve) => { let resolved = false; const cleanup = () => { if (!resolved) { resolved = true; ssh.end(); } }; const timeout = setTimeout(() => { if (!resolved) { resolved = true; console.error('SSH connectivity test timed out'); ssh.end(); resolve(false); } }, 12000); // 12 second total timeout ssh.on('ready', () => { ssh.exec('echo "SSH test successful"', (err, stream) => { if (err) { clearTimeout(timeout); cleanup(); console.error(`SSH command execution failed: ${err.message}`); resolve(false); return; } let output = ''; stream.on('data', (data: Buffer) => { output += data.toString(); }); stream.on('close', (code: number) => { clearTimeout(timeout); cleanup(); if (code === 0 && output.trim() === 'SSH test successful') { console.error('SSH connectivity test successful'); resolve(true); } else { console.error(`SSH connectivity test failed with code: ${code}`); resolve(false); } }); }); }); ssh.on('error', (error: any) => { clearTimeout(timeout); cleanup(); console.error(`SSH connectivity test failed: ${error.message}`); resolve(false); }); try { ssh.connect(sshConfig); } catch (connectError: any) { clearTimeout(timeout); cleanup(); console.error(`SSH connectivity test error: ${connectError.message}`); resolve(false); } }); } catch (error: any) { console.error(`SSH connectivity test error: ${error.message}`); return false; } } private async createSshTunnel(config: LudusConfig): Promise<boolean> { try { console.error('📡 Creating SSH tunnels for ports 8080 and 8081...'); // Use the existing SSH tunnel manager instead of duplicating tunnel logic const { LudusSSHTunnelManager } = await import('../ludusMCP/sshTunnelManager.js'); const tunnelConfig = { host: config.sshHost, port: 22, username: config.sshUser, authMethod: config.sshAuthMethod, regularPort: 8080, primaryPort: 8081, privateKeyPath: config.sshAuthMethod === 'key' ? config.sshKeyPath : undefined, privateKeyPassphrase: config.sshAuthMethod === 'key' ? config.sshKeyPassphrase : undefined, password: config.sshAuthMethod === 'password' ? config.sshPassword : undefined }; console.error(`Attempting SSH tunnel connection to ${config.sshHost}:22 as ${config.sshUser} using ${config.sshAuthMethod} auth...`); // Create a logger for the tunnel manager const logger = new Logger('SSH-Setup'); const tunnelManager = new LudusSSHTunnelManager(tunnelConfig, logger); try { await tunnelManager.connect(); console.error('SSH tunnels established successfully using tunnel manager'); // Test tunnel connectivity await new Promise(resolve => setTimeout(resolve, 2000)); const tunnelWorks = await this.testTunnelConnectivity(); if (tunnelWorks) { console.error('SSH tunnel connectivity test successful'); return true; } else { console.error('SSH tunnels created but connectivity test failed'); await tunnelManager.disconnect(); return false; } } catch (tunnelError: any) { console.error(`SSH tunnel creation failed: ${tunnelError.message}`); try { await tunnelManager.disconnect(); } catch (disconnectError) { // Ignore disconnect errors } return false; } } catch (error: any) { console.error(`SSH tunnel setup error: ${error.message}`); return false; } } private async testTunnelConnectivity(): Promise<boolean> { try { // Use native Node.js TCP socket testing instead of external curl console.error('Testing tunnel connectivity to localhost:8080...'); const isPortOpen = await new Promise<boolean>((resolve) => { const socket = new net.Socket(); const timeout = setTimeout(() => { socket.destroy(); resolve(false); }, 5000); // 5 second timeout socket.once('connect', () => { clearTimeout(timeout); socket.end(); resolve(true); }); socket.once('error', () => { clearTimeout(timeout); resolve(false); }); socket.connect(8080, '127.0.0.1'); }); if (isPortOpen) { console.error('Tunnel connectivity test successful - port 8080 is accessible'); return true; } else { console.error('Tunnel connectivity test failed - port 8080 is not accessible'); return false; } } catch (error: any) { console.error(`Tunnel connectivity test error: ${error.message}`); return false; } } private getConfigFromEnvironment(): LudusConfig | null { const requiredVars = { adminUser: 'LUDUS_ADMIN_USER', wireguardConfig: 'LUDUS_WIREGUARD_CONFIG', apiKey: 'LUDUS_API_KEY', sshHost: 'LUDUS_SSH_HOST', sshUser: 'LUDUS_SSH_USER', sshPassword: 'LUDUS_SSH_PASSWORD' }; const config: any = {}; for (const [key, envVar] of Object.entries(requiredVars)) { const value = process.env[envVar]; if (!value) { return null; // Missing required environment variable } config[key] = value; } // Optional environment variables config.ludusUrl = process.env.LUDUS_URL || 'https://198.51.100.1:8080'; config.verifySSL = Boolean(process.env.LUDUS_VERIFY?.toLowerCase() === 'true'); return config as LudusConfig; } /** * Try to retrieve credentials from keyring */ private async getConfigFromKeyring(): Promise<LudusConfig | null> { try { if (!isKeyringSupportAvailable()) { console.error(' Keyring support not available on this system'); return null; } const credentials = await getCredentials([ CREDENTIAL_KEYS.ADMIN_USER, CREDENTIAL_KEYS.CONNECTION_METHOD, CREDENTIAL_KEYS.WIREGUARD_CONFIG_PATH, CREDENTIAL_KEYS.API_KEY, CREDENTIAL_KEYS.SSH_HOST, CREDENTIAL_KEYS.SSH_USER, CREDENTIAL_KEYS.SSH_AUTH_METHOD, CREDENTIAL_KEYS.SSH_PASSWORD, CREDENTIAL_KEYS.SSH_KEY_PATH, CREDENTIAL_KEYS.SSH_KEY_PASSPHRASE ]); const connectionMethod = credentials[CREDENTIAL_KEYS.CONNECTION_METHOD] as 'wireguard' | 'ssh-tunnel' || 'wireguard'; const sshAuthMethod = credentials[CREDENTIAL_KEYS.SSH_AUTH_METHOD] as 'password' | 'key' || 'password'; // Check if all required credentials are present const baseRequiredCredentials = [ credentials[CREDENTIAL_KEYS.ADMIN_USER], credentials[CREDENTIAL_KEYS.CONNECTION_METHOD], credentials[CREDENTIAL_KEYS.API_KEY], credentials[CREDENTIAL_KEYS.SSH_HOST], credentials[CREDENTIAL_KEYS.SSH_USER], credentials[CREDENTIAL_KEYS.SSH_AUTH_METHOD] ]; const hasBaseCredentials = baseRequiredCredentials.every(cred => !!cred); const hasWireguardConfig = connectionMethod === 'ssh-tunnel' || !!credentials[CREDENTIAL_KEYS.WIREGUARD_CONFIG_PATH]; const hasSshAuth = (sshAuthMethod === 'password' && !!credentials[CREDENTIAL_KEYS.SSH_PASSWORD]) || (sshAuthMethod !== 'password' && !!credentials[CREDENTIAL_KEYS.SSH_KEY_PATH]); const hasAllCredentials = hasBaseCredentials && hasWireguardConfig && hasSshAuth; if (!hasAllCredentials) { return null; } const baseUrl = connectionMethod === 'ssh-tunnel' ? 'https://localhost:8080' : 'https://198.51.100.1:8080'; return { adminUser: credentials[CREDENTIAL_KEYS.ADMIN_USER]!, connectionMethod, wireguardConfig: credentials[CREDENTIAL_KEYS.WIREGUARD_CONFIG_PATH] || undefined, apiKey: credentials[CREDENTIAL_KEYS.API_KEY]!, sshHost: credentials[CREDENTIAL_KEYS.SSH_HOST]!, sshUser: credentials[CREDENTIAL_KEYS.SSH_USER]!, sshAuthMethod, sshPassword: credentials[CREDENTIAL_KEYS.SSH_PASSWORD] || undefined, sshKeyPath: credentials[CREDENTIAL_KEYS.SSH_KEY_PATH] || undefined, sshKeyPassphrase: credentials[CREDENTIAL_KEYS.SSH_KEY_PASSPHRASE] || undefined, ludusUrl: process.env.LUDUS_URL || baseUrl, verifySSL: process.env.LUDUS_VERIFY?.toLowerCase() === 'true' }; } catch (error: any) { console.error(` Failed to retrieve credentials from keyring: ${error.message}`); return null; } } /** * Store credentials in keyring after successful validation */ private async storeConfigInKeyring(config: LudusConfig): Promise<void> { try { if (!isKeyringSupportAvailable()) { console.error(' Keyring support not available - credentials will not be stored'); return; } const credentialsToStore: Record<string, string> = { [CREDENTIAL_KEYS.ADMIN_USER]: config.adminUser, [CREDENTIAL_KEYS.CONNECTION_METHOD]: config.connectionMethod, [CREDENTIAL_KEYS.API_KEY]: config.apiKey, [CREDENTIAL_KEYS.SSH_HOST]: config.sshHost, [CREDENTIAL_KEYS.SSH_USER]: config.sshUser, [CREDENTIAL_KEYS.SSH_AUTH_METHOD]: config.sshAuthMethod }; // Only store optional configs if they exist if (config.wireguardConfig) { credentialsToStore[CREDENTIAL_KEYS.WIREGUARD_CONFIG_PATH] = config.wireguardConfig; } if (config.sshPassword) { credentialsToStore[CREDENTIAL_KEYS.SSH_PASSWORD] = config.sshPassword; } if (config.sshKeyPath) { credentialsToStore[CREDENTIAL_KEYS.SSH_KEY_PATH] = config.sshKeyPath; } if (config.sshKeyPassphrase) { credentialsToStore[CREDENTIAL_KEYS.SSH_KEY_PASSPHRASE] = config.sshKeyPassphrase; } await storeCredentials(credentialsToStore); console.error('Credentials stored securely in keyring'); } catch (error: any) { console.error(` Failed to store credentials in keyring: ${error.message}`); } } private async promptForCredentials(): Promise<LudusConfig> { console.error('\nLudus MCP Server Initialization'); console.error('================================='); console.error('Please provide the following information (provided by your administrator):\n'); // Ask for connection method first console.error('📡 Connection Method'); console.error('Choose how to connect to the Ludus API:'); console.error(' (w) WireGuard VPN - Direct access via WireGuard tunnel'); console.error(' (s) SSH Tunnel - Access via SSH port forwarding'); const methodChoice = await this.prompt('Connection method? (w/s) [default: w]: '); const connectionMethod: 'wireguard' | 'ssh-tunnel' = methodChoice.toLowerCase() === 's' ? 'ssh-tunnel' : 'wireguard'; const adminUser = await this.prompt('Ludus Admin Username: '); let wireguardConfig: string | undefined; let ludusUrl: string; if (connectionMethod === 'wireguard') { wireguardConfig = await this.prompt('WireGuard Config Path: '); ludusUrl = 'https://198.51.100.1:8080'; } else { console.error('Using SSH tunnel mode - will connect to https://localhost:8080'); ludusUrl = 'https://localhost:8080'; } const apiKey = await this.securePrompt('API Key: '); const sshHost = await this.prompt('Ludus Server SSH Host: '); const sshUser = await this.prompt('Ludus Server SSH User: '); // Ask for SSH authentication method console.error('\nSSH Authentication Method'); console.error('Choose SSH authentication method:'); console.error(' (p) Password - Use SSH password authentication'); console.error(' (k) Key - Use SSH key-based authentication'); const authChoice = await this.prompt('SSH auth method? (p/k) [default: p]: '); let sshAuthMethod: 'password' | 'key'; let sshPassword: string | undefined; let sshKeyPath: string | undefined; let sshKeyPassphrase: string | undefined; if (authChoice.toLowerCase() === 'k') { sshAuthMethod = 'key'; sshKeyPath = await this.prompt('SSH Key Path: '); // Ask for passphrase if key is protected const hasPassphrase = await this.prompt('Does your SSH key have a passphrase? (y/n) [default: n]: '); if (hasPassphrase.toLowerCase() === 'y') { sshKeyPassphrase = await this.securePrompt('SSH Key Passphrase: '); } } else { sshAuthMethod = 'password'; sshPassword = await this.securePrompt('SSH Password: '); } const useCustomUrl = await this.prompt('Use custom Ludus URL? (y/n) [default: n]: '); if (useCustomUrl.toLowerCase() === 'y') { ludusUrl = await this.prompt('Ludus URL: '); } const verifySSL = await this.prompt('Verify SSL certificates? (y/n) [default: n]: '); return { adminUser, connectionMethod, wireguardConfig, apiKey, sshHost, sshUser, sshAuthMethod, sshPassword, sshKeyPath, sshKeyPassphrase, ludusUrl, verifySSL: verifySSL.toLowerCase() === 'y' }; } /** * Main interactive setup flow */ async runSetup(forceInteractive: boolean = false): Promise<SetupResult> { try { let config: LudusConfig; // Try to get config from different sources in order of preference const envConfig = this.getConfigFromEnvironment(); const keyringSummary = await getCredentialSummary(); const keyringSupportAvailable = isKeyringSupportAvailable(); if (forceInteractive) { // Force interactive mode - skip environment and keyring checks if (process.stdin.isTTY) { config = await this.promptForCredentials(); } else { return { success: false, message: 'Interactive setup requested but no TTY available.' }; } } else if (envConfig) { console.error('\nUsing configuration from environment variables'); config = envConfig; } else { const keyringConfig = await this.getConfigFromKeyring(); if (keyringConfig) { console.error('\nUsing stored credentials from keyring'); config = keyringConfig; } else if (process.stdin.isTTY) { // Show keyring status for debugging if (keyringSupportAvailable) { console.error('\nKeyring credentials summary:'); for (const [key, exists] of Object.entries(keyringSummary)) { console.error(` ${key}: ${exists ? '' : ''}`); } } // Interactive mode config = await this.promptForCredentials(); } else { // No TTY and no stored credentials - error return { success: false, message: 'No configuration available. Please set environment variables, run setup interactively, or use --setup-keyring flag.' }; } } console.error('\nValidating configuration...'); // Handle connection method if (config.connectionMethod === 'wireguard') { // 1. Check WireGuard availability if (!this.checkWireGuardAvailable()) { return { success: false, message: 'WireGuard is not installed or not available. Please install WireGuard first.' }; } // 2. Connect to WireGuard VPN console.error('Connecting to WireGuard VPN...'); if (!config.wireguardConfig || !await this.connectWireGuard(config.wireguardConfig)) { return { success: false, message: 'Failed to connect to WireGuard VPN. Please check your configuration file.' }; } console.error('WireGuard VPN connected'); // Important WireGuard usage guidance console.error(''); console.error('IMPORTANT: WireGuard Usage Notes'); console.error('═══════════════════════════════════════'); console.error('• WireGuard tunnels must be manually started before launching Claude Desktop'); console.error('• If WireGuard is down, the MCP client will attempt SSH tunnel fallback'); console.error('• For automatic startup, consider configuring WireGuard as a Windows service'); console.error('• To make WireGuard start automatically on boot:'); console.error(' - Use WireGuard GUI: Right-click tunnel → "Install as Service"'); console.error(' - Or manually: Run as Admin and use /installtunnelservice flag'); console.error(''); } else { // SSH tunnel mode console.error('Setting up SSH tunnel...'); if (!await this.createSshTunnel(config)) { return { success: false, message: 'Failed to create SSH tunnel. Please check your SSH credentials.' }; } console.error('SSH tunnel established'); } // 3. Validate API key console.error('🔑 Validating API key...'); if (!await this.validateApiKey(config.apiKey, config.ludusUrl)) { return { success: false, message: 'API key validation failed. Please check your API key and server connectivity.' }; } console.error('API key validated'); // 4. Test SSH connectivity console.error('🔌 Testing SSH connectivity...'); if (!await this.testSshConnectivity(config)) { return { success: false, message: 'SSH connectivity test failed. Please check your SSH configuration.' }; } console.error('SSH connectivity verified'); // 5. Store credentials in keyring if setup was successful and credentials were entered interactively if (process.stdin.isTTY && !envConfig) { await this.storeConfigInKeyring(config); } // Initialization completed - avoid console output that might interfere with MCP protocol // Final success message with WireGuard reminder if (config.connectionMethod === 'wireguard') { console.error('Setup complete!'); console.error(''); console.error('REMEMBER: For Claude Desktop usage:'); console.error(' 1. Manually start your WireGuard tunnel before launching Claude Desktop'); console.error(' 2. Or configure WireGuard as a Windows service for automatic startup'); console.error(' 3. If WireGuard is down, the MCP client will try SSH tunnel fallback'); console.error(''); } else { console.error('Setup complete!'); } return { success: true, message: 'Ludus MCP server initialized successfully', config }; } catch (error: any) { this.logger.error('Initialization failed', { error }); return { success: false, message: `Initialization failed: ${error.message}` }; } finally { this.rl.close(); } } /** * Check if setup is complete by verifying stored credentials */ static async checkSetupComplete(): Promise<boolean> { try { // Check environment variables first const envVars = ['LUDUS_ADMIN_USER', 'LUDUS_WIREGUARD_CONFIG', 'LUDUS_API_KEY', 'LUDUS_SSH_HOST', 'LUDUS_SSH_USER', 'LUDUS_SSH_PASSWORD']; if (envVars.every(v => process.env[v])) { return true; } // Check keyring if available if (isKeyringSupportAvailable()) { const summary = await getCredentialSummary(); return Object.values(summary).every(exists => exists); } return false; } catch (error) { return false; } } }