Feat/mcp enhancement (#5386)

This commit is contained in:
LiuVaayne 2025-04-27 12:15:00 +08:00 committed by GitHub
parent f47802d64f
commit 44299a8f5b
11 changed files with 376 additions and 40 deletions

View File

@ -395,7 +395,9 @@ class McpService {
try {
Logger.info('[MCP] Calling:', server.name, name, args)
const client = await this.initClient(server)
const result = await client.callTool({ name, arguments: args })
const result = await client.callTool({ name, arguments: args }, undefined, {
timeout: server.timeout ? server.timeout * 1000 : 60000 // Default timeout of 1 minute
})
return result as MCPCallToolResponse
} catch (error) {
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)

View File

@ -1187,7 +1187,16 @@
"success": "Sync MCP Servers successful",
"unauthorized": "Sync Unauthorized",
"noServersAvailable": "No MCP servers available"
}
},
"timeout": "Timeout",
"timeoutTooltip": "Timeout in seconds for requests to this server, default is 60 seconds",
"provider": "Provider",
"providerUrl": "Provider URL",
"logoUrl": "Logo URL",
"tags": "Tags",
"tagsPlaceholder": "Enter tags",
"providerPlaceholder": "Provider name",
"advancedSettings": "Advanced Settings"
},
"messages.divider": "Show divider between messages",
"messages.grid_columns": "Message grid display columns",

View File

@ -1185,7 +1185,16 @@
"success": "MCPサーバーの同期成功",
"unauthorized": "同期が許可されていません",
"noServersAvailable": "利用可能な MCP サーバーがありません"
}
},
"timeout": "タイムアウト",
"timeoutTooltip": "このサーバーへのリクエストのタイムアウト時間、デフォルトは60秒です",
"provider": "プロバイダー",
"providerUrl": "プロバイダーURL",
"logoUrl": "ロゴURL",
"tags": "タグ",
"tagsPlaceholder": "タグを入力",
"providerPlaceholder": "プロバイダー名",
"advancedSettings": "詳細設定"
},
"messages.divider": "メッセージ間に区切り線を表示",
"messages.grid_columns": "メッセージグリッドの表示列数",
@ -1479,4 +1488,4 @@
"visualization": "可視化"
}
}
}
}

View File

@ -1185,7 +1185,16 @@
"success": "Синхронизация серверов MCP успешна",
"unauthorized": "Синхронизация не разрешена",
"noServersAvailable": "Нет доступных серверов MCP"
}
},
"timeout": "Тайм-аут",
"timeoutTooltip": "Тайм-аут в секундах для запросов к этому серверу, по умолчанию 60 секунд",
"provider": "Провайдер",
"providerUrl": "URL провайдера",
"logoUrl": "URL логотипа",
"tags": "Теги",
"tagsPlaceholder": "Введите теги",
"providerPlaceholder": "Имя провайдера",
"advancedSettings": "Расширенные настройки"
},
"messages.divider": "Показывать разделитель между сообщениями",
"messages.grid_columns": "Количество столбцов сетки сообщений",
@ -1479,4 +1488,4 @@
"visualization": "Визуализация"
}
}
}
}

View File

@ -1187,7 +1187,16 @@
"success": "同步MCP服务器成功",
"unauthorized": "同步未授权",
"noServersAvailable": "无可用的 MCP 服务器"
}
},
"timeout": "超时",
"timeoutTooltip": "对该服务器请求的超时时间默认为60秒",
"provider": "提供者",
"providerUrl": "提供者网址",
"logoUrl": "标志网址",
"tags": "标签",
"tagsPlaceholder": "输入标签",
"providerPlaceholder": "提供者名称",
"advancedSettings": "高级设置"
},
"messages.divider": "消息分割线",
"messages.grid_columns": "消息网格展示列数",

View File

@ -1186,7 +1186,16 @@
"success": "同步MCP伺服器成功",
"unauthorized": "同步未授權",
"noServersAvailable": "無可用的 MCP 伺服器"
}
},
"timeout": "超時",
"timeoutTooltip": "對該伺服器請求的超時時間預設為60秒",
"provider": "提供者",
"providerUrl": "提供者網址",
"logoUrl": "標誌網址",
"tags": "標籤",
"tagsPlaceholder": "輸入標籤",
"providerPlaceholder": "提供者名稱",
"advancedSettings": "高級設定"
},
"messages.divider": "訊息間顯示分隔線",
"messages.grid_columns": "訊息網格展示列數",

