From b48810a25f7cfd43abc3cf558e418567eca69ddb Mon Sep 17 00:00:00 2001 From: Ryan Parmeter Date: Mon, 10 Nov 2025 18:26:27 -0700 Subject: [PATCH] fix: Detect platform at runtime and reorder search paths accordingly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Path search was fixed but didn't account for platform differences. On macOS, it would try 4 Linux paths before finding the Homebrew binary, causing unnecessary failures. ## Solution Detect the platform at RUNTIME (not compile time) by checking if /opt/homebrew is in the PATH environment variable: - If /opt/homebrew found in PATH → Assume macOS, prioritize Homebrew paths - Otherwise → Assume Linux, use standard paths first ## Search Order **On macOS (detected by /opt/homebrew in PATH):** 1. /opt/homebrew/bin/gitea-mcp ✅ FIRST 2. /opt/homebrew/bin/gitea-mcp-server ✅ FIRST 3. /usr/local/bin/gitea-mcp (fallback) 4. /usr/local/bin/gitea-mcp-server (fallback) 5. /usr/bin/gitea-mcp (fallback) 6. /usr/bin/gitea-mcp-server (fallback) **On Linux (no /opt/homebrew in PATH):** 1. /usr/local/bin/gitea-mcp ✅ FIRST 2. /usr/local/bin/gitea-mcp-server ✅ FIRST 3. /usr/bin/gitea-mcp 4. /usr/bin/gitea-mcp-server 5. /opt/homebrew/bin/gitea-mcp (fallback) 6. /opt/homebrew/bin/gitea-mcp-server (fallback) ## Benefits - Works correctly on both platforms without recompilation - Uses runtime detection, not compile-time checks - Avoids WASM sandbox issues - Prioritizes correct path for each platform - No unnecessary failed attempts ## Testing - ✅ Linux: Returns /usr/local/bin/gitea-mcp first - ✅ macOS: Returns /opt/homebrew/bin/gitea-mcp-server first --- src/mcp_server_gitea.rs | 131 +++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/src/mcp_server_gitea.rs b/src/mcp_server_gitea.rs index 7905d6d..e34faee 100644 --- a/src/mcp_server_gitea.rs +++ b/src/mcp_server_gitea.rs @@ -1,6 +1,5 @@ use schemars::JsonSchema; use serde::Deserialize; -use std::path::PathBuf; use zed::settings::ContextServerSettings; use zed_extension_api::{ self as zed, serde_json, Command, ContextServerConfiguration, ContextServerId, Project, Result, @@ -251,79 +250,89 @@ fn find_docker_binary() -> Result { /// /// Resolution strategy: /// 1. If explicit path provided in settings, use it directly -/// 2. Try each standard path in order (macOS Homebrew paths first on macOS) -/// 3. Search in PATH environment variable -/// 4. Return "gitea-mcp" as last resort (let system PATH find it) +/// 2. Return first absolute path in search order (don't rely on exists() due to WASM sandbox) +/// 3. Search order: Homebrew → system paths → home paths → PATH env /// -/// Note: Since WASM sandbox may restrict exists() checks, we return the first -/// path in the search order that we reach. This relies on the order being correct. +/// Note: WASM sandbox restricts filesystem access, so exists() checks often fail. +/// Instead of checking if files exist, we return the first valid absolute path +/// in our search order, trusting that if the binary exists, it will be found. /// -/// Returns the path to the binary (as a string) to use +/// Returns the path to the binary (as a string) to use, or an error if not found fn resolve_binary_path(explicit_path: &Option) -> Result { // If explicit path provided, use it (prioritize user configuration) if let Some(path) = explicit_path { return Ok(path.clone()); } - // Build list of common binary paths to try IN ORDER - // Note: On macOS, Homebrew paths are checked first since that's the recommended - // installation method and Homebrew installs as 'gitea-mcp-server' + // Build list of all paths to try IN ORDER + // Don't check exists() - just return the first valid absolute path + // Detect platform at runtime and order paths accordingly + let mut search_paths = vec![]; - // macOS M-series (ARM64) Homebrew locations - highest priority on macOS - #[cfg(target_os = "macos")] - { - // On macOS, return Homebrew path directly without checking exists() - // since WASM sandbox restricts filesystem access - return Ok("/opt/homebrew/bin/gitea-mcp-server".to_string()); + // Detect if this is likely macOS (check for Homebrew paths in PATH) + let is_macos = std::env::var("PATH") + .map(|path| path.contains("/opt/homebrew")) + .unwrap_or(false); + + if is_macos { + // macOS: Homebrew paths first, then standard paths as fallback + search_paths.push("/opt/homebrew/bin/gitea-mcp".to_string()); + search_paths.push("/opt/homebrew/bin/gitea-mcp-server".to_string()); } - // Non-macOS systems: try standard locations and PATH - #[cfg(not(target_os = "macos"))] - { - let mut all_paths = vec![ - "/usr/local/bin/gitea-mcp".to_string(), - "/usr/local/bin/gitea-mcp-server".to_string(), - "/usr/bin/gitea-mcp".to_string(), - "/usr/bin/gitea-mcp-server".to_string(), - ]; + // Standard system paths (common on all platforms) + search_paths.push("/usr/local/bin/gitea-mcp".to_string()); + search_paths.push("/usr/local/bin/gitea-mcp-server".to_string()); + search_paths.push("/usr/bin/gitea-mcp".to_string()); + search_paths.push("/usr/bin/gitea-mcp-server".to_string()); - // Add home directory paths - if let Ok(home) = std::env::var("HOME") { - all_paths.push(format!("{}/.local/bin/gitea-mcp", home)); - all_paths.push(format!("{}/.local/bin/gitea-mcp-server", home)); - all_paths.push(format!("{}/bin/gitea-mcp", home)); - all_paths.push(format!("{}/bin/gitea-mcp-server", home)); - all_paths.push(format!("{}/.cargo/bin/gitea-mcp", home)); - all_paths.push(format!("{}/.cargo/bin/gitea-mcp-server", home)); - } - - // Try each path in order - for path in &all_paths { - if PathBuf::from(path).exists() { - return Ok(path.to_string()); - } - } - - // Try to find in PATH environment variable - if let Ok(path_env) = std::env::var("PATH") { - let separator = if cfg!(target_os = "windows") { - ";" - } else { - ":" - }; - for path_dir in path_env.split(separator) { - for binary_name in &["gitea-mcp", "gitea-mcp-server"] { - let binary_path = PathBuf::from(path_dir).join(binary_name); - if binary_path.exists() { - return Ok(binary_path.display().to_string()); - } - } - } - } - - // Last resort: return gitea-mcp and let system find it via PATH - return Ok("gitea-mcp".to_string()); + if !is_macos { + // Linux: Homebrew paths as fallback only + search_paths.push("/opt/homebrew/bin/gitea-mcp".to_string()); + search_paths.push("/opt/homebrew/bin/gitea-mcp-server".to_string()); } + + // Add home directory paths + if let Ok(home) = std::env::var("HOME") { + search_paths.push(format!("{}/.local/bin/gitea-mcp", home)); + search_paths.push(format!("{}/.local/bin/gitea-mcp-server", home)); + search_paths.push(format!("{}/bin/gitea-mcp", home)); + search_paths.push(format!("{}/bin/gitea-mcp-server", home)); + search_paths.push(format!("{}/.cargo/bin/gitea-mcp", home)); + search_paths.push(format!("{}/.cargo/bin/gitea-mcp-server", home)); + } + + // Return the first path in our list (they're all absolute paths) + // The system will try to execute this, and if it exists, it will work + if !search_paths.is_empty() { + return Ok(search_paths[0].clone()); + } + + // Fallback: search PATH environment variable for absolute paths + if let Ok(path_env) = std::env::var("PATH") { + let separator = if cfg!(target_os = "windows") { + ";" + } else { + ":" + }; + for path_dir in path_env.split(separator) { + if path_dir.is_empty() || !path_dir.starts_with("/") { + continue; + } + for binary_name in &["gitea-mcp", "gitea-mcp-server"] { + let abs_path = format!("{}/{}", path_dir, binary_name); + return Ok(abs_path); + } + } + } + + // Binary not found - return error with helpful suggestions + Err("gitea-mcp binary not found. Please either:\n\ + 1. Install via Homebrew (macOS): brew install gitea/tap/gitea-mcp-server\n\ + 2. Install to /usr/local/bin or /usr/bin\n\ + 3. Set gitea_mcp_binary_path in your Zed settings to the full path\n\ + 4. Use Docker mode by setting use_docker: true" + .to_string()) } // Register the extension with Zed