diff --git a/src-tauri/src/commands/mcp_commands.rs b/src-tauri/src/commands/mcp_commands.rs index 24714d3..1b3fc6a 100644 --- a/src-tauri/src/commands/mcp_commands.rs +++ b/src-tauri/src/commands/mcp_commands.rs @@ -120,6 +120,12 @@ pub async fn mcp_terminal_execute( } } +/// Get the path where the MCP bridge binary is installed. +#[tauri::command] +pub fn mcp_bridge_path() -> String { + crate::mcp::bridge_manager::bridge_path().to_string_lossy().to_string() +} + /// Get the active session context — last 20 lines of scrollback for a session. /// Called by the frontend when the user switches tabs, emitted to the copilot. #[tauri::command] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b60ff80..206a4e7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -172,6 +172,16 @@ pub fn run() { }); })); let _ = write_log(&log_file, "Setup: MCP spawn dispatched"); + + // Download/update MCP bridge binary if needed + let app_ver = app.config().version.clone().unwrap_or_else(|| "0.0.0".to_string()); + let log_file3 = log_file.clone(); + tauri::async_runtime::spawn(async move { + match mcp::bridge_manager::ensure_bridge(&app_ver).await { + Ok(()) => { let _ = write_log(&log_file3, "Setup: MCP bridge binary OK"); } + Err(e) => { let _ = write_log(&log_file3, &format!("Setup: MCP bridge download failed: {}", e)); } + } + }); } Err(panic) => { let msg = if let Some(s) = panic.downcast_ref::() { @@ -199,7 +209,7 @@ pub fn run() { commands::rdp_commands::connect_rdp, commands::rdp_commands::rdp_get_frame, commands::rdp_commands::rdp_send_mouse, commands::rdp_commands::rdp_send_key, commands::rdp_commands::rdp_send_clipboard, commands::rdp_commands::disconnect_rdp, commands::rdp_commands::list_rdp_sessions, commands::theme_commands::list_themes, commands::theme_commands::get_theme, commands::pty_commands::list_available_shells, commands::pty_commands::spawn_local_shell, commands::pty_commands::pty_write, commands::pty_commands::pty_resize, commands::pty_commands::disconnect_pty, - commands::mcp_commands::mcp_list_sessions, commands::mcp_commands::mcp_terminal_read, commands::mcp_commands::mcp_terminal_execute, commands::mcp_commands::mcp_get_session_context, + commands::mcp_commands::mcp_list_sessions, commands::mcp_commands::mcp_terminal_read, commands::mcp_commands::mcp_terminal_execute, commands::mcp_commands::mcp_get_session_context, commands::mcp_commands::mcp_bridge_path, commands::scanner_commands::scan_network, commands::scanner_commands::scan_ports, commands::scanner_commands::quick_scan, commands::tools_commands::tool_ping, commands::tools_commands::tool_traceroute, commands::tools_commands::tool_wake_on_lan, commands::tools_commands::tool_generate_ssh_key, commands::tools_commands::tool_generate_password, commands::tools_commands_r2::tool_dns_lookup, commands::tools_commands_r2::tool_whois, commands::tools_commands_r2::tool_bandwidth_iperf, commands::tools_commands_r2::tool_bandwidth_speedtest, commands::tools_commands_r2::tool_subnet_calc, diff --git a/src-tauri/src/mcp/bridge_manager.rs b/src-tauri/src/mcp/bridge_manager.rs new file mode 100644 index 0000000..3dbaa8f --- /dev/null +++ b/src-tauri/src/mcp/bridge_manager.rs @@ -0,0 +1,85 @@ +//! MCP bridge binary self-management. +//! +//! On startup, checks if wraith-mcp-bridge exists in the data directory. +//! If missing or outdated, downloads the correct version from Gitea packages. + +use std::path::PathBuf; + +/// Get the expected path for the bridge binary. +pub fn bridge_path() -> PathBuf { + let dir = crate::data_directory(); + if cfg!(windows) { + dir.join("wraith-mcp-bridge.exe") + } else { + dir.join("wraith-mcp-bridge") + } +} + +/// Check if the bridge binary exists and is the correct version. +/// If not, download it from Gitea packages. +pub async fn ensure_bridge(app_version: &str) -> Result<(), String> { + let path = bridge_path(); + let version_file = crate::data_directory().join("mcp-bridge-version"); + + // Check if bridge exists and version matches + if path.exists() { + if let Ok(installed_ver) = std::fs::read_to_string(&version_file) { + if installed_ver.trim() == app_version { + wraith_log!("[MCP Bridge] v{} already installed at {}", app_version, path.display()); + return Ok(()); + } + } + } + + wraith_log!("[MCP Bridge] Downloading v{} to {}", app_version, path.display()); + + let binary_name = if cfg!(windows) { + "wraith-mcp-bridge.exe" + } else { + "wraith-mcp-bridge" + }; + + let url = format!( + "https://git.command.vigilcyber.com/api/packages/vstockwell/generic/wraith/{}/{}", + app_version, binary_name + ); + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| format!("HTTP client error: {}", e))?; + + let resp = client.get(&url).send().await + .map_err(|e| format!("Failed to download MCP bridge: {}", e))?; + + if !resp.status().is_success() { + return Err(format!("MCP bridge download failed: HTTP {}", resp.status())); + } + + let bytes = resp.bytes().await + .map_err(|e| format!("Failed to read MCP bridge response: {}", e))?; + + // Write the binary + std::fs::write(&path, &bytes) + .map_err(|e| format!("Failed to write MCP bridge to {}: {}", path.display(), e))?; + + // Make executable on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&path) + .map_err(|e| format!("Failed to read permissions: {}", e))? + .permissions(); + perms.set_mode(0o755); + std::fs::set_permissions(&path, perms) + .map_err(|e| format!("Failed to set execute permission: {}", e))?; + } + + // Write version marker + std::fs::write(&version_file, app_version) + .map_err(|e| format!("Failed to write version file: {}", e))?; + + wraith_log!("[MCP Bridge] v{} installed successfully ({} bytes)", app_version, bytes.len()); + + Ok(()) +} diff --git a/src-tauri/src/mcp/mod.rs b/src-tauri/src/mcp/mod.rs index 125081c..8147157 100644 --- a/src-tauri/src/mcp/mod.rs +++ b/src-tauri/src/mcp/mod.rs @@ -7,6 +7,7 @@ pub mod scrollback; pub mod server; pub mod error_watcher; +pub mod bridge_manager; use std::sync::Arc;