feat: add MCP servers via JSON quickly (#6099)

* 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 <wangan.cs@gmail.com>
This commit is contained in:
jwcrystal 2025-05-20 10:41:25 +08:00 committed by GitHub
parent 35d3c8e451
commit 279f5db817
13 changed files with 423 additions and 14 deletions

View File

@ -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',

View File

@ -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))

View File

@ -396,6 +396,26 @@ class McpService {
}
}
/**
* Check connectivity for an MCP server
*/
public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<boolean> {
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<MCPTool[]> {
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
const client = await this.initClient(server)

View File

@ -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)

View File

@ -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 (
<CodeMirror
@ -190,7 +197,7 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options,
editable={true}
// @ts-ignore 强制使用,见 react-codemirror 的 Example.tsx
theme={activeCmTheme}
extensions={enabledExtensions}
extensions={customExtensions}
onCreateEditor={(view: EditorView) => {
editorViewRef.current = view
setEditorReady(true)

View File

@ -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",

View File

@ -1266,6 +1266,14 @@
"active": "有効",
"addError": "サーバーの追加に失敗しました",
"addServer": "サーバーを追加",
"addServer.create": "クイック作成",
"addServer.importFrom": "JSONからインポート",
"addServer.importFrom.tooltip": "MCPサーバー紹介ページから設定JSONNPXまたは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つの引数を入力してください",

View File

@ -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": "Каждый аргумент с новой строки",

View File

@ -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": "每个参数占一行",

View File

@ -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": "每個參數佔一行",

View File

@ -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<AddMcpServerModalProps> = ({ visible, onClose, onSuccess, existingServers }) => {
const { t } = useTranslation()
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const dispatch = useAppDispatch()
const [editorExtensions, setEditorExtensions] = useState<Extension[]>([]) // 新增 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 (
<Modal
title={t('settings.mcp.addServer.importFrom')}
open={visible}
onOk={handleOk}
onCancel={onClose}
confirmLoading={loading}
destroyOnClose
width={600}>
<Form form={form} layout="vertical" name="add_mcp_server_form">
<Form.Item
name="serverConfig"
label={t('settings.mcp.addServer.importFrom.tooltip')}
rules={[{ required: true, message: t('settings.mcp.addServer.importFrom.placeholder') }]}>
<CodeToolbarProvider>
<CodeEditor
language="json"
onChange={handleEditorChange}
maxHeight="300px"
options={{
collapsible: true,
wrappable: true,
lineNumbers: true,
foldGutter: true,
highlightActiveLine: true,
keymap: true
}}
extensions={editorExtensions}
// 如果表單值為空,顯示範例 JSON否則顯示表單值
>
{serverConfigValue ?? initialJsonExample}
</CodeEditor>
</CodeToolbarProvider>
</Form.Item>
</Form>
</Modal>
)
}
// 解析 JSON 提取伺服器資料
const parseAndExtractServer = (
inputValue: string,
t: (key: string, options?: any) => string
): { serverToAdd: Partial<ParsedServerData> | 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<ParsedServerData> | 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

View File

@ -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<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const [editorExtensions, setEditorExtensions] = useState<Extension[]>([])
const [jsonConfig, setJsonConfig] = useState('')
const [jsonSaving, setJsonSaving] = useState(false)
const [jsonError, setJsonError] = useState('')
@ -22,6 +23,21 @@ const PopupContainer: React.FC<Props> = ({ 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<string, any> = {}
@ -137,7 +153,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
foldGutter: true,
highlightActiveLine: true,
keymap: true
}}>
}}
extensions={editorExtensions}>
{jsonConfig}
</CodeEditor>
</CodeToolbarProvider>

View File

@ -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 (
<Container>
<ListHeader>
@ -48,9 +61,28 @@ const McpServersList: FC = () => {
<Button icon={<EditOutlined />} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
</SettingTitle>
<ButtonGroup>
<Button icon={<Plus size={16} />} type="default" onClick={onAddMcpServer} shape="round">
{t('settings.mcp.addServer')}
</Button>
<Dropdown
menu={{
items: [
{
key: 'manual',
label: t('settings.mcp.addServer.create'),
onClick: () => {
onAddMcpServer()
}
},
{
key: 'quick',
label: t('settings.mcp.addServer.importFrom'),
onClick: () => setIsAddModalVisible(true)
}
]
}}
trigger={['click']}>
<Button icon={<Plus size={16} />} type="default" shape="round">
{t('settings.mcp.addServer')}
</Button>
</Dropdown>
<Button icon={<RefreshCw size={16} />} type="default" onClick={onSyncServers} shape="round">
{t('settings.mcp.sync.title', 'Sync Servers')}
</Button>
@ -111,6 +143,12 @@ const McpServersList: FC = () => {
style={{ marginTop: 20 }}
/>
)}
<AddMcpServerModal
visible={isAddModalVisible}
onClose={() => setIsAddModalVisible(false)}
onSuccess={handleAddServerSuccess}
existingServers={mcpServers} // 傳遞現有的伺服器列表
/>
</Container>
)
}