mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
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:
parent
35d3c8e451
commit
279f5db817
@ -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',
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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つの引数を入力してください",
|
||||
|
||||
@ -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": "Каждый аргумент с новой строки",
|
||||
|
||||
@ -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": "每个参数占一行",
|
||||
|
||||
@ -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": "每個參數佔一行",
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user