refactor(mcp): move inMemory servers to npx search scope

- Updated error handling in FileSystemServer to throw errors instead of exiting the process.
- Enhanced MCPService to use a more flexible server name check and updated the path for MCP_REGISTRY_PATH.
- Refined Inputbar to conditionally set enabled MCPs only if they are not empty.
- Cleaned up MCPToolsButton and MCPSettings by removing unused imports and effects.
- Updated NpxSearch to include a new npm scope and improved search result handling.
- Enhanced builtin MCP server descriptions for better clarity.
This commit is contained in:
kangfenmao 2025-04-07 14:05:22 +08:00
parent 99b37f2782
commit 8191791036
7 changed files with 63 additions and 81 deletions

View File

@ -296,7 +296,7 @@ class FileSystemServer {
// Validate that all directories exist and are accessible
this.validateDirs().catch((error) => {
console.error('Error validating allowed directories:', error)
process.exit(1)
throw new Error(`Error validating allowed directories: ${error}`)
})
this.server = new Server(
@ -321,11 +321,11 @@ class FileSystemServer {
const stats = await fs.stat(expandHome(dir))
if (!stats.isDirectory()) {
console.error(`Error: ${dir} is not a directory`)
process.exit(1)
throw new Error(`Error: ${dir} is not a directory`)
}
} catch (error) {
} catch (error: any) {
console.error(`Error accessing directory ${dir}:`, error)
process.exit(1)
throw new Error(`Error accessing directory ${dir}:`, error)
}
})
)

View File

