feat(MCPSettings): enhance MCP server management and localization updates

- Added a new SVG icon for npm in the MCP settings.
- Introduced a custom hook `useMCPServer` for retrieving a specific MCP server by ID.
- Updated localization files to include new error messages for tool and prompt loading in English, Japanese, Russian, and Chinese.
- Refactored MCP settings components for improved navigation and state management, including the use of React Router for routing.
- Enhanced the Npx search functionality and UI for better user experience.
This commit is contained in:
kangfenmao 2025-04-12 19:25:20 +08:00
parent b62c59eb52
commit 72e18fbcc1
17 changed files with 248 additions and 257 deletions

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744456106953" class="icon" viewBox="0 0 2633 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1486" xmlns:xlink="http://www.w3.org/1999/xlink" width="514.2578125" height="200"><path d="M0 0v877.843607h731.440328v146.156393H1316.724263v-146.156393h1316.724262V0z m731.440328 731.196014h-146.156393V292.312786h-146.485574v438.883228H146.485574V146.238688h584.954754z m438.880656 0v146.567869h-292.405369V146.238688H1463.209837v585.037049H1170.320984z m1316.888853 0H2341.30033V292.312786h-146.56787v438.883228h-146.485574V292.312786h-145.909508v438.883228H1609.283935V146.238688h878.008197zM1170.238688 292.477377H1316.724263v292.644539h-146.485575z" fill="#CB3837" p-id="1487"></path></svg>

After

Width:  |  Height:  |  Size: 845 B

View File

@ -26,3 +26,10 @@ export const useMCPServers = () => {
updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers)) updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers))
} }
} }
export const useMCPServer = (id: string) => {
const { mcpServers } = useMCPServers()
return {
server: mcpServers.find((server) => server.id === id)
}
}

View File

@ -1064,7 +1064,6 @@
"newServer": "MCP Server", "newServer": "MCP Server",
"npx_list": { "npx_list": {
"actions": "Actions", "actions": "Actions",
"desc": "Search and add npm packages as MCP servers",
"description": "Description", "description": "Description",
"no_packages": "No packages found", "no_packages": "No packages found",
"npm": "NPM", "npm": "NPM",
@ -1073,7 +1072,6 @@
"scope_required": "Please enter npm scope", "scope_required": "Please enter npm scope",
"search": "Search", "search": "Search",
"search_error": "Search error", "search_error": "Search error",
"title": "NPX Package List",
"usage": "Usage", "usage": "Usage",
"version": "Version" "version": "Version"
}, },
@ -1099,14 +1097,16 @@
"tools": { "tools": {
"inputSchema": "Input Schema", "inputSchema": "Input Schema",
"availableTools": "Available Tools", "availableTools": "Available Tools",
"noToolsAvailable": "No tools available" "noToolsAvailable": "No tools available",
"loadError": "Get tools Error"
}, },
"prompts": { "prompts": {
"availablePrompts": "Available Prompts", "availablePrompts": "Available Prompts",
"noPromptsAvailable": "No prompts available", "noPromptsAvailable": "No prompts available",
"arguments": "Arguments", "arguments": "Arguments",
"requiredField": "Required Field", "requiredField": "Required Field",
"genericError": "Get prompt Error" "genericError": "Get prompt Error",
"loadError": "Get prompts Error"
}, },
"deleteServer": "Delete Server", "deleteServer": "Delete Server",
"deleteServerConfirm": "Are you sure you want to delete this server?", "deleteServerConfirm": "Are you sure you want to delete this server?",

View File

@ -1063,7 +1063,6 @@
"newServer": "MCP サーバー", "newServer": "MCP サーバー",
"npx_list": { "npx_list": {
"actions": "アクション", "actions": "アクション",
"desc": "npm パッケージを検索して MCP サーバーとして追加",
"description": "説明", "description": "説明",
"no_packages": "パッケージが見つかりません", "no_packages": "パッケージが見つかりません",
"npm": "NPM", "npm": "NPM",
@ -1072,7 +1071,6 @@
"scope_required": "npm スコープを入力してください", "scope_required": "npm スコープを入力してください",
"search": "検索", "search": "検索",
"search_error": "パッケージの検索に失敗しました", "search_error": "パッケージの検索に失敗しました",
"title": "NPX パッケージリスト",
"usage": "使用法", "usage": "使用法",
"version": "バージョン" "version": "バージョン"
}, },
@ -1098,14 +1096,16 @@
"tools": { "tools": {
"inputSchema": "入力スキーマ", "inputSchema": "入力スキーマ",
"availableTools": "利用可能なツール", "availableTools": "利用可能なツール",
"noToolsAvailable": "利用可能なツールなし" "noToolsAvailable": "利用可能なツールなし",
"loadError": "ツール取得エラー"
}, },
"prompts": { "prompts": {
"availablePrompts": "利用可能なプロンプト", "availablePrompts": "利用可能なプロンプト",
"noPromptsAvailable": "利用可能なプロンプトはありません", "noPromptsAvailable": "利用可能なプロンプトはありません",
"arguments": "引数", "arguments": "引数",
"requiredField": "必須フィールド", "requiredField": "必須フィールド",
"genericError": "プロンプト取得エラー" "genericError": "プロンプト取得エラー",
"loadError": "プロンプト取得エラー"
}, },
"deleteServer": "サーバーを削除", "deleteServer": "サーバーを削除",
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?", "deleteServerConfirm": "このサーバーを削除してもよろしいですか?",

