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:')} +