feat(MCPSettings): enhance MCP server management and localization

- Added BuiltinMCPServersSection and McpResourcesSection components to display available MCP servers and resources.
- Updated navigation logic to redirect users to the MCP settings upon adding a server.
- Enhanced localization by adding new keys for built-in servers in multiple languages.
- Improved the SettingsPage layout by reordering menu items for better accessibility.
This commit is contained in:
kangfenmao 2025-07-10 12:31:09 +08:00
parent daf134f331
commit 186f0ed06f
14 changed files with 418 additions and 114 deletions

View File

@ -44,7 +44,9 @@ export function handleMcpProtocolUrl(url: URL) {
// }
// }
// cherrystudio://mcp/install?servers={base64Encode(JSON.stringify(jsonConfig))}
const data = params.get('servers')
if (data) {
const stringify = Buffer.from(data, 'base64').toString('utf8')
Logger.info('install MCP servers from urlschema: ', stringify)
@ -63,10 +65,8 @@ export function handleMcpProtocolUrl(url: URL) {
}
}
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')")
}
windowService.getMainWindow()?.show()
break
}
default:

View File

@ -1,4 +1,5 @@
import { createSelector } from '@reduxjs/toolkit'
import NavigationService from '@renderer/services/NavigationService'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { addMCPServer, deleteMCPServer, setMCPServers, updateMCPServer } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types'
@ -8,8 +9,11 @@ import { IpcChannel } from '@shared/IpcChannel'
window.electron.ipcRenderer.on(IpcChannel.Mcp_ServersChanged, (_event, servers) => {
store.dispatch(setMCPServers(servers))
})
window.electron.ipcRenderer.on(IpcChannel.Mcp_AddServer, (_event, server: MCPServer) => {
store.dispatch(addMCPServer(server))
NavigationService.navigate?.('/settings/mcp')
NavigationService.navigate?.('/settings/mcp/settings', { state: { server } })
})
const selectMcpServers = (state) => state.mcp.servers

View File

@ -1870,7 +1870,20 @@
"updateError": "Failed to update server",
"updateSuccess": "Server updated successfully",
"url": "URL",
"user": "User"
"user": "User",
"requiresConfig": "Requires Configuration",
"builtinServers": "Builtin Servers",
"more": {
"modelscope": "ModelScope Community MCP Server",
"higress": "Higress MCP Server",
"mcpso": "MCP Server Discovery Platform",
"smithery": "Smithery MCP Tools",
"glama": "Glama MCP Server Directory",
"pulsemcp": "Pulse MCP Server",
"composio": "Composio MCP Development Tools",
"official": "Official MCP Server Collection",
"awesome": "Curated MCP Server List"
}
},
"messages.divider": "Show divider between messages",
"messages.divider.tooltip": "Not applicable to bubble-style message",

View File

@ -1870,7 +1870,20 @@
"updateError": "サーバーの更新に失敗しました",
"updateSuccess": "サーバーが正常に更新されました",
"url": "URL",
"user": "ユーザー"
"user": "ユーザー",
"requiresConfig": "設定が必要",
"builtinServers": "組み込みサーバー",
"more": {
"modelscope": "魔搭コミュニティ MCP サーバー",
"higress": "Higress MCP サーバー",
"mcpso": "MCP サーバー発見プラットフォーム",
"smithery": "Smithery MCP ツール",
"glama": "Glama MCP サーバーディレクトリ",
"pulsemcp": "Pulse MCP サーバー",
"composio": "Composio MCP 開発ツール",
"official": "公式 MCP サーバーコレクション",
"awesome": "厳選された MCP サーバーリスト"
}
},
"messages.divider": "メッセージ間に区切り線を表示",
"messages.divider.tooltip": "バブルスタイルのメッセージには適用されません",

View File