View File

@ -1063,7 +1063,6 @@
"newServer": "MCP сервер", "newServer": "MCP сервер",
"npx_list": { "npx_list": {
"actions": "Действия", "actions": "Действия",
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
"description": "Описание", "description": "Описание",
"no_packages": "Ничего не найдено", "no_packages": "Ничего не найдено",
"npm": "NPM", "npm": "NPM",
@ -1072,7 +1071,6 @@
"scope_required": "Пожалуйста, введите область npm", "scope_required": "Пожалуйста, введите область npm",
"search": "Поиск", "search": "Поиск",
"search_error": "Ошибка поиска", "search_error": "Ошибка поиска",
"title": "Список пакетов NPX",
"usage": "Использование", "usage": "Использование",
"version": "Версия" "version": "Версия"
}, },
@ -1098,14 +1096,16 @@
"tools": { "tools": {
"inputSchema": "Схема ввода", "inputSchema": "Схема ввода",
"availableTools": "Доступные инструменты", "availableTools": "Доступные инструменты",
"noToolsAvailable": "Нет доступных инструментов" "noToolsAvailable": "Нет доступных инструментов",
"loadError": "Ошибка получения инструментов"
}, },
"prompts": { "prompts": {
"availablePrompts": "Доступные подсказки", "availablePrompts": "Доступные подсказки",
"noPromptsAvailable": "Нет доступных подсказок", "noPromptsAvailable": "Нет доступных подсказок",
"arguments": "Аргументы", "arguments": "Аргументы",
"requiredField": "Обязательное поле", "requiredField": "Обязательное поле",
"genericError": "Ошибка получения подсказки" "genericError": "Ошибка получения подсказки",
"loadError": "Ошибка получения подсказок"
}, },
"deleteServer": "Удалить сервер", "deleteServer": "Удалить сервер",
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?", "deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",

View File

@ -1064,7 +1064,6 @@
"newServer": "MCP 服务器", "newServer": "MCP 服务器",
"npx_list": { "npx_list": {
"actions": "操作", "actions": "操作",
"desc": "搜索并添加 npm 包作为 MCP 服务",
"description": "描述", "description": "描述",
"no_packages": "未找到包", "no_packages": "未找到包",
"npm": "NPM", "npm": "NPM",
@ -1073,7 +1072,6 @@
"scope_required": "请输入 npm 作用域", "scope_required": "请输入 npm 作用域",
"search": "搜索", "search": "搜索",
"search_error": "搜索失败", "search_error": "搜索失败",
"title": "NPX 包列表",
"usage": "用法", "usage": "用法",
"version": "版本" "version": "版本"
}, },
@ -1099,14 +1097,16 @@
"tools": { "tools": {
"inputSchema": "输入模式", "inputSchema": "输入模式",
"availableTools": "可用工具", "availableTools": "可用工具",
"noToolsAvailable": "无可用工具" "noToolsAvailable": "无可用工具",
"loadError": "获取工具失败"
}, },
"prompts": { "prompts": {
"availablePrompts": "可用提示", "availablePrompts": "可用提示",
"noPromptsAvailable": "无可用提示", "noPromptsAvailable": "无可用提示",
"arguments": "参数", "arguments": "参数",
"requiredField": "必填字段", "requiredField": "必填字段",
"genericError": "获取提示错误" "genericError": "获取提示错误",
"loadError": "获取提示失败"
}, },
"deleteServer": "删除服务器", "deleteServer": "删除服务器",
"deleteServerConfirm": "确定要删除此服务器吗?", "deleteServerConfirm": "确定要删除此服务器吗?",

View File

