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:
parent
b62c59eb52
commit
72e18fbcc1
1
src/renderer/src/assets/images/mcp/npm.svg
Normal file
1
src/renderer/src/assets/images/mcp/npm.svg
Normal 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 |
@ -26,3 +26,10 @@ export const useMCPServers = () => {
|
||||
updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers))
|
||||
}
|
||||
}
|
||||
|
||||
export const useMCPServer = (id: string) => {
|
||||
const { mcpServers } = useMCPServers()
|
||||
return {
|
||||
server: mcpServers.find((server) => server.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1064,7 +1064,6 @@
|
||||
"newServer": "MCP Server",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
"desc": "Search and add npm packages as MCP servers",
|
||||
"description": "Description",
|
||||
"no_packages": "No packages found",
|
||||
"npm": "NPM",
|
||||
@ -1073,7 +1072,6 @@
|
||||
"scope_required": "Please enter npm scope",
|
||||
"search": "Search",
|
||||
"search_error": "Search error",
|
||||
"title": "NPX Package List",
|
||||
"usage": "Usage",
|
||||
"version": "Version"
|
||||
},
|
||||
@ -1099,14 +1097,16 @@
|
||||
"tools": {
|
||||
"inputSchema": "Input Schema",
|
||||
"availableTools": "Available Tools",
|
||||
"noToolsAvailable": "No tools available"
|
||||
"noToolsAvailable": "No tools available",
|
||||
"loadError": "Get tools Error"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "Available Prompts",
|
||||
"noPromptsAvailable": "No prompts available",
|
||||
"arguments": "Arguments",
|
||||
"requiredField": "Required Field",
|
||||
"genericError": "Get prompt Error"
|
||||
"genericError": "Get prompt Error",
|
||||
"loadError": "Get prompts Error"
|
||||
},
|
||||
"deleteServer": "Delete Server",
|
||||
"deleteServerConfirm": "Are you sure you want to delete this server?",
|
||||
@ -1393,4 +1393,4 @@
|
||||
"visualization": "Visualization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1063,7 +1063,6 @@
|
||||
"newServer": "MCP サーバー",
|
||||
"npx_list": {
|
||||
"actions": "アクション",
|
||||
"desc": "npm パッケージを検索して MCP サーバーとして追加",
|
||||
"description": "説明",
|
||||
"no_packages": "パッケージが見つかりません",
|
||||
"npm": "NPM",
|
||||
@ -1072,7 +1071,6 @@
|
||||
"scope_required": "npm スコープを入力してください",
|
||||
"search": "検索",
|
||||
"search_error": "パッケージの検索に失敗しました",
|
||||
"title": "NPX パッケージリスト",
|
||||
"usage": "使用法",
|
||||
"version": "バージョン"
|
||||
},
|
||||
@ -1098,14 +1096,16 @@
|
||||
"tools": {
|
||||
"inputSchema": "入力スキーマ",
|
||||
"availableTools": "利用可能なツール",
|
||||
"noToolsAvailable": "利用可能なツールなし"
|
||||
"noToolsAvailable": "利用可能なツールなし",
|
||||
"loadError": "ツール取得エラー"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "利用可能なプロンプト",
|
||||
"noPromptsAvailable": "利用可能なプロンプトはありません",
|
||||
"arguments": "引数",
|
||||
"requiredField": "必須フィールド",
|
||||
"genericError": "プロンプト取得エラー"
|
||||
"genericError": "プロンプト取得エラー",
|
||||
"loadError": "プロンプト取得エラー"
|
||||
},
|
||||
"deleteServer": "サーバーを削除",
|
||||
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
|
||||
|
||||
@ -1063,7 +1063,6 @@
|
||||
"newServer": "MCP сервер",
|
||||
"npx_list": {
|
||||
"actions": "Действия",
|
||||
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
|
||||
"description": "Описание",
|
||||
"no_packages": "Ничего не найдено",
|
||||
"npm": "NPM",
|
||||
@ -1072,7 +1071,6 @@
|
||||
"scope_required": "Пожалуйста, введите область npm",
|
||||
"search": "Поиск",
|
||||
"search_error": "Ошибка поиска",
|
||||
"title": "Список пакетов NPX",
|
||||
"usage": "Использование",
|
||||
"version": "Версия"
|
||||
},
|
||||
@ -1098,14 +1096,16 @@
|
||||
"tools": {
|
||||
"inputSchema": "Схема ввода",
|
||||
"availableTools": "Доступные инструменты",
|
||||
"noToolsAvailable": "Нет доступных инструментов"
|
||||
"noToolsAvailable": "Нет доступных инструментов",
|
||||
"loadError": "Ошибка получения инструментов"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "Доступные подсказки",
|
||||
"noPromptsAvailable": "Нет доступных подсказок",
|
||||
"arguments": "Аргументы",
|
||||
"requiredField": "Обязательное поле",
|
||||
"genericError": "Ошибка получения подсказки"
|
||||
"genericError": "Ошибка получения подсказки",
|
||||
"loadError": "Ошибка получения подсказок"
|
||||
},
|
||||
"deleteServer": "Удалить сервер",
|
||||
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
|
||||
|
||||
@ -1064,7 +1064,6 @@
|
||||
"newServer": "MCP 服务器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索并添加 npm 包作为 MCP 服务",
|
||||
"description": "描述",
|
||||
"no_packages": "未找到包",
|
||||
"npm": "NPM",
|
||||
@ -1073,7 +1072,6 @@
|
||||
"scope_required": "请输入 npm 作用域",
|
||||
"search": "搜索",
|
||||
"search_error": "搜索失败",
|
||||
"title": "NPX 包列表",
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
@ -1099,14 +1097,16 @@
|
||||
"tools": {
|
||||
"inputSchema": "输入模式",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "无可用工具"
|
||||
"noToolsAvailable": "无可用工具",
|
||||
"loadError": "获取工具失败"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "可用提示",
|
||||
"noPromptsAvailable": "无可用提示",
|
||||
"arguments": "参数",
|
||||
"requiredField": "必填字段",
|
||||
"genericError": "获取提示错误"
|
||||
"genericError": "获取提示错误",
|
||||
"loadError": "获取提示失败"
|
||||
},
|
||||
"deleteServer": "删除服务器",
|
||||
"deleteServerConfirm": "确定要删除此服务器吗?",
|
||||
|
||||
@ -1063,7 +1063,6 @@
|
||||
"newServer": "MCP 伺服器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索並添加 npm 包作為 MCP 服務",
|
||||
"description": "描述",
|
||||
"no_packages": "未找到包",
|
||||
"npm": "NPM",
|
||||
@ -1072,7 +1071,6 @@
|
||||
"scope_required": "請輸入 npm 作用域",
|
||||
"search": "搜索",
|
||||
"search_error": "搜索失敗",
|
||||
"title": "NPX 包列表",
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
@ -1098,14 +1096,16 @@
|
||||
"tools": {
|
||||
"inputSchema": "輸入模式",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "無可用工具"
|
||||
"noToolsAvailable": "無可用工具",
|
||||
"loadError": "獲取工具失敗"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "可用提示",
|
||||
"noPromptsAvailable": "無可用提示",
|
||||
"arguments": "參數",
|
||||
"requiredField": "必填欄位",
|
||||
"genericError": "獲取提示錯誤"
|
||||
"genericError": "獲取提示錯誤",
|
||||
"loadError": "獲取提示失敗"
|
||||
},
|
||||
"deleteServer": "刪除伺服器",
|
||||
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
|
||||
|
||||
@ -912,7 +912,6 @@
|
||||
"noServers": "Δεν έχουν ρυθμιστεί διακομιστές",
|
||||
"npx_list": {
|
||||
"actions": "Ενέργειες",
|
||||
"desc": "Αναζητήστε και προσθέστε πακέτα npm ως υπηρεσίες MCP",
|
||||
"description": "Περιγραφή",
|
||||
"no_packages": "Δεν βρέθηκαν πακέτα",
|
||||
"npm": "NPM",
|
||||
@ -921,7 +920,6 @@
|
||||
"scope_required": "Παρακαλώ εισαγάγετε το σκοπό του npm",
|
||||
"search": "Αναζήτηση",
|
||||
"search_error": "Η αναζήτηση απέτυχε",
|
||||
"title": "Λίστα πακέτων NPX",
|
||||
"usage": "Χρήση",
|
||||
"version": "Έκδοση"
|
||||
},
|
||||
|
||||
@ -912,7 +912,6 @@
|
||||
"noServers": "No se han configurado servidores",
|
||||
"npx_list": {
|
||||
"actions": "Acciones",
|
||||
"desc": "Buscar y agregar paquetes npm como servicios MCP",
|
||||
"description": "Descripción",
|
||||
"no_packages": "No se encontraron paquetes",
|
||||
"npm": "NPM",
|
||||
@ -921,7 +920,6 @@
|
||||
"scope_required": "Por favor ingrese el ámbito npm",
|
||||
"search": "Buscar",
|
||||
"search_error": "Error de búsqueda",
|
||||
"title": "Lista de paquetes NPX",
|
||||
"usage": "Uso",
|
||||
"version": "Versión"
|
||||
},
|
||||
|
||||
@ -912,7 +912,6 @@
|
||||
"noServers": "Aucun serveur configuré",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
"desc": "Rechercher et ajouter un package npm en tant que service MCP",
|
||||
"description": "Description",
|
||||
"no_packages": "Aucun package trouvé",
|
||||
"npm": "NPM",
|
||||
@ -921,7 +920,6 @@
|
||||
"scope_required": "Veuillez entrer le scope npm",
|
||||
"search": "Rechercher",
|
||||
"search_error": "La recherche a échoué",
|
||||
"title": "Liste des packages NPX",
|
||||
"usage": "Utilisation",
|
||||
"version": "Version"
|
||||
},
|
||||
|
||||
@ -912,7 +912,6 @@
|
||||
"noServers": "Nenhum servidor configurado",
|
||||
"npx_list": {
|
||||
"actions": "Ações",
|
||||
"desc": "Pesquise e adicione pacotes npm como serviço MCP",
|
||||
"description": "Descrição",
|
||||
"no_packages": "Nenhum pacote encontrado",
|
||||
"npm": "NPM",
|
||||
@ -921,7 +920,6 @@
|
||||
"scope_required": "Insira o escopo npm",
|
||||
"search": "Pesquisar",
|
||||
"search_error": "Falha na pesquisa",
|
||||
"title": "Lista de Pacotes NPX",
|
||||
"usage": "Uso",
|
||||
"version": "Versão"
|
||||
},
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { Center, VStack } from '@renderer/components/Layout'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { Alert, Button } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
|
||||
@ -21,6 +21,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
const [bunPath, setBunPath] = useState<string | null>(null)
|
||||
const [binariesDir, setBinariesDir] = useState<string | null>(null)
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const checkBinaries = async () => {
|
||||
const uvExists = await window.api.isBinaryExist('uv')
|
||||
@ -78,7 +79,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
|
||||
className="nodrag"
|
||||
color={installed ? 'green' : 'danger'}
|
||||
onClick={() => EventEmitter.emit('mcp:mcp-install')}
|
||||
onClick={() => navigate('/settings/mcp/mcp-install')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
@ -58,6 +59,8 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
||||
const [registry, setRegistry] = useState<Registry[]>()
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio')
|
||||
setServerType(serverType)
|
||||
@ -114,10 +117,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
setLoadingServer(server.id)
|
||||
const localTools = await window.api.mcp.listTools(server)
|
||||
setTools(localTools)
|
||||
// window.message.success(t('settings.mcp.toolsLoaded'))
|
||||
} catch (error) {
|
||||
window.message.error({
|
||||
content: t('settings.mcp.toolsLoadError') + formatError(error),
|
||||
content: t('settings.mcp.tools.loadError') + ' ' + formatError(error),
|
||||
key: 'mcp-tools-error'
|
||||
})
|
||||
} finally {
|
||||
@ -134,7 +136,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
setPrompts(localPrompts)
|
||||
} catch (error) {
|
||||
window.message.error({
|
||||
content: t('settings.mcp.promptsLoadError') + formatError(error),
|
||||
content: t('settings.mcp.prompts.loadError') + ' ' + formatError(error),
|
||||
key: 'mcp-prompts-error'
|
||||
})
|
||||
setPrompts([])
|
||||
@ -258,6 +260,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
await window.api.mcp.removeServer(server)
|
||||
deleteMCPServer(server.id)
|
||||
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
|
||||
navigate('/settings/mcp')
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
|
||||
@ -2,15 +2,16 @@ import { EditOutlined, ExportOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { isWindows } from '@renderer/config/constant'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
import EditMcpJsonPopup from './EditMcpJsonPopup'
|
||||
import InstallNpxUv from './InstallNpxUv'
|
||||
|
||||
export const McpSettingsNavbar = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const onClick = () => window.open('https://mcp.so/', '_blank')
|
||||
|
||||
return (
|
||||
@ -19,7 +20,7 @@ export const McpSettingsNavbar = () => {
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={() => EventEmitter.emit('mcp:npx-search')}
|
||||
onClick={() => navigate('/settings/mcp/npx-search')}
|
||||
icon={<SearchOutlined />}
|
||||
className="nodrag"
|
||||
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import npmLogo from '@renderer/assets/images/mcp/npm.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'
|
||||
@ -11,8 +11,6 @@ import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
|
||||
interface SearchResult {
|
||||
name: string
|
||||
description: string
|
||||
@ -29,9 +27,7 @@ let _searchResults: SearchResult[] = []
|
||||
|
||||
const NpxSearch: FC<{
|
||||
setSelectedMcpServer: (server: MCPServer) => void
|
||||
setRoute: (route: string | null) => void
|
||||
}> = ({ setSelectedMcpServer, setRoute }) => {
|
||||
const { theme } = useTheme()
|
||||
}> = ({ setSelectedMcpServer }) => {
|
||||
const { t } = useTranslation()
|
||||
const { Text, Link } = Typography
|
||||
|
||||
@ -39,7 +35,7 @@ const NpxSearch: FC<{
|
||||
const [npmScope, setNpmScope] = useState('@cherry')
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
|
||||
const { addMCPServer } = useMCPServers()
|
||||
const { addMCPServer, mcpServers } = useMCPServers()
|
||||
|
||||
_searchResults = searchResults
|
||||
|
||||
@ -119,118 +115,134 @@ const NpxSearch: FC<{
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme} css={SettingGroup}>
|
||||
<div>
|
||||
<SettingTitle>
|
||||
{t('settings.mcp.npx_list.title')} <Text type="secondary">{t('settings.mcp.npx_list.desc')}</Text>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space.Compact style={{ width: '100%', marginBottom: 10 }}>
|
||||
<Container>
|
||||
<Center>
|
||||
<Space direction="vertical" style={{ marginBottom: 20, width: 500 }}>
|
||||
<Center style={{ marginBottom: 20 }}>
|
||||
<img src={npmLogo} alt="npm" width={100} />
|
||||
</Center>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder={t('settings.mcp.npx_list.scope_placeholder')}
|
||||
value={npmScope}
|
||||
onChange={(e) => setNpmScope(e.target.value)}
|
||||
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>
|
||||
<HStack alignItems="center" mt="-5px" mb="5px">
|
||||
<HStack alignItems="center" justifyContent="center">
|
||||
{npmScopes.map((scope) => (
|
||||
<Tag
|
||||
key={scope}
|
||||
bordered={false}
|
||||
onClick={() => {
|
||||
setNpmScope(scope)
|
||||
handleNpmSearch(scope)
|
||||
}}
|
||||
style={{ cursor: searchLoading ? 'not-allowed' : 'pointer' }}>
|
||||
style={{
|
||||
cursor: searchLoading ? 'not-allowed' : 'pointer',
|
||||
borderRadius: 100,
|
||||
backgroundColor: 'var(--color-background-mute)'
|
||||
}}>
|
||||
{scope}
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
</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>
|
||||
{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)
|
||||
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
|
||||
|
||||
if (buildInServer) {
|
||||
addMCPServer(buildInServer)
|
||||
if (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' })
|
||||
setSelectedMcpServer(buildInServer)
|
||||
setRoute(null)
|
||||
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' })
|
||||
setSelectedMcpServer(newServer)
|
||||
setRoute(null)
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
setSelectedMcpServer(newServer)
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const ResultList = styled.div`
|
||||
flex: 1;
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
export default NpxSearch
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { ArrowLeftOutlined, CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
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 { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
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 { SettingContainer } from '..'
|
||||
import { SettingContainer, SettingTitle } from '..'
|
||||
import InstallNpxUv from './InstallNpxUv'
|
||||
import McpSettings from './McpSettings'
|
||||
import NpxSearch from './NpxSearch'
|
||||
@ -20,18 +20,13 @@ const MCPSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { mcpServers, addMCPServer } = useMCPServers()
|
||||
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(null)
|
||||
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
|
||||
const { theme } = useTheme()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const unsubs = [
|
||||
EventEmitter.on('mcp:npx-search', () => setRoute('npx-search')),
|
||||
EventEmitter.on('mcp:mcp-install', () => setRoute('mcp-install'))
|
||||
]
|
||||
return () => unsubs.forEach((unsub) => unsub())
|
||||
}, [])
|
||||
const location = useLocation()
|
||||
const pathname = location.pathname
|
||||
|
||||
const onAddMcpServer = async () => {
|
||||
const onAddMcpServer = useCallback(async () => {
|
||||
const newServer = {
|
||||
id: nanoid(),
|
||||
name: t('settings.mcp.newServer'),
|
||||
@ -45,13 +40,12 @@ const MCPSettings: FC = () => {
|
||||
addMCPServer(newServer)
|
||||
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
|
||||
setSelectedMcpServer(newServer)
|
||||
setRoute(null)
|
||||
}
|
||||
}, [addMCPServer, t])
|
||||
|
||||
useEffect(() => {
|
||||
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
|
||||
setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
|
||||
}, [mcpServers, route, selectedMcpServer])
|
||||
}, [mcpServers, selectedMcpServer])
|
||||
|
||||
useEffect(() => {
|
||||
// Check if the selected server still exists in the updated mcpServers list
|
||||
@ -65,98 +59,90 @@ const MCPSettings: FC = () => {
|
||||
}
|
||||
}, [mcpServers, selectedMcpServer])
|
||||
|
||||
const MainContent = useMemo(() => {
|
||||
if (route === 'npx-search' || isEmpty(mcpServers)) {
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<NpxSearch
|
||||
setRoute={(route) => setRoute(route as 'npx-search' | 'mcp-install' | null)}
|
||||
setSelectedMcpServer={setSelectedMcpServer}
|
||||
/>
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
const McpServersList = useCallback(
|
||||
() => (
|
||||
<GridContainer>
|
||||
<GridHeader>
|
||||
<SettingTitle>{t('settings.mcp.newServer')}</SettingTitle>
|
||||
</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)
|
||||
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') {
|
||||
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)
|
||||
}
|
||||
const isHome = pathname === '/settings/mcp'
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{selectedMcpServer ? (
|
||||
<DetailViewContainer>
|
||||
<BackButtonContainer>
|
||||
<BackButton onClick={goBackToGrid}>
|
||||
{!isHome && (
|
||||
<BackButtonContainer>
|
||||
<Link to="/settings/mcp">
|
||||
<BackButton>
|
||||
<ArrowLeftOutlined /> {t('common.back')}
|
||||
</BackButton>
|
||||
</BackButtonContainer>
|
||||
<DetailContent>{MainContent}</DetailContent>
|
||||
</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>
|
||||
</Link>
|
||||
</BackButtonContainer>
|
||||
)}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(HStack)`
|
||||
const Container = styled(VStack)`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
@ -251,20 +237,13 @@ const AddServerText = styled.div`
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const DetailViewContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const BackButtonContainer = styled.div`
|
||||
padding: 16px 0 0 20px;
|
||||
padding: 12px 0 0 12px;
|
||||
width: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const DetailContent = styled.div`
|
||||
const MainContainer = styled.div`
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
ThunderboltOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||
import ModelSettings from '@renderer/pages/settings/ModelSettings/ModelSettings'
|
||||
// 导入useAppSelector
|
||||
@ -46,26 +45,22 @@ const SettingsPage: FC = () => {
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
|
||||
{pathname === '/settings/mcp' && <McpSettingsNavbar />}
|
||||
{pathname.includes('/settings/mcp') && <McpSettingsNavbar />}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<SettingMenus>
|
||||
{!isLocalAi && (
|
||||
<>
|
||||
<MenuItemLink to="/settings/provider">
|
||||
<MenuItem className={isRoute('/settings/provider')}>
|
||||
<CloudOutlined />
|
||||
{t('settings.provider.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/model">
|
||||
<MenuItem className={isRoute('/settings/model')}>
|
||||
<i className="iconfont icon-ai-model" />
|
||||
{t('settings.model')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
</>
|
||||
)}
|
||||
<MenuItemLink to="/settings/provider">
|
||||
<MenuItem className={isRoute('/settings/provider')}>
|
||||
<CloudOutlined />
|
||||
{t('settings.provider.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/model">
|
||||
<MenuItem className={isRoute('/settings/model')}>
|
||||
<i className="iconfont icon-ai-model" />
|
||||
{t('settings.model')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/web-search">
|
||||
<MenuItem className={isRoute('/settings/web-search')}>
|
||||
<GlobalOutlined />
|
||||
@ -134,13 +129,13 @@ const SettingsPage: FC = () => {
|
||||
<Route path="provider" element={<ProvidersList />} />
|
||||
<Route path="model" element={<ModelSettings />} />
|
||||
<Route path="web-search" element={<WebSearchSettings />} />
|
||||
<Route path="mcp" element={<MCPSettings />} />
|
||||
<Route path="general/*" element={<GeneralSettings />} />
|
||||
<Route path="mcp/*" element={<MCPSettings />} />
|
||||
<Route path="general" element={<GeneralSettings />} />
|
||||
<Route path="display" element={<DisplaySettings />} />
|
||||
{showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />}
|
||||
<Route path="shortcut" element={<ShortcutSettings />} />
|
||||
<Route path="quickAssistant" element={<QuickAssistantSettings />} />
|
||||
<Route path="data/*" element={<DataSettings />} />
|
||||
<Route path="data" element={<DataSettings />} />
|
||||
<Route path="about" element={<AboutSettings />} />
|
||||
<Route path="quickPhrase" element={<QuickPhraseSettings />} />
|
||||
</Routes>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user