View File

@ -5,7 +5,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { MCPServer } from '@renderer/types'
import { Button, Empty, Tag } from 'antd'
import { MonitorCheck, Plus, RefreshCw, Settings2 } from 'lucide-react'
import { MonitorCheck, Plus, RefreshCw, Settings2, SquareArrowOutUpRight } from 'lucide-react'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
@ -61,7 +61,17 @@ const McpServersList: FC = () => {
<ServerCard key={server.id} onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })}>
<ServerHeader>
<ServerName>
{server.logoUrl && <ServerLogo src={server.logoUrl} alt={`${server.name} logo`} />}
<ServerNameText>{server.name}</ServerNameText>
{server.providerUrl && (
<Button
size="small"
type="text"
onClick={() => window.open(server.providerUrl, '_blank')}
icon={<SquareArrowOutUpRight size={14} />}
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}></Button>
)}
<ServerIcon>
<MonitorCheck size={16} color={server.isActive ? 'var(--color-primary)' : 'var(--color-text-3)'} />
</ServerIcon>
@ -76,9 +86,20 @@ const McpServersList: FC = () => {
</ServerHeader>
<ServerDescription>{server.description}</ServerDescription>
<ServerFooter>
<Tag color="default" style={{ borderRadius: 20, margin: 0 }}>
<Tag color="processing" style={{ borderRadius: 20, margin: 0, fontWeight: 500 }}>
{t(`settings.mcp.types.${server.type || 'stdio'}`)}
</Tag>
{server.provider && (
<Tag color="success" style={{ borderRadius: 20, margin: 0, fontWeight: 500 }}>
{server.provider}
</Tag>
)}
{server.tags &&
server.tags.map((tag) => (
<Tag key={tag} color="default" style={{ borderRadius: 20, margin: 0 }}>
{tag}
</Tag>
))}
</ServerFooter>
</ServerCard>
)}
@ -136,6 +157,14 @@ const ServerCard = styled.div`
}
`
const ServerLogo = styled.img`
width: 24px;
height: 24px;
border-radius: 4px;
object-fit: cover;
margin-right: 8px;
`
const ServerHeader = styled.div`
display: flex;
align-items: center;
@ -155,7 +184,7 @@ const ServerName = styled.div`
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 10px;
gap: 4px;
`
const ServerNameText = styled.span`
@ -183,7 +212,8 @@ const ServerDescription = styled.div`
const ServerFooter = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
justify-content: flex-start;
margin-top: 10px;
`

View File