@ -1063,7 +1063,6 @@
"newServer": "MCP 伺服器", "newServer": "MCP 伺服器",
"npx_list": { "npx_list": {
"actions": "操作", "actions": "操作",
"desc": "搜索並添加 npm 包作為 MCP 服務",
"description": "描述", "description": "描述",
"no_packages": "未找到包", "no_packages": "未找到包",
"npm": "NPM", "npm": "NPM",
@ -1072,7 +1071,6 @@
"scope_required": "請輸入 npm 作用域", "scope_required": "請輸入 npm 作用域",
"search": "搜索", "search": "搜索",
"search_error": "搜索失敗", "search_error": "搜索失敗",
"title": "NPX 包列表",
"usage": "用法", "usage": "用法",
"version": "版本" "version": "版本"
}, },
@ -1098,14 +1096,16 @@
"tools": { "tools": {
"inputSchema": "輸入模式", "inputSchema": "輸入模式",
"availableTools": "可用工具", "availableTools": "可用工具",
"noToolsAvailable": "無可用工具" "noToolsAvailable": "無可用工具",
"loadError": "獲取工具失敗"
}, },
"prompts": { "prompts": {
"availablePrompts": "可用提示", "availablePrompts": "可用提示",
"noPromptsAvailable": "無可用提示", "noPromptsAvailable": "無可用提示",
"arguments": "參數", "arguments": "參數",
"requiredField": "必填欄位", "requiredField": "必填欄位",
"genericError": "獲取提示錯誤" "genericError": "獲取提示錯誤",
"loadError": "獲取提示失敗"
}, },
"deleteServer": "刪除伺服器", "deleteServer": "刪除伺服器",
"deleteServerConfirm": "確定要刪除此伺服器嗎?", "deleteServerConfirm": "確定要刪除此伺服器嗎?",

View File

@ -912,7 +912,6 @@
"noServers": "Δεν έχουν ρυθμιστεί διακομιστές", "noServers": "Δεν έχουν ρυθμιστεί διακομιστές",
"npx_list": { "npx_list": {
"actions": "Ενέργειες", "actions": "Ενέργειες",
"desc": "Αναζητήστε και προσθέστε πακέτα npm ως υπηρεσίες MCP",
"description": "Περιγραφή", "description": "Περιγραφή",
"no_packages": "Δεν βρέθηκαν πακέτα", "no_packages": "Δεν βρέθηκαν πακέτα",
"npm": "NPM", "npm": "NPM",
@ -921,7 +920,6 @@
"scope_required": "Παρακαλώ εισαγάγετε το σκοπό του npm", "scope_required": "Παρακαλώ εισαγάγετε το σκοπό του npm",
"search": "Αναζήτηση", "search": "Αναζήτηση",
"search_error": "Η αναζήτηση απέτυχε", "search_error": "Η αναζήτηση απέτυχε",
"title": "Λίστα πακέτων NPX",
"usage": "Χρήση", "usage": "Χρήση",
"version": "Έκδοση" "version": "Έκδοση"
}, },

View File

@ -912,7 +912,6 @@
"noServers": "No se han configurado servidores", "noServers": "No se han configurado servidores",
"npx_list": { "npx_list": {
"actions": "Acciones", "actions": "Acciones",
"desc": "Buscar y agregar paquetes npm como servicios MCP",
"description": "Descripción", "description": "Descripción",
"no_packages": "No se encontraron paquetes", "no_packages": "No se encontraron paquetes",
"npm": "NPM", "npm": "NPM",
@ -921,7 +920,6 @@
"scope_required": "Por favor ingrese el ámbito npm", "scope_required": "Por favor ingrese el ámbito npm",
"search": "Buscar", "search": "Buscar",
"search_error": "Error de búsqueda", "search_error": "Error de búsqueda",
"title": "Lista de paquetes NPX",
"usage": "Uso", "usage": "Uso",
"version": "Versión" "version": "Versión"
}, },

View File

@ -912,7 +912,6 @@
"noServers": "Aucun serveur configuré", "noServers": "Aucun serveur configuré",
"npx_list": { "npx_list": {
"actions": "Actions", "actions": "Actions",
"desc": "Rechercher et ajouter un package npm en tant que service MCP",
"description": "Description", "description": "Description",
"no_packages": "Aucun package trouvé", "no_packages": "Aucun package trouvé",
"npm": "NPM", "npm": "NPM",
@ -921,7 +920,6 @@
"scope_required": "Veuillez entrer le scope npm", "scope_required": "Veuillez entrer le scope npm",
"search": "Rechercher", "search": "Rechercher",
"search_error": "La recherche a échoué", "search_error": "La recherche a échoué",
"title": "Liste des packages NPX",
"usage": "Utilisation", "usage": "Utilisation",
"version": "Version" "version": "Version"
}, },

