diff --git a/resources/scripts/install-bun.js b/resources/scripts/install-bun.js new file mode 100644 index 00000000..ada5be15 --- /dev/null +++ b/resources/scripts/install-bun.js @@ -0,0 +1,221 @@ +const fs = require('fs') +const path = require('path') +const os = require('os') +const { execSync } = require('child_process') +const https = require('https') + +// Base URL for downloading bun binaries +const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download' +const DEFAULT_BUN_VERSION = '1.2.5' // Default fallback version + +// Mapping of platform+arch to binary package name +const BUN_PACKAGES = { + 'darwin-arm64': 'bun-darwin-aarch64.zip', + 'darwin-x64': 'bun-darwin-x64.zip', + 'win32-x64': 'bun-windows-x64.zip', + 'win32-x64-baseline': 'bun-windows-x64-baseline.zip', + 'linux-x64': 'bun-linux-x64.zip', + 'linux-x64-baseline': 'bun-linux-x64-baseline.zip', + 'linux-arm64': 'bun-linux-aarch64.zip', + // MUSL variants + 'linux-musl-x64': 'bun-linux-x64-musl.zip', + 'linux-musl-x64-baseline': 'bun-linux-x64-musl-baseline.zip', + 'linux-musl-arm64': 'bun-linux-aarch64-musl.zip' +} + +/** + * Fetches the latest version of bun from GitHub API + * @returns {Promise} The latest version tag (without 'bun-v' prefix) + */ +async function getLatestBunVersion() { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: '/repos/oven-sh/bun/releases/latest', + headers: { + 'User-Agent': 'cherry-studio-install-script' + } + } + + const req = https.get(options, (res) => { + if (res.statusCode !== 200) { + reject(new Error(`Request failed with status code ${res.statusCode}`)) + return + } + + let data = '' + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + try { + const release = JSON.parse(data) + // Remove the 'bun-v' prefix if present + const version = release.tag_name.startsWith('bun-v') + ? release.tag_name.substring(5) + : release.tag_name.startsWith('v') + ? release.tag_name.substring(1) + : release.tag_name + resolve(version) + } catch (error) { + reject(new Error(`Failed to parse GitHub API response: ${error.message}`)) + } + }) + }) + + req.on('error', (error) => { + reject(new Error(`Failed to fetch latest version: ${error.message}`)) + }) + + req.end() + }) +} + +/** + * Downloads and extracts the bun binary for the specified platform and architecture + * @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux') + * @param {string} arch Architecture to download for (e.g., 'x64', 'arm64') + * @param {string} version Version of bun to download + * @param {boolean} isMusl Whether to use MUSL variant for Linux + * @param {boolean} isBaseline Whether to use baseline variant + */ +async function downloadBunBinary(platform, arch, version = DEFAULT_BUN_VERSION, isMusl = false, isBaseline = false) { + let platformKey = isMusl ? `${platform}-musl-${arch}` : `${platform}-${arch}` + if (isBaseline) { + platformKey += '-baseline' + } + const packageName = BUN_PACKAGES[platformKey] + + if (!packageName) { + console.error(`No binary available for ${platformKey}`) + return false + } + + // Create output directory structure + const archDir = path.join(os.homedir(), '.cherrystudio', 'bin') + // Ensure directories exist + fs.mkdirSync(archDir, { recursive: true }) + + // Download URL for the specific binary + const downloadUrl = `${BUN_RELEASE_BASE_URL}/bun-v${version}/${packageName}` + const tempdir = os.tmpdir() + // Create a temporary file for the downloaded binary + const localFilename = path.join(tempdir, packageName) + + try { + console.log(`Downloading bun ${version} for ${platformKey}...`) + console.log(`URL: ${downloadUrl}`) + + // Download the file + execSync(`curl --fail -L -o "${localFilename}" "${downloadUrl}"`, { stdio: 'inherit' }) + + // Extract the zip file + console.log(`Extracting ${packageName} to ${archDir}...`) + execSync(`unzip -o "${localFilename}" -d "${tempdir}"`, { stdio: 'inherit' }) + execSync(`mv ${tempdir}/${packageName.split('.')[0]}/* ${archDir}/`, { stdio: 'inherit' }) + + // Clean up the downloaded file + fs.unlinkSync(localFilename) + + console.log(`Successfully installed bun ${version} for ${platformKey}`) + return true + } catch (error) { + console.error(`Error installing bun for ${platformKey}: ${error.message}`) + if (fs.existsSync(localFilename)) { + fs.unlinkSync(localFilename) + } + return false + } +} + +/** + * Detects current platform and architecture + */ +function detectPlatformAndArch() { + const platform = os.platform() + const arch = os.arch() + const isMusl = platform === 'linux' && detectIsMusl() + + return { platform, arch, isMusl } +} + +/** + * Attempts to detect if running on MUSL libc + */ +function detectIsMusl() { + try { + // Simple check for Alpine Linux which uses MUSL + const output = execSync('cat /etc/os-release').toString() + return output.toLowerCase().includes('alpine') + } catch (error) { + return false + } +} + +/** + * Main function to install bun + */ +async function installBun() { + const args = process.argv.slice(2) + const specifiedVersion = args.find((arg) => !arg.startsWith('--')) + + // Get the latest version if no specific version is provided + const version = specifiedVersion || (await getLatestBunVersion()) + console.log(`Using bun version: ${version}`) + + const specificPlatform = args.find((arg) => arg.startsWith('--platform='))?.split('=')[1] + const specificArch = args.find((arg) => arg.startsWith('--arch='))?.split('=')[1] + const specificMusl = args.includes('--musl') + const specificBaseline = args.includes('--baseline') + const installAll = args.includes('--all') + + if (installAll) { + console.log(`Installing all bun ${version} binaries...`) + for (const platformKey in BUN_PACKAGES) { + let platform, + arch, + isMusl = false, + isBaseline = false + + if (platformKey.includes('-musl-')) { + const [platformPart, archPart] = platformKey.split('-musl-') + platform = platformPart + isMusl = true + + if (archPart.includes('-baseline')) { + ;[arch] = archPart.split('-baseline') + isBaseline = true + } else { + arch = archPart + } + } else if (platformKey.includes('-baseline')) { + const [platformPart, archPart] = platformKey.split('-') + platform = platformPart + arch = archPart.replace('-baseline', '') + isBaseline = true + } else { + ;[platform, arch] = platformKey.split('-') + } + + await downloadBunBinary(platform, arch, version, isMusl, isBaseline) + } + } else { + const { platform, arch, isMusl } = detectPlatformAndArch() + const targetPlatform = specificPlatform || platform + const targetArch = specificArch || arch + const targetMusl = specificMusl || isMusl + const targetBaseline = specificBaseline || false + + console.log( + `Installing bun ${version} for ${targetPlatform}-${targetArch}${targetMusl ? ' (MUSL)' : ''}${targetBaseline ? ' (baseline)' : ''}...` + ) + await downloadBunBinary(targetPlatform, targetArch, version, targetMusl, targetBaseline) + } +} + +// Run the installation +installBun().catch((error) => { + console.error('Installation failed:', error) + process.exit(1) +}) diff --git a/resources/scripts/install-uv.js b/resources/scripts/install-uv.js new file mode 100644 index 00000000..245dbe10 --- /dev/null +++ b/resources/scripts/install-uv.js @@ -0,0 +1,202 @@ +const fs = require('fs') +const path = require('path') +const os = require('os') +const { execSync } = require('child_process') +const https = require('https') + +// Base URL for downloading uv binaries +const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download' +const DEFAULT_UV_VERSION = '0.6.6' + +// Mapping of platform+arch to binary package name +const UV_PACKAGES = { + 'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz', + 'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz', + 'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip', + 'win32-ia32': 'uv-i686-pc-windows-msvc.zip', + 'win32-x64': 'uv-x86_64-pc-windows-msvc.zip', + 'linux-arm64': 'uv-aarch64-unknown-linux-gnu.tar.gz', + 'linux-ia32': 'uv-i686-unknown-linux-gnu.tar.gz', + 'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.tar.gz', + 'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.tar.gz', + 'linux-s390x': 'uv-s390x-unknown-linux-gnu.tar.gz', + 'linux-x64': 'uv-x86_64-unknown-linux-gnu.tar.gz', + 'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.tar.gz', + // MUSL variants + 'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz', + 'linux-musl-ia32': 'uv-i686-unknown-linux-musl.tar.gz', + 'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.tar.gz', + 'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.tar.gz', + 'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz' +} + +/** + * Fetches the latest version of uv from GitHub API + * @returns {Promise} The latest version tag (without 'v' prefix) + */ +async function getLatestUvVersion() { + return new Promise((resolve, reject) => { + const options = { + hostname: 'api.github.com', + path: '/repos/astral-sh/uv/releases/latest', + headers: { + 'User-Agent': 'cherry-studio-install-script' + } + } + + const req = https.get(options, (res) => { + if (res.statusCode !== 200) { + reject(new Error(`Request failed with status code ${res.statusCode}`)) + return + } + + let data = '' + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + try { + const release = JSON.parse(data) + // Remove the 'v' prefix if present + const version = release.tag_name.startsWith('v') ? release.tag_name.substring(1) : release.tag_name + resolve(version) + } catch (error) { + reject(new Error(`Failed to parse GitHub API response: ${error.message}`)) + } + }) + }) + + req.on('error', (error) => { + reject(new Error(`Failed to fetch latest version: ${error.message}`)) + }) + + req.end() + }) +} + +/** + * Downloads and extracts the uv binary for the specified platform and architecture + * @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux') + * @param {string} arch Architecture to download for (e.g., 'x64', 'arm64') + * @param {string} version Version of uv to download + * @param {boolean} isMusl Whether to use MUSL variant for Linux + */ +async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, isMusl = false) { + const platformKey = isMusl ? `${platform}-musl-${arch}` : `${platform}-${arch}` + const packageName = UV_PACKAGES[platformKey] + + if (!packageName) { + console.error(`No binary available for ${platformKey}`) + return false + } + + // Create output directory structure + const archDir = path.join(os.homedir(), '.cherrystudio', 'bin') + // Ensure directories exist + fs.mkdirSync(archDir, { recursive: true }) + + // Download URL for the specific binary + const downloadUrl = `${UV_RELEASE_BASE_URL}/${version}/${packageName}` + const tempdir = os.tmpdir() + const localFilename = path.join(tempdir, packageName) + + try { + console.log(`Downloading uv ${version} for ${platformKey}...`) + console.log(`URL: ${downloadUrl}`) + + // Download the file + execSync(`curl --fail -L -o "${localFilename}" "${downloadUrl}"`, { stdio: 'inherit' }) + + // Extract based on file extension + console.log(`Extracting ${packageName} to ${archDir}...`) + if (packageName.endsWith('.tar.gz')) { + execSync(`tar -xzf "${localFilename}" -C "${tempdir}"`, { stdio: 'inherit' }) + } else if (packageName.endsWith('.zip')) { + execSync(`unzip -o "${localFilename}" -d "${tempdir}"`, { stdio: 'inherit' }) + } + + execSync(`mv ${tempdir}/${packageName.split('.')[0]}/* ${archDir}/`, { stdio: 'inherit' }) + + // Clean up the downloaded file + fs.unlinkSync(localFilename) + + console.log(`Successfully installed uv ${version} for ${platform}-${arch}`) + return true + } catch (error) { + console.error(`Error installing uv for ${platformKey}: ${error.message}`) + if (fs.existsSync(localFilename)) { + fs.unlinkSync(localFilename) + } + return false + } +} + +/** + * Detects current platform and architecture + */ +function detectPlatformAndArch() { + const platform = os.platform() + const arch = os.arch() + const isMusl = platform === 'linux' && detectIsMusl() + + return { platform, arch, isMusl } +} + +/** + * Attempts to detect if running on MUSL libc + */ +function detectIsMusl() { + try { + // Simple check for Alpine Linux which uses MUSL + const output = execSync('cat /etc/os-release').toString() + return output.toLowerCase().includes('alpine') + } catch (error) { + return false + } +} + +/** + * Main function to install uv + */ +async function installUv() { + const args = process.argv.slice(2) + const specifiedVersion = args.find((arg) => !arg.startsWith('--')) + + // Get the latest version if no specific version is provided + const version = specifiedVersion || (await getLatestUvVersion()) + console.log(`Using uv version: ${version}`) + + const specificPlatform = args.find((arg) => arg.startsWith('--platform='))?.split('=')[1] + const specificArch = args.find((arg) => arg.startsWith('--arch='))?.split('=')[1] + const specificMusl = args.includes('--musl') + const installAll = args.includes('--all') + + if (installAll) { + console.log(`Installing all uv ${version} binaries...`) + for (const platformKey in UV_PACKAGES) { + const [platformArch, musl] = platformKey.split('-musl-') + if (musl) { + const [platform, arch] = platformArch.split('-') + await downloadUvBinary(platform, arch, version, true) + } else { + const [platform, arch] = platformKey.split('-') + await downloadUvBinary(platform, arch, version, false) + } + } + } else { + const { platform, arch, isMusl } = detectPlatformAndArch() + const targetPlatform = specificPlatform || platform + const targetArch = specificArch || arch + const targetMusl = specificMusl || isMusl + + console.log(`Installing uv ${version} for ${targetPlatform}-${targetArch}${targetMusl ? ' (MUSL)' : ''}...`) + await downloadUvBinary(targetPlatform, targetArch, version, targetMusl) + } +} + +// Run the installation +installUv().catch((error) => { + console.error('Installation failed:', error) + process.exit(1) +}) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index ec163741..cf668f74 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,5 +1,6 @@ import fs from 'node:fs' +import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import { MCPServer, Shortcut, ThemeMode } from '@types' import { BrowserWindow, ipcMain, session, shell } from 'electron' import log from 'electron-log' @@ -232,6 +233,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup()) + ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name)) + ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name)) + ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js')) + ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js')) + // Listen for changes in MCP servers and notify renderer mcpService.on('servers-updated', (servers) => { mainWindow?.webContents.send('mcp:servers-updated', servers) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index ebc5e298..f95f6519 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -1,10 +1,13 @@ import { isLinux, isMac, isWin } from '@main/constant' +import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import type { Client } from '@modelcontextprotocol/sdk/client/index.js' import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { MCPServer, MCPTool } from '@types' import log from 'electron-log' import { EventEmitter } from 'events' +import os from 'os' +import path from 'path' import { v4 as uuidv4 } from 'uuid' import { windowService } from './WindowService' @@ -32,6 +35,7 @@ export default class MCPService extends EventEmitter { constructor() { super() this.createServerLoadingPromise() + this.init().catch((err) => this.logError('Failed to initialize MCP service', err)) } /** @@ -59,7 +63,7 @@ export default class MCPService extends EventEmitter { // Initialize if not already initialized if (!this.initialized) { - this.init().catch(this.logError('Failed to initialize MCP service')) + this.init().catch((err) => this.logError('Failed to initialize MCP service', err)) } } @@ -125,8 +129,8 @@ export default class MCPService extends EventEmitter { /** * Helper to create consistent error logging functions */ - private logError(message: string) { - return (err: Error) => log.error(`[MCP] ${message}:`, err) + private logError(message: string, err?: any): void { + log.error(`[MCP] ${message}`, err) } /** @@ -137,7 +141,7 @@ export default class MCPService extends EventEmitter { const { Client } = await import('@modelcontextprotocol/sdk/client/index.js') return Client } catch (err) { - log.error('[MCP] Failed to import Client:', err) + this.logError('Failed to import Client:', err) throw err } } @@ -323,10 +327,38 @@ export default class MCPService extends EventEmitter { transport = new this.sseTransport!(new URL(baseUrl)) } else if (command) { let cmd: string = command + const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') + log.info(`[MCP] Using binaries directory: ${binariesDir}`) if (command === 'npx') { - cmd = process.platform === 'win32' ? `${command}.cmd` : command + // check if cmd exists, if not exist, install it using `node scripts/install-bun.js` + const isBunExist = await isBinaryExists('bun') + if (!isBunExist) { + log.info(`[MCP] Installing bun...`) + await runInstallScript('install-bun.js') + } + cmd = getBinaryPath('bun') + log.info(`[MCP] Using command: ${cmd}`) + + // add -x to args if args exist + if (args && args.length > 0) { + if (!args.includes('-y')) { + args.unshift('-y') + } + if (!args.includes('x')) { + args.unshift('x') + } + } + } else if (command === 'uvx') { + // check if cmd exists, if not exist, install it using `node scripts/install-uv.js` + const isUvxExist = await isBinaryExists('uvx') + if (!isUvxExist) { + log.info(`[MCP] Installing uvx...`) + await runInstallScript('install-uv.js') + } + cmd = getBinaryPath('uvx') } + log.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) transport = new this.stdioTransport!({ command: cmd, args, @@ -386,6 +418,7 @@ export default class MCPService extends EventEmitter { */ public async listTools(serverName?: string): Promise { await this.ensureInitialized() + log.info(`[MCP] Listing tools from ${serverName || 'all active servers'}`) try { // If server name provided, list tools for that server only @@ -397,18 +430,19 @@ export default class MCPService extends EventEmitter { let allTools: MCPTool[] = [] for (const clientName in this.clients) { + log.info(`[MCP] Listing tools from ${clientName}`) try { const tools = await this.listToolsFromServer(clientName) allTools = allTools.concat(tools) } catch (error) { - this.logError(`[MCP] Error listing tools for ${clientName}`) + this.logError(`Error listing tools for ${clientName}`, error) } } log.info(`[MCP] Total tools listed: ${allTools.length}`) return allTools } catch (error) { - this.logError('Error listing tools:') + this.logError('Error listing tools:', error) return [] } } @@ -417,11 +451,13 @@ export default class MCPService extends EventEmitter { * Helper method to list tools from a specific server */ private async listToolsFromServer(serverName: string): Promise { + log.info(`[MCP] start list tools from ${serverName}:`) if (!this.clients[serverName]) { throw new Error(`MCP Client ${serverName} not found`) } - const { tools } = await this.clients[serverName].listTools() + + log.info(`[MCP] Tools from ${serverName}:`, tools) return tools.map((tool: any) => ({ ...tool, serverName, @@ -500,7 +536,7 @@ export default class MCPService extends EventEmitter { try { await this.activate(server) } catch (error) { - this.logError(`Failed to activate server ${server.name}`) + this.logError(`Failed to activate server ${server.name}`, error) this.emit('server-error', { name: server.name, error }) } }) diff --git a/src/main/utils/process.ts b/src/main/utils/process.ts new file mode 100644 index 00000000..294c2141 --- /dev/null +++ b/src/main/utils/process.ts @@ -0,0 +1,50 @@ +import { spawn } from 'child_process' +import log from 'electron-log' +import fs from 'fs' +import os from 'os' +import path from 'path' + +import { getResourcePath } from '.' + +export function runInstallScript(scriptPath: string): Promise { + return new Promise((resolve, reject) => { + const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath) + log.info(`Running script at: ${installScriptPath}`) + + const nodeProcess = spawn(process.execPath, [installScriptPath], { + env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } + }) + + nodeProcess.stdout.on('data', (data) => { + log.info(`Script output: ${data}`) + }) + + nodeProcess.stderr.on('data', (data) => { + log.error(`Script error: ${data}`) + }) + + nodeProcess.on('close', (code) => { + if (code === 0) { + log.info('Script completed successfully') + resolve() + } else { + log.error(`Script exited with code ${code}`) + reject(new Error(`Process exited with code ${code}`)) + } + }) + }) +} + +export function getBinaryPath(name: string): string { + const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') + let cmd = path.join(binariesDir, name) + cmd = process.platform === 'win32' ? `${cmd}.exe` : cmd + return cmd +} + +export function isBinaryExists(name: string): Promise { + return new Promise((resolve) => { + const cmd = getBinaryPath(name) + resolve(fs.existsSync(cmd)) + }) +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index cf6b9be9..2de7c79f 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -135,6 +135,10 @@ declare global { // status cleanup: () => Promise } + isBinaryExist: (name: string) => Promise + getBinaryPath: (name: string) => Promise + installUVBinary: () => Promise + installBunBinary: () => Promise } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index a050cc36..a780df33 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -119,7 +119,13 @@ const api = { }, shell: { openExternal: shell.openExternal - } + }, + + // Binary related APIs + isBinaryExist: (name: string) => ipcRenderer.invoke('app:is-binary-exist', name), + getBinaryPath: (name: string) => ipcRenderer.invoke('app:get-binary-path', name), + installUVBinary: () => ipcRenderer.invoke('app:install-uv-binary'), + installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary') } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 83e62831..3dc9cb48 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -825,6 +825,12 @@ "updateError": "Failed to update server", "url": "URL", "toggleError": "Toggle failed", + "dependenciesInstalling": "Installing dependencies...", + "dependenciesInstall": "Install Dependencies", + "installSuccess": "Dependencies installed successfully", + "installError": "Failed to install dependencies", + "missingDependencies": "is Missing, please install it to continue.", + "install": "Install", "npx_list": { "title": "NPX Package List", "desc": "Search and add npm packages as MCP servers", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 3f4321d3..6299318b 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -825,6 +825,12 @@ "updateError": "サーバーの更新に失敗しました", "url": "URL", "toggleError": "切り替えに失敗しました", + "dependenciesInstalling": "依存関係をインストール中...", + "dependenciesInstall": "依存関係をインストール", + "installSuccess": "依存関係のインストールに成功しました", + "installError": "依存関係のインストールに失敗しました", + "missingDependencies": "が不足しています。続行するにはインストールしてください。", + "install": "インストール", "npx_list": { "title": "NPX パッケージリスト", "desc": "npm パッケージを検索して MCP サーバーとして追加", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index c26d15db..ad60841a 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -825,6 +825,12 @@ "updateError": "Ошибка обновления сервера", "url": "URL", "toggleError": "Переключение не удалось", + "dependenciesInstalling": "Установка зависимостей...", + "dependenciesInstall": "Установить зависимости", + "installSuccess": "Зависимости успешно установлены", + "installError": "Не удалось установить зависимости", + "missingDependencies": "отсутствует, пожалуйста, установите для продолжения.", + "install": "Установить", "npx_list": { "title": "Список пакетов NPX", "desc": "Поиск и добавление npm пакетов в качестве MCP серверов", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 629da17e..cec887e4 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -825,6 +825,12 @@ "updateError": "更新服务器失败", "url": "URL", "toggleError": "切换失败", + "dependenciesInstalling": "正在安装依赖项...", + "dependenciesInstall": "安装依赖项", + "installSuccess": "依赖项安装成功", + "installError": "安装依赖项失败", + "missingDependencies": "缺失,请安装它以继续", + "install": "安装", "npx_list": { "title": "NPX 包列表", "desc": "搜索并添加 npm 包作为 MCP 服务", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 0deae2e0..4691e984 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -825,6 +825,12 @@ "updateError": "更新伺服器失敗", "url": "URL", "toggleError": "切換失敗", + "dependenciesInstalling": "正在安裝相依套件...", + "dependenciesInstall": "安裝相依套件", + "installSuccess": "相依套件安裝成功", + "installError": "安裝相依套件失敗", + "missingDependencies": "缺失,請安裝它以繼續", + "install": "安裝", "npx_list": { "title": "NPX 包列表", "desc": "搜索並添加 npm 包作為 MCP 服務", diff --git a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx new file mode 100644 index 00000000..db5c5d4d --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx @@ -0,0 +1,114 @@ +import { Alert, Button } from 'antd' +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { SettingRow, SettingSubtitle } from '..' + +const InstallNpxUv: FC = () => { + const [isUvInstalled, setIsUvInstalled] = useState(false) + const [isBunInstalled, setIsBunInstalled] = useState(false) + const [isInstallingUv, setIsInstallingUv] = useState(false) + const [isInstallingBun, setIsInstallingBun] = useState(false) + const { t } = useTranslation() + + const checkBinaries = async () => { + const uvExists = await window.api.isBinaryExist('uv') + const bunExists = await window.api.isBinaryExist('bun') + + setIsUvInstalled(uvExists) + setIsBunInstalled(bunExists) + } + + const installUV = async () => { + try { + setIsInstallingUv(true) + await window.api.installUVBinary() + setIsUvInstalled(true) + setIsInstallingUv(false) + } catch (error: any) { + window.message.error(`${t('settings.mcp.installError')}: ${error.message}`) + setIsInstallingUv(false) + checkBinaries() + } + } + + const installBun = async () => { + try { + setIsInstallingBun(true) + await window.api.installBunBinary() + setIsBunInstalled(true) + setIsInstallingBun(false) + } catch (error: any) { + window.message.error(`${t('settings.mcp.installError')}: ${error.message}`) + setIsInstallingBun(false) + checkBinaries() + } + } + + useEffect(() => { + checkBinaries() + }, []) + + if (isUvInstalled && isBunInstalled) { + return null + } + + return ( + + {!isUvInstalled && ( + + + {isUvInstalled ? 'UV Installed' : `UV ${t('settings.mcp.missingDependencies')}`} + + + + } + /> + )} + {!isBunInstalled && ( + + + {isBunInstalled ? 'Bun Installed' : `Bun ${t('settings.mcp.missingDependencies')}`} + + + + } + /> + )} + + ) +} + +const Container = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 20px; + gap: 10px; +` + +export default InstallNpxUv diff --git a/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx b/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx index 47eb2c8c..2f88780d 100644 --- a/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx @@ -36,6 +36,7 @@ const NpxSearch: FC = () => { } setSearchLoading(true) + try { // Call npxFinder to search for packages const packages = await npxFinder(npmScope) diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 61a3d2e8..1b887bf4 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..' import AddMcpServerPopup from './AddMcpServerPopup' +import InstallNpxUv from './InstallNpxUv' import NpxSearch from './NpxSearch' const MCPSettings: FC = () => { @@ -199,6 +200,7 @@ const MCPSettings: FC = () => { return ( + {t('settings.mcp.title')} @@ -207,7 +209,7 @@ const MCPSettings: FC = () => { - + {t('settings.mcp.config_description')} @@ -239,6 +241,7 @@ const MCPSettings: FC = () => { columns={columns} rowKey="name" pagination={false} + size="small" locale={{ emptyText: t('settings.mcp.noServers') }} rowClassName={(record) => (!record.isActive ? 'inactive-row' : '')} onRow={(record) => ({ diff --git a/src/renderer/src/providers/OpenAIProvider.ts b/src/renderer/src/providers/OpenAIProvider.ts index 45689465..915a304b 100644 --- a/src/renderer/src/providers/OpenAIProvider.ts +++ b/src/renderer/src/providers/OpenAIProvider.ts @@ -516,9 +516,7 @@ export default class OpenAIProvider extends BaseProvider { reqMessages.push({ role: 'tool', - content: isString(toolCallResponse.content) - ? toolCallResponse.content - : JSON.stringify(toolCallResponse.content), + content: toolCallResponse.content, tool_call_id: toolCall.id } as ChatCompletionToolMessageParam)