From a58f23f68e84b40b868561c56d372abdfbc08014 Mon Sep 17 00:00:00 2001 From: LiuVaayne <10231735+vaayne@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:11:23 +0800 Subject: [PATCH] feat(McpServersList): add ModelScope sync functionality (#5250) * feat: add sync servers functionality and UI components for MCP settings * Rename 'Discover Models' to 'Discover MCP Servers' Replace Radio button group with Select dropdown for provider selection in the sync servers popup UI and update related styling Rename 'Discover Models' to 'Discover MCP Servers' * Add error messages for MCP server sync Improve error handling in modelscopeSyncUtils with new localization strings for unauthorized access and no available servers. Update error message handling in all language files and use nanoid for unique server naming. --- src/renderer/src/i18n/locales/en-us.json | 16 + src/renderer/src/i18n/locales/ja-jp.json | 16 + src/renderer/src/i18n/locales/ru-ru.json | 16 + src/renderer/src/i18n/locales/zh-cn.json | 16 + src/renderer/src/i18n/locales/zh-tw.json | 16 + .../settings/MCPSettings/McpServersList.tsx | 24 +- .../settings/MCPSettings/SyncServersPopup.tsx | 348 ++++++++++++++++++ .../MCPSettings/modelscopeSyncUtils.ts | 129 +++++++ 8 files changed, 577 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx create mode 100644 src/renderer/src/pages/settings/MCPSettings/modelscopeSyncUtils.ts diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 5210369ba4..b7bc9d67bb 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1171,6 +1171,22 @@ "sse": "SSE", "streamableHttp": "Streamable HTTP", "stdio": "STDIO" + }, + "sync": { + "title": "Sync Servers", + "selectProvider": "Select Provider:", + "discoverMcpServers": "Discover MCP Servers", + "discoverMcpServersDescription": "Visit the platform to discover available MCP servers", + "getToken": "Get API Token", + "getTokenDescription": "Retrieve your personal API token from your account", + "setToken": "Enter Your Token", + "tokenRequired": "API Token is required", + "tokenPlaceholder": "Enter API token here", + "button": "Sync", + "error": "Sync MCP Servers error", + "success": "Sync MCP Servers successful", + "unauthorized": "Sync Unauthorized", + "noServersAvailable": "No MCP servers available" } }, "messages.divider": "Show divider between messages", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index a15f4981be..ed5eaad203 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1169,6 +1169,22 @@ "sse": "SSE", "streamableHttp": "ストリーミング", "stdio": "STDIO" + }, + "sync": { + "title": "サーバーの同期", + "selectProvider": "プロバイダーを選択:", + "discoverMcpServers": "MCPサーバーを発見", + "discoverMcpServersDescription": "プラットフォームを訪れて利用可能なMCPサーバーを発見", + "getToken": "API トークンを取得する", + "getTokenDescription": "アカウントから個人用 API トークンを取得します", + "setToken": "トークンを入力してください", + "tokenRequired": "API トークンは必須です", + "tokenPlaceholder": "ここに API トークンを入力してください", + "button": "同期する", + "error": "MCPサーバーの同期エラー", + "success": "MCPサーバーの同期成功", + "unauthorized": "同期が許可されていません", + "noServersAvailable": "利用可能な MCP サーバーがありません" } }, "messages.divider": "メッセージ間に区切り線を表示", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index db2d2ed128..650a654a19 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1169,6 +1169,22 @@ "sse": "SSE", "streamableHttp": "Потоковый HTTP", "stdio": "STDIO" + }, + "sync": { + "title": "Синхронизация серверов", + "selectProvider": "Выберите провайдера:", + "discoverMcpServers": "Обнаружить серверы MCP", + "discoverMcpServersDescription": "Посетите платформу, чтобы обнаружить доступные серверы MCP", + "getToken": "Получить API токен", + "getTokenDescription": "Получите персональный API токен из вашей учетной записи", + "setToken": "Введите ваш токен", + "tokenRequired": "Требуется API токен", + "tokenPlaceholder": "Введите API токен здесь", + "button": "Синхронизировать", + "error": "Ошибка синхронизации серверов MCP", + "success": "Синхронизация серверов MCP успешна", + "unauthorized": "Синхронизация не разрешена", + "noServersAvailable": "Нет доступных серверов MCP" } }, "messages.divider": "Показывать разделитель между сообщениями", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index d30dbf9f69..c97bd1fc02 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1171,6 +1171,22 @@ "sse": "SSE", "streamableHttp": "流式", "stdio": "STDIO" + }, + "sync": { + "title": "同步服务器", + "selectProvider": "选择提供商:", + "discoverMcpServers": "发现MCP服务器", + "discoverMcpServersDescription": "访问平台以发现可用的MCP服务器", + "getToken": "获取 API 令牌", + "getTokenDescription": "从您的帐户中获取个人 API 令牌", + "setToken": "输入您的令牌", + "tokenRequired": "需要 API 令牌", + "tokenPlaceholder": "在此输入 API 令牌", + "button": "同步", + "error": "同步MCP服务器出错", + "success": "同步MCP服务器成功", + "unauthorized": "同步未授权", + "noServersAvailable": "无可用的 MCP 服务器" } }, "messages.divider": "消息分割线", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index c95768eff4..644d2977fe 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1170,6 +1170,22 @@ "sse": "SSE", "streamableHttp": "流式", "stdio": "STDIO" + }, + "sync": { + "title": "同步伺服器", + "selectProvider": "選擇提供者:", + "discoverMcpServers": "發現MCP伺服器", + "discoverMcpServersDescription": "訪問平台以發現可用的MCP伺服器", + "getToken": "獲取 API 令牌", + "getTokenDescription": "從您的帳戶獲取個人 API 令牌", + "setToken": "輸入您的令牌", + "tokenRequired": "需要 API 令牌", + "tokenPlaceholder": "在此輸入 API 令牌", + "button": "同步", + "error": "同步MCP伺服器出錯", + "success": "同步MCP伺服器成功", + "unauthorized": "同步未授權", + "noServersAvailable": "無可用的 MCP 伺服器" } }, "messages.divider": "訊息間顯示分隔線", diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index bd1df406e9..fda6dc4fae 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -5,7 +5,7 @@ import Scrollbar from '@renderer/components/Scrollbar' import { useMCPServers } from '@renderer/hooks/useMCPServers' import { MCPServer } from '@renderer/types' import { Button, Empty, Tag } from 'antd' -import { MonitorCheck, Plus, Settings2 } from 'lucide-react' +import { MonitorCheck, Plus, RefreshCw, Settings2 } from 'lucide-react' import { FC, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router' @@ -13,6 +13,8 @@ import styled from 'styled-components' import { SettingTitle } from '..' import EditMcpJsonPopup from './EditMcpJsonPopup' +import SyncServersPopup from './SyncServersPopup' + const McpServersList: FC = () => { const { mcpServers, addMCPServer, updateMcpServers } = useMCPServers() const { t } = useTranslation() @@ -34,6 +36,10 @@ const McpServersList: FC = () => { window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' }) }, [addMCPServer, navigate, t]) + const onSyncServers = useCallback(() => { + SyncServersPopup.show(mcpServers) + }, [mcpServers]) + return ( @@ -41,9 +47,14 @@ const McpServersList: FC = () => { {t('settings.mcp.newServer')} + + + + {(server: MCPServer) => ( @@ -176,4 +187,9 @@ const ServerFooter = styled.div` margin-top: 10px; ` +const ButtonGroup = styled.div` + display: flex; + gap: 8px; +` + export default McpServersList diff --git a/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx new file mode 100644 index 0000000000..b5adb93a0f --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx @@ -0,0 +1,348 @@ +import { TopView } from '@renderer/components/TopView' +import { useMCPServers } from '@renderer/hooks/useMCPServers' +import { MCPServer } from '@renderer/types' +import { Button, Form, Input, Modal, Select } from 'antd' +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils' + +// Provider configuration interface +interface ProviderConfig { + key: string + name: string + description: string + discoverUrl: string + apiKeyUrl: string + tokenFieldName: string + getToken: () => string | null + saveToken: (token: string) => void + syncServers: (token: string, existingServers: MCPServer[]) => Promise +} + +// Provider configurations +const providers: ProviderConfig[] = [ + { + key: 'modelscope', + name: 'ModelScope', + description: 'ModelScope 平台 MCP 服务', + discoverUrl: 'https://www.modelscope.cn/mcp?hosted=1&page=1', + apiKeyUrl: 'https://www.modelscope.cn/my/myaccesstoken', + tokenFieldName: 'modelScopeToken', + getToken: getModelScopeToken, + saveToken: saveModelScopeToken, + syncServers: syncModelScopeServers + } +] + +interface Props { + resolve: (data: any) => void + existingServers: MCPServer[] +} + +const PopupContainer: React.FC = ({ resolve, existingServers }) => { + const { addMCPServer } = useMCPServers() + const [open, setOpen] = useState(true) + const [isSyncing, setIsSyncing] = useState(false) + const [selectedProviderKey, setSelectedProviderKey] = useState(providers[0].key) + const [tokens, setTokens] = useState>({}) + const { t } = useTranslation() + const [form] = Form.useForm() + + // Get the currently selected provider config + const selectedProvider = providers.find((p) => p.key === selectedProviderKey) || providers[0] + + useEffect(() => { + // Initialize tokens for all providers + const initialTokens: Record = {} + + providers.forEach((provider) => { + const token = provider.getToken() + if (token) { + initialTokens[provider.tokenFieldName] = token + form.setFieldsValue({ [provider.tokenFieldName]: token }) + } + }) + + setTokens(initialTokens) + }, [form]) + + const handleSync = useCallback(async () => { + try { + await form.validateFields() + } catch (error) { + return + } + + setIsSyncing(true) + + try { + const token = form.getFieldValue(selectedProvider.tokenFieldName) + + // Save token if present + if (token) { + selectedProvider.saveToken(token) + setTokens((prev) => ({ ...prev, [selectedProvider.tokenFieldName]: token })) + } + + // Sync servers + const result = await selectedProvider.syncServers(token, existingServers) + + if (result.success && result.addedServers?.length > 0) { + // Add the new servers to the store + for (const server of result.addedServers) { + addMCPServer(server) + } + window.message.success(result.message) + setOpen(false) + } else { + // Show message but keep dialog open + if (result.success) { + window.message.info(result.message) + } else { + window.message.error(result.message) + } + } + } catch (error: any) { + window.message.error(`${t('settings.mcp.sync.error')}: ${error.message}`) + } finally { + setIsSyncing(false) + } + }, [addMCPServer, existingServers, form, selectedProvider, t]) + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + SyncServersPopup.hide = onCancel + + // Check if sync button should be disabled + const isSyncDisabled = () => { + const token = tokens[selectedProvider.tokenFieldName] + return !token + } + + return ( + + + {/* Only show provider selector if there are multiple providers */} + + + {t('settings.mcp.sync.selectProvider', 'Select Provider:')} +