@ -1870,7 +1870,20 @@
"updateError": "Ошибка обновления сервера",
"updateSuccess": "Сервер успешно обновлен",
"url": "URL",
"user": "Пользователь"
"user": "Пользователь",
"requiresConfig": "Требуется настройка",
"builtinServers": "Встроенные серверы",
"more": {
"modelscope": "Сервер MCP сообщества ModelScope",
"higress": "Сервер Higress MCP",
"mcpso": "Платформа поиска серверов MCP",
"smithery": "Инструменты Smithery MCP",
"glama": "Каталог серверов Glama MCP",
"pulsemcp": "Сервер Pulse MCP",
"composio": "Инструменты разработки Composio MCP",
"official": "Официальная коллекция серверов MCP",
"awesome": "Кураторский список серверов MCP"
}
},
"messages.divider": "Показывать разделитель между сообщениями",
"messages.divider.tooltip": "Не применимо к сообщениям в стиле пузырей",

View File

@ -1870,7 +1870,20 @@
"updateError": "更新服务器失败",
"updateSuccess": "服务器更新成功",
"url": "URL",
"user": "用户"
"user": "用户",
"requiresConfig": "需要配置",
"builtinServers": "内置服务器",
"more": {
"modelscope": "魔搭社区 MCP 服务器",
"higress": "Higress MCP 服务器",
"mcpso": "MCP 服务器发现平台",
"smithery": "Smithery MCP 工具",
"glama": "Glama MCP 服务器目录",
"pulsemcp": "Pulse MCP 服务器",
"composio": "Composio MCP 开发工具",
"official": "官方 MCP 服务器集合",
"awesome": "精选的 MCP 服务器列表"
}
},
"messages.divider": "消息分割线",
"messages.divider.tooltip": "不适用于气泡样式消息",

View File

@ -1870,7 +1870,20 @@
"updateError": "更新伺服器失敗",
"updateSuccess": "伺服器更新成功",
"url": "URL",
"user": "用戶"
"user": "用戶",
"requiresConfig": "需要配置",
"builtinServers": "內置伺服器",
"more": {
"modelscope": "魔搭社區 MCP 伺服器",
"higress": "Higress MCP 伺服器",
"mcpso": "MCP 伺服器發現平台",
"smithery": "Smithery MCP 工具",
"glama": "Glama MCP 伺服器目錄",
"pulsemcp": "Pulse MCP 伺服器",
"composio": "Composio MCP 開發工具",
"official": "官方 MCP 伺服器集合",
"awesome": "精選的 MCP 伺服器清單"
}
},
"messages.divider": "訊息間顯示分隔線",
"messages.divider.tooltip": "不適用於氣泡樣式消息",

View File

