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 && (
-
-
- } radius="full" isIconOnly />
-
-
- )}
+
-
+
+
+ navigate('/settings/mcp/servers')}
+ icon={}
+ titleStyle={{ fontWeight: 500 }}
+ />
+
+ navigate('/settings/mcp/builtin')}
+ icon={}
+ titleStyle={{ fontWeight: 500 }}
+ />
+ navigate('/settings/mcp/marketplaces')}
+ icon={}
+ titleStyle={{ fontWeight: 500 }}
+ />
+
+ {providers.map((provider) => (
+ navigate(`/settings/mcp/${provider.key}`)}
+ icon={providerIcons[provider.key] || }
+ titleStyle={{ fontWeight: 500 }}
+ />
+ ))}
+
+
+ {!isHomePage() && (
+
+
+ } radius="full" isIconOnly />
+
+
+ )}
- } />
+ } />
+ } />
} />
{
}
/>
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ {providers.map((provider) => (
+ }
+ />
+ ))}
-
+
-
+
)
}
+const Container = styled(RowFlex)`
+ flex: 1;
+`
+
+const MainContainer = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: row;
+ width: 100%;
+ height: calc(100vh - var(--navbar-height) - 6px);
+ overflow: hidden;
+`
+
+const MenuList = styled(Scrollbar)`
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ width: var(--settings-width);
+ padding: 12px;
+ padding-bottom: 48px;
+ border-right: 0.5px solid var(--color-border);
+ height: calc(100vh - var(--navbar-height));
+`
+
+const RightContainer = styled(Scrollbar)`
+ flex: 1;
+ position: relative;
+`
+
+const ProviderIcon = styled.img`
+ width: 24px;
+ height: 24px;
+ object-fit: cover;
+ border-radius: 50%;
+ background-color: var(--color-background-soft);
+`
+
+const ContentWrapper = styled.div`
+ padding: 20px;
+ overflow-y: auto;
+ height: 100%;
+`
+
const BackButtonContainer = styled.div`
display: flex;
align-items: center;
@@ -70,10 +225,4 @@ const BackButtonContainer = styled.div`
z-index: 1000;
`
-const MainContainer = styled.div`
- display: flex;
- flex: 1;
- width: 100%;
-`
-
export default MCPSettings
diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/config.ts b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts
new file mode 100644
index 0000000000..a3201b0bc3
--- /dev/null
+++ b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts
@@ -0,0 +1,76 @@
+import { getAI302Token, saveAI302Token, syncAi302Servers } from './302ai'
+import { getBailianToken, saveBailianToken, syncBailianServers } from './bailian'
+import { getTokenLanYunToken, LANYUN_KEY_HOST, saveTokenLanYunToken, syncTokenLanYunServers } from './lanyun'
+import { getModelScopeToken, MODELSCOPE_HOST, saveModelScopeToken, syncModelScopeServers } from './modelscope'
+import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './tokenflux'
+import type { MCPServer } from '@renderer/types'
+
+export 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
+}
+
+export const providers: ProviderConfig[] = [
+ {
+ key: 'modelscope',
+ name: 'ModelScope',
+ description: 'ModelScope 平台 MCP 服务',
+ discoverUrl: `${MODELSCOPE_HOST}/mcp?hosted=1&page=1`,
+ apiKeyUrl: `${MODELSCOPE_HOST}/my/myaccesstoken`,
+ tokenFieldName: 'modelScopeToken',
+ getToken: getModelScopeToken,
+ saveToken: saveModelScopeToken,
+ syncServers: syncModelScopeServers
+ },
+ {
+ key: 'tokenflux',
+ name: 'TokenFlux',
+ description: 'TokenFlux 平台 MCP 服务',
+ discoverUrl: `${TOKENFLUX_HOST}/mcps`,
+ apiKeyUrl: `${TOKENFLUX_HOST}/dashboard/api-keys`,
+ tokenFieldName: 'tokenfluxToken',
+ getToken: getTokenFluxToken,
+ saveToken: saveTokenFluxToken,
+ syncServers: syncTokenFluxServers
+ },
+ {
+ key: 'lanyun',
+ name: '蓝耘科技',
+ description: '蓝耘科技云平台 MCP 服务',
+ discoverUrl: 'https://mcp.lanyun.net',
+ apiKeyUrl: LANYUN_KEY_HOST,
+ tokenFieldName: 'tokenLanyunToken',
+ getToken: getTokenLanYunToken,
+ saveToken: saveTokenLanYunToken,
+ syncServers: syncTokenLanYunServers
+ },
+ {
+ key: '302ai',
+ name: '302.AI',
+ description: '302.AI 平台 MCP 服务',
+ discoverUrl: 'https://302.ai',
+ apiKeyUrl: 'https://dash.302.ai/apis/list',
+ tokenFieldName: 'token302aiToken',
+ getToken: getAI302Token,
+ saveToken: saveAI302Token,
+ syncServers: syncAi302Servers
+ },
+ {
+ key: 'bailian',
+ name: '阿里云百炼',
+ description: '百炼平台服务',
+ discoverUrl: `https://bailian.console.aliyun.com/?tab=mcp#/mcp-market`,
+ apiKeyUrl: `https://bailian.console.aliyun.com/?tab=app#/api-key`,
+ tokenFieldName: 'bailianToken',
+ getToken: getBailianToken,
+ saveToken: saveBailianToken,
+ syncServers: syncBailianServers
+ }
+]
\ No newline at end of file
diff --git a/src/renderer/src/pages/settings/MCPSettings/utils.ts b/src/renderer/src/pages/settings/MCPSettings/utils.ts
new file mode 100644
index 0000000000..675732ec92
--- /dev/null
+++ b/src/renderer/src/pages/settings/MCPSettings/utils.ts
@@ -0,0 +1,51 @@
+/**
+ * MCP Settings 页面导航工具函数
+ */
+
+export const MCPRoutes = {
+ // 管理类页面
+ servers: '/settings/mcp/servers',
+ npxSearch: '/settings/mcp/npx-search',
+ mcpInstall: '/settings/mcp/mcp-install',
+
+ // 发现类页面
+ builtin: '/settings/mcp/builtin',
+ marketplaces: '/settings/mcp/marketplaces',
+
+ // 服务商页面
+ modelscope: '/settings/mcp/modelscope',
+ tokenflux: '/settings/mcp/tokenflux',
+ lanyun: '/settings/mcp/lanyun',
+ '302ai': '/settings/mcp/302ai',
+ bailian: '/settings/mcp/bailian',
+} as const
+
+/**
+ * 导航到 MCP 设置页面的指定部分
+ * @param page 页面标识
+ * @returns 路由路径
+ */
+export function getMCPRoute(page: keyof typeof MCPRoutes): string {
+ return MCPRoutes[page]
+}
+
+/**
+ * 生成 MCP 服务商页面路由
+ * @param providerKey 服务商标识
+ * @returns 路由路径
+ */
+export function getMCPProviderRoute(providerKey: string): string {
+ return `/settings/mcp/${providerKey}`
+}
+
+/**
+ * 生成 MCP 服务器设置页面路由
+ * @param serverId 服务器ID
+ * @returns 路由路径
+ */
+export function getMCPServerSettingsRoute(serverId: string): string {
+ return `/settings/mcp/settings/${serverId}`
+}
+
+// 类型定义
+export type MCPPage = keyof typeof MCPRoutes
\ No newline at end of file