diff --git a/.gitignore b/.gitignore index 9209dd769..d2f46cc8f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ src-tauri/resources/themes src-tauri/Cargo.lock src-tauri/icons !src-tauri/icons/icon.png -src-tauri/gen/apple \ No newline at end of file +src-tauri/gen/apple +src-tauri/resources/bin diff --git a/package.json b/package.json index bb519bd9f..e276fc47c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "install:cortex:linux:darwin": "cd src-tauri/binaries && ./download.sh", "install:cortex:win32": "cd src-tauri/binaries && download.bat", "install:cortex": "run-script-os", + "download:bin": "node ./scripts/download-bin.js", "dev:tauri": "yarn build:icon && yarn copy:assets:tauri && tauri dev", "build:icon": "tauri icon ./src-tauri/icons/icon.png", "build:server": "cd server && yarn build", @@ -51,6 +52,8 @@ "jest-environment-jsdom": "^29.7.0", "rimraf": "^3.0.2", "run-script-os": "^1.1.6", + "tar": "^4.4.19", + "unzipper": "^0.12.3", "wait-on": "^7.0.1" }, "version": "0.0.0", diff --git a/scripts/download-bin.js b/scripts/download-bin.js new file mode 100755 index 000000000..618f8f785 --- /dev/null +++ b/scripts/download-bin.js @@ -0,0 +1,219 @@ +console.log('Script is running') +// scripts/download.js +import https from 'https' +import fs, { copyFile, mkdirSync } from 'fs' +import os from 'os' +import path from 'path' +import unzipper from 'unzipper' +import tar from 'tar' +import { copySync } from 'cpx' + +function download(url, dest) { + return new Promise((resolve, reject) => { + console.log(`Downloading ${url} to ${dest}`) + const file = fs.createWriteStream(dest) + https + .get(url, (response) => { + console.log(`Response status code: ${response.statusCode}`) + if ( + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location + ) { + // Handle redirect + const redirectURL = response.headers.location + console.log(`Redirecting to ${redirectURL}`) + download(redirectURL, dest).then(resolve, reject) // Recursive call + return + } else if (response.statusCode !== 200) { + reject(`Failed to get '${url}' (${response.statusCode})`) + return + } + response.pipe(file) + file.on('finish', () => { + file.close(resolve) + }) + }) + .on('error', (err) => { + fs.unlink(dest, () => reject(err.message)) + }) + }) +} + +async function decompress(filePath, targetDir) { + console.log(`Decompressing ${filePath} to ${targetDir}`) + if (filePath.endsWith('.zip')) { + await fs + .createReadStream(filePath) + .pipe(unzipper.Extract({ path: targetDir })) + .promise() + } else if (filePath.endsWith('.tar.gz')) { + await tar.x({ + file: filePath, + cwd: targetDir, + }) + } else { + throw new Error(`Unsupported archive format: ${filePath}`) + } +} + +function getPlatformArch() { + const platform = os.platform() // 'darwin', 'linux', 'win32' + const arch = os.arch() // 'x64', 'arm64', etc. + + let bunPlatform, uvPlatform + + if (platform === 'darwin') { + bunPlatform = arch === 'arm64' ? 'darwin-aarch64' : 'darwin-x86' + uvPlatform = + arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin' + } else if (platform === 'linux') { + bunPlatform = arch === 'arm64' ? 'linux-aarch64' : 'linux-x64' + uvPlatform = arch === 'arm64' ? 'aarch64-unknown-linux-gnu' : 'x86_64-unknown-linux-gnu' + } else if (platform === 'win32') { + bunPlatform = 'windows-x64' // Bun has limited Windows support + uvPlatform = 'x86_64-pc-windows-msvc' + } else { + throw new Error(`Unsupported platform: ${platform}`) + } + + return { bunPlatform, uvPlatform } +} + +async function main() { + console.log('Starting main function') + const platform = os.platform() + const { bunPlatform, uvPlatform } = getPlatformArch() + console.log(`bunPlatform: ${bunPlatform}, uvPlatform: ${uvPlatform}`) + + const binDir = 'src-tauri/resources/bin' + const tempBinDir = 'scripts/dist' + const bunPath = `${tempBinDir}/bun-${bunPlatform}.zip` + let uvPath = `${tempBinDir}/uv-${uvPlatform}.tar.gz` + if (platform === 'win32') { + uvPath = `${tempBinDir}/uv-${uvPlatform}.zip` + } + try { + mkdirSync('scripts/dist') + } catch (err) { + // Expect EEXIST error if the directory already exists + } + + // Adjust these URLs based on latest releases + const bunVersion = '1.2.10' // Example Bun version + const bunUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/bun-${bunPlatform}.zip` + + const uvVersion = '0.6.17' // Example UV version + let uvUrl = `https://github.com/astral-sh/uv/releases/download/${uvVersion}/uv-${uvPlatform}.tar.gz` + if (platform === 'win32') { + uvUrl = `https://github.com/astral-sh/uv/releases/download/${uvVersion}/uv-${uvPlatform}.zip` + } + + console.log(`Downloading Bun for ${bunPlatform}...`) + await download(bunUrl, path.join(tempBinDir, `bun-${bunPlatform}.zip`)) + await decompress(bunPath, tempBinDir) + try { + copySync( + path.join(tempBinDir, `bun-${bunPlatform}`, 'bun'), + path.join(binDir) + ) + if(platform === 'darwin') { + copyFile(path.join(binDir, 'bun'), path.join(binDir, 'bun-x86_64-apple-darwin'), (err) => { + if (err) { + console.log("Error Found:", err); + } + }) + copyFile(path.join(binDir, 'bun'), path.join(binDir, 'bun-aarch64-apple-darwin'), (err) => { + if (err) { + console.log("Error Found:", err); + } + }) + } else if (platform === 'linux') { + copyFile(path.join(binDir, 'bun'), path.join(binDir, 'bun-x86_64-unknown-linux-gnu'), (err) => { + if (err) { + console.log("Error Found:", err); + } + }) + } + } catch (err) { + // Expect EEXIST error + } + try { + copySync( + path.join(tempBinDir, `bun-${bunPlatform}`, 'bun.exe'), + path.join(binDir) + ) + if (platform === 'win32') { + copyFile(path.join(binDir, 'bun.exe'), path.join(binDir, 'bun-x86_64-pc-windows-msvc.exe'), (err) => { + if (err) { + console.log("Error Found:", err); + } + }) + } + } catch (err) { + // Expect EEXIST error + } + console.log('Bun downloaded.') + + console.log(`Downloading UV for ${uvPlatform}...`) + if (platform === 'win32') { + await download(uvUrl, path.join(tempBinDir, `uv-${uvPlatform}.zip`)) + } else { + await download(uvUrl, path.join(tempBinDir, `uv-${uvPlatform}.tar.gz`)) + } + await decompress(uvPath, tempBinDir) + try { + copySync( + path.join(tempBinDir, `uv-${uvPlatform}`, 'uv'), + path.join(binDir) + ) + if (platform === 'darwin') { + copyFile(path.join(binDir, 'uv'), path.join(binDir, 'uv-x86_64-apple-darwin'), (err) => { + if (err) { + console.log("Error Found:", err); + } + }) + copyFile(path.join(binDir, 'uv'), path.join(binDir, 'uv-aarch64-apple-darwin'), (err) => { + if (err) { + console.log("Error Found:", err); + } + }) + } else if (platform === 'linux') { + copyFile(path.join(binDir, 'uv'), path.join(binDir, 'uv-x86_64-unknown-linux-gnu'), (err) => { + if (err) { + console.log("Error Found:", err); + } + }) + } + } catch (err) { + // Expect EEXIST error + } + try { + copySync( + path.join(tempBinDir, 'uv.exe'), + path.join(binDir) + ) + if (platform === 'win32') { + copyFile(path.join(binDir, 'uv.exe'), path.join(binDir, 'uv-x86_64-pc-windows-msvc.exe'), (err) => { + if (err) { + console.log("Error Found:", err); + } + }) + } + } catch (err) { + // Expect EEXIST error + } + console.log('UV downloaded.') + + console.log('Downloads completed.') +} + +// Ensure the downloads directory exists +if (!fs.existsSync('downloads')) { + fs.mkdirSync('downloads') +} + +main().catch((err) => { + console.error('Error:', err) + process.exit(1) +}) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d60adc524..4cbdde105 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -41,6 +41,7 @@ rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", branch = "mai "tower", ] } uuid = { version = "1.7", features = ["v4"] } +env = "1.0.1" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" diff --git a/src-tauri/src/core/mcp.rs b/src-tauri/src/core/mcp.rs index 0635b2e27..91ffdbafd 100644 --- a/src-tauri/src/core/mcp.rs +++ b/src-tauri/src/core/mcp.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, env, sync::Arc}; use rmcp::{service::RunningService, transport::TokioChildProcess, RoleClient, ServiceExt}; use serde_json::Value; @@ -33,9 +33,26 @@ pub async fn run_mcp_commands( if let Some(server_map) = mcp_servers.get("mcpServers").and_then(Value::as_object) { log::info!("MCP Servers: {server_map:#?}"); + let exe_path = env::current_exe().expect("Failed to get current exe path"); + let exe_parent_path = exe_path.parent().expect("Executable must have a parent directory"); + let bin_path = exe_parent_path.to_path_buf(); for (name, config) in server_map { if let Some((command, args, envs)) = extract_command_args(config) { - let mut cmd = Command::new(command); + let mut cmd = Command::new(command.clone()); + if command.clone() == "npx" { + let bun_x_path = format!("{}/bun", bin_path.display()); + cmd = Command::new(bun_x_path); + cmd.arg("x"); + } + + if command.clone() == "uvx" { + let bun_x_path = format!("{}/uv", bin_path.display()); + cmd = Command::new(bun_x_path); + cmd.arg("tool run"); + cmd.arg("run"); + } + println!("Command: {cmd:#?}"); + args.iter().filter_map(Value::as_str).for_each(|arg| { cmd.arg(arg); }); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f2ae334b3..3e74e9fc7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -69,6 +69,6 @@ "resources/themes/**/*", "resources/pre-install/**/*" ], - "externalBin": ["binaries/cortex-server"] + "externalBin": ["binaries/cortex-server", "resources/bin/bun", "resources/bin/uv"] } }