@ -3,8 +3,28 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd'
import { Button, Collapse, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import {
AlignLeft,
Building2,
Clock,
Code,
Database,
FileText,
Globe,
Image,
Link,
ListPlus,
MessageSquare,
Package,
Server,
Settings,
Tag,
Terminal,
Type,
Wrench
} from 'lucide-react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router'
@ -26,6 +46,12 @@ interface MCPFormValues {
env?: string
isActive: boolean
headers?: string
timeout?: number
provider?: string
providerUrl?: string
logoUrl?: string
tags?: string[]
}
interface Registry {
@ -84,6 +110,7 @@ const McpSettings: React.FC = () => {
const navigate = useNavigate()
// Initialize form values whenever the server changes
useEffect(() => {
const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio')
setServerType(serverType)
@ -109,6 +136,7 @@ const McpSettings: React.FC = () => {
}
}
// Initialize basic fields
form.setFieldsValue({
name: server.name,
description: server.description,
@ -117,6 +145,7 @@ const McpSettings: React.FC = () => {
command: server.command || '',
registryUrl: server.registryUrl || '',
isActive: server.isActive,
timeout: server.timeout,
args: server.args ? server.args.join('\n') : '',
env: server.env
? Object.entries(server.env)
@ -129,8 +158,18 @@ const McpSettings: React.FC = () => {
.join('\n')
: ''
})
// Initialize advanced fields separately to ensure they're captured
// even if the Collapse panel is closed
form.setFieldsValue({
provider: server.provider || '',
providerUrl: server.providerUrl || '',
logoUrl: server.logoUrl || '',
tags: server.tags || []
})
}, [server, form])
// Watch for serverType changes
useEffect(() => {
const currentServerType = form.getFieldValue('serverType')
if (currentServerType) {
@ -219,7 +258,13 @@ const McpSettings: React.FC = () => {
description: values.description,
isActive: values.isActive,
registryUrl: values.registryUrl,
searchKey: server.searchKey
searchKey: server.searchKey,
timeout: values.timeout || server.timeout,
// Preserve existing advanced properties if not set in the form
provider: values.provider || server.provider,
providerUrl: values.providerUrl || server.providerUrl,
logoUrl: values.logoUrl || server.logoUrl,
tags: values.tags || server.tags
}
// set stdio or sse server
@ -392,7 +437,12 @@ const McpSettings: React.FC = () => {
const tabs = [
{
key: 'settings',
label: t('settings.mcp.tabs.general'),
label: (
<Flex align="center" gap={8}>
<Settings size={16} />
{t('settings.mcp.tabs.general')}
</Flex>
),
children: (
<Form
form={form}
@ -403,16 +453,36 @@ const McpSettings: React.FC = () => {
width: 'calc(100% + 10px)',
paddingRight: '10px'
}}>
<Form.Item name="name" label={t('settings.mcp.name')} rules={[{ required: true, message: '' }]}>
<Form.Item
name="name"
label={
<FormLabelWithIcon>
<Type size={16} />
{t('settings.mcp.name')}
</FormLabelWithIcon>
}
rules={[{ required: true, message: '' }]}>
<Input placeholder={t('common.name')} disabled={server.type === 'inMemory'} />
</Form.Item>
<Form.Item name="description" label={t('settings.mcp.description')}>
<Form.Item
name="description"
label={
<FormLabelWithIcon>
<AlignLeft size={16} />
{t('settings.mcp.description')}
</FormLabelWithIcon>
}>
<TextArea rows={2} placeholder={t('common.description')} />
</Form.Item>
{server.type !== 'inMemory' && (
<Form.Item
name="serverType"
label={t('settings.mcp.type')}
label={
<FormLabelWithIcon>
<Server size={16} />
{t('settings.mcp.type')}
</FormLabelWithIcon>
}
rules={[{ required: true }]}
initialValue="stdio">
<Radio.Group
@ -429,12 +499,25 @@ const McpSettings: React.FC = () => {
<>
<Form.Item
name="baseUrl"
label={t('settings.mcp.url')}
label={
<FormLabelWithIcon>
<Link size={16} />
{t('settings.mcp.url')}
</FormLabelWithIcon>
}
rules={[{ required: serverType === 'sse', message: '' }]}
tooltip={t('settings.mcp.baseUrlTooltip')}>
<Input placeholder="http://localhost:3000/sse" />
</Form.Item>
<Form.Item name="headers" label={t('settings.mcp.headers')} tooltip={t('settings.mcp.headersTooltip')}>
<Form.Item
name="headers"
label={
<FormLabelWithIcon>
<Code size={16} />
{t('settings.mcp.headers')}
</FormLabelWithIcon>
}
tooltip={t('settings.mcp.headersTooltip')}>
<TextArea
rows={3}
placeholder={`Content-Type=application/json\nAuthorization=Bearer token`}
@ -447,12 +530,25 @@ const McpSettings: React.FC = () => {
<>
<Form.Item
name="baseUrl"
label={t('settings.mcp.url')}
label={
<FormLabelWithIcon>
<Link size={16} />
{t('settings.mcp.url')}
</FormLabelWithIcon>
}
rules={[{ required: serverType === 'streamableHttp', message: '' }]}
tooltip={t('settings.mcp.baseUrlTooltip')}>
<Input placeholder="http://localhost:3000/mcp" />
</Form.Item>
<Form.Item name="headers" label={t('settings.mcp.headers')} tooltip={t('settings.mcp.headersTooltip')}>
<Form.Item
name="headers"
label={
<FormLabelWithIcon>
<Code size={16} />
{t('settings.mcp.headers')}
</FormLabelWithIcon>
}
tooltip={t('settings.mcp.headersTooltip')}>
<TextArea
rows={3}
placeholder={`Content-Type=application/json\nAuthorization=Bearer token`}
@ -465,7 +561,12 @@ const McpSettings: React.FC = () => {
<>
<Form.Item
name="command"
label={t('settings.mcp.command')}
label={
<FormLabelWithIcon>
<Terminal size={16} />
{t('settings.mcp.command')}
</FormLabelWithIcon>
}
rules={[{ required: serverType === 'stdio', message: '' }]}>
<Input placeholder="uvx or npx" onChange={(e) => handleCommandChange(e.target.value)} />
</Form.Item>
@ -473,7 +574,12 @@ const McpSettings: React.FC = () => {
{isShowRegistry && registry && (
<Form.Item
name="registryUrl"
label={t('settings.mcp.registry')}
label={
<FormLabelWithIcon>
<Package size={16} />
{t('settings.mcp.registry')}
</FormLabelWithIcon>
}
tooltip={t('settings.mcp.registryTooltip')}>
<Radio.Group>
<Radio
@ -498,26 +604,136 @@ const McpSettings: React.FC = () => {
</Form.Item>
)}
<Form.Item name="args" label={t('settings.mcp.args')} tooltip={t('settings.mcp.argsTooltip')}>
<Form.Item
name="args"
label={
<FormLabelWithIcon>
<ListPlus size={16} />
{t('settings.mcp.args')}
</FormLabelWithIcon>
}
tooltip={t('settings.mcp.argsTooltip')}>
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
<Form.Item
name="env"
label={
<FormLabelWithIcon>
<Settings size={16} />
{t('settings.mcp.env')}
</FormLabelWithIcon>
}
tooltip={t('settings.mcp.envTooltip')}>
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
</>
)}
{serverType === 'inMemory' && (
<>
<Form.Item name="args" label={t('settings.mcp.args')} tooltip={t('settings.mcp.argsTooltip')}>
<Form.Item
name="args"
label={
<FormLabelWithIcon>
<ListPlus size={16} />
{t('settings.mcp.args')}
</FormLabelWithIcon>
}
tooltip={t('settings.mcp.argsTooltip')}>
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
<Form.Item
name="env"
label={
<FormLabelWithIcon>
<Settings size={16} />
{t('settings.mcp.env')}
</FormLabelWithIcon>
}
tooltip={t('settings.mcp.envTooltip')}>
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
</>
)}
<Form.Item
name="timeout"
label={
<FormLabelWithIcon>
<Clock size={16} />
{t('settings.mcp.timeout', 'Timeout')}
</FormLabelWithIcon>
}
tooltip={t(
'settings.mcp.timeoutTooltip',
'Timeout in seconds for requests to this server, default is 60 seconds'
)}>
<Input type="number" min={1} placeholder="60" addonAfter="s" />
</Form.Item>
<Collapse
ghost
style={{ marginBottom: 16 }}
defaultActiveKey={[]}
items={[
{
key: 'advanced',
label: t('settings.mcp.advancedSettings', 'Advanced Settings'),
children: (
<>
<Form.Item
name="provider"
label={
<FormLabelWithIcon>
<Building2 size={16} />
{t('settings.mcp.provider', 'Provider')}
</FormLabelWithIcon>
}>
<Input placeholder={t('settings.mcp.providerPlaceholder', 'Provider name')} />
</Form.Item>
<Form.Item
name="providerUrl"
label={
<FormLabelWithIcon>
<Globe size={16} />
{t('settings.mcp.providerUrl', 'Provider URL')}
</FormLabelWithIcon>
}>
<Input placeholder={t('settings.mcp.providerUrlPlaceholder', 'https://provider-website.com')} />
</Form.Item>
<Form.Item
name="logoUrl"
label={
<FormLabelWithIcon>
<Image size={16} />
{t('settings.mcp.logoUrl', 'Logo URL')}
</FormLabelWithIcon>
}>
<Input placeholder={t('settings.mcp.logoUrlPlaceholder', 'https://example.com/logo.png')} />
</Form.Item>
<Form.Item
name="tags"
label={
<FormLabelWithIcon>
<Tag size={16} />
{t('settings.mcp.tags', 'Tags')}
</FormLabelWithIcon>
}>
<Select
mode="tags"
style={{ width: '100%' }}
placeholder={t('settings.mcp.tagsPlaceholder', 'Enter tags')}
tokenSeparators={[',']}
/>
</Form.Item>
</>
)
}
]}
/>
</Form>
)
}
@ -525,7 +741,12 @@ const McpSettings: React.FC = () => {
if (server.searchKey) {
tabs.push({
key: 'description',
label: t('settings.mcp.tabs.description'),
label: (
<Flex align="center" gap={8}>
<FileText size={16} />
{t('settings.mcp.tabs.description')}
</Flex>
),
children: <MCPDescription searchKey={server.searchKey} />
})
}
@ -534,17 +755,32 @@ const McpSettings: React.FC = () => {
tabs.push(
{
key: 'tools',
label: t('settings.mcp.tabs.tools'),
label: (
<Flex align="center" gap={8}>
<Wrench size={16} />
{t('settings.mcp.tabs.tools')}
</Flex>
),
children: <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />
},
{
key: 'prompts',
label: t('settings.mcp.tabs.prompts'),
label: (
<Flex align="center" gap={8}>
<MessageSquare size={16} />
{t('settings.mcp.tabs.prompts')}
</Flex>
),
children: <MCPPromptsSection prompts={prompts} />
},
{
key: 'resources',
label: t('settings.mcp.tabs.resources'),
label: (
<Flex align="center" gap={8}>
<Database size={16} />
{t('settings.mcp.tabs.resources')}
</Flex>
),
children: <MCPResourcesSection resources={resources} />
}
)
@ -593,4 +829,9 @@ const ServerName = styled.span`
font-weight: 500;
`
const FormLabelWithIcon = styled(Flex)`
align-items: center;
gap: 8px;
`
export default McpSettings

View File

@ -27,6 +27,8 @@ interface ModelScopeServer {
chinese_name?: string
description?: string
operational_urls?: { url: string }[]
tags?: string[]
logo_url?: string
}
interface ModelScopeSyncResult {
@ -103,7 +105,11 @@ export const syncModelScopeServers = async (
command: '',
args: [],
env: {},
isActive: true
isActive: true,
provider: 'ModelScope',
providerUrl: `https://www.modelscope.cn/mcp/servers/@${server.id}`,
logoUrl: server.logo_url || '',
tags: server.tags || []
}
addedServers.push(mcpServer)

View File

@ -59,7 +59,8 @@ export const builtinMCPServers: MCPServer[] = [
type: 'stdio',
command: 'npx',
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'],
isActive: false
isActive: false,
provider: 'CherryAI'
},
{
id: nanoid(),
@ -70,14 +71,16 @@ export const builtinMCPServers: MCPServer[] = [
isActive: true,
env: {
MEMORY_FILE_PATH: 'YOUR_MEMORY_FILE_PATH'
}
},
provider: 'CherryAI'
},
{
id: nanoid(),
name: '@cherry/sequentialthinking',
type: 'inMemory',
description: '一个 MCP 服务器实现,提供了通过结构化思维过程进行动态和反思性问题解决的工具',
isActive: true
isActive: true,
provider: 'CherryAI'
},
{
id: nanoid(),
@ -88,21 +91,24 @@ export const builtinMCPServers: MCPServer[] = [
isActive: false,
env: {
BRAVE_API_KEY: 'YOUR_API_KEY'
}
},
provider: 'CherryAI'
},
{
id: nanoid(),
name: '@cherry/fetch',
type: 'inMemory',
description: '用于获取 URL 网页内容的 MCP 服务器',
isActive: true
isActive: true,
provider: 'CherryAI'
},
{
id: nanoid(),
name: '@cherry/filesystem',
type: 'inMemory',
description: '实现文件系统操作的模型上下文协议MCP的 Node.js 服务器',
isActive: false
isActive: false,
provider: 'CherryAI'
},
{
id: nanoid(),
@ -112,7 +118,8 @@ export const builtinMCPServers: MCPServer[] = [
isActive: false,
env: {
DIFY_KEY: 'YOUR_DIFY_KEY'
}
},
provider: 'CherryAI'
}
]

View File

@ -404,6 +404,11 @@ export interface MCPServer {
configSample?: MCPConfigSample
headers?: Record<string, string> // Custom headers to be sent with requests to this server
searchKey?: string
provider?: string // Provider name for this server like ModelScope, Higress, etc.
providerUrl?: string // URL of the MCP server in provider's website or documentation
logoUrl?: string // URL of the MCP server's logo
tags?: string[] // List of tags associated with this server
timeout?: number // Timeout in seconds for requests to this server, default is 60 seconds
}
export interface MCPToolInputSchema {