View File

@ -912,7 +912,6 @@
"noServers": "Nenhum servidor configurado", "noServers": "Nenhum servidor configurado",
"npx_list": { "npx_list": {
"actions": "Ações", "actions": "Ações",
"desc": "Pesquise e adicione pacotes npm como serviço MCP",
"description": "Descrição", "description": "Descrição",
"no_packages": "Nenhum pacote encontrado", "no_packages": "Nenhum pacote encontrado",
"npm": "NPM", "npm": "NPM",
@ -921,7 +920,6 @@
"scope_required": "Insira o escopo npm", "scope_required": "Insira o escopo npm",
"search": "Pesquisar", "search": "Pesquisar",
"search_error": "Falha na pesquisa", "search_error": "Falha na pesquisa",
"title": "Lista de Pacotes NPX",
"usage": "Uso", "usage": "Uso",
"version": "Versão" "version": "Versão"
}, },

View File

@ -1,9 +1,9 @@
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons' import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
import { Center, VStack } from '@renderer/components/Layout' import { Center, VStack } from '@renderer/components/Layout'
import { EventEmitter } from '@renderer/services/EventService'
import { Alert, Button } from 'antd' import { Alert, Button } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingDescription, SettingRow, SettingSubtitle } from '..' import { SettingDescription, SettingRow, SettingSubtitle } from '..'
@ -21,6 +21,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
const [bunPath, setBunPath] = useState<string | null>(null) const [bunPath, setBunPath] = useState<string | null>(null)
const [binariesDir, setBinariesDir] = useState<string | null>(null) const [binariesDir, setBinariesDir] = useState<string | null>(null)
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate()
const checkBinaries = async () => { const checkBinaries = async () => {
const uvExists = await window.api.isBinaryExist('uv') const uvExists = await window.api.isBinaryExist('uv')
@ -78,7 +79,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />} icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
className="nodrag" className="nodrag"
color={installed ? 'green' : 'danger'} color={installed ? 'green' : 'danger'}
onClick={() => EventEmitter.emit('mcp:mcp-install')} onClick={() => navigate('/settings/mcp/mcp-install')}
/> />
) )
} }

View File

