From 29b5ba787b95819e98b8308e3c69df70f5b8db79 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 28 Mar 2025 03:31:15 +0800 Subject: [PATCH] refactor: mcp service --- ...extprotocol-sdk-npm-1.6.1-b46313efe7.patch | 26 - .../pkce-challenge-npm-4.1.0-fbc51695a3.patch | 18 + package.json | 7 +- resources/scripts/download.js | 59 +- src/main/ipc.ts | 32 +- src/main/resources/icon.ico | Bin 361102 -> 0 bytes src/main/services/FileService.ts | 4 + src/main/services/FileStorage.ts | 5 + src/main/services/MCPService.ts | 737 +++--------------- src/main/utils/index.ts | 4 + src/preload/index.d.ts | 14 +- src/preload/index.ts | 13 +- src/renderer/src/assets/styles/index.scss | 2 +- .../src/components/Icons/ToolsCallingIcon.tsx | 2 +- .../src/components/IndicatorLight.tsx | 24 +- .../src/components/ListItem/index.tsx | 11 +- src/renderer/src/components/app/Navbar.tsx | 4 +- src/renderer/src/hooks/useAppInit.ts | 2 - src/renderer/src/hooks/useMCPServers.ts | 85 +- src/renderer/src/i18n/locales/en-us.json | 13 +- src/renderer/src/i18n/locales/ja-jp.json | 15 +- src/renderer/src/i18n/locales/ru-ru.json | 13 +- src/renderer/src/i18n/locales/zh-cn.json | 13 +- src/renderer/src/i18n/locales/zh-tw.json | 13 +- src/renderer/src/i18n/translate/el-gr.json | 4 - src/renderer/src/i18n/translate/es-es.json | 4 - src/renderer/src/i18n/translate/fr-fr.json | 4 - src/renderer/src/i18n/translate/pt-pt.json | 4 - .../src/pages/home/Inputbar/Inputbar.tsx | 4 +- .../pages/home/Inputbar/MCPToolsButton.tsx | 75 +- .../src/pages/home/Messages/MessageTools.tsx | 8 +- src/renderer/src/pages/home/Navbar.tsx | 6 +- .../src/pages/knowledge/KnowledgePage.tsx | 8 +- .../MCPSettings/AddMcpServerPopup.tsx | 241 ------ .../settings/MCPSettings/EditMcpJsonPopup.tsx | 152 ---- .../settings/MCPSettings/InstallNpxUv.tsx | 11 +- .../settings/MCPSettings/McpSettings.tsx | 241 ++++++ .../pages/settings/MCPSettings/NpxSearch.tsx | 63 +- .../src/pages/settings/MCPSettings/index.tsx | 329 ++++---- .../src/pages/settings/SettingsPage.tsx | 3 +- src/renderer/src/providers/OpenAIProvider.ts | 1 + src/renderer/src/services/ApiService.ts | 10 +- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/mcp.ts | 10 +- src/renderer/src/store/migrate.ts | 10 + src/renderer/src/types/index.ts | 2 + src/renderer/src/utils/mcp-tools.ts | 28 +- src/utils/file.ts | 58 ++ yarn.lock | 50 +- 49 files changed, 861 insertions(+), 1583 deletions(-) delete mode 100644 .yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch create mode 100644 .yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch delete mode 100644 src/main/resources/icon.ico delete mode 100644 src/renderer/src/pages/settings/MCPSettings/AddMcpServerPopup.tsx delete mode 100644 src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx create mode 100644 src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx create mode 100644 src/utils/file.ts 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 07f1f670cb236807c3b7963e0a58015bac3dc8e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 361102 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 q^ds01_ => { 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 ( - -
- - - - - -