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:
parent
99b37f2782
commit
8191791036
@ -296,7 +296,7 @@ class FileSystemServer {
|
|||||||
// Validate that all directories exist and are accessible
|
// Validate that all directories exist and are accessible
|
||||||
this.validateDirs().catch((error) => {
|
this.validateDirs().catch((error) => {
|
||||||
console.error('Error validating allowed directories:', error)
|
console.error('Error validating allowed directories:', error)
|
||||||
process.exit(1)
|
throw new Error(`Error validating allowed directories: ${error}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.server = new Server(
|
this.server = new Server(
|
||||||
@ -321,11 +321,11 @@ class FileSystemServer {
|
|||||||
const stats = await fs.stat(expandHome(dir))
|
const stats = await fs.stat(expandHome(dir))
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
console.error(`Error: ${dir} is not a directory`)
|
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)
|
console.error(`Error accessing directory ${dir}:`, error)
|
||||||
process.exit(1)
|
throw new Error(`Error accessing directory ${dir}:`, error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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 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()
|
const binPath = await getBinaryPath()
|
||||||
makeSureDirExists(binPath)
|
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') {
|
} else if (server.command === 'uvx' || server.command === 'uv') {
|
||||||
|
|||||||
@ -497,8 +497,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
// Clear previous state
|
// Clear previous state
|
||||||
// Reset to assistant default model
|
// Reset to assistant default model
|
||||||
assistant.defaultModel && setModel(assistant.defaultModel)
|
assistant.defaultModel && setModel(assistant.defaultModel)
|
||||||
|
|
||||||
// Reset to assistant knowledge mcp servers
|
// Reset to assistant knowledge mcp servers
|
||||||
setEnabledMCPs(assistant.mcpServers || [])
|
!isEmpty(assistant.mcpServers) && setEnabledMCPs(assistant.mcpServers || [])
|
||||||
|
|
||||||
addTopic(topic)
|
addTopic(topic)
|
||||||
setActiveTopic(topic)
|
setActiveTopic(topic)
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { initializeMCPServers } from '@renderer/store/mcp'
|
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPServer } from '@renderer/types'
|
||||||
import { Tooltip } from 'antd'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
export interface MCPToolsButtonRef {
|
export interface MCPToolsButtonRef {
|
||||||
@ -21,11 +19,10 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
|
const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
|
||||||
const { activedMcpServers, mcpServers } = useMCPServers()
|
const { activedMcpServers } = useMCPServers()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const quickPanel = useQuickPanel()
|
const quickPanel = useQuickPanel()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
|
||||||
const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id))
|
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
|
return newList
|
||||||
}, [activedMcpServers, t, enabledMCPs, toggelEnableMCP, navigate])
|
}, [activedMcpServers, t, enabledMCPs, toggelEnableMCP, navigate])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initializeMCPServers(mcpServers, dispatch)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const openQuickPanel = useCallback(() => {
|
const openQuickPanel = useCallback(() => {
|
||||||
quickPanel.open({
|
quickPanel.open({
|
||||||
title: t('settings.mcp.title'),
|
title: t('settings.mcp.title'),
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { nanoid } from '@reduxjs/toolkit'
|
|||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
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 { Button, Card, Flex, Input, Space, Spin, Tag, Typography } from 'antd'
|
||||||
import { npxFinder } from 'npx-scope-finder'
|
import { npxFinder } from 'npx-scope-finder'
|
||||||
import { type FC, useEffect, useState } from 'react'
|
import { type FC, useEffect, useState } from 'react'
|
||||||
@ -19,9 +19,10 @@ interface SearchResult {
|
|||||||
usage: string
|
usage: string
|
||||||
npmLink: string
|
npmLink: string
|
||||||
fullName: string
|
fullName: string
|
||||||
|
type: 'stdio' | 'sse' | 'inMemory'
|
||||||
}
|
}
|
||||||
|
|
||||||
const npmScopes = ['@modelcontextprotocol', '@gongrzhe', '@mcpmarket']
|
const npmScopes = ['@cherry', '@modelcontextprotocol', '@gongrzhe', '@mcpmarket']
|
||||||
|
|
||||||
let _searchResults: SearchResult[] = []
|
let _searchResults: SearchResult[] = []
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ const NpxSearch: FC = () => {
|
|||||||
const { Text, Link } = Typography
|
const { Text, Link } = Typography
|
||||||
|
|
||||||
// Add new state variables for npm scope search
|
// Add new state variables for npm scope search
|
||||||
const [npmScope, setNpmScope] = useState('@modelcontextprotocol')
|
const [npmScope, setNpmScope] = useState('@cherry')
|
||||||
const [searchLoading, setSearchLoading] = useState(false)
|
const [searchLoading, setSearchLoading] = useState(false)
|
||||||
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
|
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
|
||||||
const { addMCPServer } = useMCPServers()
|
const { addMCPServer } = useMCPServers()
|
||||||
@ -41,7 +42,7 @@ const NpxSearch: FC = () => {
|
|||||||
// Add new function to handle npm scope search
|
// Add new function to handle npm scope search
|
||||||
const handleNpmSearch = async (scopeOverride?: string) => {
|
const handleNpmSearch = async (scopeOverride?: string) => {
|
||||||
const searchScope = scopeOverride || npmScope
|
const searchScope = scopeOverride || npmScope
|
||||||
console.log('handleNpmSearch', searchScope)
|
|
||||||
if (!searchScope.trim()) {
|
if (!searchScope.trim()) {
|
||||||
window.message.warning({ content: t('settings.mcp.npx_list.scope_required'), key: 'mcp-npx-scope-required' })
|
window.message.warning({ content: t('settings.mcp.npx_list.scope_required'), key: 'mcp-npx-scope-required' })
|
||||||
return
|
return
|
||||||
@ -51,6 +52,22 @@ const NpxSearch: FC = () => {
|
|||||||
return
|
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)
|
setSearchLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -58,7 +75,7 @@ const NpxSearch: FC = () => {
|
|||||||
const packages = await npxFinder(searchScope)
|
const packages = await npxFinder(searchScope)
|
||||||
|
|
||||||
// Map the packages to our desired format
|
// Map the packages to our desired format
|
||||||
const formattedResults = packages.map((pkg) => {
|
const formattedResults: SearchResult[] = packages.map((pkg) => {
|
||||||
return {
|
return {
|
||||||
key: pkg.name,
|
key: pkg.name,
|
||||||
name: pkg.name?.split('/')[1] || '',
|
name: pkg.name?.split('/')[1] || '',
|
||||||
@ -66,7 +83,8 @@ const NpxSearch: FC = () => {
|
|||||||
version: pkg.version || 'Latest',
|
version: pkg.version || 'Latest',
|
||||||
usage: `npx ${pkg.name}`,
|
usage: `npx ${pkg.name}`,
|
||||||
npmLink: pkg.links?.npm || `https://www.npmjs.com/package/${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 />}
|
icon={<PlusOutlined />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// 创建一个临时的 MCP 服务器对象
|
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
|
||||||
const tempServer: MCPServer = {
|
|
||||||
|
if (buildInServer) {
|
||||||
|
addMCPServer(buildInServer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addMCPServer({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: record.name,
|
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}`,
|
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`,
|
||||||
command: 'npx',
|
command: 'npx',
|
||||||
args: ['-y', record.fullName],
|
args: ['-y', record.fullName],
|
||||||
isActive: false
|
isActive: false,
|
||||||
}
|
type: record.type
|
||||||
addMCPServer(tempServer)
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@ -8,13 +8,11 @@ import Scrollbar from '@renderer/components/Scrollbar'
|
|||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { EventEmitter } from '@renderer/services/EventService'
|
import { EventEmitter } from '@renderer/services/EventService'
|
||||||
import { initializeMCPServers } from '@renderer/store/mcp'
|
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPServer } from '@renderer/types'
|
||||||
import { Dropdown, MenuProps, Segmented } from 'antd'
|
import { Dropdown, MenuProps } from 'antd'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingContainer } from '..'
|
import { SettingContainer } from '..'
|
||||||
@ -28,18 +26,6 @@ const MCPSettings: FC = () => {
|
|||||||
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
|
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
|
||||||
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
|
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
|
||||||
const { theme } = useTheme()
|
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(() => {
|
useEffect(() => {
|
||||||
const unsubs = [
|
const unsubs = [
|
||||||
@ -49,11 +35,6 @@ const MCPSettings: FC = () => {
|
|||||||
return () => unsubs.forEach((unsub) => unsub())
|
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 onAddMcpServer = async () => {
|
||||||
const newServer = {
|
const newServer = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
@ -133,33 +114,17 @@ const MCPSettings: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<McpListContainer>
|
<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>
|
<McpList>
|
||||||
{mcpListType === 'user' && (
|
<ListItem
|
||||||
<ListItem
|
key="add"
|
||||||
key="add"
|
title={t('settings.mcp.addServer')}
|
||||||
title={t('settings.mcp.addServer')}
|
active={false}
|
||||||
active={false}
|
onClick={onAddMcpServer}
|
||||||
onClick={onAddMcpServer}
|
icon={<PlusOutlined />}
|
||||||
icon={<PlusOutlined />}
|
titleStyle={{ fontWeight: 500 }}
|
||||||
titleStyle={{ fontWeight: 500 }}
|
style={{ width: '100%', marginTop: -2 }}
|
||||||
style={{ width: '100%', marginTop: -2 }}
|
/>
|
||||||
/>
|
<DragableList list={mcpServers} onUpdate={updateMcpServers}>
|
||||||
)}
|
|
||||||
<DragableList list={servers} onUpdate={updateMcpServers}>
|
|
||||||
{(server: MCPServer) => (
|
{(server: MCPServer) => (
|
||||||
<Dropdown menu={{ items: getMenuItems(server) }} trigger={['contextMenu']} key={server.id}>
|
<Dropdown menu={{ items: getMenuItems(server) }} trigger={['contextMenu']} key={server.id}>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -55,7 +55,8 @@ export const builtinMCPServers: MCPServer[] = [
|
|||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: '@cherry/mcp-auto-install',
|
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',
|
command: 'npx',
|
||||||
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
|
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
|
||||||
isActive: false
|
isActive: false
|
||||||
@ -65,15 +66,14 @@ export const builtinMCPServers: MCPServer[] = [
|
|||||||
name: '@cherry/memory',
|
name: '@cherry/memory',
|
||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
description:
|
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
|
isActive: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: '@cherry/sequentialthinking',
|
name: '@cherry/sequentialthinking',
|
||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
description:
|
description: '一个 MCP 服务器实现,提供了通过结构化思维过程进行动态和反思性问题解决的工具',
|
||||||
'An MCP server implementation that provides a tool for dynamic and reflective problem-solving through a structured thinking process.',
|
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -81,21 +81,21 @@ export const builtinMCPServers: MCPServer[] = [
|
|||||||
name: '@cherry/brave-search',
|
name: '@cherry/brave-search',
|
||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
description:
|
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
|
isActive: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: '@cherry/fetch',
|
name: '@cherry/fetch',
|
||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
description: 'An MCP server for fetching URLs / Youtube video transcript.',
|
description: '用于获取 URL 网页内容的 MCP 服务器',
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: '@cherry/filesystem',
|
name: '@cherry/filesystem',
|
||||||
type: 'inMemory',
|
type: 'inMemory',
|
||||||
description: 'Node.js server implementing Model Context Protocol (MCP) for filesystem operations.',
|
description: '实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器',
|
||||||
isActive: false
|
isActive: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user