From 8191791036a83683f6e8d86c037d1582fb3850d4 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Mon, 7 Apr 2025 14:05:22 +0800 Subject: [PATCH] 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. --- src/main/mcpServers/filesystem.ts | 8 +-- src/main/services/MCPService.ts | 4 +- .../src/pages/home/Inputbar/Inputbar.tsx | 3 +- .../pages/home/Inputbar/MCPToolsButton.tsx | 12 +--- .../pages/settings/MCPSettings/NpxSearch.tsx | 46 +++++++++++---- .../src/pages/settings/MCPSettings/index.tsx | 57 ++++--------------- src/renderer/src/store/mcp.ts | 14 ++--- 7 files changed, 63 insertions(+), 81 deletions(-) diff --git a/src/main/mcpServers/filesystem.ts b/src/main/mcpServers/filesystem.ts index 0da8a1e5..624f11c6 100644 --- a/src/main/mcpServers/filesystem.ts +++ b/src/main/mcpServers/filesystem.ts @@ -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) } }) ) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index ee9eb8f1..b2262513 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -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') { diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index cb7304f9..5fb739e9 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -497,8 +497,9 @@ const Inputbar: FC = ({ 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) diff --git a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx index 43fe0387..a7d0da64 100644 --- a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx @@ -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 = ({ 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 = ({ 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'), diff --git a/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx b/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx index 2611d2ca..560833bb 100644 --- a/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx @@ -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(_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={} 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 + }) }} /> diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 0218af9e..ae84646d 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -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(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,33 +114,17 @@ const MCPSettings: FC = () => { return ( - - setMcpListType(value as 'system' | 'user')} - /> - - {mcpListType === 'user' && ( - } - titleStyle={{ fontWeight: 500 }} - style={{ width: '100%', marginTop: -2 }} - /> - )} - + } + titleStyle={{ fontWeight: 500 }} + style={{ width: '100%', marginTop: -2 }} + /> + {(server: MCPServer) => (
diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index 46e73b4b..1d075e70 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -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 } ]