From 8bb8081f315afac144506687e63960b0ea05e49b Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 27 Mar 2025 15:15:01 +0800 Subject: [PATCH 01/16] feat(Messages): add foldSelected property to assistant messages for improved message handling --- src/renderer/src/store/messages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/src/store/messages.ts b/src/renderer/src/store/messages.ts index 6e244b892..510d409e7 100644 --- a/src/renderer/src/store/messages.ts +++ b/src/renderer/src/store/messages.ts @@ -278,6 +278,7 @@ export const sendMessage = const assistantMessage = getAssistantMessage({ assistant, topic }) assistantMessage.askId = userMessage.id assistantMessage.status = 'sending' + assistantMessage.foldSelected = true assistantMessages.push(assistantMessage) } From bbc7b20183754f93a54be066b7a806626ec4aff1 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 27 Mar 2025 15:15:15 +0800 Subject: [PATCH 02/16] lint: fix code format --- src/main/ipc.ts | 2 +- .../src/components/DragableList/index.tsx | 2 +- .../src/context/SyntaxHighlighterProvider.tsx | 2 +- src/renderer/src/context/ThemeProvider.tsx | 6 +-- .../home/Messages/MessageGroupModelList.tsx | 4 +- .../mini/home/components/FeatureMenus.tsx | 11 ++-- .../windows/mini/home/components/InputBar.tsx | 53 ++++++++++--------- 7 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b2cbfd2fd..a4841f5c8 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -17,8 +17,8 @@ import FileStorage from './services/FileStorage' import { GeminiService } from './services/GeminiService' import KnowledgeService from './services/KnowledgeService' import MCPService from './services/MCPService' -import ObsidianVaultService from './services/ObsidianVaultService' import * as NutstoreService from './services/NutstoreService' +import ObsidianVaultService from './services/ObsidianVaultService' import { ProxyConfig, proxyManager } from './services/ProxyManager' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' diff --git a/src/renderer/src/components/DragableList/index.tsx b/src/renderer/src/components/DragableList/index.tsx index a5c2db91a..cc281e7b0 100644 --- a/src/renderer/src/components/DragableList/index.tsx +++ b/src/renderer/src/components/DragableList/index.tsx @@ -8,8 +8,8 @@ import { OnDragStartResponder, ResponderProvided } from '@hello-pangea/dnd' -import VirtualList from 'rc-virtual-list' import { droppableReorder } from '@renderer/utils' +import VirtualList from 'rc-virtual-list' import { FC } from 'react' interface Props { diff --git a/src/renderer/src/context/SyntaxHighlighterProvider.tsx b/src/renderer/src/context/SyntaxHighlighterProvider.tsx index bbb4624d2..3933b7864 100644 --- a/src/renderer/src/context/SyntaxHighlighterProvider.tsx +++ b/src/renderer/src/context/SyntaxHighlighterProvider.tsx @@ -82,7 +82,7 @@ export const SyntaxHighlighterProvider: React.FC = ({ childre [highlighter, highlighterTheme] ) - return {children} + return {children} } export const useSyntaxHighlighter = () => { diff --git a/src/renderer/src/context/ThemeProvider.tsx b/src/renderer/src/context/ThemeProvider.tsx index 113898f4f..24c6bce1b 100644 --- a/src/renderer/src/context/ThemeProvider.tsx +++ b/src/renderer/src/context/ThemeProvider.tsx @@ -57,11 +57,7 @@ export const ThemeProvider: React.FC = ({ children, defaultT } }) - return ( - - {children} - - ) + return {children} } export const useTheme = () => use(ThemeContext) diff --git a/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx b/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx index c81d9adc5..695d3a05e 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx @@ -25,7 +25,9 @@ const MessageGroupModelList: FC = ({ messages, setSe return ( - dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}> + dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}> void } -const FeatureMenus = forwardRef(({ text, setRoute, onSendMessage }, ref) => { +const FeatureMenus = ({ + ref, + text, + setRoute, + onSendMessage +}: FeatureMenusProps & { ref?: React.RefObject }) => { const { t } = useTranslation() const [selectedIndex, setSelectedIndex] = useState(0) @@ -94,7 +99,7 @@ const FeatureMenus = forwardRef(({ text, set ) -}) +} FeatureMenus.displayName = 'FeatureMenus' const FeatureList = styled(Scrollbar)` diff --git a/src/renderer/src/windows/mini/home/components/InputBar.tsx b/src/renderer/src/windows/mini/home/components/InputBar.tsx index cb29c6aff..3a8006160 100644 --- a/src/renderer/src/windows/mini/home/components/InputBar.tsx +++ b/src/renderer/src/windows/mini/home/components/InputBar.tsx @@ -2,7 +2,7 @@ import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import { useRuntime } from '@renderer/hooks/useRuntime' import { Input as AntdInput } from 'antd' import { InputRef } from 'rc-input/lib/interface' -import React, { forwardRef, useRef } from 'react' +import React, { useRef } from 'react' import styled from 'styled-components' interface InputBarProps { @@ -14,30 +14,35 @@ interface InputBarProps { handleChange: (e: React.ChangeEvent) => void } -const InputBar = forwardRef( - ({ text, model, placeholder, handleKeyDown, handleChange }, ref) => { - const { generating } = useRuntime() - const inputRef = useRef(null) - if (!generating) { - setTimeout(() => inputRef.current?.input?.focus(), 0) - } - return ( - - - - - ) +const InputBar = ({ + ref, + text, + model, + placeholder, + handleKeyDown, + handleChange +}: InputBarProps & { ref?: React.RefObject }) => { + const { generating } = useRuntime() + const inputRef = useRef(null) + if (!generating) { + setTimeout(() => inputRef.current?.input?.focus(), 0) } -) + return ( + + + + + ) +} InputBar.displayName = 'InputBar' const InputWrapper = styled.div` From 41191f6132d91be7d9797c743151a3b55a76d629 Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat <43230886+MyPrototypeWhat@users.noreply.github.com> Date: Thu, 27 Mar 2025 17:15:16 +0800 Subject: [PATCH 03/16] feat: mcp auto server (#3996) * feat: add configuration file management to MCPService - Introduced methods to ensure the existence of a configuration file, load configurations from it, and save server configurations. - Updated the MCPService class to handle server configurations more effectively, improving initialization and error handling. - Added dependency on chokidar for file system watching. * feat: enhance MCPService configuration handling - Improved configuration management by adding compatibility for both old and new server formats. - Updated methods to ensure configuration file existence, load configurations, and save server data more effectively. - Refined server initialization logic to handle updates and notifications to Redux more efficiently. - Removed unnecessary waiting for server data from Redux during initialization. * feat: enhance MCPService default configuration handling - Added logic to create a default configuration if none exists, improving the initialization process. - Implemented migration of server configurations from Redux to file, ensuring data consistency. - Updated methods to handle nested server structures and improved error handling during server updates. * refactor: clean up MCPService by removing redundant console logs and unused updateServerInRedux method - Eliminated unnecessary console log statements to improve code readability. - Removed the unused updateServerInRedux method, streamlining the MCPService class. - Maintained existing functionality while enhancing code clarity. --- package.json | 2 + src/main/services/MCPService.ts | 182 +++++++++++++++++++++++++------- yarn.lock | 13 ++- 3 files changed, 157 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 4571179dd..de1cd72aa 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@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", @@ -109,6 +110,7 @@ "@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", diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index f234d4d17..136e73d13 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -1,28 +1,38 @@ +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 { EventEmitter } from 'events' import { v4 as uuidv4 } from 'uuid' import { CacheService } from './CacheService' import { windowService } from './WindowService' +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]: any } = {} + 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 = { @@ -33,6 +43,8 @@ export default class MCPService extends EventEmitter { 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)) } @@ -46,23 +58,112 @@ export default class MCPService extends EventEmitter { }) } + 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') + } + } + + private async loadConfigFromFile(): Promise { + try { + const data = await fs.readFile(this.configPath, 'utf-8') + const config = JSON.parse(data) + + 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: MCPServer[]): void { + public setServers(servers: any): void { + // 如果已初始化,则更新服务器列表并保存到文件 this.servers = servers - log.info(`[MCP] Received ${servers.length} servers from Redux`) + 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`) - // Mark servers as loaded and resolve the waiting promise - if (!this.readyState.serversLoaded && this.readyState.resolve) { - this.readyState.serversLoaded = true - this.readyState.resolve() - this.readyState.resolve = null - } + // 如果未初始化,则标记已加载并解决 Promise + if (!this.readyState.serversLoaded && this.readyState.resolve) { + this.readyState.serversLoaded = true + this.readyState.resolve() + this.readyState.resolve = null + } - // Initialize if not already initialized - if (!this.initialized) { - this.init().catch((err) => this.logError('Failed to initialize MCP service', err)) + // 初始化服务 + // this.init().catch((err) => this.logError('Failed to initialize MCP service', err)) } } @@ -70,20 +171,14 @@ export default class MCPService extends EventEmitter { * Initialize the MCP service if not already initialized */ public async init(): Promise { - // If already initialized, return immediately if (this.initialized) return - - // If initialization is in progress, return that promise if (this.initPromise) return this.initPromise this.initPromise = (async () => { try { log.info('[MCP] Starting initialization') - // Wait for servers to be loaded from Redux - await this.waitForServers() - - // Load SDK components in parallel for better performance + // 加载 SDK 组件 const [Client, StdioTransport, SSETransport] = await Promise.all([ this.importClient(), this.importStdioClientTransport(), @@ -94,16 +189,35 @@ export default class MCPService extends EventEmitter { this.stdioTransport = StdioTransport this.sseTransport = SSETransport - // Mark as initialized before loading servers - this.initialized = true + // 等待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) + } - // Load active servers + // 标记为已初始化并解决 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 // Reset flag on error + this.initialized = false log.error('[MCP] Failed to initialize:', err) throw err } finally { @@ -114,21 +228,10 @@ export default class MCPService extends EventEmitter { return this.initPromise } - /** - * Wait for servers to be loaded from Redux - */ - private async waitForServers(): Promise { - if (!this.readyState.serversLoaded && this.readyState.promise) { - log.info('[MCP] Waiting for servers data from Redux...') - await this.readyState.promise - log.info('[MCP] Servers received, continuing initialization') - } - } - /** * Helper to create consistent error logging functions */ - private logError(message: string, err?: any): void { + private logError(message: string, err?: unknown): void { log.error(`[MCP] ${message}`, err) } @@ -532,6 +635,7 @@ export default class MCPService extends EventEmitter { * 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) { @@ -603,11 +707,11 @@ export default class MCPService extends EventEmitter { } // 只添加不存在的路径 - newPaths.forEach((path) => { + for (const path of newPaths) { if (path && !existingPaths.has(path)) { existingPaths.add(path) } - }) + } // 转换回字符串 return Array.from(existingPaths).join(pathSeparator) diff --git a/yarn.lock b/yarn.lock index 0b7d4fa44..86410e710 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,6 +3182,15 @@ __metadata: languageName: node linkType: hard +"@types/chokidar@npm:^2.1.7": + version: 2.1.7 + resolution: "@types/chokidar@npm:2.1.7" + dependencies: + chokidar: "npm:*" + checksum: 10c0/e296861b45a90da59a871cc09020e1a8b1111b4a954a2f104ea0a0be31f5b565a35710e9d54670288ca9bdf0c7e71d7d070aaf212db03ee14c1bda93db2f1086 + languageName: node + linkType: hard + "@types/d3-color@npm:*": version: 3.1.3 resolution: "@types/d3-color@npm:3.1.3" @@ -3793,6 +3802,7 @@ __metadata: "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch" "@tryfabric/martian": "npm:^1.2.4" "@types/adm-zip": "npm:^0" + "@types/chokidar": "npm:^2.1.7" "@types/fs-extra": "npm:^11" "@types/lodash": "npm:^4.17.5" "@types/markdown-it": "npm:^14" @@ -3811,6 +3821,7 @@ __metadata: axios: "npm:^1.7.3" babel-plugin-styled-components: "npm:^2.1.4" browser-image-compression: "npm:^2.0.2" + chokidar: "npm:^4.0.3" dayjs: "npm:^1.11.11" dexie: "npm:^4.0.8" dexie-react-hooks: "npm:^1.1.7" @@ -5004,7 +5015,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^4.0.0": +"chokidar@npm:*, chokidar@npm:^4.0.0, chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" dependencies: From 710171278a56a568c10e3ece7b0439b39c6a8c5c Mon Sep 17 00:00:00 2001 From: Chen Tao <70054568+eeee0717@users.noreply.github.com> Date: Thu, 27 Mar 2025 20:04:37 +0800 Subject: [PATCH 04/16] =?UTF-8?q?fix(Reranker):=20=E4=BF=AE=E5=A4=8Drerank?= =?UTF-8?q?=20400=20and=20=E5=AE=8C=E5=96=84=E9=94=99=E8=AF=AF=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=20(#4013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(Reranker): enhance error handling with detailed error messages and early return for empty results --- src/main/reranker/BaseReranker.ts | 11 +++++++++++ src/main/reranker/JinaReranker.ts | 6 ++++-- src/main/reranker/SiliconFlowReranker.ts | 6 ++++-- src/main/reranker/VoyageReranker.ts | 6 ++++-- src/main/services/KnowledgeService.ts | 3 +++ 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/reranker/BaseReranker.ts b/src/main/reranker/BaseReranker.ts index 543829089..58a642691 100644 --- a/src/main/reranker/BaseReranker.ts +++ b/src/main/reranker/BaseReranker.ts @@ -17,4 +17,15 @@ export default abstract class BaseReranker { 'Content-Type': 'application/json' } } + + public formatErrorMessage(url: string, error: any, requestBody: any) { + const errorDetails = { + url: url, + message: error.message, + status: error.response?.status, + statusText: error.response?.statusText, + requestBody: requestBody + } + return JSON.stringify(errorDetails, null, 2) + } } diff --git a/src/main/reranker/JinaReranker.ts b/src/main/reranker/JinaReranker.ts index ed14f4746..718774ee2 100644 --- a/src/main/reranker/JinaReranker.ts +++ b/src/main/reranker/JinaReranker.ts @@ -47,8 +47,10 @@ export default class JinaReranker extends BaseReranker { .filter((doc): doc is ExtractChunkData => doc !== undefined) .sort((a, b) => b.score - a.score) } catch (error: any) { - console.error('Jina Reranker API 错误:', error.status) - throw new Error(`${error} - BaseUrl: ${baseURL}`) + const errorDetails = this.formatErrorMessage(url, error, requestBody) + + console.error('Jina Reranker API Error:', errorDetails) + throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`) } } } diff --git a/src/main/reranker/SiliconFlowReranker.ts b/src/main/reranker/SiliconFlowReranker.ts index bba8d5405..d37f547b2 100644 --- a/src/main/reranker/SiliconFlowReranker.ts +++ b/src/main/reranker/SiliconFlowReranker.ts @@ -49,8 +49,10 @@ export default class SiliconFlowReranker extends BaseReranker { .filter((doc): doc is ExtractChunkData => doc !== undefined) .sort((a, b) => b.score - a.score) } catch (error: any) { - console.error('SiliconFlow Reranker API 错误:', error.status) - throw new Error(`${error} - BaseUrl: ${baseURL}`) + const errorDetails = this.formatErrorMessage(url, error, requestBody) + + console.error('SiliconFlow Reranker API 错误:', errorDetails) + throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`) } } } diff --git a/src/main/reranker/VoyageReranker.ts b/src/main/reranker/VoyageReranker.ts index c48fbb9bc..0cfc024ee 100644 --- a/src/main/reranker/VoyageReranker.ts +++ b/src/main/reranker/VoyageReranker.ts @@ -53,8 +53,10 @@ export default class VoyageReranker extends BaseReranker { .filter((doc): doc is ExtractChunkData => doc !== undefined) .sort((a, b) => b.score - a.score) } catch (error: any) { - console.error('Voyage Reranker API 错误:', error.message || error) - throw new Error(`${error} - BaseUrl: ${baseURL}`) + const errorDetails = this.formatErrorMessage(url, error, requestBody) + + console.error('Voyage Reranker API Error:', errorDetails) + throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`) } } } diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts index b224458a1..0e8cb9de3 100644 --- a/src/main/services/KnowledgeService.ts +++ b/src/main/services/KnowledgeService.ts @@ -475,6 +475,9 @@ class KnowledgeService { _: Electron.IpcMainInvokeEvent, { search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] } ): Promise => { + if (results.length === 0) { + return results + } return await new Reranker(base).rerank(search, results) } } From bb6fdd2db7af3ec4d5a91875d1daf06acf1958f8 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 28 Mar 2025 03:45:35 +0800 Subject: [PATCH 05/16] Revert "feat: Use logo instead of avatar" This reverts commit aee0f9ea3f148224da6cd392f3b20511ad1bb600. --- src/renderer/src/assets/images/avatar.png | Bin 35501 -> 11736 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/renderer/src/assets/images/avatar.png b/src/renderer/src/assets/images/avatar.png index 70fc62f49ceda671051355660c2c29a1dffb24ba..c9c59363c5e7a0014221f2a16e6f4bc51b5b420d 100644 GIT binary patch literal 11736 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 Date: Fri, 28 Mar 2025 03:31:15 +0800 Subject: [PATCH 06/16] 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 830f101d0..000000000 --- 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 000000000..c28db0e19 --- /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 de1cd72aa..59f8dac78 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 270f8cbed..2e9d83a9e 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 a4841f5c8..26630c209 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 136e73d13..670136e58 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 972364f58..295956216 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 cfc5acad8..e109e1e1f 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 bff488518..4d3ebbc17 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 2151d8c76..09659681f 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 323c9570f..7a591d231 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 c7eaa8bdc..edce665e6 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 7c2b785f2..7e2165c5d 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 db6ed9fa7..9b0bb823a 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 844060e52..752ed0719 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 cd513977e..553df561e 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 7604210c1..0f762b82f 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 b4501f8ab..99ae427d2 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 a7c94c270..fbe8c6f27 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 73e62e73f..57e6598d3 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 4f778ace7..6f6789522 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 0af37342b..6b46257f2 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 f16124847..f97513058 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 10cd4d4dd..9a1c96002 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 b4a7288f9..9dc210f94 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 5fe831bff..eb96e1384 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 0eaaef688..2d5e41f7a 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 eec10f953..cd3d334bd 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 8129816ce..a94e53c66 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 5a729237a..000000000 --- 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 ( - -
- - - - - -