@ -106,10 +106,10 @@ class McpService {
}
// if the server name is mcp-auto-install, use the mcp-registry.json file in the bin directory
if (server.name === 'mcp-auto-install') {
if (server.name.includes('mcp-auto-install')) {
const binPath = await getBinaryPath()
makeSureDirExists(binPath)
server.env.MCP_REGISTRY_PATH = path.join(binPath, 'mcp-registry.json')
server.env.MCP_REGISTRY_PATH = path.join(binPath, '..', 'config', 'mcp-registry.json')
}
}
} else if (server.command === 'uvx' || server.command === 'uv') {

View File

@ -497,8 +497,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
// Clear previous state
// Reset to assistant default model
assistant.defaultModel && setModel(assistant.defaultModel)
// Reset to assistant knowledge mcp servers
setEnabledMCPs(assistant.mcpServers || [])
!isEmpty(assistant.mcpServers) && setEnabledMCPs(assistant.mcpServers || [])
addTopic(topic)
setActiveTopic(topic)

View File

@ -1,12 +1,10 @@
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { initializeMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types'
import { Tooltip } from 'antd'
import { FC, useCallback, useEffect, useImperativeHandle, useMemo } from 'react'
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import { useNavigate } from 'react-router'
export interface MCPToolsButtonRef {
@ -21,11 +19,10 @@ interface Props {
}
const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
const { activedMcpServers, mcpServers } = useMCPServers()
const { activedMcpServers } = useMCPServers()
const { t } = useTranslation()
const quickPanel = useQuickPanel()
const navigate = useNavigate()
const dispatch = useDispatch()
const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id))
@ -48,11 +45,6 @@ const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarB
return newList
}, [activedMcpServers, t, enabledMCPs, toggelEnableMCP, navigate])
useEffect(() => {
initializeMCPServers(mcpServers, dispatch)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const openQuickPanel = useCallback(() => {
quickPanel.open({
title: t('settings.mcp.title'),

View File

@ -3,7 +3,7 @@ import { nanoid } from '@reduxjs/toolkit'
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import type { MCPServer } from '@renderer/types'
import { builtinMCPServers } from '@renderer/store/mcp'
import { Button, Card, Flex, Input, Space, Spin, Tag, Typography } from 'antd'
import { npxFinder } from 'npx-scope-finder'
import { type FC, useEffect, useState } from 'react'
@ -19,9 +19,10 @@ interface SearchResult {
usage: string
npmLink: string
fullName: string
type: 'stdio' | 'sse' | 'inMemory'
}
const npmScopes = ['@modelcontextprotocol', '@gongrzhe', '@mcpmarket']
const npmScopes = ['@cherry', '@modelcontextprotocol', '@gongrzhe', '@mcpmarket']
let _searchResults: SearchResult[] = []
@ -31,7 +32,7 @@ const NpxSearch: FC = () => {
const { Text, Link } = Typography
// Add new state variables for npm scope search
const [npmScope, setNpmScope] = useState('@modelcontextprotocol')
const [npmScope, setNpmScope] = useState('@cherry')
const [searchLoading, setSearchLoading] = useState(false)
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
const { addMCPServer } = useMCPServers()
@ -41,7 +42,7 @@ const NpxSearch: FC = () => {
// Add new function to handle npm scope search
const handleNpmSearch = async (scopeOverride?: string) => {
const searchScope = scopeOverride || npmScope
console.log('handleNpmSearch', searchScope)
if (!searchScope.trim()) {
window.message.warning({ content: t('settings.mcp.npx_list.scope_required'), key: 'mcp-npx-scope-required' })
return
@ -51,6 +52,22 @@ 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 {
@ -58,7 +75,7 @@ const NpxSearch: FC = () => {
const packages = await npxFinder(searchScope)
// Map the packages to our desired format
const formattedResults = packages.map((pkg) => {
const formattedResults: SearchResult[] = packages.map((pkg) => {
return {
key: pkg.name,
name: pkg.name?.split('/')[1] || '',
@ -66,7 +83,8 @@ const NpxSearch: FC = () => {
version: pkg.version || 'Latest',
usage: `npx ${pkg.name}`,
npmLink: pkg.links?.npm || `https://www.npmjs.com/package/${pkg.name}`,
fullName: pkg.name || ''
fullName: pkg.name || '',
type: 'stdio'
}
})
@ -157,16 +175,22 @@ const NpxSearch: FC = () => {
icon={<PlusOutlined />}
size="small"
onClick={() => {
// 创建一个临时的 MCP 服务器对象
const tempServer: MCPServer = {
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
if (buildInServer) {
addMCPServer(buildInServer)
return
}
addMCPServer({
id: nanoid(),
name: record.name,
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`,
command: 'npx',
args: ['-y', record.fullName],
isActive: false
}
addMCPServer(tempServer)
isActive: false,
type: record.type
})
}}
/>
</Flex>

View File

@ -8,13 +8,11 @@ import Scrollbar from '@renderer/components/Scrollbar'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { EventEmitter } from '@renderer/services/EventService'
import { initializeMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types'
import { Dropdown, MenuProps, Segmented } from 'antd'
import { Dropdown, MenuProps } from 'antd'
import { isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch } from 'react-redux'
import styled from 'styled-components'
import { SettingContainer } from '..'
@ -28,18 +26,6 @@ const MCPSettings: FC = () => {
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
const { theme } = useTheme()
const dispatch = useDispatch()
const [mcpListType, setMcpListType] = useState<'system' | 'user'>('user')
const systemServers = mcpServers.filter((server) => {
return server.type === 'inMemory'
})
const userServers = mcpServers.filter((server) => {
return server.type !== 'inMemory'
})
const servers = mcpListType === 'system' ? systemServers : userServers
useEffect(() => {
const unsubs = [
@ -49,11 +35,6 @@ const MCPSettings: FC = () => {
return () => unsubs.forEach((unsub) => unsub())
}, [])
useEffect(() => {
initializeMCPServers(mcpServers, dispatch)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // Empty dependency array to run only once
const onAddMcpServer = async () => {
const newServer = {
id: nanoid(),
@ -133,22 +114,7 @@ const MCPSettings: FC = () => {
return (
<Container>
<McpListContainer>
<McpListHeader>
<Segmented
size="middle"
style={{ width: '100%' }}
block
shape="round"
value={mcpListType}
options={[
{ value: 'user', label: t('settings.mcp.user') },
{ value: 'system', label: t('settings.mcp.system') }
]}
onChange={(value) => setMcpListType(value as 'system' | 'user')}
/>
</McpListHeader>
<McpList>
{mcpListType === 'user' && (
<ListItem
key="add"
title={t('settings.mcp.addServer')}
@ -158,8 +124,7 @@ const MCPSettings: FC = () => {
titleStyle={{ fontWeight: 500 }}
style={{ width: '100%', marginTop: -2 }}
/>
)}
<DragableList list={servers} onUpdate={updateMcpServers}>
<DragableList list={mcpServers} onUpdate={updateMcpServers}>
{(server: MCPServer) => (
<Dropdown menu={{ items: getMenuItems(server) }} trigger={['contextMenu']} key={server.id}>
<div>

View File

@ -55,7 +55,8 @@ export const builtinMCPServers: MCPServer[] = [
{
id: nanoid(),
name: '@cherry/mcp-auto-install',
description: 'Automatically install MCP services (Beta version)',
description: '自动安装 MCP 服务测试版https://docs.cherry-ai.com/advanced-basic/mcp/auto-install',
type: 'stdio',
command: 'npx',
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
isActive: false
@ -65,15 +66,14 @@ export const builtinMCPServers: MCPServer[] = [
name: '@cherry/memory',
type: 'inMemory',
description:
'A basic implementation of persistent memory using a local knowledge graph. This lets Claude remember information about the user across chats. https://github.com/modelcontextprotocol/servers/tree/main/src/memory',
'基于本地知识图谱的持久性记忆基础实现。这使得模型能够在不同对话间记住用户的相关信息。需要配置 MEMORY_FILE_PATH 环境变量。https://github.com/modelcontextprotocol/servers/tree/main/src/memory',
isActive: true
},
{
id: nanoid(),
name: '@cherry/sequentialthinking',
type: 'inMemory',
description:
'An MCP server implementation that provides a tool for dynamic and reflective problem-solving through a structured thinking process.',
description: '一个 MCP 服务器实现,提供了通过结构化思维过程进行动态和反思性问题解决的工具',
isActive: true
},
{
@ -81,21 +81,21 @@ export const builtinMCPServers: MCPServer[] = [
name: '@cherry/brave-search',
type: 'inMemory',
description:
'An MCP server implementation that integrates the Brave Search API, providing both web and local search capabilities.',
'一个集成了Brave 搜索 API 的 MCP 服务器实现,提供网页与本地搜索双重功能。需要配置 BRAVE_API_KEY 环境变量',
isActive: false
},
{
id: nanoid(),
name: '@cherry/fetch',
type: 'inMemory',
description: 'An MCP server for fetching URLs / Youtube video transcript.',
description: '用于获取 URL 网页内容的 MCP 服务器',
isActive: true
},
{
id: nanoid(),
name: '@cherry/filesystem',
type: 'inMemory',
description: 'Node.js server implementing Model Context Protocol (MCP) for filesystem operations.',
description: '实现文件系统操作的模型上下文协议MCP的 Node.js 服务器',
isActive: false
}
]