feat: run mcp with bundled bun and uv (#4949)

* feat: run mcp with bundled bun and uv

* chore: clean up

* chore: pull binaries windows, linux (#4963)

* fix: get bun and uv from execution path

* fix: macos

* fix: typo

---------

Co-authored-by: vansangpfiev <vansangpfiev@gmail.com>
This commit is contained in:
Louis 2025-05-12 13:07:18 +07:00
parent dea29ff8c4
commit 52ac14ad06
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
6 changed files with 245 additions and 4 deletions

3
.gitignore vendored
View File

@ -43,4 +43,5 @@ src-tauri/resources/themes
src-tauri/Cargo.lock
src-tauri/icons
!src-tauri/icons/icon.png
src-tauri/gen/apple
src-tauri/gen/apple
src-tauri/resources/bin

View File

@ -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",

219
scripts/download-bin.js Executable file
View File

@ -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)
})

View File

@ -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"

View File

@ -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);
});

View File

@ -69,6 +69,6 @@
"resources/themes/**/*",
"resources/pre-install/**/*"
],
"externalBin": ["binaries/cortex-server"]
"externalBin": ["binaries/cortex-server", "resources/bin/bun", "resources/bin/uv"]
}
}