@ -5,6 +5,7 @@ import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
@ -58,6 +59,8 @@ const McpSettings: React.FC<Props> = ({ server }) => {
const [isShowRegistry, setIsShowRegistry] = useState(false) const [isShowRegistry, setIsShowRegistry] = useState(false)
const [registry, setRegistry] = useState<Registry[]>() const [registry, setRegistry] = useState<Registry[]>()
const navigate = useNavigate()
useEffect(() => { useEffect(() => {
const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio') const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio')
setServerType(serverType) setServerType(serverType)
@ -114,10 +117,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
setLoadingServer(server.id) setLoadingServer(server.id)
const localTools = await window.api.mcp.listTools(server) const localTools = await window.api.mcp.listTools(server)
setTools(localTools) setTools(localTools)
// window.message.success(t('settings.mcp.toolsLoaded'))
} catch (error) { } catch (error) {
window.message.error({ window.message.error({
content: t('settings.mcp.toolsLoadError') + formatError(error), content: t('settings.mcp.tools.loadError') + ' ' + formatError(error),
key: 'mcp-tools-error' key: 'mcp-tools-error'
}) })
} finally { } finally {
@ -134,7 +136,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
setPrompts(localPrompts) setPrompts(localPrompts)
} catch (error) { } catch (error) {
window.message.error({ window.message.error({
content: t('settings.mcp.promptsLoadError') + formatError(error), content: t('settings.mcp.prompts.loadError') + ' ' + formatError(error),
key: 'mcp-prompts-error' key: 'mcp-prompts-error'
}) })
setPrompts([]) setPrompts([])
@ -258,6 +260,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
await window.api.mcp.removeServer(server) await window.api.mcp.removeServer(server)
deleteMCPServer(server.id) deleteMCPServer(server.id)
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' }) window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
navigate('/settings/mcp')
} }
}) })
} catch (error: any) { } catch (error: any) {

View File

@ -2,15 +2,16 @@ import { EditOutlined, ExportOutlined, SearchOutlined } from '@ant-design/icons'
import { NavbarRight } from '@renderer/components/app/Navbar' import { NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { isWindows } from '@renderer/config/constant' import { isWindows } from '@renderer/config/constant'
import { EventEmitter } from '@renderer/services/EventService'
import { Button } from 'antd' import { Button } from 'antd'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import EditMcpJsonPopup from './EditMcpJsonPopup' import EditMcpJsonPopup from './EditMcpJsonPopup'
import InstallNpxUv from './InstallNpxUv' import InstallNpxUv from './InstallNpxUv'
export const McpSettingsNavbar = () => { export const McpSettingsNavbar = () => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate()
const onClick = () => window.open('https://mcp.so/', '_blank') const onClick = () => window.open('https://mcp.so/', '_blank')
return ( return (
@ -19,7 +20,7 @@ export const McpSettingsNavbar = () => {
<Button <Button
size="small" size="small"
type="text" type="text"
onClick={() => EventEmitter.emit('mcp:npx-search')} onClick={() => navigate('/settings/mcp/npx-search')}
icon={<SearchOutlined />} icon={<SearchOutlined />}
className="nodrag" className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}> style={{ fontSize: 13, height: 28, borderRadius: 20 }}>

View File

@ -1,7 +1,7 @@
import { PlusOutlined, SearchOutlined } from '@ant-design/icons' import { CheckOutlined, PlusOutlined } from '@ant-design/icons'
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import { HStack } from '@renderer/components/Layout' import npmLogo from '@renderer/assets/images/mcp/npm.svg'
import { useTheme } from '@renderer/context/ThemeProvider' import { Center, HStack } from '@renderer/components/Layout'
import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { builtinMCPServers } from '@renderer/store/mcp' import { builtinMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
@ -11,8 +11,6 @@ import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingDivider, SettingGroup, SettingTitle } from '..'
interface SearchResult { interface SearchResult {
name: string name: string
description: string description: string
@ -29,9 +27,7 @@ let _searchResults: SearchResult[] = []
const NpxSearch: FC<{ const NpxSearch: FC<{
setSelectedMcpServer: (server: MCPServer) => void setSelectedMcpServer: (server: MCPServer) => void
setRoute: (route: string | null) => void }> = ({ setSelectedMcpServer }) => {
}> = ({ setSelectedMcpServer, setRoute }) => {
const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
const { Text, Link } = Typography const { Text, Link } = Typography
@ -39,7 +35,7 @@ const NpxSearch: FC<{
const [npmScope, setNpmScope] = useState('@cherry') 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, mcpServers } = useMCPServers()
_searchResults = searchResults _searchResults = searchResults
@ -119,118 +115,134 @@ const NpxSearch: FC<{
}, []) }, [])
return ( return (
<SettingGroup theme={theme} css={SettingGroup}> <Container>
<div> <Center>
<SettingTitle> <Space direction="vertical" style={{ marginBottom: 20, width: 500 }}>
{t('settings.mcp.npx_list.title')} <Text type="secondary">{t('settings.mcp.npx_list.desc')}</Text> <Center style={{ marginBottom: 20 }}>
</SettingTitle> <img src={npmLogo} alt="npm" width={100} />
<SettingDivider /> </Center>
<Space.Compact style={{ width: '100%' }}>
<Space direction="vertical" style={{ width: '100%' }}>
<Space.Compact style={{ width: '100%', marginBottom: 10 }}>
<Input <Input
placeholder={t('settings.mcp.npx_list.scope_placeholder')} placeholder={t('settings.mcp.npx_list.scope_placeholder')}
value={npmScope} value={npmScope}
onChange={(e) => setNpmScope(e.target.value)} onChange={(e) => setNpmScope(e.target.value)}
onPressEnter={() => handleNpmSearch(npmScope)} onPressEnter={() => handleNpmSearch(npmScope)}
size="large"
styles={{ input: { borderRadius: 100 } }}
/> />
<Button icon={<SearchOutlined />} onClick={() => handleNpmSearch(npmScope)} disabled={searchLoading}>
{t('settings.mcp.npx_list.search')}
</Button>
</Space.Compact> </Space.Compact>
<HStack alignItems="center" mt="-5px" mb="5px"> <HStack alignItems="center" justifyContent="center">
{npmScopes.map((scope) => ( {npmScopes.map((scope) => (
<Tag <Tag
key={scope} key={scope}
bordered={false}
onClick={() => { onClick={() => {
setNpmScope(scope) setNpmScope(scope)
handleNpmSearch(scope) handleNpmSearch(scope)
}} }}
style={{ cursor: searchLoading ? 'not-allowed' : 'pointer' }}> style={{
cursor: searchLoading ? 'not-allowed' : 'pointer',
borderRadius: 100,
backgroundColor: 'var(--color-background-mute)'
}}>
{scope} {scope}
</Tag> </Tag>
))} ))}
</HStack> </HStack>
</Space> </Space>
</div> </Center>
{searchLoading && (
<Center>
<Spin />
</Center>
)}
{!searchLoading && (
<ResultList>
{searchResults?.map((record) => {
const isInstalled = mcpServers.some((server) => server.name === record.name)
return (
<Card
size="small"
key={record.name}
title={
<Typography.Title level={5} style={{ margin: 0 }} className="selectable">
{record.name}
</Typography.Title>
}
extra={
<Flex>
<Tag bordered={false} color="processing">
v{record.version}
</Tag>
<Button
type="text"
icon={
isInstalled ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <PlusOutlined />
}
size="small"
onClick={() => {
if (isInstalled) {
return
}
<ResultList> const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
{searchLoading ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin />
</div>
) : (
searchResults?.map((record) => (
<Card
size="small"
key={record.name}
title={
<Typography.Title level={5} style={{ margin: 0 }} className="selectable">
{record.name}
</Typography.Title>
}
extra={
<Flex>
<Tag bordered={false} color="processing">
v{record.version}
</Tag>
<Button
type="text"
icon={<PlusOutlined />}
size="small"
onClick={() => {
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
if (buildInServer) { if (buildInServer) {
addMCPServer(buildInServer) addMCPServer(buildInServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
setSelectedMcpServer(buildInServer)
return
}
const newServer = {
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,
type: record.type
}
addMCPServer(newServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' }) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
setSelectedMcpServer(buildInServer) setSelectedMcpServer(newServer)
setRoute(null) }}
return />
} </Flex>
const newServer = { }>
id: nanoid(), <Space direction="vertical" size="small">
name: record.name, <Text className="selectable">{record.description}</Text>
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`, <Text type="secondary" className="selectable">
command: 'npx', {t('settings.mcp.npx_list.usage')}: {record.usage}
args: ['-y', record.fullName], </Text>
isActive: false, <Link href={record.npmLink} target="_blank" rel="noopener noreferrer">
type: record.type {record.npmLink}
} </Link>
</Space>
addMCPServer(newServer) </Card>
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' }) )
setSelectedMcpServer(newServer) })}
setRoute(null) </ResultList>
}} )}
/> </Container>
</Flex>
}>
<Space direction="vertical" size="small">
<Text className="selectable">{record.description}</Text>
<Text type="secondary" className="selectable">
{t('settings.mcp.npx_list.usage')}: {record.usage}
</Text>
<Link href={record.npmLink} target="_blank" rel="noopener noreferrer">
{record.npmLink}
</Link>
</Space>
</Card>
))
)}
</ResultList>
</SettingGroup>
) )
} }
const ResultList = styled.div` const Container = styled.div`
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
width: calc(100% + 10px); `
const ResultList = styled.div`
flex: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
width: 100%;
padding-right: 4px; padding-right: 4px;
overflow-y: scroll; overflow-y: auto;
` `
export default NpxSearch export default NpxSearch

View File

@ -1,17 +1,17 @@
import { ArrowLeftOutlined, CodeOutlined, PlusOutlined } from '@ant-design/icons' import { ArrowLeftOutlined, CodeOutlined, PlusOutlined } from '@ant-design/icons'
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import IndicatorLight from '@renderer/components/IndicatorLight' import IndicatorLight from '@renderer/components/IndicatorLight'
import { HStack, VStack } from '@renderer/components/Layout' import { VStack } 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 { EventEmitter } from '@renderer/services/EventService'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { isEmpty } from 'lodash' import { FC, useCallback, useEffect, useState } from 'react'
import { FC, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Route, Routes, useLocation, useNavigate } from 'react-router'
import { Link } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer } from '..' import { SettingContainer, SettingTitle } from '..'
import InstallNpxUv from './InstallNpxUv' import InstallNpxUv from './InstallNpxUv'
import McpSettings from './McpSettings' import McpSettings from './McpSettings'
import NpxSearch from './NpxSearch' import NpxSearch from './NpxSearch'
@ -20,18 +20,13 @@ const MCPSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { mcpServers, addMCPServer } = useMCPServers() const { mcpServers, addMCPServer } = useMCPServers()
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(null) const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(null)
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
const { theme } = useTheme() const { theme } = useTheme()
const navigate = useNavigate()
useEffect(() => { const location = useLocation()
const unsubs = [ const pathname = location.pathname
EventEmitter.on('mcp:npx-search', () => setRoute('npx-search')),
EventEmitter.on('mcp:mcp-install', () => setRoute('mcp-install'))
]
return () => unsubs.forEach((unsub) => unsub())
}, [])
const onAddMcpServer = async () => { const onAddMcpServer = useCallback(async () => {
const newServer = { const newServer = {
id: nanoid(), id: nanoid(),
name: t('settings.mcp.newServer'), name: t('settings.mcp.newServer'),
@ -45,13 +40,12 @@ const MCPSettings: FC = () => {
addMCPServer(newServer) addMCPServer(newServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' }) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
setSelectedMcpServer(newServer) setSelectedMcpServer(newServer)
setRoute(null) }, [addMCPServer, t])
}
useEffect(() => { useEffect(() => {
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id) const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
setSelectedMcpServer(_selectedMcpServer || mcpServers[0]) setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
}, [mcpServers, route, selectedMcpServer]) }, [mcpServers, selectedMcpServer])
useEffect(() => { useEffect(() => {
// Check if the selected server still exists in the updated mcpServers list // Check if the selected server still exists in the updated mcpServers list
@ -65,98 +59,90 @@ const MCPSettings: FC = () => {
} }
}, [mcpServers, selectedMcpServer]) }, [mcpServers, selectedMcpServer])
const MainContent = useMemo(() => { const McpServersList = useCallback(
if (route === 'npx-search' || isEmpty(mcpServers)) { () => (
return ( <GridContainer>
<SettingContainer theme={theme}> <GridHeader>
<NpxSearch <SettingTitle>{t('settings.mcp.newServer')}</SettingTitle>
setRoute={(route) => setRoute(route as 'npx-search' | 'mcp-install' | null)} </GridHeader>
setSelectedMcpServer={setSelectedMcpServer} <ServersGrid>
/> <AddServerCard onClick={onAddMcpServer}>
</SettingContainer> <PlusOutlined style={{ fontSize: 24 }} />
) <AddServerText>{t('settings.mcp.addServer')}</AddServerText>
} </AddServerCard>
{mcpServers.map((server) => (
<ServerCard
key={server.id}
onClick={() => {
setSelectedMcpServer(server)
navigate(`/settings/mcp/server/${server.id}`)
}}>
<ServerHeader>
<ServerIcon>
<CodeOutlined />
</ServerIcon>
<ServerName>{server.name}</ServerName>
<StatusIndicator>
<IndicatorLight
size={6}
color={server.isActive ? 'green' : 'var(--color-text-3)'}
animation={server.isActive}
shadow={false}
/>
</StatusIndicator>
</ServerHeader>
<ServerDescription>
{server.description &&
server.description.substring(0, 60) + (server.description.length > 60 ? '...' : '')}
</ServerDescription>
</ServerCard>
))}
</ServersGrid>
</GridContainer>
),
[mcpServers, navigate, onAddMcpServer, t]
)
if (route === 'mcp-install') { const isHome = pathname === '/settings/mcp'
return (
<SettingContainer theme={theme}>
<InstallNpxUv />
</SettingContainer>
)
}
if (selectedMcpServer) {
return <McpSettings server={selectedMcpServer} />
}
return (
<NpxSearch
setRoute={(route) => setRoute(route as 'npx-search' | 'mcp-install' | null)}
setSelectedMcpServer={setSelectedMcpServer}
/>
)
}, [mcpServers, route, selectedMcpServer, theme])
const goBackToGrid = () => {
setSelectedMcpServer(null)
}
return ( return (
<Container> <Container>
{selectedMcpServer ? ( {!isHome && (
<DetailViewContainer> <BackButtonContainer>
<BackButtonContainer> <Link to="/settings/mcp">
<BackButton onClick={goBackToGrid}> <BackButton>
<ArrowLeftOutlined /> {t('common.back')} <ArrowLeftOutlined /> {t('common.back')}
</BackButton> </BackButton>
</BackButtonContainer> </Link>
<DetailContent>{MainContent}</DetailContent> </BackButtonContainer>
</DetailViewContainer>
) : (
<GridContainer>
<GridHeader>
<h2>{t('settings.mcp.newServer')}</h2>
</GridHeader>
<ServersGrid>
<AddServerCard onClick={onAddMcpServer}>
<PlusOutlined style={{ fontSize: 24 }} />
<AddServerText>{t('settings.mcp.addServer')}</AddServerText>
</AddServerCard>
{mcpServers.map((server) => (
<ServerCard
key={server.id}
onClick={() => {
setSelectedMcpServer(server)
setRoute(null)
}}>
<ServerHeader>
<ServerIcon>
<CodeOutlined />
</ServerIcon>
<ServerName>{server.name}</ServerName>
<StatusIndicator>
<IndicatorLight
size={6}
color={server.isActive ? 'green' : 'var(--color-text-3)'}
animation={server.isActive}
shadow={false}
/>
</StatusIndicator>
</ServerHeader>
<ServerDescription>
{server.description &&
server.description.substring(0, 60) + (server.description.length > 60 ? '...' : '')}
</ServerDescription>
</ServerCard>
))}
</ServersGrid>
</GridContainer>
)} )}
<MainContainer>
<Routes>
<Route path="/" element={<McpServersList />} />
<Route path="server/:id" element={selectedMcpServer ? <McpSettings server={selectedMcpServer} /> : null} />
<Route
path="npx-search"
element={
<SettingContainer theme={theme}>
<NpxSearch setSelectedMcpServer={setSelectedMcpServer} />
</SettingContainer>
}
/>
<Route
path="mcp-install"
element={
<SettingContainer theme={theme}>
<InstallNpxUv />
</SettingContainer>
}
/>
</Routes>
</MainContainer>
</Container> </Container>
) )
} }
const Container = styled(HStack)` const Container = styled(VStack)`
flex: 1; flex: 1;
` `
@ -251,20 +237,13 @@ const AddServerText = styled.div`
font-weight: 500; font-weight: 500;
` `
const DetailViewContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
position: relative;
`
const BackButtonContainer = styled.div` const BackButtonContainer = styled.div`
padding: 16px 0 0 20px; padding: 12px 0 0 12px;
width: 100%; width: 100%;
background-color: var(--color-background);
` `
const DetailContent = styled.div` const MainContainer = styled.div`
flex: 1; flex: 1;
width: 100%; width: 100%;
` `

View File

@ -12,7 +12,6 @@ import {
ThunderboltOutlined ThunderboltOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { isLocalAi } from '@renderer/config/env'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings' import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
// 导入useAppSelector // 导入useAppSelector
@ -46,26 +45,22 @@ const SettingsPage: FC = () => {
<Container> <Container>
<Navbar> <Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
{pathname === '/settings/mcp' && <McpSettingsNavbar />} {pathname.includes('/settings/mcp') && <McpSettingsNavbar />}
</Navbar> </Navbar>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<SettingMenus> <SettingMenus>
{!isLocalAi && ( <MenuItemLink to="/settings/provider">
<> <MenuItem className={isRoute('/settings/provider')}>
<MenuItemLink to="/settings/provider"> <CloudOutlined />
<MenuItem className={isRoute('/settings/provider')}> {t('settings.provider.title')}
<CloudOutlined /> </MenuItem>
{t('settings.provider.title')} </MenuItemLink>
</MenuItem> <MenuItemLink to="/settings/model">
</MenuItemLink> <MenuItem className={isRoute('/settings/model')}>
<MenuItemLink to="/settings/model"> <i className="iconfont icon-ai-model" />
<MenuItem className={isRoute('/settings/model')}> {t('settings.model')}
<i className="iconfont icon-ai-model" /> </MenuItem>
{t('settings.model')} </MenuItemLink>
</MenuItem>
</MenuItemLink>
</>
)}
<MenuItemLink to="/settings/web-search"> <MenuItemLink to="/settings/web-search">
<MenuItem className={isRoute('/settings/web-search')}> <MenuItem className={isRoute('/settings/web-search')}>
<GlobalOutlined /> <GlobalOutlined />
@ -134,13 +129,13 @@ const SettingsPage: FC = () => {
<Route path="provider" element={<ProvidersList />} /> <Route path="provider" element={<ProvidersList />} />
<Route path="model" element={<ModelSettings />} /> <Route path="model" element={<ModelSettings />} />
<Route path="web-search" element={<WebSearchSettings />} /> <Route path="web-search" element={<WebSearchSettings />} />
<Route path="mcp" element={<MCPSettings />} /> <Route path="mcp/*" element={<MCPSettings />} />
<Route path="general/*" element={<GeneralSettings />} /> <Route path="general" element={<GeneralSettings />} />
<Route path="display" element={<DisplaySettings />} /> <Route path="display" element={<DisplaySettings />} />
{showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />} {showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />}
<Route path="shortcut" element={<ShortcutSettings />} /> <Route path="shortcut" element={<ShortcutSettings />} />
<Route path="quickAssistant" element={<QuickAssistantSettings />} /> <Route path="quickAssistant" element={<QuickAssistantSettings />} />
<Route path="data/*" element={<DataSettings />} /> <Route path="data" element={<DataSettings />} />
<Route path="about" element={<AboutSettings />} /> <Route path="about" element={<AboutSettings />} />
<Route path="quickPhrase" element={<QuickPhraseSettings />} /> <Route path="quickPhrase" element={<QuickPhraseSettings />} />
</Routes> </Routes>