@ -126,6 +126,7 @@ const Container = styled.div`
align-items: center;
gap: 10px;
position: relative;
margin-bottom: 8px;
`
const UserWrap = styled.div`

View File

@ -0,0 +1,170 @@
import { CheckOutlined, PlusOutlined } from '@ant-design/icons'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { builtinMCPServers } from '@renderer/store/mcp'
import { Button, Popover, Tag } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingTitle } from '..'
const BuiltinMCPServersSection: FC = () => {
const { t } = useTranslation()
const { addMCPServer, mcpServers } = useMCPServers()
return (
<>
<SettingTitle style={{ gap: 3 }}>{t('settings.mcp.builtinServers')}</SettingTitle>
<ServersGrid>
{builtinMCPServers.map((server) => {
const isInstalled = mcpServers.some((existingServer) => existingServer.name === server.name)
return (
<ServerCard key={server.id}>
<ServerHeader>
<ServerName>
<ServerNameText>{server.name}</ServerNameText>
</ServerName>
<StatusIndicator>
<Button
type="text"
icon={isInstalled ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <PlusOutlined />}
size="small"
onClick={() => {
if (isInstalled) {
return
}
addMCPServer(server)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-builtin-server' })
}}
disabled={isInstalled}
/>
</StatusIndicator>
</ServerHeader>
<Popover
content={<PopoverContent>{server.description}</PopoverContent>}
title={server.name}
trigger="hover"
placement="topLeft"
overlayStyle={{ maxWidth: 400 }}>
<ServerDescription>
{server.description}
<MoreIndicator>...</MoreIndicator>
</ServerDescription>
</Popover>
<ServerFooter>
<Tag color="processing" style={{ borderRadius: 20, margin: 0, fontWeight: 500 }}>
{t(`settings.mcp.types.${server.type || 'stdio'}`)}
</Tag>
{server.env && Object.keys(server.env).length > 0 && (
<Tag color="warning" style={{ borderRadius: 20, margin: 0, fontWeight: 500 }}>
{t('settings.mcp.requiresConfig')}
</Tag>
)}
</ServerFooter>
</ServerCard>
)
})}
</ServersGrid>
</>
)
}
const ServersGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
margin-bottom: 20px;
`
const ServerCard = styled.div`
display: flex;
flex-direction: column;
border: 0.5px solid var(--color-border);
border-radius: var(--list-item-border-radius);
padding: 10px 16px;
transition: all 0.2s ease;
background-color: var(--color-background);
height: 125px;
cursor: default;
&:hover {
border-color: var(--color-primary);
}
`
const ServerHeader = styled.div`
display: flex;
align-items: center;
margin-bottom: 5px;
`
const ServerName = styled.div`
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 4px;
`
const ServerNameText = styled.span`
font-size: 15px;
font-weight: 500;
`
const StatusIndicator = styled.div`
margin-left: 8px;
display: flex;
align-items: center;
gap: 8px;
`
const ServerDescription = styled.div`
font-size: 12px;
color: var(--color-text-2);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
width: 100%;
word-break: break-word;
height: 50px;
cursor: pointer;
position: relative;
&:hover {
color: var(--color-text-1);
}
`
const MoreIndicator = styled.span`
position: absolute;
bottom: 0;
right: 0;
background: var(--color-background);
color: var(--color-primary);
font-weight: 500;
padding-left: 8px;
`
const PopoverContent = styled.div`
max-width: 350px;
line-height: 1.5;
font-size: 14px;
color: var(--color-text-1);
white-space: pre-wrap;
word-break: break-word;
`
const ServerFooter = styled.div`
display: flex;
align-items: center;
gap: 4px;
justify-content: flex-start;
margin-top: 10px;
`
export default BuiltinMCPServersSection

View File

@ -0,0 +1,152 @@
import { ExternalLink } from 'lucide-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingTitle } from '..'
const mcpResources = [
{
name: 'modelscope.cn',
url: 'https://www.modelscope.cn/mcp',
logo: 'https://g.alicdn.com/sail-web/maas/2.7.35/favicon/128.ico',
descriptionKey: 'settings.mcp.more.modelscope'
},
{
name: 'mcp.higress.ai',
url: 'https://mcp.higress.ai/',
logo: 'https://framerusercontent.com/images/FD5yBobiBj4Evn0qf11X7iQ9csk.png',
descriptionKey: 'settings.mcp.more.higress'
},
{
name: 'mcp.so',
url: 'https://mcp.so/',
logo: 'https://mcp.so/favicon.ico',
descriptionKey: 'settings.mcp.more.mcpso'
},
{
name: 'smithery.ai',
url: 'https://smithery.ai/',
logo: 'https://smithery.ai/logo.svg',
descriptionKey: 'settings.mcp.more.smithery'
},
{
name: 'glama.ai',
url: 'https://glama.ai/mcp/servers',
logo: 'https://glama.ai/favicon.ico',
descriptionKey: 'settings.mcp.more.glama'
},
{
name: 'pulsemcp.com',
url: 'https://www.pulsemcp.com',
logo: 'https://www.pulsemcp.com/favicon.svg',
descriptionKey: 'settings.mcp.more.pulsemcp'
},
{
name: 'mcp.composio.dev',
url: 'https://mcp.composio.dev/',
logo: 'https://composio.dev/wp-content/uploads/2025/02/Fevicon-composio.png',
descriptionKey: 'settings.mcp.more.composio'
},
{
name: 'Model Context Protocol Servers',
url: 'https://github.com/modelcontextprotocol/servers',
logo: 'https://avatars.githubusercontent.com/u/182288589',
descriptionKey: 'settings.mcp.more.official'
},
{
name: 'Awesome MCP Servers',
url: 'https://github.com/punkpeye/awesome-mcp-servers',
logo: 'https://github.githubassets.com/assets/github-logo-55c5b9a1fe52.png',
descriptionKey: 'settings.mcp.more.awesome'
}
]
const McpResourcesSection: FC = () => {
const { t } = useTranslation()
return (
<>
<SettingTitle style={{ gap: 3 }}>{t('settings.mcp.findMore')}</SettingTitle>
<ResourcesGrid>
{mcpResources.map((resource) => (
<ResourceCard key={resource.name} onClick={() => window.open(resource.url, '_blank', 'noopener,noreferrer')}>
<ResourceHeader>
<ResourceLogo src={resource.logo} alt={`${resource.name} logo`} />
<ResourceName>{resource.name}</ResourceName>
<ExternalLinkIcon>
<ExternalLink size={14} />
</ExternalLinkIcon>
</ResourceHeader>
<ResourceDescription>{t(resource.descriptionKey)}</ResourceDescription>
</ResourceCard>
))}
</ResourcesGrid>
</>
)
}
const ResourcesGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
margin-bottom: 20px;
`
const ResourceCard = styled.div`
display: flex;
flex-direction: column;
border: 0.5px solid var(--color-border);
border-radius: var(--list-item-border-radius);
padding: 12px 16px;
transition: all 0.2s ease;
background-color: var(--color-background);
cursor: pointer;
height: 80px;
&:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
`
const ResourceHeader = styled.div`
display: flex;
align-items: center;
margin-bottom: 8px;
`
const ResourceLogo = styled.img`
width: 20px;
height: 20px;
border-radius: 4px;
object-fit: cover;
margin-right: 8px;
`
const ResourceName = styled.span`
font-size: 14px;
font-weight: 500;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
const ExternalLinkIcon = styled.div`
color: var(--color-text-3);
display: flex;
align-items: center;
`
const ResourceDescription = styled.div`
font-size: 12px;
color: var(--color-text-2);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
`
export default McpResourcesSection

