diff --git a/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch b/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch deleted file mode 100644 index 830f101d..00000000 --- a/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/dist/cjs/client/stdio.js b/dist/cjs/client/stdio.js -index 2ada8771c5f76673b5021d6453c6bdd7e0b88013..89f6ea9ca7de86294d3d966f3454b98e19bfe534 100644 ---- a/dist/cjs/client/stdio.js -+++ b/dist/cjs/client/stdio.js -@@ -68,7 +68,7 @@ class StdioClientTransport { - this._process = (0, node_child_process_1.spawn)(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], { - env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(), - stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"], -- shell: false, -+ shell: process.platform === 'win32' ? true : false, - signal: this._abortController.signal, - windowsHide: node_process_1.default.platform === "win32" && isElectron(), - cwd: this._serverParams.cwd, -diff --git a/dist/esm/client/stdio.js b/dist/esm/client/stdio.js -index 387c982fd40fd8db9790a78e1a05c9ecb81501c0..7b7e60a306bca73149609015a27e904a0a68ca02 100644 ---- a/dist/esm/client/stdio.js -+++ b/dist/esm/client/stdio.js -@@ -61,7 +61,7 @@ export class StdioClientTransport { - this._process = spawn(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], { - env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(), - stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"], -- shell: false, -+ shell: process.platform === 'win32' ? true : false, - signal: this._abortController.signal, - windowsHide: process.platform === "win32" && isElectron(), - cwd: this._serverParams.cwd, diff --git a/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch b/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch new file mode 100644 index 00000000..c28db0e1 --- /dev/null +++ b/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch @@ -0,0 +1,18 @@ +diff --git a/dist/index.node.js b/dist/index.node.js +index bb108cbc210af5b99e864fd1dd8c555e948ecf7a..8ef8c1aab59215c21d161c0e52125724528ecab8 100644 +--- a/dist/index.node.js ++++ b/dist/index.node.js +@@ -1,8 +1,11 @@ + let crypto; + crypto = + globalThis.crypto?.webcrypto ?? // Node.js 16 REPL has globalThis.crypto as node:crypto +- globalThis.crypto ?? // Node.js 18+ +- (await import("node:crypto")).webcrypto; // Node.js 16 non-REPL ++ globalThis.crypto ?? // Node.js 18+ ++ (async() => { ++ const crypto = await import("node:crypto"); ++ return crypto.webcrypto; ++ })(); + /** + * Creates an array of length `size` of random bytes + * @param size diff --git a/package.json b/package.json index de1cd72a..59f8dac7 100644 --- a/package.json +++ b/package.json @@ -65,13 +65,11 @@ "@electron/notarize": "^2.5.0", "@google/generative-ai": "^0.21.0", "@langchain/community": "^0.3.36", - "@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch", "@notionhq/client": "^2.2.15", "@tryfabric/martian": "^1.2.4", "@types/react-infinite-scroll-component": "^5.0.0", "@xyflow/react": "^12.4.4", "adm-zip": "^0.5.16", - "chokidar": "^4.0.3", "docx": "^9.0.2", "electron-log": "^5.1.5", "electron-store": "^8.2.0", @@ -105,12 +103,12 @@ "@google/genai": "^0.4.0", "@hello-pangea/dnd": "^16.6.0", "@kangfenmao/keyv-storage": "^0.1.0", + "@modelcontextprotocol/sdk": "^1.8.0", "@notionhq/client": "^2.2.15", "@reduxjs/toolkit": "^2.2.5", "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch", "@tryfabric/martian": "^1.2.4", "@types/adm-zip": "^0", - "@types/chokidar": "^2.1.7", "@types/fs-extra": "^11", "@types/lodash": "^4.17.5", "@types/markdown-it": "^14", @@ -185,7 +183,8 @@ "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", "@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", - "openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch" + "openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch", + "pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch" }, "packageManager": "yarn@4.6.0", "lint-staged": { diff --git a/resources/scripts/download.js b/resources/scripts/download.js index 270f8cbe..2e9d83a9 100644 --- a/resources/scripts/download.js +++ b/resources/scripts/download.js @@ -1,8 +1,5 @@ -const { ProxyAgent } = require('undici') -const { SocksProxyAgent } = require('socks-proxy-agent') const https = require('https') const fs = require('fs') -const { pipeline } = require('stream/promises') /** * Downloads a file from a URL with redirect handling @@ -11,42 +8,28 @@ const { pipeline } = require('stream/promises') * @returns {Promise} Promise that resolves when download is complete */ async function downloadWithRedirects(url, destinationPath) { - const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY - if (proxyUrl.startsWith('socks')) { - const proxyAgent = new SocksProxyAgent(proxyUrl) - return new Promise((resolve, reject) => { - const request = (url) => { - https - .get(url, { agent: proxyAgent }, (response) => { - if (response.statusCode == 301 || response.statusCode == 302) { - request(response.headers.location) - return - } - if (response.statusCode !== 200) { - reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)) - return - } - const file = fs.createWriteStream(destinationPath) - response.pipe(file) - file.on('finish', () => resolve()) - }) - .on('error', (err) => { - reject(err) - }) - } - request(url) - }) - } else { - const proxyAgent = new ProxyAgent(proxyUrl) - const response = await fetch(url, { - dispatcher: proxyAgent - }) - if (!response.ok) { - throw new Error(`Download failed: ${response.status} ${response.statusText}`) + return new Promise((resolve, reject) => { + const request = (url) => { + https + .get(url, (response) => { + if (response.statusCode == 301 || response.statusCode == 302) { + request(response.headers.location) + return + } + if (response.statusCode !== 200) { + reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)) + return + } + const file = fs.createWriteStream(destinationPath) + response.pipe(file) + file.on('finish', () => resolve()) + }) + .on('error', (err) => { + reject(err) + }) } - const file = fs.createWriteStream(destinationPath) - await pipeline(response.body, file) - } + request(url) + }) } module.exports = { downloadWithRedirects } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index a4841f5c..26630c20 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -2,7 +2,7 @@ import fs from 'node:fs' import { isMac, isWin } from '@main/constant' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' -import { MCPServer, Shortcut, ThemeMode } from '@types' +import { Shortcut, ThemeMode } from '@types' import { BrowserWindow, ipcMain, session, shell } from 'electron' import log from 'electron-log' @@ -16,7 +16,7 @@ import FileService from './services/FileService' import FileStorage from './services/FileStorage' import { GeminiService } from './services/GeminiService' import KnowledgeService from './services/KnowledgeService' -import MCPService from './services/MCPService' +import mcpService from './services/MCPService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ProxyConfig, proxyManager } from './services/ProxyManager' @@ -31,7 +31,6 @@ import { compress, decompress } from './utils/zip' const fileManager = new FileStorage() const backupManager = new BackupManager() const exportService = new ExportService(fileManager) -const mcpService = new MCPService() const obsidianVaultService = new ObsidianVaultService() export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { @@ -264,36 +263,15 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ) // Register MCP handlers - ipcMain.on('mcp:servers-from-renderer', (_, servers) => mcpService.setServers(servers)) - ipcMain.handle('mcp:list-servers', async () => mcpService.listAvailableServices()) - ipcMain.handle('mcp:add-server', async (_, server: MCPServer) => mcpService.addServer(server)) - ipcMain.handle('mcp:update-server', async (_, server: MCPServer) => mcpService.updateServer(server)) - ipcMain.handle('mcp:delete-server', async (_, serverName: string) => mcpService.deleteServer(serverName)) - ipcMain.handle('mcp:set-server-active', async (_, { name, isActive }) => - mcpService.setServerActive({ name, isActive }) - ) - - // According to preload, this should take no parameters, but our implementation accepts - // an optional serverName for better flexibility - ipcMain.handle('mcp:list-tools', async (_, serverName?: string) => mcpService.listTools(serverName)) - ipcMain.handle('mcp:call-tool', async (_, params: { client: string; name: string; args: any }) => - mcpService.callTool(params) - ) - - ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup()) + ipcMain.handle('mcp:remove-server', mcpService.removeServer) + ipcMain.handle('mcp:list-tools', mcpService.listTools) + ipcMain.handle('mcp:call-tool', mcpService.callTool) 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) - }) - - app.on('before-quit', () => mcpService.cleanup()) - //copilot ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage) ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken) diff --git a/src/main/resources/icon.ico b/src/main/resources/icon.ico deleted file mode 100644 index 07f1f670..00000000 Binary files a/src/main/resources/icon.ico and /dev/null differ diff --git a/src/main/services/FileService.ts b/src/main/services/FileService.ts index 39255e15..82034d1c 100644 --- a/src/main/services/FileService.ts +++ b/src/main/services/FileService.ts @@ -2,6 +2,10 @@ import fs from 'node:fs' export default class FileService { public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) { + const stats = fs.statSync(path) + if (stats.isDirectory()) { + throw new Error(`Cannot read directory: ${path}`) + } return fs.readFileSync(path, 'utf8') } } diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 9557e17c..e6c4bd91 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -213,6 +213,11 @@ class FileStorage { public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise => { const filePath = path.join(this.storageDir, id) + const stats = await fs.promises.stat(filePath) + + if (stats.isDirectory()) { + throw new Error(`Cannot read directory: ${filePath}`) + } if (documentExts.includes(path.extname(filePath))) { const originalCwd = process.cwd() diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 136e73d1..670136e5 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -1,719 +1,156 @@ -import { EventEmitter } from 'node:events' -import { promises as fs } from 'node:fs' -import { join } from 'node:path' - -import { isLinux, isMac, isWin } from '@main/constant' import { getBinaryPath } 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 { app } from 'electron' -import log from 'electron-log' -import { v4 as uuidv4 } from 'uuid' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import { MCPServer } from '@types' +import Logger from 'electron-log' -import { CacheService } from './CacheService' -import { windowService } from './WindowService' +class McpService { + private client: Client | null = null + private clients: Map = new Map() -interface ActiveServer { - client: Client - server: MCPServer -} - -/** - * Service for managing Model Context Protocol servers and tools - */ -export default class MCPService extends EventEmitter { - private servers: MCPServer[] = [] - private activeServers: Map = new Map() - private clients: { [key: string]: Client } = {} - private Client: typeof Client | undefined - private stdioTransport: typeof StdioClientTransport | undefined - private sseTransport: typeof SSEClientTransport | undefined - private initialized = false - private initPromise: Promise | null = null - private configPath: string - - // Simplified server loading state management - private readyState = { - serversLoaded: false, - promise: null as Promise | null, - resolve: null as ((value: void) => void) | null - } - - constructor() { - super() - const userDataPath = app.getPath('userData') - this.configPath = join(userDataPath, 'cherry-mcp-servers.json') - this.createServerLoadingPromise() - this.init().catch((err) => this.logError('Failed to initialize MCP service', err)) - } - - /** - * Create a promise that resolves when servers are loaded - */ - private createServerLoadingPromise(): void { - this.readyState.promise = new Promise((resolve) => { - this.readyState.resolve = resolve + private getServerKey(server: MCPServer): string { + return JSON.stringify({ + baseUrl: server.baseUrl, + command: server.command, + args: server.args, + env: server.env, + id: server.id }) } - private async ensureConfigExists(): Promise { - try { - await fs.access(this.configPath) - } catch { - const defaultServers = { - name: 'mcp-auto-install', - command: 'npx', - args: ['-y', '@mcpmarket/mcp-auto-install', 'connect'], - env: { - MCP_SETTINGS_PATH: this.configPath - }, - isActive: true - } - const defaultConfig = { - mcpServers: { - 'mcp-auto-install': defaultServers - } - } - // 尝试从Redux获取已有配置 - try { - const mainWindow = windowService.getMainWindow() - if (mainWindow) { - const servers = await mainWindow.webContents.executeJavaScript(` - window.store.getState().mcp.servers - `) - if (servers && servers.length > 0) { - // 将从Redux获取的配置保存到文件 - await this.saveConfigToFile(servers.concat([defaultServers])) - log.info('[MCP] Migrated servers config from Redux to file') - return - } - } - } catch (error) { - log.warn('[MCP] Failed to get servers from Redux:', error) - } - - // 如果没有Redux配置,则创建默认配置 - await fs.writeFile(this.configPath, JSON.stringify(defaultConfig, null, 2)) - log.info('[MCP] Created default config file') - } + constructor() { + this.initClient = this.initClient.bind(this) + this.listTools = this.listTools.bind(this) + this.callTool = this.callTool.bind(this) + this.closeClient = this.closeClient.bind(this) + this.removeServer = this.removeServer.bind(this) } - private async loadConfigFromFile(): Promise { - try { - const data = await fs.readFile(this.configPath, 'utf-8') - const config = JSON.parse(data) + async initClient(server: MCPServer) { + const serverKey = this.getServerKey(server) - if (config.mcpServers && typeof config.mcpServers === 'object') { - console.log('读写读写读写', config) - return Object.entries(config.mcpServers).map(([name, serverData]) => ({ - name, - ...(serverData as Omit) - })) - } - - return [] - } catch (error) { - log.error('[MCP] Error loading config file:', error) - return [] - } - } - - private async saveConfigToFile(servers: MCPServer[]): Promise { - try { - // 将数组转换为对象结构 - const mcpServers = servers.reduce( - (acc, server) => { - const { name, ...serverData } = server - acc[name] = serverData - return acc - }, - {} as Record> - ) - - const config = { mcpServers } - await fs.writeFile(this.configPath, JSON.stringify(config, null, 2)) - } catch (error) { - log.error('[MCP] Error saving config file:', error) - throw error - } - } - - /** - * Set servers received from Redux and trigger initialization if needed - */ - public setServers(servers: any): void { - // 如果已初始化,则更新服务器列表并保存到文件 - this.servers = servers - if (this.initialized) { - log.info(`[MCP] Received ${servers.length} servers from Redux, saving to file`) - // 保存到文件 - this.saveConfigToFile(servers).catch((err) => { - log.error('[MCP] Failed to save servers to file:', err) - }) - } else { - log.info(`[MCP] Received ${servers.length} servers from Redux, but service not initialized yet`) - - // 如果未初始化,则标记已加载并解决 Promise - if (!this.readyState.serversLoaded && this.readyState.resolve) { - this.readyState.serversLoaded = true - this.readyState.resolve() - this.readyState.resolve = null - } - - // 初始化服务 - // this.init().catch((err) => this.logError('Failed to initialize MCP service', err)) - } - } - - /** - * Initialize the MCP service if not already initialized - */ - public async init(): Promise { - if (this.initialized) return - if (this.initPromise) return this.initPromise - - this.initPromise = (async () => { - try { - log.info('[MCP] Starting initialization') - - // 加载 SDK 组件 - const [Client, StdioTransport, SSETransport] = await Promise.all([ - this.importClient(), - this.importStdioClientTransport(), - this.importSSEClientTransport() - ]) - - this.Client = Client - this.stdioTransport = StdioTransport - this.sseTransport = SSETransport - - // 等待Redux初始化完成后再加载配置 - if (!this.readyState.serversLoaded && this.readyState.promise) { - await this.readyState.promise - } - // 确保配置文件存在 - await this.ensureConfigExists() - // 从文件加载配置 - const serversFromFile = await this.loadConfigFromFile() - if (serversFromFile.length > 0) { - this.servers = serversFromFile - // 将从文件加载的配置通知给 Redux - this.notifyReduxServersChanged(serversFromFile) - } - - // 标记为已初始化并解决 readyState 的 Promise - this.initialized = true - if (this.readyState.resolve) { - this.readyState.serversLoaded = true - this.readyState.resolve() - this.readyState.resolve = null - } - - // 加载活跃服务器 - await this.loadActiveServers() - log.info('[MCP] Initialization successfully') - - return - } catch (err) { - this.initialized = false - log.error('[MCP] Failed to initialize:', err) - throw err - } finally { - this.initPromise = null - } - })() - - return this.initPromise - } - - /** - * Helper to create consistent error logging functions - */ - private logError(message: string, err?: unknown): void { - log.error(`[MCP] ${message}`, err) - } - - /** - * Import the MCP client SDK - */ - private async importClient() { - try { - const { Client } = await import('@modelcontextprotocol/sdk/client/index.js') - return Client - } catch (err) { - this.logError('Failed to import Client:', err) - throw err - } - } - - /** - * Import the stdio transport - */ - private async importStdioClientTransport() { - try { - const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js') - return StdioClientTransport - } catch (err) { - log.error('[MCP] Failed to import StdioTransport:', err) - throw err - } - } - - /** - * Import the SSE transport - */ - private async importSSEClientTransport() { - try { - const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js') - return SSEClientTransport - } catch (err) { - log.error('[MCP] Failed to import SSETransport:', err) - throw err - } - } - - /** - * List all available MCP servers - */ - public async listAvailableServices(): Promise { - await this.ensureInitialized() - return this.servers - } - - /** - * Ensure the service is initialized before operations - */ - private async ensureInitialized() { - if (!this.initialized) { - log.debug('[MCP] Ensuring initialization') - await this.init() - } - } - - /** - * Add a new MCP server - */ - public async addServer(server: MCPServer): Promise { - await this.ensureInitialized() - - // Check for duplicate name - if (this.servers.some((s) => s.name === server.name)) { - throw new Error(`Server with name ${server.name} already exists`) - } - - // Activate if needed - if (server.isActive) { - await this.activate(server) - } - - // Add to servers list - this.servers = [...this.servers, server] - this.notifyReduxServersChanged(this.servers) - } - - /** - * Update an existing MCP server - */ - public async updateServer(server: MCPServer): Promise { - await this.ensureInitialized() - - const index = this.servers.findIndex((s) => s.name === server.name) - if (index === -1) { - throw new Error(`Server ${server.name} not found`) - } - - // Check activation status change - const wasActive = this.servers[index].isActive - if (wasActive && !server.isActive) { - await this.deactivate(server.name) - } else if (!wasActive && server.isActive) { - await this.activate(server) - } else { - await this.restartServer(server) - } - - // Update servers list - const updatedServers = [...this.servers] - updatedServers[index] = server - this.servers = updatedServers - - // Notify Redux - this.notifyReduxServersChanged(updatedServers) - } - - public async restartServer(_server: MCPServer): Promise { - await this.ensureInitialized() - - const server = this.servers.find((s) => s.name === _server.name) - - if (server) { - if (server.isActive) { - await this.deactivate(server.name) - } - await this.activate(server) - } - } - /** - * Delete an MCP server - */ - public async deleteServer(serverName: string): Promise { - await this.ensureInitialized() - - // Deactivate if running - if (this.clients[serverName]) { - await this.deactivate(serverName) - } - - // Update servers list - const filteredServers = this.servers.filter((s) => s.name !== serverName) - this.servers = filteredServers - this.notifyReduxServersChanged(filteredServers) - } - - /** - * Set a server's active state - */ - public async setServerActive(params: { name: string; isActive: boolean }): Promise { - await this.ensureInitialized() - - const { name, isActive } = params - const server = this.servers.find((s) => s.name === name) - - if (!server) { - throw new Error(`Server ${name} not found`) - } - - // Activate or deactivate as needed - if (isActive) { - await this.activate(server) - } else { - await this.deactivate(name) - } - - // Update server status - server.isActive = isActive - this.notifyReduxServersChanged([...this.servers]) - } - - /** - * Notify Redux in the renderer process about server changes - */ - private notifyReduxServersChanged(servers: MCPServer[]): void { - const mainWindow = windowService.getMainWindow() - if (mainWindow) { - mainWindow.webContents.send('mcp:servers-changed', servers) - } - } - - /** - * Activate an MCP server - */ - public async activate(server: MCPServer): Promise { - await this.ensureInitialized() - - const { name, baseUrl, command, env } = server - const args = [...(server.args || [])] - - // Skip if already running - if (this.clients[name]) { - log.info(`[MCP] Server ${name} is already running`) + // Check if we already have a client for this server configuration + const existingClient = this.clients.get(serverKey) + if (existingClient) { + this.client = existingClient return } + // If there's an existing client for a different server, close it + if (this.client) { + await this.closeClient() + } + + // Create new client instance for each connection + this.client = new Client({ name: 'McpService', version: '1.0.0' }, { capabilities: {} }) + + const args = [...(server.args || [])] + let transport: StdioClientTransport | SSEClientTransport try { // Create appropriate transport based on configuration - if (baseUrl) { - transport = new this.sseTransport!(new URL(baseUrl)) - } else if (command) { - let cmd: string = command - if (command === 'npx') { + if (server.baseUrl) { + transport = new SSEClientTransport(new URL(server.baseUrl)) + } else if (server.command) { + let cmd = server.command + + if (server.command === 'npx') { cmd = await getBinaryPath('bun') if (cmd === 'bun') { cmd = 'npx' } - log.info(`[MCP] Using command: ${cmd}`) + Logger.info(`[MCP] Using command: ${cmd}`) // add -x to args if args exist if (args && args.length > 0) { if (!args.includes('-y')) { - args.unshift('-y') + !args.includes('-y') && args.unshift('-y') } if (cmd.includes('bun') && !args.includes('x')) { args.unshift('x') } } - } else if (command === 'uvx') { + } + + if (server.command === 'uvx') { cmd = await getBinaryPath('uvx') } - log.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) + Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) - transport = new this.stdioTransport!({ + transport = new StdioClientTransport({ command: cmd, args, - stderr: 'pipe', - env: { - PATH: this.getEnhancedPath(process.env.PATH || ''), - ...env - } + env: server.env }) } else { throw new Error('Either baseUrl or command must be provided') } - // Create and connect client - const client = new this.Client!({ name, version: '1.0.0' }, { capabilities: {} }) + await this.client.connect(transport) - await client.connect(transport) + // Store the new client in the cache + this.clients.set(serverKey, this.client) - // Store client and server info - this.clients[name] = client - this.activeServers.set(name, { client, server }) - - log.info(`[MCP] Activated server: ${server.name}`) - this.emit('server-started', { name }) - } catch (error) { - log.error(`[MCP] Error activating server ${name}:`, error) - this.setServerActive({ name, isActive: false }) + Logger.info(`[MCP] Activated server: ${server.name}`) + } catch (error: any) { + Logger.error(`[MCP] Error activating server ${server.name}:`, error) throw error } } - /** - * Deactivate an MCP server - */ - public async deactivate(name: string): Promise { - await this.ensureInitialized() - - if (!this.clients[name]) { - log.warn(`[MCP] Server ${name} is not running`) - return - } - - try { - log.info(`[MCP] Stopping server: ${name}`) - await this.clients[name].close() - delete this.clients[name] - this.activeServers.delete(name) - this.emit('server-stopped', { name }) - } catch (error) { - log.error(`[MCP] Error deactivating server ${name}:`, error) - throw error - } - } - - /** - * List available tools from active MCP servers - */ - 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 - if (serverName) { - return await this.listToolsFromServer(serverName) - } - - // Otherwise list tools from all active servers - 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(`Error listing tools for ${clientName}`, error) + async closeClient() { + if (this.client) { + // Remove the client from the cache + for (const [key, client] of this.clients.entries()) { + if (client === this.client) { + this.clients.delete(key) + break } } - log.info(`[MCP] Total tools listed: ${allTools.length}`) - return allTools - } catch (error) { - this.logError('Error listing tools:', error) - return [] + await this.client.close() + this.client = null } } - /** - * 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 cacheKey = `mcp:list_tool:${serverName}` + async removeServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) { + await this.closeClient() + this.clients.delete(this.getServerKey(server)) + } - if (CacheService.has(cacheKey)) { - log.info(`[MCP] Tools from ${serverName} loaded from cache`) - // Check if cache is still valid - const cachedTools = CacheService.get(cacheKey) - if (cachedTools && cachedTools.length > 0) { - return cachedTools - } - CacheService.remove(cacheKey) - } - - const { tools } = await this.clients[serverName].listTools() - - const transformedTools = tools.map((tool: any) => ({ + async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) { + await this.initClient(server) + const { tools } = await this.client!.listTools() + return tools.map((tool) => ({ ...tool, - serverName, - id: 'f' + uuidv4().replace(/-/g, '') + serverId: server.id, + serverName: server.name })) - - // Cache the tools for 5 minutes - if (transformedTools.length > 0) { - CacheService.set(cacheKey, transformedTools, 5 * 60 * 1000) - } - - log.info(`[MCP] Tools from ${serverName}:`, transformedTools) - return transformedTools } /** * Call a tool on an MCP server */ - public async callTool(params: { client: string; name: string; args: any }): Promise { - await this.ensureInitialized() - - const { client, name, args } = params - - if (!this.clients[client]) { - throw new Error(`MCP Client ${client} not found`) - } - - log.info('[MCP] Calling:', client, name, args) + public async callTool( + _: Electron.IpcMainInvokeEvent, + { server, name, args }: { server: MCPServer; name: string; args: any } + ): Promise { + await this.initClient(server) try { - return await this.clients[client].callTool({ - name, - arguments: args - }) + Logger.info('[MCP] Calling:', server.name, name, args) + const result = await this.client!.callTool({ name, arguments: args }) + return result } catch (error) { - log.error(`[MCP] Error calling tool ${name} on ${client}:`, error) + Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error) throw error } } - - /** - * Clean up all MCP resources - */ - public async cleanup(): Promise { - const clientNames = Object.keys(this.clients) - - if (clientNames.length === 0) { - log.info('[MCP] No active servers to clean up') - return - } - - log.info(`[MCP] Cleaning up ${clientNames.length} active servers`) - - // Deactivate all clients - await Promise.allSettled( - clientNames.map((name) => - this.deactivate(name).catch((err) => { - log.error(`[MCP] Error during cleanup of ${name}:`, err) - }) - ) - ) - - this.clients = {} - this.activeServers.clear() - log.info('[MCP] All servers cleaned up') - } - - /** - * Load all active servers - */ - private async loadActiveServers(): Promise { - console.log('loadActiveServers', this.servers) - const activeServers = this.servers.filter((server) => server.isActive) - - if (activeServers.length === 0) { - log.info('[MCP] No active servers to load') - return - } - - log.info(`[MCP] Start loading ${activeServers.length} active servers`) - - // Activate servers in parallel for better performance - await Promise.allSettled( - activeServers.map(async (server) => { - try { - await this.activate(server) - } catch (error) { - this.logError(`Failed to activate server ${server.name}`, error) - this.emit('server-error', { name: server.name, error }) - } - }) - ) - - log.info(`[MCP] End loading ${Object.keys(this.clients).length} active servers`) - } - - /** - * Get enhanced PATH including common tool locations - */ - private getEnhancedPath(originalPath: string): string { - // 将原始 PATH 按分隔符分割成数组 - const pathSeparator = process.platform === 'win32' ? ';' : ':' - const existingPaths = new Set(originalPath.split(pathSeparator).filter(Boolean)) - const homeDir = process.env.HOME || process.env.USERPROFILE || '' - - // 定义要添加的新路径 - const newPaths: string[] = [] - - if (isMac) { - newPaths.push( - '/bin', - '/usr/bin', - '/usr/local/bin', - '/usr/local/sbin', - '/opt/homebrew/bin', - '/opt/homebrew/sbin', - '/usr/local/opt/node/bin', - `${homeDir}/.nvm/current/bin`, - `${homeDir}/.npm-global/bin`, - `${homeDir}/.yarn/bin`, - `${homeDir}/.cargo/bin`, - '/opt/local/bin' - ) - } - - if (isLinux) { - newPaths.push( - '/bin', - '/usr/bin', - '/usr/local/bin', - `${homeDir}/.nvm/current/bin`, - `${homeDir}/.npm-global/bin`, - `${homeDir}/.yarn/bin`, - `${homeDir}/.cargo/bin`, - '/snap/bin' - ) - } - - if (isWin) { - newPaths.push(`${process.env.APPDATA}\\npm`, `${homeDir}\\AppData\\Local\\Yarn\\bin`, `${homeDir}\\.cargo\\bin`) - } - - // 只添加不存在的路径 - for (const path of newPaths) { - if (path && !existingPaths.has(path)) { - existingPaths.add(path) - } - } - - // 转换回字符串 - return Array.from(existingPaths).join(pathSeparator) - } } + +export default new McpService() diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts index 972364f5..29595621 100644 --- a/src/main/utils/index.ts +++ b/src/main/utils/index.ts @@ -42,3 +42,7 @@ export function dumpPersistState() { } return JSON.stringify(persistState) } + +export const runAsyncFunction = async (fn: () => void) => { + await fn() +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index cfc5acad..e109e1e1 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -146,17 +146,9 @@ declare global { openExternal: (url: string, options?: OpenExternalOptions) => Promise } mcp: { - // servers - listServers: () => Promise - addServer: (server: MCPServer) => Promise - updateServer: (server: MCPServer) => Promise - deleteServer: (serverName: string) => Promise - setServerActive: (name: string, isActive: boolean) => Promise - // tools - listTools: () => Promise - callTool: ({ client, name, args }: { client: string; name: string; args: any }) => Promise - // status - cleanup: () => Promise + removeServer: (server: MCPServer) => Promise + listTools: (server: MCPServer) => Promise + callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise } copilot: { getAuthMessage: ( diff --git a/src/preload/index.ts b/src/preload/index.ts index bff48851..4d3ebbc1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -120,15 +120,10 @@ const api = { ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey) }, mcp: { - listServers: () => ipcRenderer.invoke('mcp:list-servers'), - addServer: (server: MCPServer) => ipcRenderer.invoke('mcp:add-server', server), - updateServer: (server: MCPServer) => ipcRenderer.invoke('mcp:update-server', server), - deleteServer: (serverName: string) => ipcRenderer.invoke('mcp:delete-server', serverName), - setServerActive: (name: string, isActive: boolean) => - ipcRenderer.invoke('mcp:set-server-active', { name, isActive }), - listTools: (serverName?: string) => ipcRenderer.invoke('mcp:list-tools', serverName), - callTool: (params: { client: string; name: string; args: any }) => ipcRenderer.invoke('mcp:call-tool', params), - cleanup: () => ipcRenderer.invoke('mcp:cleanup') + removeServer: (server: MCPServer) => ipcRenderer.invoke('mcp:remove-server', server), + listTools: (server: MCPServer) => ipcRenderer.invoke('mcp:list-tools', server), + callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => + ipcRenderer.invoke('mcp:call-tool', { server, name, args }) }, shell: { openExternal: shell.openExternal diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 2151d8c7..09659681 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -19,7 +19,7 @@ --color-gray-2: #414853; --color-gray-3: #32363f; - --color-text-1: rgba(255, 255, 245, 0.86); + --color-text-1: rgba(255, 255, 245, 0.9); --color-text-2: rgba(235, 235, 245, 0.6); --color-text-3: rgba(235, 235, 245, 0.38); diff --git a/src/renderer/src/components/Icons/ToolsCallingIcon.tsx b/src/renderer/src/components/Icons/ToolsCallingIcon.tsx index 323c9570..7a591d23 100644 --- a/src/renderer/src/components/Icons/ToolsCallingIcon.tsx +++ b/src/renderer/src/components/Icons/ToolsCallingIcon.tsx @@ -23,7 +23,7 @@ const Container = styled.div` ` const Icon = styled(ToolOutlined)` - color: #d97757; + color: var(--color-primary); font-size: 15px; margin-right: 6px; ` diff --git a/src/renderer/src/components/IndicatorLight.tsx b/src/renderer/src/components/IndicatorLight.tsx index c7eaa8bd..edce665e 100644 --- a/src/renderer/src/components/IndicatorLight.tsx +++ b/src/renderer/src/components/IndicatorLight.tsx @@ -4,15 +4,25 @@ import styled from 'styled-components' interface IndicatorLightProps { color: string + size?: number + shadow?: boolean + style?: React.CSSProperties + animation?: boolean } -const Light = styled.div<{ color: string }>` - width: 8px; - height: 8px; +const Light = styled.div<{ + color: string + size: number + shadow?: boolean + style?: React.CSSProperties + animation?: boolean +}>` + width: ${({ size }) => size}px; + height: ${({ size }) => size}px; border-radius: 50%; background-color: ${({ color }) => color}; - box-shadow: 0 0 6px ${({ color }) => color}; - animation: pulse 2s infinite; + box-shadow: ${({ shadow, color }) => (shadow ? `0 0 6px ${color}` : 'none')}; + animation: ${({ animation }) => (animation ? 'pulse 2s infinite' : 'none')}; @keyframes pulse { 0% { @@ -27,9 +37,9 @@ const Light = styled.div<{ color: string }>` } ` -const IndicatorLight: React.FC = ({ color }) => { +const IndicatorLight: React.FC = ({ color, size = 8, shadow = true, style, animation = true }) => { const actualColor = color === 'green' ? '#22c55e' : color - return + return } export default IndicatorLight diff --git a/src/renderer/src/components/ListItem/index.tsx b/src/renderer/src/components/ListItem/index.tsx index 7c2b785f..7e2165c5 100644 --- a/src/renderer/src/components/ListItem/index.tsx +++ b/src/renderer/src/components/ListItem/index.tsx @@ -8,17 +8,20 @@ interface ListItemProps { subtitle?: string titleStyle?: React.CSSProperties onClick?: () => void + rightContent?: ReactNode + style?: React.CSSProperties } -const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick }: ListItemProps) => { +const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick, rightContent, style }: ListItemProps) => { return ( - + {icon && {icon}} {title} {subtitle && {subtitle}} + {rightContent && {rightContent}} ) @@ -84,4 +87,8 @@ const SubtitleText = styled.div` color: var(--color-text-3); ` +const RightContentWrapper = styled.div` + margin-left: auto; +` + export default ListItem diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index db6ed9fa..9b0bb823 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -1,4 +1,4 @@ -import { isMac } from '@renderer/config/constant' +import { isMac, isWindows } from '@renderer/config/constant' import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import type { FC, PropsWithChildren } from 'react' import type { HTMLAttributes } from 'react' @@ -63,4 +63,6 @@ const NavbarRightContainer = styled.div` display: flex; align-items: center; padding: 0 12px; + padding-right: ${isWindows ? '140px' : 12}; + justify-content: flex-end; ` diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 844060e5..752ed071 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -11,7 +11,6 @@ import { useEffect } from 'react' import { useDefaultModel } from './useAssistant' import useFullScreenNotice from './useFullScreenNotice' -import { useInitMCPServers } from './useMCPServers' import { useRuntime } from './useRuntime' import { useSettings } from './useSettings' import useUpdateHandler from './useUpdateHandler' @@ -26,7 +25,6 @@ export function useAppInit() { useUpdateHandler() useFullScreenNotice() - useInitMCPServers() useEffect(() => { avatar?.value && dispatch(setAvatar(avatar.value)) diff --git a/src/renderer/src/hooks/useMCPServers.ts b/src/renderer/src/hooks/useMCPServers.ts index cd513977..553df561 100644 --- a/src/renderer/src/hooks/useMCPServers.ts +++ b/src/renderer/src/hooks/useMCPServers.ts @@ -1,7 +1,6 @@ -import store, { useAppSelector } from '@renderer/store' -import { setMCPServers } from '@renderer/store/mcp' +import store, { useAppDispatch, useAppSelector } from '@renderer/store' +import { addMCPServer, deleteMCPServer, setMCPServers, updateMCPServer } from '@renderer/store/mcp' import { MCPServer } from '@renderer/types' -import { useEffect } from 'react' const ipcRenderer = window.electron.ipcRenderer @@ -13,82 +12,16 @@ ipcRenderer.on('mcp:servers-changed', (_event, servers) => { export const useMCPServers = () => { const mcpServers = useAppSelector((state) => state.mcp.servers) const activedMcpServers = useAppSelector((state) => state.mcp.servers?.filter((server) => server.isActive)) - - const addMCPServer = async (server: MCPServer) => { - try { - await window.api.mcp.addServer(server) - // Main process will send back updated servers via mcp:servers-changed - } catch (error) { - console.error('Failed to add MCP server:', error) - throw error - } - } - - const updateMCPServer = async (server: MCPServer) => { - try { - await window.api.mcp.updateServer(server) - // Main process will send back updated servers via mcp:servers-changed - } catch (error) { - console.error('Failed to update MCP server:', error) - throw error - } - } - - const deleteMCPServer = async (name: string) => { - try { - await window.api.mcp.deleteServer(name) - // Main process will send back updated servers via mcp:servers-changed - } catch (error) { - console.error('Failed to delete MCP server:', error) - throw error - } - } - - const setMCPServerActive = async (name: string, isActive: boolean) => { - try { - await window.api.mcp.setServerActive(name, isActive) - // Main process will send back updated servers via mcp:servers-changed - } catch (error) { - console.error('Failed to set MCP server active status:', error) - throw error - } - } - - const getActiveMCPServers = () => { - return mcpServers.filter((server) => server.isActive) - } + const dispatch = useAppDispatch() return { mcpServers, activedMcpServers, - addMCPServer, - updateMCPServer, - deleteMCPServer, - setMCPServerActive, - getActiveMCPServers + addMCPServer: (server: MCPServer) => dispatch(addMCPServer(server)), + updateMCPServer: (server: MCPServer) => dispatch(updateMCPServer(server)), + deleteMCPServer: (id: string) => dispatch(deleteMCPServer(id)), + setMCPServerActive: (server: MCPServer, isActive: boolean) => dispatch(updateMCPServer({ ...server, isActive })), + getActiveMCPServers: () => mcpServers.filter((server) => server.isActive), + updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers)) } } - -export const useInitMCPServers = () => { - const mcpServers = useAppSelector((state) => state.mcp.servers) - // const dispatch = useAppDispatch() - - // Send servers to main process when they change in Redux - useEffect(() => { - ipcRenderer.send('mcp:servers-from-renderer', mcpServers) - }, [mcpServers]) - - // Initial load of MCP servers from main process - // useEffect(() => { - // const loadServers = async () => { - // try { - // const servers = await window.api.mcp.listServers() - // dispatch(setMCPServers(servers)) - // } catch (error) { - // console.error('Failed to load MCP servers:', error) - // } - // } - - // loadServers() - // }, [dispatch]) -} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 7604210c..0f762b82 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -961,10 +961,7 @@ "argsTooltip": "Each argument on a new line", "baseUrlTooltip": "Remote server base URL", "command": "Command", - "commandRequired": "Please enter a command", "config_description": "Configure Model Context Protocol servers", - "confirmDelete": "Delete Server", - "confirmDeleteMessage": "Are you sure you want to delete the server?", "deleteError": "Failed to delete server", "deleteSuccess": "Server deleted successfully", "dependenciesInstall": "Install Dependencies", @@ -975,7 +972,8 @@ "editServer": "Edit Server", "env": "Environment Variables", "envTooltip": "Format: KEY=value, one per line", - "findMore": "Find More MCP Servers", + "findMore": "Find More MCP", + "searchNpx": "Search MCP", "install": "Install", "installError": "Failed to install dependencies", "installSuccess": "Dependencies installed successfully", @@ -985,8 +983,8 @@ "jsonSaveSuccess": "JSON configuration has been saved.", "missingDependencies": "is Missing, please install it to continue.", "name": "Name", - "nameRequired": "Please enter a server name", "noServers": "No servers configured", + "newServer": "MCP Server", "npx_list": { "actions": "Actions", "desc": "Search and add npm packages as MCP servers", @@ -1002,10 +1000,13 @@ "usage": "Usage", "version": "Version" }, + "errors": { + "32000": "MCP server failed to start, please check the parameters according to the tutorial" + }, "serverPlural": "servers", "serverSingular": "server", "title": "MCP Servers", - "toggleError": "Toggle failed", + "startError": "Start failed", "type": "Type", "updateError": "Failed to update server", "updateSuccess": "Server updated successfully", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index b4501f8a..99ae427d 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -960,10 +960,7 @@ "argsTooltip": "1行に1つの引数を入力してください", "baseUrlTooltip": "リモートURLアドレス", "command": "コマンド", - "commandRequired": "コマンドを入力してください", "config_description": "モデルコンテキストプロトコルサーバーの設定", - "confirmDelete": "サーバーを削除", - "confirmDeleteMessage": "本当にこのサーバーを削除しますか?", "deleteError": "サーバーの削除に失敗しました", "deleteSuccess": "サーバーが正常に削除されました", "dependenciesInstall": "依存関係をインストール", @@ -974,7 +971,8 @@ "editServer": "サーバーを編集", "env": "環境変数", "envTooltip": "形式: KEY=value, 1行に1つ", - "findMore": "MCP サーバーを見つける", + "findMore": "MCP を見つける", + "searchNpx": "MCP を検索", "install": "インストール", "installError": "依存関係のインストールに失敗しました", "installSuccess": "依存関係のインストールに成功しました", @@ -984,8 +982,8 @@ "jsonSaveSuccess": "JSON設定が保存されました。", "missingDependencies": "が不足しています。続行するにはインストールしてください。", "name": "名前", - "nameRequired": "サーバー名を入力してください", "noServers": "サーバーが設定されていません", + "newServer": "MCP サーバー", "npx_list": { "actions": "アクション", "desc": "npm パッケージを検索して MCP サーバーとして追加", @@ -1004,11 +1002,14 @@ "serverPlural": "サーバー", "serverSingular": "サーバー", "title": "MCP サーバー", - "toggleError": "切り替えに失敗しました", + "startError": "起動に失敗しました", "type": "タイプ", "updateError": "サーバーの更新に失敗しました", "updateSuccess": "サーバーが正常に更新されました", - "url": "URL" + "url": "URL", + "errors": { + "32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください" + } }, "messages.divider": "メッセージ間に区切り線を表示", "messages.grid_columns": "メッセージグリッドの表示列数", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index a7c94c27..fbe8c6f2 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -960,10 +960,7 @@ "argsTooltip": "Каждый аргумент с новой строки", "baseUrlTooltip": "Адрес удаленного URL", "command": "Команда", - "commandRequired": "Пожалуйста, введите команду", "config_description": "Настройка серверов протокола контекста модели", - "confirmDelete": "Удалить сервер", - "confirmDeleteMessage": "Вы уверены, что хотите удалить этот сервер?", "deleteError": "Не удалось удалить сервер", "deleteSuccess": "Сервер успешно удален", "dependenciesInstall": "Установить зависимости", @@ -974,7 +971,8 @@ "editServer": "Редактировать сервер", "env": "Переменные окружения", "envTooltip": "Формат: KEY=value, по одной на строку", - "findMore": "Найти больше MCP серверов", + "findMore": "Найти больше MCP", + "searchNpx": "Найти MCP", "install": "Установить", "installError": "Не удалось установить зависимости", "installSuccess": "Зависимости успешно установлены", @@ -984,8 +982,8 @@ "jsonSaveSuccess": "JSON конфигурация сохранена", "missingDependencies": "отсутствует, пожалуйста, установите для продолжения.", "name": "Имя", - "nameRequired": "Пожалуйста, введите имя сервера", "noServers": "Серверы не настроены", + "newServer": "MCP сервер", "npx_list": { "actions": "Действия", "desc": "Поиск и добавление npm пакетов в качестве MCP серверов", @@ -1001,10 +999,13 @@ "usage": "Использование", "version": "Версия" }, + "errors": { + "32000": "MCP сервер не запущен, пожалуйста, проверьте параметры" + }, "serverPlural": "серверы", "serverSingular": "сервер", "title": "Серверы MCP", - "toggleError": "Переключение не удалось", + "startError": "Запуск не удалось", "type": "Тип", "updateError": "Ошибка обновления сервера", "updateSuccess": "Сервер успешно обновлен", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 73e62e73..57e6598d 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -961,10 +961,7 @@ "argsTooltip": "每个参数占一行", "baseUrlTooltip": "远程 URL 地址", "command": "命令", - "commandRequired": "请输入命令", "config_description": "配置模型上下文协议服务器", - "confirmDelete": "删除服务器", - "confirmDeleteMessage": "您确定要删除该服务器吗?", "deleteError": "删除服务器失败", "deleteSuccess": "服务器删除成功", "dependenciesInstall": "安装依赖项", @@ -975,7 +972,8 @@ "editServer": "编辑服务器", "env": "环境变量", "envTooltip": "格式:KEY=value,每行一个", - "findMore": "更多 MCP 服务器", + "findMore": "更多 MCP", + "searchNpx": "搜索 MCP", "install": "安装", "installError": "安装依赖项失败", "installSuccess": "依赖项安装成功", @@ -985,8 +983,8 @@ "jsonSaveSuccess": "JSON配置已保存", "missingDependencies": "缺失,请安装它以继续", "name": "名称", - "nameRequired": "请输入服务器名称", "noServers": "未配置服务器", + "newServer": "MCP 服务器", "npx_list": { "actions": "操作", "desc": "搜索并添加 npm 包作为 MCP 服务", @@ -1002,10 +1000,13 @@ "usage": "用法", "version": "版本" }, + "errors": { + "32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整" + }, "serverPlural": "服务器", "serverSingular": "服务器", "title": "MCP 服务器", - "toggleError": "切换失败", + "startError": "启动失败", "type": "类型", "updateError": "更新服务器失败", "updateSuccess": "服务器更新成功", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4f778ace..6f678952 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -960,10 +960,7 @@ "argsTooltip": "每個參數佔一行", "baseUrlTooltip": "遠端 URL 地址", "command": "指令", - "commandRequired": "請輸入指令", "config_description": "設定模型上下文協議伺服器", - "confirmDelete": "刪除伺服器", - "confirmDeleteMessage": "您確定要刪除該伺服器嗎?", "deleteError": "刪除伺服器失敗", "deleteSuccess": "伺服器刪除成功", "dependenciesInstall": "安裝相依套件", @@ -974,7 +971,8 @@ "editServer": "編輯伺服器", "env": "環境變數", "envTooltip": "格式:KEY=value,每行一個", - "findMore": "更多 MCP 伺服器", + "findMore": "更多 MCP", + "searchNpx": "搜索 MCP", "install": "安裝", "installError": "安裝相依套件失敗", "installSuccess": "相依套件安裝成功", @@ -984,8 +982,8 @@ "jsonSaveSuccess": "JSON配置已儲存", "missingDependencies": "缺失,請安裝它以繼續", "name": "名稱", - "nameRequired": "請輸入伺服器名稱", "noServers": "未設定伺服器", + "newServer": "MCP 伺服器", "npx_list": { "actions": "操作", "desc": "搜索並添加 npm 包作為 MCP 服務", @@ -1001,10 +999,13 @@ "usage": "用法", "version": "版本" }, + "errors": { + "32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整" + }, "serverPlural": "伺服器", "serverSingular": "伺服器", "title": "MCP 伺服器", - "toggleError": "切換失敗", + "startError": "啟動失敗", "type": "類型", "updateError": "更新伺服器失敗", "updateSuccess": "伺服器更新成功", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 0af37342..6b46257f 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -886,10 +886,7 @@ "argsTooltip": "Κάθε παράμετρος σε μια γραμμή", "baseUrlTooltip": "Σύνδεσμος Απομακρυσμένης διεύθυνσης URL", "command": "Εντολή", - "commandRequired": "Παρακαλώ εισάγετε την εντολή", "config_description": "Ρυθμίζει το πλαίσιο συντονισμού πρωτοκόλλων διακομιστή", - "confirmDelete": "Διαγραφή διακομιστή", - "confirmDeleteMessage": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον διακομιστή;", "deleteError": "Αποτυχία διαγραφής διακομιστή", "deleteSuccess": "Ο διακομιστής διαγράφηκε επιτυχώς", "dependenciesInstall": "Εγκατάσταση εξαρτήσεων", @@ -910,7 +907,6 @@ "jsonSaveSuccess": "Η διαμορφωτική ρύθμιση JSON αποθηκεύτηκε επιτυχώς", "missingDependencies": "Απο缺失, παρακαλώ εγκαταστήστε το για να συνεχίσετε", "name": "Όνομα", - "nameRequired": "Παρακαλώ εισάγετε το όνομα του διακομιστή", "noServers": "Δεν έχουν ρυθμιστεί διακομιστές", "npx_list": { "actions": "Ενέργειες", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index f1612484..f9751305 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -886,10 +886,7 @@ "argsTooltip": "Cada argumento en una línea", "baseUrlTooltip": "Dirección URL remota", "command": "Comando", - "commandRequired": "Por favor ingrese el comando", "config_description": "Configurar modelo de contexto del protocolo del servidor", - "confirmDelete": "Eliminar servidor", - "confirmDeleteMessage": "¿Está seguro de que desea eliminar este servidor?", "deleteError": "Fallo al eliminar servidor", "deleteSuccess": "Servidor eliminado exitosamente", "dependenciesInstall": "Instalar dependencias", @@ -910,7 +907,6 @@ "jsonSaveSuccess": "Configuración JSON guardada exitosamente", "missingDependencies": "Faltan, instalelas para continuar", "name": "Nombre", - "nameRequired": "Por favor ingrese el nombre del servidor", "noServers": "No se han configurado servidores", "npx_list": { "actions": "Acciones", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 10cd4d4d..9a1c9600 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -886,10 +886,7 @@ "argsTooltip": "Chaque argument sur une ligne", "baseUrlTooltip": "Adresse URL distante", "command": "Commande", - "commandRequired": "Veuillez entrer une commande", "config_description": "Configurer le modèle du protocole de contexte du serveur", - "confirmDelete": "Supprimer le serveur", - "confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer ce serveur ?", "deleteError": "Échec de la suppression du serveur", "deleteSuccess": "Serveur supprimé avec succès", "dependenciesInstall": "Installer les dépendances", @@ -910,7 +907,6 @@ "jsonSaveSuccess": "Configuration JSON sauvegardée", "missingDependencies": "Manquantes, veuillez les installer pour continuer", "name": "Nom", - "nameRequired": "Veuillez entrer le nom du serveur", "noServers": "Aucun serveur configuré", "npx_list": { "actions": "Actions", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index b4a7288f..9dc210f9 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -886,10 +886,7 @@ "argsTooltip": "Cada argumento em uma linha", "baseUrlTooltip": "Endereço de URL remoto", "command": "Comando", - "commandRequired": "Digite o comando", "config_description": "Configurar modelo de protocolo de contexto do servidor", - "confirmDelete": "Excluir servidor", - "confirmDeleteMessage": "Tem certeza de que deseja excluir este servidor?", "deleteError": "Falha ao excluir servidor", "deleteSuccess": "Servidor excluído com sucesso", "dependenciesInstall": "Instalar dependências", @@ -910,7 +907,6 @@ "jsonSaveSuccess": "Configuração JSON salva com sucesso", "missingDependencies": "Ausente, instale para continuar", "name": "Nome", - "nameRequired": "Digite o nome do servidor", "noServers": "Nenhum servidor configurado", "npx_list": { "actions": "Ações", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 5fe831bf..eb96e138 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -618,9 +618,9 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const toggelEnableMCP = (mcp: MCPServer) => { setEnabledMCPs((prev) => { - const exists = prev.some((item) => item.name === mcp.name) + const exists = prev.some((item) => item.id === mcp.id) if (exists) { - return prev.filter((item) => item.name !== mcp.name) + return prev.filter((item) => item.id !== mcp.id) } else { return [...prev, mcp] } diff --git a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx index 0eaaef68..2d5e41f7 100644 --- a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx @@ -27,19 +27,14 @@ const MCPToolsButton: FC = ({ enabledMCPs, toggelEnableMCP, ToolbarButton // Check if all active servers are enabled const activeServers = mcpServers.filter((s) => s.isActive) - const anyEnable = activeServers.some((server) => - enabledMCPs.some((enabledServer) => enabledServer.name === server.name) - ) + const anyEnable = activeServers.some((server) => enabledMCPs.some((enabledServer) => enabledServer.id === server.id)) - const enableAll = () => - mcpServers.forEach((s) => { - toggelEnableMCP(s) - }) + const enableAll = () => mcpServers.forEach(toggelEnableMCP) const disableAll = () => mcpServers.forEach((s) => { enabledMCPs.forEach((enabledServer) => { - if (enabledServer.name === s.name) { + if (enabledServer.id === s.id) { toggelEnableMCP(s) } }) @@ -64,32 +59,34 @@ const MCPToolsButton: FC = ({ enabledMCPs, toggelEnableMCP, ToolbarButton - {mcpServers.length > 0 ? ( - mcpServers - .filter((s) => s.isActive) - .map((server) => ( - -
-
{server.name}
- {server.description && ( - -
{truncateText(server.description)}
-
- )} - {server.baseUrl &&
{server.baseUrl}
} -
- s.name === server.name)} - onChange={() => toggelEnableMCP(server)} - /> -
- )) - ) : ( -
-
{t('settings.mcp.noServers')}
-
- )} + + {mcpServers.length > 0 ? ( + mcpServers + .filter((s) => s.isActive) + .map((server) => ( + +
+
{server.name}
+ {server.description && ( + +
{truncateText(server.description)}
+
+ )} + {server.baseUrl &&
{server.baseUrl}
} +
+ s.id === server.id)} + onChange={() => toggelEnableMCP(server)} + /> +
+ )) + ) : ( +
+
{t('settings.mcp.noServers')}
+
+ )} +
) @@ -106,7 +103,7 @@ const MCPToolsButton: FC = ({ enabledMCPs, toggelEnableMCP, ToolbarButton overlayClassName="mention-models-dropdown"> - 0 ? '#d97757' : 'var(--color-icon)' }} /> + 0 ? 'var(--color-primary)' : 'var(--color-icon)' }} /> @@ -127,6 +124,10 @@ const McpServerItems = styled.div` font-weight: 500; font-size: 14px; color: var(--color-text-1); + max-width: 400px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } .server-description { @@ -177,4 +178,8 @@ const DropdownHeader = styled.div` } ` +const DropdownBody = styled.div` + padding-bottom: 10px; +` + export default MCPToolsButton diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/MessageTools.tsx index eec10f95..cd3d334b 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTools.tsx @@ -100,7 +100,7 @@ const MessageTools: FC = ({ message }) => { ), children: isDone && result && ( - +
{JSON.stringify(result, null, 2)}
) @@ -129,9 +129,8 @@ const MessageTools: FC = ({ message }) => { onCancel={() => setExpandedResponse(null)} footer={null} width="80%" - styles={{ - body: { maxHeight: '80vh', overflow: 'auto' } - }}> + centered + styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}> {expandedResponse && ( = ({ activeAssistant }) => { )} - + {!showAssistants && ( diff --git a/src/renderer/src/pages/knowledge/KnowledgePage.tsx b/src/renderer/src/pages/knowledge/KnowledgePage.tsx index 8129816c..a94e53c6 100644 --- a/src/renderer/src/pages/knowledge/KnowledgePage.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgePage.tsx @@ -6,13 +6,12 @@ import { SearchOutlined, SettingOutlined } from '@ant-design/icons' -import { Navbar, NavbarCenter, NavbarRight as NavbarRightFromComponents } from '@renderer/components/app/Navbar' +import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import DragableList from '@renderer/components/DragableList' import { HStack } from '@renderer/components/Layout' import ListItem from '@renderer/components/ListItem' import PromptPopup from '@renderer/components/Popups/PromptPopup' import Scrollbar from '@renderer/components/Scrollbar' -import { isWindows } from '@renderer/config/constant' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useShortcut } from '@renderer/hooks/useShortcuts' import { NavbarIcon } from '@renderer/pages/home/Navbar' @@ -252,9 +251,4 @@ const NarrowIcon = styled(NavbarIcon)` } ` -const NavbarRight = styled(NavbarRightFromComponents)` - min-width: auto; - padding-right: ${isWindows ? '140px' : 15}; -` - export default KnowledgePage diff --git a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerPopup.tsx deleted file mode 100644 index 5a729237..00000000 --- a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerPopup.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { TopView } from '@renderer/components/TopView' -import { useAppSelector } from '@renderer/store' -import { MCPServer } from '@renderer/types' -import { Form, Input, Modal, Radio, Switch } from 'antd' -import TextArea from 'antd/es/input/TextArea' -import React, { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' - -interface ShowParams { - server?: MCPServer - create?: boolean -} - -interface Props extends ShowParams { - resolve: (data: any) => void -} - -interface MCPFormValues { - name: string - description?: string - serverType: 'sse' | 'stdio' - baseUrl?: string - command?: string - args?: string - env?: string - isActive: boolean -} - -const PopupContainer: React.FC = ({ server, create, resolve }) => { - const [open, setOpen] = useState(true) - const { t } = useTranslation() - const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio') - const mcpServers = useAppSelector((state) => state.mcp.servers) - const [form] = Form.useForm() - const [loading, setLoading] = useState(false) - - useEffect(() => { - if (server) { - // Determine server type based on server properties - const serverType = server.baseUrl ? 'sse' : 'stdio' - setServerType(serverType) - - form.setFieldsValue({ - name: server.name, - description: server.description, - serverType: serverType, - baseUrl: server.baseUrl || '', - command: server.command || '', - args: server.args ? server.args.join('\n') : '', - env: server.env - ? Object.entries(server.env) - .map(([key, value]) => `${key}=${value}`) - .join('\n') - : '', - isActive: server.isActive - }) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - // Watch the serverType field to update the form layout dynamically - useEffect(() => { - const type = form.getFieldValue('serverType') - type && setServerType(type) - }, [form]) - - const onOK = async () => { - setLoading(true) - try { - const values = await form.validateFields() - const mcpServer: MCPServer = { - name: values.name, - description: values.description, - isActive: values.isActive - } - - if (values.serverType === 'sse') { - mcpServer.baseUrl = values.baseUrl - } else { - mcpServer.command = values.command - mcpServer.args = values.args ? values.args.split('\n').filter((arg) => arg.trim() !== '') : [] - - const env: Record = {} - if (values.env) { - values.env.split('\n').forEach((line) => { - if (line.trim()) { - const [key, ...chunks] = line.split('=') - const value = chunks.join('=') - if (key && value) { - env[key.trim()] = value.trim() - } - } - }) - } - mcpServer.env = Object.keys(env).length > 0 ? env : undefined - } - - if (server && !create) { - try { - await window.api.mcp.updateServer(mcpServer) - window.message.success(t('settings.mcp.updateSuccess')) - setLoading(false) - setOpen(false) - form.resetFields() - } catch (error: any) { - window.message.error(`${t('settings.mcp.updateError')}: ${error.message}`) - setLoading(false) - } - } else { - // Check for duplicate name - if (mcpServers.some((server: MCPServer) => server.name === mcpServer.name)) { - window.message.error(t('settings.mcp.duplicateName')) - setLoading(false) - return - } - - try { - await window.api.mcp.addServer(mcpServer) - window.message.success(t('settings.mcp.addSuccess')) - setLoading(false) - setOpen(false) - form.resetFields() - } catch (error: any) { - window.message.error(`${t('settings.mcp.addError')}: ${error.message}`) - setLoading(false) - } - } - } catch (error: any) { - setLoading(false) - } - } - - const onCancel = () => { - setOpen(false) - } - - const onClose = () => { - resolve({}) - } - - AddMcpServerPopup.hide = onCancel - - return ( - -
- - - - - -