From 279f5db817afd1eb7908107c67d26688a31232b5 Mon Sep 17 00:00:00 2001 From: jwcrystal <121911854+jwcrystal@users.noreply.github.com> Date: Tue, 20 May 2025 10:41:25 +0800 Subject: [PATCH] feat: add MCP servers via JSON quickly (#6099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add MCP servers via JSON quickly * refactor(MCPSettings): Extract JSON parsing logic into a helper function * feat: json linter for EditMcpJsonPopup * feat(mcp): confirm the MCP server status as connection * refactor(AddMcpServerModal): 移除冗余注释并修复加载状态 * feat(MCPSettings): Add support for SSE and streamableHttp formats and optimize server configuration parsing - Add server type validation to ensure the type is stdio, SSE, or streamableHttp - Optimize JSON parsing logic to ensure server configuration completeness and validity - Update example text to provide more detailed configuration examples * fix(MCPSettings): fix AddMcpServerModal default baseUrl login 移除serverToAdd.url作为baseUrl的备选值,确保baseUrl仅使用serverToAdd.baseUrl的值 * feat(MCPSettings): support CodeEditor in AddMcpServerModal * fix: Remove unnecessary type checks for JSON parsing login * fix(MCPSettings): fix compatibility issues with the URL field when parsing server data * refactor: remove unnessary cdoe * chore: Add a server dropdown button to integrate new features in UI - Integrate the two buttons for adding a server into a single dropdown menu to enhance user experience and simplify the interface * chroe: modify the Dropdown items' name of addServer * refactor(i18n): unify the translation for the MCP server import function --------- Co-authored-by: one --- packages/shared/IpcChannel.ts | 1 + src/main/ipc.ts | 3 + src/main/services/MCPService.ts | 20 ++ src/preload/index.ts | 3 +- .../src/components/CodeEditor/index.tsx | 17 +- src/renderer/src/i18n/locales/en-us.json | 8 + src/renderer/src/i18n/locales/ja-jp.json | 8 + src/renderer/src/i18n/locales/ru-ru.json | 8 + src/renderer/src/i18n/locales/zh-cn.json | 8 + src/renderer/src/i18n/locales/zh-tw.json | 8 + .../MCPSettings/AddMcpServerModal.tsx | 282 ++++++++++++++++++ .../settings/MCPSettings/EditMcpJsonPopup.tsx | 21 +- .../settings/MCPSettings/McpServersList.tsx | 50 +++- 13 files changed, 423 insertions(+), 14 deletions(-) create mode 100644 src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 7c536015a9..ce50f4680e 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -52,6 +52,7 @@ export enum IpcChannel { Mcp_GetInstallInfo = 'mcp:get-install-info', Mcp_ServersChanged = 'mcp:servers-changed', Mcp_ServersUpdated = 'mcp:servers-updated', + Mcp_CheckConnectivity = 'mcp:check-connectivity', //copilot Copilot_GetAuthMessage = 'copilot:get-auth-message', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index e40b19aca1..608d57534b 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -320,6 +320,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.Mcp_ListResources, (event, server) => getMcpInstance().listResources(event, server)) ipcMain.handle(IpcChannel.Mcp_GetResource, (event, params) => getMcpInstance().getResource(event, params)) ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, () => getMcpInstance().getInstallInfo()) + ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, (event, params) => + getMcpInstance().checkMcpConnectivity(event, params) + ) ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name)) ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name)) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 90e50ec65a..83cd082697 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -396,6 +396,26 @@ class McpService { } } + /** + * Check connectivity for an MCP server + */ + public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise { + Logger.info(`[MCP] Checking connectivity for server: ${server.name}`) + try { + const client = await this.initClient(server) + // Attempt to list tools as a way to check connectivity + await client.listTools() + Logger.info(`[MCP] Connectivity check successful for server: ${server.name}`) + return true + } catch (error) { + Logger.error(`[MCP] Connectivity check failed for server: ${server.name}`, error) + // Close the client if connectivity check fails to ensure a clean state for the next attempt + const serverKey = this.getServerKey(server) + await this.closeClient(serverKey) + return false + } + } + private async listToolsImpl(server: MCPServer): Promise { Logger.info(`[MCP] Listing tools for server: ${server.name}`) const client = await this.initClient(server) diff --git a/src/preload/index.ts b/src/preload/index.ts index f70ca04d20..417055a29d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -149,7 +149,8 @@ const api = { listResources: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListResources, server), getResource: ({ server, uri }: { server: MCPServer; uri: string }) => ipcRenderer.invoke(IpcChannel.Mcp_GetResource, { server, uri }), - getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo) + getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo), + checkMcpConnectivity: (server: any) => ipcRenderer.invoke(IpcChannel.Mcp_CheckConnectivity, server) }, shell: { openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options) diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx index 124912e277..6d1f059006 100644 --- a/src/renderer/src/components/CodeEditor/index.tsx +++ b/src/renderer/src/components/CodeEditor/index.tsx @@ -29,6 +29,8 @@ interface Props { wrappable?: boolean keymap?: boolean } & BasicSetupOptions + /** 用于追加 extensions */ + extensions?: Extension[] /** 用于覆写编辑器的样式,会直接传给 CodeMirror 的 style 属性 */ style?: React.CSSProperties } @@ -38,7 +40,7 @@ interface Props { * * 目前必须和 CodeToolbar 配合使用。 */ -const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, style }: Props) => { +const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, extensions, style }: Props) => { const { fontSize, codeShowLineNumbers: _lineNumbers, @@ -177,9 +179,14 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, ]) }, [handleSave]) - const enabledExtensions = useMemo(() => { - return [...langExtension, ...(isUnwrapped ? [] : [EditorView.lineWrapping]), ...(enableKeymap ? [saveKeymap] : [])] - }, [enableKeymap, langExtension, isUnwrapped, saveKeymap]) + const customExtensions = useMemo(() => { + return [ + ...(extensions ?? []), + ...langExtension, + ...(isUnwrapped ? [] : [EditorView.lineWrapping]), + ...(enableKeymap ? [saveKeymap] : []) + ] + }, [extensions, langExtension, isUnwrapped, enableKeymap, saveKeymap]) return ( { editorViewRef.current = view setEditorReady(true) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 0536ced621..d5adc09e64 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1270,6 +1270,14 @@ "active": "Active", "addError": "Failed to add server", "addServer": "Add Server", + "addServer.create": "Quick Create", + "addServer.importFrom": "Import from JSON", + "addServer.importFrom.tooltip": "Please copy the configuration JSON (prioritizing\n NPX or UVX configurations) from the MCP Servers introduction page and paste it into the input box.", + "addServer.importFrom.placeholder": "Paste MCP server JSON config", + "addServer.importFrom.invalid": "Invalid input, please check JSON format", + "addServer.importFrom.nameExists": "Server already exists: {{name}}", + "addServer.importFrom.oneServer": "Only one MCP server configuration at a time", + "addServer.importFrom.connectionFailed": "Connection failed", "addSuccess": "Server added successfully", "args": "Arguments", "argsTooltip": "Each argument on a new line", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 3e16858f80..6bcd0ab9a1 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1266,6 +1266,14 @@ "active": "有効", "addError": "サーバーの追加に失敗しました", "addServer": "サーバーを追加", + "addServer.create": "クイック作成", + "addServer.importFrom": "JSONからインポート", + "addServer.importFrom.tooltip": "MCPサーバー紹介ページから設定JSON(NPXまたはUVX設定を優先)をコピーし、入力ボックスに貼り付けてください。", + "addServer.importFrom.placeholder": "MCPサーバーJSON設定を貼り付け", + "addServer.importFrom.invalid": "無効な入力です。JSON形式を確認してください。", + "addServer.importFrom.nameExists": "サーバーはすでに存在します: {{name}}", + "addServer.importFrom.oneServer": "一度に1つのMCPサーバー設定のみを保存できます", + "addServer.importFrom.connectionFailed": "接続に失敗しました", "addSuccess": "サーバーが正常に追加されました", "args": "引数", "argsTooltip": "1行に1つの引数を入力してください", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 2f3a2e1baf..89c493361a 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1266,6 +1266,14 @@ "active": "Активен", "addError": "Ошибка добавления сервера", "addServer": "Добавить сервер", + "addServer.create": "Быстрое создание", + "addServer.importFrom": "Импорт из JSON", + "addServer.importFrom.tooltip": "Скопируйте JSON-конфигурацию (приоритет NPX или UVX конфигураций) со страницы введения MCP Servers и вставьте ее в поле ввода.", + "addServer.importFrom.placeholder": "Вставьте JSON-конфигурацию сервера MCP", + "addServer.importFrom.invalid": "Неверный ввод, проверьте формат JSON", + "addServer.importFrom.nameExists": "Сервер уже существует: {{name}}", + "addServer.importFrom.oneServer": "Можно сохранить только один конфигурационный файл MCP", + "addServer.importFrom.connectionFailed": "Сбой подключения", "addSuccess": "Сервер успешно добавлен", "args": "Аргументы", "argsTooltip": "Каждый аргумент с новой строки", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index ca72417a8d..37bfb50575 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1270,6 +1270,14 @@ "active": "启用", "addError": "添加服务器失败", "addServer": "添加服务器", + "addServer.create": "快速创建", + "addServer.importFrom": "从 JSON 导入", + "addServer.importFrom.tooltip": "请从 MCP Servers 的介绍页面复制配置JSON(优先使用\n NPX或 UVX 配置),并粘贴到输入框中。", + "addServer.importFrom.placeholder": "粘贴 MCP 服务器 JSON 配置", + "addServer.importFrom.invalid": "无效输入,请检查 JSON 格式", + "addServer.importFrom.nameExists": "服务器已存在:{{name}}", + "addServer.importFrom.oneServer": "每次只能保存一個 MCP 伺服器配置", + "addServer.importFrom.connectionFailed": "連接失敗", "addSuccess": "服务器添加成功", "args": "参数", "argsTooltip": "每个参数占一行", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index f4e1adbb65..3cc6777a88 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1269,6 +1269,14 @@ "active": "啟用", "addError": "添加伺服器失敗", "addServer": "新增伺服器", + "addServer.create": "快速創建", + "addServer.importFrom": "從 JSON 導入", + "addServer.importFrom.tooltip": "請從 MCP Servers 的介紹頁面複製配置JSON(優先使用\n NPX或 UVX 配置),並粘貼到輸入框中。", + "addServer.importFrom.placeholder": "貼上 MCP 伺服器 JSON 設定", + "addServer.importFrom.invalid": "無效的輸入,請檢查 JSON 格式", + "addServer.importFrom.nameExists": "伺服器已存在:{{name}}", + "addServer.importFrom.oneServer": "每次只能保存一個 MCP 伺服器配置", + "addServer.importFrom.connectionFailed": "連線失敗", "addSuccess": "伺服器新增成功", "args": "參數", "argsTooltip": "每個參數佔一行", diff --git a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx new file mode 100644 index 0000000000..bc75508f14 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx @@ -0,0 +1,282 @@ +import { nanoid } from '@reduxjs/toolkit' +import CodeEditor from '@renderer/components/CodeEditor' +import { CodeToolbarProvider } from '@renderer/components/CodeToolbar' +import { useAppDispatch } from '@renderer/store' +import { setMCPServerActive } from '@renderer/store/mcp' +import { MCPServer } from '@renderer/types' +import { Extension } from '@uiw/react-codemirror' +import { Form, Modal } from 'antd' +import { FC, useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +interface AddMcpServerModalProps { + visible: boolean + onClose: () => void + onSuccess: (server: MCPServer) => void + existingServers: MCPServer[] +} + +interface ParsedServerData extends MCPServer { + url?: string // JSON 可能包含此欄位,而不是 baseUrl +} + +// 預設的 JSON 範例內容 +const initialJsonExample = `// 示例 JSON (stdio): +// { +// "mcpServers": { +// "stdio-server-example": { +// "command": "npx", +// "args": ["-y", "mcp-server-example"] +// } +// } +// } + +// 示例 JSON (sse): +// { +// "mcpServers": { +// "sse-server-example": { +// "type": "sse", +// "url": "http://localhost:3000" +// } +// } +// } + +// 示例 JSON (streamableHttp): +// { +// "mcpServers": { +// "streamable-http-example": { +// "type": "streamableHttp", +// "url": "http://localhost:3001" +// } +// } +// } +` + +const AddMcpServerModal: FC = ({ visible, onClose, onSuccess, existingServers }) => { + const { t } = useTranslation() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const dispatch = useAppDispatch() + const [editorExtensions, setEditorExtensions] = useState([]) // 新增 editorExtensions 狀態 + + // 載入 CodeMirror JSON Linter 擴充功能 + useEffect(() => { + let isMounted = true + Promise.all([ + import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter), + import('@codemirror/lint').then((mod) => mod.linter) + ]).then(([jsonParseLinter, linter]) => { + if (isMounted) { + setEditorExtensions([linter(jsonParseLinter())]) + } + }) + return () => { + isMounted = false + } + }, []) + + const handleOk = async () => { + try { + const values = await form.validateFields() + const inputValue = values.serverConfig.trim() + setLoading(true) + + const { serverToAdd, error } = parseAndExtractServer(inputValue, t) + + if (error) { + form.setFields([ + { + name: 'serverConfig', + errors: [error] + } + ]) + setLoading(false) + return + } + + // 檢查重複名稱 + if (existingServers && existingServers.some((server) => server.name === serverToAdd!.name)) { + form.setFields([ + { + name: 'serverConfig', + errors: [t('settings.mcp.addServer.importFrom.nameExists', { name: serverToAdd!.name })] + } + ]) + setLoading(false) + return + } + + // 如果成功解析並通過所有檢查,立即加入伺服器(非啟用狀態)並關閉對話框 + const newServer: MCPServer = { + id: nanoid(), + name: serverToAdd!.name!, + description: serverToAdd!.description ?? '', + baseUrl: serverToAdd!.baseUrl ?? serverToAdd!.url ?? '', + command: serverToAdd!.command ?? '', + args: serverToAdd!.args || [], + env: serverToAdd!.env || {}, + isActive: false, + type: serverToAdd!.type, + logoUrl: serverToAdd!.logoUrl, + provider: serverToAdd!.provider, + providerUrl: serverToAdd!.providerUrl, + tags: serverToAdd!.tags, + configSample: serverToAdd!.configSample + } + + onSuccess(newServer) + form.resetFields() + onClose() + + // 在背景非同步檢查伺服器可用性並更新狀態 + window.api.mcp + .checkMcpConnectivity(newServer) + .then((isConnected) => { + console.log(`Connectivity check for ${newServer.name}: ${isConnected}`) + dispatch(setMCPServerActive({ id: newServer.id, isActive: isConnected })) + }) + .catch((connError: any) => { + console.error(`Connectivity check failed for ${newServer.name}:`, connError) + window.message.error({ + content: t(`${newServer.name} settings.mcp.addServer.importFrom.connectionFailed`), + key: 'mcp-quick-add-failed' + }) + }) + } finally { + setLoading(false) + } + } + + // CodeEditor 內容變更時的回呼函式 + const handleEditorChange = useCallback( + (newContent: string) => { + form.setFieldsValue({ serverConfig: newContent }) + // 可選:如果希望即時驗證,可以取消註解下一行 + // form.validateFields(['serverConfig']); + }, + [form] + ) + + const serverConfigValue = form.getFieldValue('serverConfig') + + return ( + +
+ + + + {serverConfigValue ?? initialJsonExample} + + + +
+
+ ) +} + +// 解析 JSON 提取伺服器資料 +const parseAndExtractServer = ( + inputValue: string, + t: (key: string, options?: any) => string +): { serverToAdd: Partial | null; error: string | null } => { + const trimmedInput = inputValue.trim() + + let parsedJson + try { + parsedJson = JSON.parse(trimmedInput) + } catch (e) { + // JSON 解析失敗,返回錯誤 + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } + } + + let serverToAdd: Partial | null = null + + // 檢查是否包含多個伺服器配置 (適用於 JSON 格式) + if ( + parsedJson.mcpServers && + typeof parsedJson.mcpServers === 'object' && + Object.keys(parsedJson.mcpServers).length > 1 + ) { + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.multipleServers') } + } else if (Array.isArray(parsedJson) && parsedJson.length > 1) { + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.multipleServers') } + } + + if ( + parsedJson.mcpServers && + typeof parsedJson.mcpServers === 'object' && + Object.keys(parsedJson.mcpServers).length > 0 + ) { + // Case 1: {"mcpServers": {"serverName": {...}}} + const firstServerKey = Object.keys(parsedJson.mcpServers)[0] + const potentialServer = parsedJson.mcpServers[firstServerKey] + if (typeof potentialServer === 'object' && potentialServer !== null) { + serverToAdd = { ...potentialServer } + serverToAdd!.name = potentialServer.name ?? firstServerKey + } else { + console.error('Invalid server data under mcpServers key:', potentialServer) + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } + } + } else if (Array.isArray(parsedJson) && parsedJson.length > 0) { + // Case 2: [{...}, ...] - 取第一個伺服器,確保它是物件 + if (typeof parsedJson[0] === 'object' && parsedJson[0] !== null) { + serverToAdd = { ...parsedJson[0] } + serverToAdd!.name = parsedJson[0].name ?? t('settings.mcp.newServer') + } else { + console.error('Invalid server data in array:', parsedJson[0]) + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } + } + } else if ( + typeof parsedJson === 'object' && + parsedJson !== null && + !Array.isArray(parsedJson) && + !parsedJson.mcpServers // 確保是直接的伺服器物件 + ) { + // Case 3: {...} (單一伺服器物件) + // 檢查物件是否為空 + if (Object.keys(parsedJson).length > 0) { + serverToAdd = { ...parsedJson } + serverToAdd!.name = parsedJson.name ?? t('settings.mcp.newServer') + } else { + // 空物件,視為無效 + serverToAdd = null + } + } else { + // 無效結構或空的 mcpServers + serverToAdd = null + } + + // 確保 serverToAdd 存在且 name 存在 + if (!serverToAdd || !serverToAdd.name) { + console.error('Invalid JSON structure for server config or missing name:', parsedJson) + return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') } + } + + return { serverToAdd, error: null } +} + +export default AddMcpServerModal diff --git a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx index c8726bfa15..32228dc96e 100644 --- a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx @@ -4,16 +4,17 @@ import { TopView } from '@renderer/components/TopView' import { useAppDispatch, useAppSelector } from '@renderer/store' import { setMCPServers } from '@renderer/store/mcp' import { MCPServer } from '@renderer/types' +import { Extension } from '@uiw/react-codemirror' import { Modal, Typography } from 'antd' import { useCallback, 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 [editorExtensions, setEditorExtensions] = useState([]) const [jsonConfig, setJsonConfig] = useState('') const [jsonSaving, setJsonSaving] = useState(false) const [jsonError, setJsonError] = useState('') @@ -22,6 +23,21 @@ const PopupContainer: React.FC = ({ resolve }) => { const dispatch = useAppDispatch() const { t } = useTranslation() + useEffect(() => { + let isMounted = true + Promise.all([ + import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter), + import('@codemirror/lint').then((mod) => mod.linter) + ]).then(([jsonParseLinter, linter]) => { + if (isMounted) { + setEditorExtensions([linter(jsonParseLinter())]) + } + }) + return () => { + isMounted = false + } + }, []) + useEffect(() => { try { const mcpServersObj: Record = {} @@ -137,7 +153,8 @@ const PopupContainer: React.FC = ({ resolve }) => { foldGutter: true, highlightActiveLine: true, keymap: true - }}> + }} + extensions={editorExtensions}> {jsonConfig} diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index 3f6b6eb8ae..ed2102530c 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -4,14 +4,15 @@ import DragableList from '@renderer/components/DragableList' import Scrollbar from '@renderer/components/Scrollbar' import { useMCPServers } from '@renderer/hooks/useMCPServers' import { MCPServer } from '@renderer/types' -import { Button, Empty, Tag } from 'antd' +import { Button, Dropdown, Empty, Tag } from 'antd' import { MonitorCheck, Plus, RefreshCw, Settings2, SquareArrowOutUpRight } from 'lucide-react' -import { FC, useCallback } from 'react' +import { FC, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router' import styled from 'styled-components' import { SettingTitle } from '..' +import AddMcpServerModal from './AddMcpServerModal' import EditMcpJsonPopup from './EditMcpJsonPopup' import SyncServersPopup from './SyncServersPopup' @@ -19,6 +20,7 @@ const McpServersList: FC = () => { const { mcpServers, addMCPServer, updateMcpServers } = useMCPServers() const { t } = useTranslation() const navigate = useNavigate() + const [isAddModalVisible, setIsAddModalVisible] = useState(false) const onAddMcpServer = useCallback(async () => { const newServer = { @@ -31,7 +33,7 @@ const McpServersList: FC = () => { env: {}, isActive: false } - await addMCPServer(newServer) + addMCPServer(newServer) navigate(`/settings/mcp/settings`, { state: { server: newServer } }) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' }) }, [addMCPServer, navigate, t]) @@ -40,6 +42,17 @@ const McpServersList: FC = () => { SyncServersPopup.show(mcpServers) }, [mcpServers]) + const handleAddServerSuccess = useCallback( + async (server: MCPServer) => { + addMCPServer(server) + setIsAddModalVisible(false) + window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' }) + // Optionally navigate to the new server's settings page + // navigate(`/settings/mcp/settings`, { state: { server } }) + }, + [addMCPServer, t] + ) + return ( @@ -48,9 +61,28 @@ const McpServersList: FC = () => { + { + onAddMcpServer() + } + }, + { + key: 'quick', + label: t('settings.mcp.addServer.importFrom'), + onClick: () => setIsAddModalVisible(true) + } + ] + }} + trigger={['click']}> + + @@ -111,6 +143,12 @@ const McpServersList: FC = () => { style={{ marginTop: 20 }} /> )} + setIsAddModalVisible(false)} + onSuccess={handleAddServerSuccess} + existingServers={mcpServers} // 傳遞現有的伺服器列表 + /> ) }