diff --git a/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx b/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx index d7d5e9b5ce..0b1b15f0a4 100644 --- a/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx @@ -16,7 +16,7 @@ const BuiltinMCPServerList: FC = () => { return ( <> - {t('settings.mcp.builtinServers')} + {t('settings.mcp.builtinServers')} {builtinMCPServers.map((server) => { const isInstalled = mcpServers.some((existingServer) => existingServer.name === server.name) diff --git a/src/renderer/src/pages/settings/MCPSettings/McpMarketList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpMarketList.tsx index 274fa93686..363b922c98 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpMarketList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpMarketList.tsx @@ -74,7 +74,7 @@ const McpMarketList: FC = () => { return ( <> - {t('settings.mcp.findMore')} + {t('settings.mcp.findMore')} {mcpMarkets.map((resource) => ( window.open(resource.url, '_blank', 'noopener,noreferrer')}> diff --git a/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx new file mode 100644 index 0000000000..0c5a7b2bf5 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx @@ -0,0 +1,186 @@ +import { Flex, RowFlex } from '@cherrystudio/ui' +import { useMCPServers } from '@renderer/hooks/useMCPServers' +import type { MCPServer } from '@renderer/types' +import { Button, Divider, Input, Space } from 'antd' +import Link from 'antd/es/typography/Link' +import { SquareArrowOutUpRight } from 'lucide-react' +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { SettingHelpLink, SettingHelpTextRow, SettingSubtitle } from '..' +import type { ProviderConfig } from './providers/config' + +interface Props { + provider: ProviderConfig + existingServers: MCPServer[] +} + +const McpProviderSettings: React.FC = ({ provider, existingServers }) => { + const { addMCPServer, updateMCPServer } = useMCPServers() + const [isFetching, setIsFetching] = useState(false) + const [token, setToken] = useState('') + const [availableServers, setAvailableServers] = useState([]) + const { t } = useTranslation() + + useEffect(() => { + const savedToken = provider.getToken() + if (savedToken) { + setToken(savedToken) + } + }, [provider]) + + const handleFetch = useCallback(async () => { + if (!token.trim()) { + window.toast.error(t('settings.mcp.sync.tokenRequired', 'API Token is required')) + return + } + + setIsFetching(true) + + try { + provider.saveToken(token) + const result = await provider.syncServers(token, existingServers) + + if (result.success) { + setAvailableServers(result.addedServers || []) + window.toast.success(t('settings.mcp.fetch.success', 'Successfully fetched MCP servers')) + } else { + window.toast.error(result.message) + } + } catch (error: any) { + window.toast.error(`${t('settings.mcp.sync.error')}: ${error.message}`) + } finally { + setIsFetching(false) + } + }, [existingServers, provider, t, token]) + + const isFetchDisabled = !token + + return ( + + + + {provider.name} + {provider.discoverUrl && ( + + + + + {t('settings.provider.api_key.label')} + + setToken(e.target.value)} + spellCheck={false} + /> + + + + {provider.apiKeyUrl && ( + + {t('settings.provider.get_api_key')} + + )} + + + + {availableServers.length > 0 && ( + <> + + {t('settings.mcp.available.servers', 'Available MCP Servers')} + + + {availableServers.map((server) => ( + + + {server.name} + {server.description} + + {(() => { + const isAlreadyAdded = existingServers.some(existing => existing.id === server.id) + return ( + + ) + })()} + + ))} + + + )} + + ) +} + +const DetailContainer = styled.div` + padding: 20px; + display: flex; + flex-direction: column; +` + +const ProviderHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +` + +const ProviderName = styled.span` + font-size: 14px; + font-weight: 500; + margin-right: -2px; +` + +const ServerList = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 8px; +` + +const ServerItem = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border: 1px solid var(--color-border); + border-radius: 8px; + background-color: var(--color-background); +` + +const ServerInfo = styled.div` + display: flex; + flex-direction: column; + flex: 1; +` + +const ServerName = styled.div` + font-weight: 500; + font-size: 14px; + margin-bottom: 4px; +` + +const ServerDescription = styled.div` + color: var(--color-text-secondary); + font-size: 12px; +` + +export default McpProviderSettings diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 886db14997..196f177309 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -1,39 +1,128 @@ import { ArrowLeftOutlined } from '@ant-design/icons' -import { Button } from '@cherrystudio/ui' -import { ErrorBoundary } from '@renderer/components/ErrorBoundary' +import { Button, DividerWithText, ListItem } from '@cherrystudio/ui' +import { RowFlex, Scrollbar } from '@cherrystudio/ui' +import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp' +import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png' +import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png' +import ModelScopeProviderLogo from '@renderer/assets/images/providers/modelscope.png' +import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png' import { useTheme } from '@renderer/context/ThemeProvider' +import { useMCPServers } from '@renderer/hooks/useMCPServers' +import { FolderCog, Package, ShoppingBag } from 'lucide-react' import type { FC } from 'react' -import { Route, Routes, useLocation } from 'react-router' +import { useTranslation } from 'react-i18next' +import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router' import { Link } from 'react-router-dom' import styled from 'styled-components' import { SettingContainer } from '..' +import BuiltinMCPServerList from './BuiltinMCPServerList' import InstallNpxUv from './InstallNpxUv' +import McpMarketList from './McpMarketList' +import ProviderDetail from './McpProviderSettings' import McpServersList from './McpServersList' import McpSettings from './McpSettings' import NpxSearch from './NpxSearch' +import { providers } from './providers/config' const MCPSettings: FC = () => { const { theme } = useTheme() - + const { t } = useTranslation() + const { mcpServers } = useMCPServers() + const navigate = useNavigate() const location = useLocation() - const pathname = location.pathname - const isHome = pathname === '/settings/mcp' + // 获取当前激活的页面 + const getActiveView = () => { + const path = location.pathname + + // 精确匹配路径 + if (path === '/settings/mcp/builtin') return 'builtin' + if (path === '/settings/mcp/marketplaces') return 'marketplaces' + + // 检查是否是服务商页面 - 精确匹配 + for (const provider of providers) { + if (path === `/settings/mcp/${provider.key}`) { + return provider.key + } + } + + // 其他所有情况(包括 servers、settings/:serverId、npx-search、mcp-install)都属于 servers + return 'servers' + } + + const activeView = getActiveView() + + // 判断是否为主页面(是否显示返回按钮) + const isHomePage = () => { + const path = location.pathname + // 主页面不显示返回按钮 + if (path === '/settings/mcp' || path === '/settings/mcp/servers') return true + if (path === '/settings/mcp/builtin' || path === '/settings/mcp/marketplaces') return true + + // 服务商页面也是主页面 + return providers.some((p) => path === `/settings/mcp/${p.key}`) + } + + // Provider icons map + const providerIcons: Record = { + modelscope: , + tokenflux: , + lanyun: , + '302ai': , + bailian: + } return ( - - {!isHome && ( - - -