View File

@ -14,7 +14,9 @@ import styled from 'styled-components'
import { SettingTitle } from '..'
import AddMcpServerModal from './AddMcpServerModal'
import BuiltinMCPServersSection from './BuiltinMCPServersSection'
import EditMcpJsonPopup from './EditMcpJsonPopup'
import McpResourcesSection from './McpResourcesSection'
import SyncServersPopup from './SyncServersPopup'
const McpServersList: FC = () => {
@ -179,6 +181,10 @@ const McpServersList: FC = () => {
style={{ marginTop: 20 }}
/>
)}
<McpResourcesSection />
<BuiltinMCPServersSection />
<AddMcpServerModal
visible={isAddModalVisible}
onClose={() => setIsAddModalVisible(false)}
@ -295,6 +301,7 @@ const ServerFooter = styled.div`
const ButtonGroup = styled.div`
display: flex;
align-items: center;
gap: 8px;
`

View File

@ -2,77 +2,17 @@ import { NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import { isLinux, isWin } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { Button, Dropdown, Menu, type MenuProps } from 'antd'
import { ChevronDown, Search } from 'lucide-react'
import { Button } from 'antd'
import { Search } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import InstallNpxUv from './InstallNpxUv'
const mcpResources = [
{
name: 'Model Context Protocol Servers',
url: 'https://github.com/modelcontextprotocol/servers',
logo: 'https://avatars.githubusercontent.com/u/182288589'
},
{
name: 'Awesome MCP Servers',
url: 'https://github.com/punkpeye/awesome-mcp-servers',
logo: 'https://github.githubassets.com/assets/github-logo-55c5b9a1fe52.png'
},
{
name: 'mcp.so',
url: 'https://mcp.so/',
logo: 'https://mcp.so/favicon.ico'
},
{
name: 'modelscope.cn',
url: 'https://www.modelscope.cn/mcp',
logo: 'https://g.alicdn.com/sail-web/maas/2.7.35/favicon/128.ico'
},
{
name: 'mcp.higress.ai',
url: 'https://mcp.higress.ai/',
logo: 'https://framerusercontent.com/images/FD5yBobiBj4Evn0qf11X7iQ9csk.png'
},
{
name: 'smithery.ai',
url: 'https://smithery.ai/',
logo: 'https://smithery.ai/logo.svg'
},
{
name: 'glama.ai',
url: 'https://glama.ai/mcp/servers',
logo: 'https://glama.ai/favicon.ico'
},
{
name: 'pulsemcp.com',
url: 'https://www.pulsemcp.com',
logo: 'https://www.pulsemcp.com/favicon.svg'
},
{
name: 'mcp.composio.dev',
url: 'https://mcp.composio.dev/',
logo: 'https://composio.dev/wp-content/uploads/2025/02/Fevicon-composio.png'
}
]
export const McpSettingsNavbar = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const resourceMenuItems: MenuProps['items'] = mcpResources.map(({ name, url, logo }) => ({
key: name,
label: (
<Menu.Item
onClick={() => window.open(url, '_blank', 'noopener,noreferrer')}
style={{ backgroundColor: 'transparent' }}
icon={<img src={logo} alt={name} style={{ width: 20, height: 20, borderRadius: 5, marginRight: 10 }} />}>
{name}
</Menu.Item>
)
}))
return (
<NavbarRight style={{ paddingRight: useFullscreen() ? '12px' : isWin ? 150 : isLinux ? 120 : 12 }}>
<HStack alignItems="center" gap={5}>
@ -85,16 +25,6 @@ export const McpSettingsNavbar = () => {
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
{t('settings.mcp.searchNpx')}
</Button>
<Dropdown menu={{ items: resourceMenuItems }} trigger={['click']}>
<Button
size="small"
type="text"
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20, display: 'flex', alignItems: 'center' }}>
{t('settings.mcp.findMore')}
<ChevronDown size={16} />
</Button>
</Dropdown>
<InstallNpxUv mini />
</HStack>
</NavbarRight>

View File

@ -3,7 +3,6 @@ import { nanoid } from '@reduxjs/toolkit'
import logo from '@renderer/assets/images/cherry-text-logo.svg'
import { Center, HStack } from '@renderer/components/Layout'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { builtinMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types'
import { getMcpConfigSampleFromReadme } from '@renderer/utils'
import { Button, Card, Flex, Input, Space, Spin, Tag, Typography } from 'antd'
@ -23,7 +22,7 @@ interface SearchResult {
configSample?: MCPServer['configSample']
}
const npmScopes = ['@cherry', '@modelcontextprotocol', '@gongrzhe', '@mcpmarket']
const npmScopes = ['@modelcontextprotocol', '@gongrzhe', '@mcpmarket']
let _searchResults: SearchResult[] = []
@ -32,7 +31,7 @@ const NpxSearch: FC = () => {
const { Text, Link } = Typography
// Add new state variables for npm scope search
const [npmScope, setNpmScope] = useState('@cherry')
const [npmScope, setNpmScope] = useState('@modelcontextprotocol')
const [searchLoading, setSearchLoading] = useState(false)
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
const { addMCPServer, mcpServers } = useMCPServers()
@ -52,22 +51,6 @@ const NpxSearch: FC = () => {
return
}
if (searchScope === '@cherry') {
setSearchResults(
builtinMCPServers.map((server) => ({
key: server.id,
name: server.name,
description: server.description || '',
version: '1.0.0',
usage: '参考下方链接中的使用说明',
npmLink: 'https://docs.cherry-ai.com/advanced-basic/mcp/in-memory',
fullName: server.name,
type: server.type || 'inMemory'
}))
)
return
}
setSearchLoading(true)
try {
@ -190,14 +173,6 @@ const NpxSearch: FC = () => {
return
}
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
if (buildInServer) {
addMCPServer(buildInServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
return
}
const newServer = {
id: nanoid(),
name: record.name,

View File

@ -71,18 +71,18 @@ const SettingsPage: FC = () => {
{t('settings.display.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/tool">
<MenuItem className={isRoute('/settings/tool')}>
<PencilRuler size={18} />
{t('settings.tool.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/mcp">
<MenuItem className={isRoute('/settings/mcp')}>
<SquareTerminal size={18} />
{t('settings.mcp.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/tool">
<MenuItem className={isRoute('/settings/tool')}>
<PencilRuler size={18} />
{t('settings.tool.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/shortcut">
<MenuItem className={isRoute('/settings/shortcut')}>
<Command size={18} />