From 403ed8cbf4da0e97e3f31876863ff8373a5a9953 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 28 Mar 2025 11:15:49 +0800 Subject: [PATCH] fix: mcp install ui --- src/main/ipc.ts | 1 + src/main/services/MCPService.ts | 14 +- src/main/utils/process.ts | 12 +- src/preload/index.d.ts | 1 + src/preload/index.ts | 3 +- src/renderer/src/i18n/locales/en-us.json | 4 +- src/renderer/src/i18n/locales/ja-jp.json | 4 +- src/renderer/src/i18n/locales/ru-ru.json | 4 +- src/renderer/src/i18n/locales/zh-cn.json | 4 +- src/renderer/src/i18n/locales/zh-tw.json | 4 +- .../src/pages/home/Messages/MessageTools.tsx | 17 +- .../settings/MCPSettings/EditMcpJsonPopup.tsx | 152 ++++++++++++++++++ .../settings/MCPSettings/InstallNpxUv.tsx | 139 +++++++++++----- .../settings/MCPSettings/McpSettings.tsx | 2 - .../MCPSettings/McpSettingsNavbar.tsx | 49 ++++++ .../src/pages/settings/MCPSettings/index.tsx | 82 +++++----- .../src/pages/settings/SettingsPage.tsx | 3 +- 17 files changed, 388 insertions(+), 107 deletions(-) create mode 100644 src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx create mode 100644 src/renderer/src/pages/settings/MCPSettings/McpSettingsNavbar.tsx diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 26630c20..85817efc 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -266,6 +266,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle('mcp:remove-server', mcpService.removeServer) ipcMain.handle('mcp:list-tools', mcpService.listTools) ipcMain.handle('mcp:call-tool', mcpService.callTool) + ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo) ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name)) ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name)) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 0b33269d..e37a9b4f 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -1,4 +1,7 @@ -import { getBinaryPath } from '@main/utils/process' +import os from 'node:os' +import path from 'node:path' + +import { getBinaryName, getBinaryPath } from '@main/utils/process' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' @@ -152,6 +155,15 @@ class McpService { throw error } } + + public async getInstallInfo() { + const dir = path.join(os.homedir(), '.cherrystudio', 'bin') + const uvName = await getBinaryName('uv') + const bunName = await getBinaryName('bun') + const uvPath = path.join(dir, uvName) + const bunPath = path.join(dir, bunName) + return { dir, uvPath, bunPath } + } } export default new McpService() diff --git a/src/main/utils/process.ts b/src/main/utils/process.ts index e4151c01..e10cdc4b 100644 --- a/src/main/utils/process.ts +++ b/src/main/utils/process.ts @@ -35,12 +35,18 @@ export function runInstallScript(scriptPath: string): Promise { }) } +export async function getBinaryName(name: string): Promise { + if (process.platform === 'win32') { + return `${name}.exe` + } + return name +} + export async function getBinaryPath(name: string): Promise { - let cmd = process.platform === 'win32' ? `${name}.exe` : name + const binaryName = await getBinaryName(name) const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binariesDirExists = await fs.existsSync(binariesDir) - cmd = binariesDirExists ? path.join(binariesDir, cmd) : name - return cmd + return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName } export async function isBinaryExists(name: string): Promise { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index e109e1e1..bb2b3efa 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -149,6 +149,7 @@ declare global { removeServer: (server: MCPServer) => Promise listTools: (server: MCPServer) => Promise callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise + getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }> } copilot: { getAuthMessage: ( diff --git a/src/preload/index.ts b/src/preload/index.ts index 4d3ebbc1..53a6dd7a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -123,7 +123,8 @@ const api = { 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 }) + ipcRenderer.invoke('mcp:call-tool', { server, name, args }), + getInstallInfo: () => ipcRenderer.invoke('mcp:get-install-info') }, shell: { openExternal: shell.openExternal diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 0f762b82..eb89df31 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1010,7 +1010,9 @@ "type": "Type", "updateError": "Failed to update server", "updateSuccess": "Server updated successfully", - "url": "URL" + "url": "URL", + "editMcpJson": "Edit MCP Configuration", + "installHelp": "Get Installation Help" }, "messages.divider": "Show divider between messages", "messages.grid_columns": "Message grid display columns", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 99ae427d..fa643781 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1009,7 +1009,9 @@ "url": "URL", "errors": { "32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください" - } + }, + "editMcpJson": "MCP 設定を編集", + "installHelp": "インストールヘルプを取得" }, "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 fbe8c6f2..118f1b5b 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1009,7 +1009,9 @@ "type": "Тип", "updateError": "Ошибка обновления сервера", "updateSuccess": "Сервер успешно обновлен", - "url": "URL" + "url": "URL", + "editMcpJson": "Редактировать MCP 配置", + "installHelp": "Получить помощь по установке" }, "messages.divider": "Показывать разделитель между сообщениями", "messages.grid_columns": "Количество столбцов сетки сообщений", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 57e6598d..0bcc8fce 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1010,7 +1010,9 @@ "type": "类型", "updateError": "更新服务器失败", "updateSuccess": "服务器更新成功", - "url": "URL" + "url": "URL", + "editMcpJson": "编辑 MCP 配置", + "installHelp": "获取安装帮助" }, "messages.divider": "消息分割线", "messages.grid_columns": "消息网格展示列数", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 6f678952..d7133db2 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1009,7 +1009,9 @@ "type": "類型", "updateError": "更新伺服器失敗", "updateSuccess": "伺服器更新成功", - "url": "URL" + "url": "URL", + "editMcpJson": "編輯 MCP 配置", + "installHelp": "獲取安裝幫助" }, "messages.divider": "訊息間顯示分隔線", "messages.grid_columns": "訊息網格展示列數", diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/MessageTools.tsx index cd3d334b..59fc0269 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTools.tsx @@ -101,7 +101,7 @@ const MessageTools: FC = ({ message }) => { ), children: isDone && result && ( -
{JSON.stringify(result, null, 2)}
+ {JSON.stringify(result, null, 2)}
) }) @@ -144,7 +144,7 @@ const MessageTools: FC = ({ message }) => { aria-label={t('common.copy')}> -
{expandedResponse.content}
+ {expandedResponse.content} )} @@ -255,13 +255,14 @@ const ToolResponseContainer = styled.div` max-height: 300px; border-top: 1px solid var(--color-border); position: relative; +` - pre { - margin: 0; - white-space: pre-wrap; - word-break: break-word; - color: var(--color-text); - } +const CodeBlock = styled.pre` + margin: 0; + white-space: pre-wrap; + word-break: break-word; + color: var(--color-text); + font-family: ubuntu; ` const ExpandedResponseContainer = styled.div` diff --git a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx new file mode 100644 index 00000000..bc9a9463 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx @@ -0,0 +1,152 @@ +import { TopView } from '@renderer/components/TopView' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { setMCPServers } from '@renderer/store/mcp' +import { MCPServer } from '@renderer/types' +import { Modal, Typography } from 'antd' +import TextArea from 'antd/es/input/TextArea' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +interface Props { + resolve: (data: any) => void +} + +const PopupContainer: React.FC = ({ resolve }) => { + const [open, setOpen] = useState(true) + const [jsonConfig, setJsonConfig] = useState('') + const [jsonSaving, setJsonSaving] = useState(false) + const [jsonError, setJsonError] = useState('') + const mcpServers = useAppSelector((state) => state.mcp.servers) + + const dispatch = useAppDispatch() + const { t } = useTranslation() + + useEffect(() => { + try { + const mcpServersObj: Record = {} + + mcpServers.forEach((server) => { + const { id, ...serverData } = server + mcpServersObj[id] = serverData + }) + + const standardFormat = { + mcpServers: mcpServersObj + } + + const formattedJson = JSON.stringify(standardFormat, null, 2) + setJsonConfig(formattedJson) + setJsonError('') + } catch (error) { + console.error('Failed to format JSON:', error) + setJsonError(t('settings.mcp.jsonFormatError')) + } + }, [mcpServers, t]) + + const onOk = async () => { + setJsonSaving(true) + + try { + if (!jsonConfig.trim()) { + dispatch(setMCPServers([])) + window.message.success(t('settings.mcp.jsonSaveSuccess')) + setJsonError('') + setJsonSaving(false) + return + } + + const parsedConfig = JSON.parse(jsonConfig) + + if (!parsedConfig.mcpServers || typeof parsedConfig.mcpServers !== 'object') { + throw new Error(t('settings.mcp.invalidMcpFormat')) + } + + const serversArray: MCPServer[] = [] + + for (const [id, serverConfig] of Object.entries(parsedConfig.mcpServers)) { + const server: MCPServer = { + id, + isActive: false, + ...(serverConfig as any) + } + serversArray.push(server) + } + + dispatch(setMCPServers(serversArray)) + + window.message.success(t('settings.mcp.jsonSaveSuccess')) + setJsonError('') + setOpen(false) + } catch (error: any) { + console.error('Failed to save JSON config:', error) + setJsonError(error.message || t('settings.mcp.jsonSaveError')) + window.message.error(t('settings.mcp.jsonSaveError')) + } finally { + setJsonSaving(false) + } + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + EditMcpJsonPopup.hide = onCancel + + return ( + +
+ + {jsonError ? {jsonError} : ''} + +
+