feat: MCP bridge auto-download — Wraith manages its own companion binary
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m53s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m53s
On startup, Wraith checks if wraith-mcp-bridge exists in the data
directory. If missing or version mismatch, downloads the correct
version from Gitea packages automatically. No installer changes needed.
Flow:
1. Check data_dir/wraith-mcp-bridge.exe exists
2. Check data_dir/mcp-bridge-version matches app version
3. If not, download from packages/vstockwell/generic/wraith/{ver}/
4. Set execute permissions on Unix
5. Write version marker
Also exposes mcp_bridge_path command so the frontend can show the
path in settings for users to add to PATH or configure Claude Code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
beac33614a
commit
f22f85ac00
@ -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.
|
/// 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.
|
/// Called by the frontend when the user switches tabs, emitted to the copilot.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@ -172,6 +172,16 @@ pub fn run() {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
let _ = write_log(&log_file, "Setup: MCP spawn dispatched");
|
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) => {
|
Err(panic) => {
|
||||||
let msg = if let Some(s) = panic.downcast_ref::<String>() {
|
let msg = if let Some(s) = panic.downcast_ref::<String>() {
|
||||||
@ -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::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::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::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::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::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,
|
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,
|
||||||
|
|||||||
85
src-tauri/src/mcp/bridge_manager.rs
Normal file
85
src-tauri/src/mcp/bridge_manager.rs
Normal file
@ -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(())
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@
|
|||||||
pub mod scrollback;
|
pub mod scrollback;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod error_watcher;
|
pub mod error_watcher;
|
||||||
|
pub mod bridge_manager;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user