diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 24a7ff2e88..93ec79c8a2 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -340,7 +340,68 @@ pub(crate) async fn resolve_mcp_server_command() -> Result<(String, Vec) fn resolve_mcp_server_command_sync() -> Option<(String, Vec)> { let runtime = resolve_node_runtime(); - resolve_managed_mcp_command(runtime.as_ref(), locate_mcp_bin) + if let Some(command) = runtime.as_ref().and_then(mcp_command_for_runtime) { + return Some(command); + } + + let path_shim = locate_mcp_bin(); + resolve_mise_mcp_command_from_path(runtime.as_ref(), path_shim.as_deref()) + .or_else(|| resolve_managed_mcp_command(runtime.as_ref(), || path_shim)) +} + +fn resolve_mise_mcp_command_from_path( + runtime: Option<&NodeRuntime>, + shim_path: Option<&Path>, +) -> Option<(String, Vec)> { + let shim_path = shim_path?; + if !is_shims_command_path(shim_path) { + return None; + } + let mise_path = locate_command("mise").map(PathBuf::from).and_then(|path| canonical_runtime_path(&path))?; + resolve_mise_mcp_command(runtime, Some(shim_path), |command| mise_which_path(&mise_path, command)) +} + +fn resolve_mise_mcp_command( + runtime: Option<&NodeRuntime>, + shim_path: Option<&Path>, + mut resolve_mise_command: impl FnMut(&str) -> Option, +) -> Option<(String, Vec)> { + let runtime = runtime?; + if !is_mcp_compatible_node_version(&runtime.node_version) { + return None; + } + let shim_path = shim_path?; + if !is_shims_command_path(shim_path) { + return None; + } + + let runtime_node = canonical_runtime_path(&runtime.node_path)?; + let mise_node = resolve_mise_command("node").and_then(|path| canonical_runtime_path(&path))?; + if runtime_node != mise_node { + return None; + } + + let shim_path = canonical_runtime_path(shim_path)?; + let script_path = resolve_mise_command("dbx-mcp-server").and_then(|path| canonical_runtime_path(&path))?; + if script_path == shim_path { + return None; + } + let (_, package) = mcp_package_from_script(&script_path)?; + let node_version = parse_node_version(&runtime.node_version)?; + if !mcp_package_supports_node(&package, node_version) { + return None; + } + + Some((path_string(&runtime_node), vec![path_string(&package.script_path)])) +} + +fn is_shims_command_path(path: &Path) -> bool { + path.is_file() && path.parent().and_then(Path::file_name).is_some_and(|name| name.eq_ignore_ascii_case("shims")) +} + +fn mise_which_path(mise_path: &Path, command: &str) -> Option { + let output = direct_command_stdout(mise_path, &["which", command]).ok()?; + first_non_empty_line(output).and_then(|path| canonical_runtime_path(Path::new(&path))) } fn resolve_managed_mcp_command( @@ -1134,8 +1195,9 @@ mod tests { use super::{ canonical_runtime_path, is_mcp_compatible_node_version, mcp_command_for_runtime, mcp_native_binary_path_for, mcp_package, normalized_reported_path, npm_cli_candidates, parse_minimum_node_version, parse_node_version, - prefer_runtime, require_managed_mcp_command, resolve_managed_mcp_command, stdout_after_shell_marker, - NodeRuntime, NodeVersion, MCP_MIN_NODE_VERSION_REQUIREMENT, MCP_PACKAGE_NAME, SHELL_COMMAND_MARKER, + prefer_runtime, require_managed_mcp_command, resolve_managed_mcp_command, resolve_mise_mcp_command, + stdout_after_shell_marker, NodeRuntime, NodeVersion, MCP_MIN_NODE_VERSION_REQUIREMENT, MCP_PACKAGE_NAME, + SHELL_COMMAND_MARKER, }; #[cfg(not(windows))] use super::{shell_command_script, shell_quote}; @@ -1389,6 +1451,116 @@ mod tests { assert_eq!(command.1, vec!["/runtime/node-24-mcp/dist/index.js"]); } + #[test] + fn mcp_command_resolves_native_mise_shim_to_declared_package_entry() { + use std::cell::Cell; + use std::time::{SystemTime, UNIX_EPOCH}; + + let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let dir = std::env::temp_dir().join(format!("dbx mise shim test {} {nonce}", std::process::id())); + let shim_dir = dir.join("mise").join("shims"); + let shim_path = shim_dir.join("dbx-mcp-server.exe"); + let node_path = dir.join("mise").join("installs").join("node").join("22.23.1").join("node.exe"); + let package_root = dir + .join("mise") + .join("installs") + .join("npm-db-server") + .join("node_modules") + .join("@dbx-app") + .join("mcp-server"); + let script_path = package_root.join("bin").join("dbx-mcp-server.js"); + + std::fs::create_dir_all(&shim_dir).unwrap(); + std::fs::create_dir_all(node_path.parent().unwrap()).unwrap(); + std::fs::create_dir_all(script_path.parent().unwrap()).unwrap(); + std::fs::write(&shim_path, "native mise shim\n").unwrap(); + std::fs::write(&node_path, "real node runtime\n").unwrap(); + std::fs::write(&script_path, "// declared MCP entry\n").unwrap(); + std::fs::write( + package_root.join("package.json"), + r#"{"name":"@dbx-app/mcp-server","version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=18.18.0"}}"#, + ) + .unwrap(); + + assert!(super::node_script_from_launcher(&shim_path).is_none()); + let node_path = canonical_runtime_path(&node_path).unwrap(); + let script_path = canonical_runtime_path(&script_path).unwrap(); + let compatible_runtime = runtime_with_version_and_root( + node_path.to_string_lossy().as_ref(), + dir.join("npm-root").to_string_lossy().as_ref(), + None, + "v22.23.1", + ); + + let command = resolve_mise_mcp_command(Some(&compatible_runtime), Some(&shim_path), |command| match command { + "node" => Some(node_path.clone()), + "dbx-mcp-server" => Some(script_path.clone()), + _ => None, + }) + .unwrap(); + + assert_eq!(command.0, node_path.to_string_lossy()); + assert_eq!(command.1, vec![script_path.to_string_lossy()]); + + let wrong_node = dir.join("other-node.exe"); + std::fs::write(&wrong_node, "wrong node runtime\n").unwrap(); + assert!(resolve_mise_mcp_command(Some(&compatible_runtime), Some(&shim_path), |command| match command { + "node" => Some(wrong_node.clone()), + "dbx-mcp-server" => Some(script_path.clone()), + _ => None, + }) + .is_none()); + + let old_runtime = runtime_with_version_and_root( + node_path.to_string_lossy().as_ref(), + dir.join("old-npm-root").to_string_lossy().as_ref(), + None, + "v18.17.1", + ); + assert!(resolve_mise_mcp_command(Some(&old_runtime), Some(&shim_path), |command| match command { + "node" => Some(node_path.clone()), + "dbx-mcp-server" => Some(script_path.clone()), + _ => None, + }) + .is_none()); + + assert!(resolve_mise_mcp_command(Some(&compatible_runtime), Some(&shim_path), |_| None).is_none()); + let unrelated_script = package_root.join("bin").join("other.js"); + std::fs::write(&unrelated_script, "// not the declared entry\n").unwrap(); + assert!(resolve_mise_mcp_command(Some(&compatible_runtime), Some(&shim_path), |command| match command { + "node" => Some(node_path.clone()), + "dbx-mcp-server" => Some(unrelated_script.clone()), + _ => None, + }) + .is_none()); + + let ordinary_bin = dir.join("bin"); + let ordinary_shim = ordinary_bin.join("dbx-mcp-server.exe"); + std::fs::create_dir_all(&ordinary_bin).unwrap(); + std::fs::write(&ordinary_shim, "ordinary native shim\n").unwrap(); + let resolver_calls = Cell::new(0); + assert!(resolve_mise_mcp_command(Some(&compatible_runtime), Some(&ordinary_shim), |_| { + resolver_calls.set(resolver_calls.get() + 1); + None + }) + .is_none()); + assert_eq!(resolver_calls.get(), 0); + + std::fs::write( + package_root.join("package.json"), + r#"{"name":"@dbx-app/mcp-server","version":"0.4.44","bin":{"dbx-mcp-server":"bin/dbx-mcp-server.js"},"engines":{"node":">=23.0.0"}}"#, + ) + .unwrap(); + assert!(resolve_mise_mcp_command(Some(&compatible_runtime), Some(&shim_path), |command| match command { + "node" => Some(node_path.clone()), + "dbx-mcp-server" => Some(script_path.clone()), + _ => None, + }) + .is_none()); + + let _ = std::fs::remove_dir_all(dir); + } + #[test] fn incompatible_runtime_does_not_fall_back_to_available_mcp_shim() { let incompatible = runtime_with_version_and_root(