diff --git a/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx b/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx index 99c00f7e..a4e7e276 100644 --- a/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx @@ -9,7 +9,7 @@ import { Button, Card, Flex, Input, Space, Spin, Tag, Typography } from 'antd' import { npxFinder } from 'npx-scope-finder' import { type FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import styled, { css } from 'styled-components' +import styled from 'styled-components' import { SettingDivider, SettingGroup, SettingTitle } from '..' @@ -27,7 +27,10 @@ const npmScopes = ['@cherry', '@modelcontextprotocol', '@gongrzhe', '@mcpmarket' let _searchResults: SearchResult[] = [] -const NpxSearch: FC = () => { +const NpxSearch: FC<{ + setSelectedMcpServer: (server: MCPServer) => void + setRoute: (route: string | null) => void +}> = ({ setSelectedMcpServer, setRoute }) => { const { theme } = useTheme() const { t } = useTranslation() const { Text, Link } = Typography @@ -116,7 +119,7 @@ const NpxSearch: FC = () => { }, []) return ( - +
{t('settings.mcp.npx_list.title')} {t('settings.mcp.npx_list.desc')} @@ -181,10 +184,11 @@ const NpxSearch: FC = () => { if (buildInServer) { addMCPServer(buildInServer) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' }) + setSelectedMcpServer(buildInServer) + setRoute(null) return } - - addMCPServer({ + 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}`, @@ -192,8 +196,12 @@ const NpxSearch: FC = () => { 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) }} /> @@ -215,14 +223,6 @@ const NpxSearch: FC = () => { ) } -const SettingGroupCss = css` - height: 100%; - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 0; -` - const ResultList = styled.div` flex: 1; display: flex; diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index e982dc1b..e6b02e85 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -1,17 +1,13 @@ -import { CodeOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons' +import { ArrowLeftOutlined, CodeOutlined, PlusOutlined } from '@ant-design/icons' import { nanoid } from '@reduxjs/toolkit' -import DragableList from '@renderer/components/DragableList' import IndicatorLight from '@renderer/components/IndicatorLight' import { HStack, VStack } from '@renderer/components/Layout' -import ListItem from '@renderer/components/ListItem' -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 { MCPServer } from '@renderer/types' -import { Dropdown, MenuProps } from 'antd' import { isEmpty } from 'lodash' -import { FC, useCallback, useEffect, useMemo, useState } from 'react' +import { FC, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -22,8 +18,8 @@ import NpxSearch from './NpxSearch' const MCPSettings: FC = () => { const { t } = useTranslation() - const { mcpServers, addMCPServer, updateMcpServers, deleteMCPServer } = useMCPServers() - const [selectedMcpServer, setSelectedMcpServer] = useState(mcpServers[0]) + const { mcpServers, addMCPServer } = useMCPServers() + const [selectedMcpServer, setSelectedMcpServer] = useState(null) const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null) const { theme } = useTheme() @@ -49,50 +45,34 @@ const MCPSettings: FC = () => { addMCPServer(newServer) window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' }) setSelectedMcpServer(newServer) + setRoute(null) } - const onDeleteMcpServer = useCallback( - async (server: MCPServer) => { - try { - await window.api.mcp.removeServer(server) - await deleteMCPServer(server.id) - window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' }) - } catch (error: any) { - window.message.error({ - content: `${t('settings.mcp.deleteError')}: ${error.message}`, - key: 'mcp-list' - }) - } - }, - [deleteMCPServer, t] - ) - - const getMenuItems = useCallback( - (server: MCPServer) => { - const menus: MenuProps['items'] = [ - { - label: t('common.delete'), - danger: true, - key: 'delete', - icon: , - onClick: () => onDeleteMcpServer(server) - } - ] - return menus - }, - [onDeleteMcpServer, t] - ) - useEffect(() => { const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id) setSelectedMcpServer(_selectedMcpServer || mcpServers[0]) }, [mcpServers, route, selectedMcpServer]) + useEffect(() => { + // Check if the selected server still exists in the updated mcpServers list + if (selectedMcpServer) { + const serverExists = mcpServers.some((server) => server.id === selectedMcpServer.id) + if (!serverExists) { + setSelectedMcpServer(null) + } + } else { + setSelectedMcpServer(null) + } + }, [mcpServers, selectedMcpServer]) + const MainContent = useMemo(() => { if (route === 'npx-search' || isEmpty(mcpServers)) { return ( - + setRoute(route as 'npx-search' | 'mcp-install' | null)} + setSelectedMcpServer={setSelectedMcpServer} + /> ) } @@ -108,53 +88,70 @@ const MCPSettings: FC = () => { return } - return + return ( + setRoute(route as 'npx-search' | 'mcp-install' | null)} + setSelectedMcpServer={setSelectedMcpServer} + /> + ) }, [mcpServers, route, selectedMcpServer, theme]) + const goBackToGrid = () => { + setSelectedMcpServer(null) + } + return ( - - - } - titleStyle={{ fontWeight: 500 }} - style={{ width: '100%', marginTop: -2 }} - /> - - {(server: MCPServer) => ( - -
- { - setSelectedMcpServer(server) - setRoute(null) - }} - titleStyle={{ fontWeight: 500 }} - icon={} - rightContent={ - - } - /> -
-
- )} -
-
-
- {MainContent} + {selectedMcpServer ? ( + + + + {t('common.back')} + + + {MainContent} + + ) : ( + + +

{t('settings.mcp.newServer')}

+
+ + + + {t('settings.mcp.addServer')} + + + {mcpServers.map((server) => ( + { + setSelectedMcpServer(server) + setRoute(null) + }}> + + + + + {server.name} + + + + + + {server.description && + server.description.substring(0, 60) + (server.description.length > 60 ? '...' : '')} + + + ))} + +
+ )}
) } @@ -163,22 +160,129 @@ const Container = styled(HStack)` flex: 1; ` -const McpListContainer = styled(VStack)` - width: var(--settings-width); - border-right: 0.5px solid var(--color-border); +const GridContainer = styled(VStack)` + width: 100%; height: calc(100vh - var(--navbar-height)); + padding: 20px; ` -const McpList = styled(Scrollbar)` - display: flex; - flex: 1; - flex-direction: column; - gap: 5px; +const GridHeader = styled.div` width: 100%; - padding: 12px; - .iconfont { - color: var(--color-text-2); - line-height: 16px; + padding-bottom: 16px; + + h2 { + font-size: 20px; + margin: 0; + } +` + +const ServersGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; + width: 100%; + overflow-y: auto; + padding: 2px; +` + +const ServerCard = styled.div` + display: flex; + flex-direction: column; + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 16px; + cursor: pointer; + transition: all 0.2s ease; + height: 140px; + background-color: var(--color-bg-1); + + &:hover { + border-color: var(--color-primary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + } +` + +const ServerHeader = styled.div` + display: flex; + align-items: center; + margin-bottom: 12px; +` + +const ServerIcon = styled.div` + font-size: 18px; + color: var(--color-primary); + margin-right: 8px; +` + +const ServerName = styled.div` + font-weight: 500; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +` + +const StatusIndicator = styled.div` + margin-left: 8px; +` + +const ServerDescription = styled.div` + font-size: 12px; + color: var(--color-text-2); + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +` + +const AddServerCard = styled(ServerCard)` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-style: dashed; + background-color: transparent; + color: var(--color-text-2); +` + +const AddServerText = styled.div` + margin-top: 12px; + 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; + width: 100%; +` + +const DetailContent = styled.div` + flex: 1; + width: 100%; +` + +const BackButton = styled.div` + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--color-text-1); + cursor: pointer; + padding: 6px 12px; + border-radius: 4px; + margin-bottom: 10px; + background-color: var(--color-bg-1); + + &:hover { + color: var(--color-primary); + background-color: var(--color-bg-2); } ` diff --git a/src/renderer/src/pages/settings/index.tsx b/src/renderer/src/pages/settings/index.tsx index 55d32988..d8f07b1e 100644 --- a/src/renderer/src/pages/settings/index.tsx +++ b/src/renderer/src/pages/settings/index.tsx @@ -10,6 +10,7 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>` height: calc(100vh - var(--navbar-height)); padding: 20px; padding-top: 15px; + padding-bottom: 75px; overflow-y: scroll; font-family: Ubuntu; background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};