feat: mcp tools (#4069)
* feat(McpSettings): add MCP tools section and fetch tools on server activation * refactor(McpService): improve client management and connection handling * feat(McpService): add server management functions for restart and stop * feat(McpTool): add tools section with input schema and availability messages * feat(McpService): add unique IDs to tools and update function name mapping * feat(McpService): implement caching for tool listings and enhance tool structure * feat(McpToolsButton): streamline active server handling and update dropdown rendering * fix(mcp-tools): update tool lookup to use unique IDs and add warning for missing tools
This commit is contained in:
parent
d3584d2d39
commit
3f40cc28ac
@ -264,6 +264,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// Register MCP handlers
|
||||
ipcMain.handle('mcp:remove-server', mcpService.removeServer)
|
||||
ipcMain.handle('mcp:restart-server', mcpService.restartServer)
|
||||
ipcMain.handle('mcp:stop-server', mcpService.stopServer)
|
||||
ipcMain.handle('mcp:list-tools', mcpService.listTools)
|
||||
ipcMain.handle('mcp:call-tool', mcpService.callTool)
|
||||
ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo)
|
||||
|
||||
@ -5,12 +5,14 @@ import { getBinaryName, getBinaryPath } from '@main/utils/process'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { MCPServer } from '@types'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { MCPServer, MCPTool } from '@types'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
|
||||
class McpService {
|
||||
private client: Client | null = null
|
||||
private clients: Map<string, Client> = new Map()
|
||||
|
||||
private getServerKey(server: MCPServer): string {
|
||||
@ -29,25 +31,30 @@ class McpService {
|
||||
this.callTool = this.callTool.bind(this)
|
||||
this.closeClient = this.closeClient.bind(this)
|
||||
this.removeServer = this.removeServer.bind(this)
|
||||
this.restartServer = this.restartServer.bind(this)
|
||||
this.stopServer = this.stopServer.bind(this)
|
||||
}
|
||||
|
||||
async initClient(server: MCPServer) {
|
||||
async initClient(server: MCPServer): Promise<Client> {
|
||||
const serverKey = this.getServerKey(server)
|
||||
|
||||
// Check if we already have a client for this server configuration
|
||||
const existingClient = this.clients.get(serverKey)
|
||||
if (existingClient) {
|
||||
this.client = existingClient
|
||||
return
|
||||
// Check if the existing client is still connected
|
||||
const pingResult = await existingClient.ping()
|
||||
Logger.info(`[MCP] Ping result for ${server.name}:`, pingResult)
|
||||
// If the ping fails, remove the client from the cache
|
||||
// and create a new one
|
||||
if (!pingResult) {
|
||||
this.clients.delete(serverKey)
|
||||
} else {
|
||||
return existingClient
|
||||
}
|
||||
|
||||
// If there's an existing client for a different server, close it
|
||||
if (this.client) {
|
||||
await this.closeClient()
|
||||
}
|
||||
|
||||
// Create new client instance for each connection
|
||||
this.client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} })
|
||||
const client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} })
|
||||
|
||||
const args = [...(server.args || [])]
|
||||
|
||||
@ -95,46 +102,76 @@ class McpService {
|
||||
throw new Error('Either baseUrl or command must be provided')
|
||||
}
|
||||
|
||||
await this.client.connect(transport)
|
||||
await client.connect(transport)
|
||||
|
||||
// Store the new client in the cache
|
||||
this.clients.set(serverKey, this.client)
|
||||
this.clients.set(serverKey, client)
|
||||
|
||||
Logger.info(`[MCP] Activated server: ${server.name}`)
|
||||
return client
|
||||
} catch (error: any) {
|
||||
Logger.error(`[MCP] Error activating server ${server.name}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async closeClient() {
|
||||
if (this.client) {
|
||||
async closeClient(serverKey: string) {
|
||||
const client = this.clients.get(serverKey)
|
||||
if (client) {
|
||||
// Remove the client from the cache
|
||||
for (const [key, client] of this.clients.entries()) {
|
||||
if (client === this.client) {
|
||||
this.clients.delete(key)
|
||||
break
|
||||
await client.close()
|
||||
Logger.info(`[MCP] Closed server: ${serverKey}`)
|
||||
this.clients.delete(serverKey)
|
||||
} else {
|
||||
Logger.warn(`[MCP] No client found for server: ${serverKey}`)
|
||||
}
|
||||
}
|
||||
|
||||
await this.client.close()
|
||||
this.client = null
|
||||
}
|
||||
async stopServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
const serverKey = this.getServerKey(server)
|
||||
Logger.info(`[MCP] Stopping server: ${server.name}`)
|
||||
await this.closeClient(serverKey)
|
||||
}
|
||||
|
||||
async removeServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
await this.closeClient()
|
||||
this.clients.delete(this.getServerKey(server))
|
||||
const serverKey = this.getServerKey(server)
|
||||
const existingClient = this.clients.get(serverKey)
|
||||
if (existingClient) {
|
||||
await this.closeClient(serverKey)
|
||||
}
|
||||
}
|
||||
|
||||
async restartServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
Logger.info(`[MCP] Restarting server: ${server.name}`)
|
||||
const serverKey = this.getServerKey(server)
|
||||
await this.closeClient(serverKey)
|
||||
await this.initClient(server)
|
||||
}
|
||||
|
||||
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
await this.initClient(server)
|
||||
const { tools } = await this.client!.listTools()
|
||||
return tools.map((tool) => ({
|
||||
const client = await this.initClient(server)
|
||||
const cacheKey = `mcp:list_tool:${server.id}`
|
||||
if (CacheService.has(cacheKey)) {
|
||||
Logger.info(`[MCP] Tools from ${server.name} loaded from cache`)
|
||||
const cachedTools = CacheService.get<MCPTool[]>(cacheKey)
|
||||
if (cachedTools && cachedTools.length > 0) {
|
||||
return cachedTools
|
||||
}
|
||||
}
|
||||
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
|
||||
const { tools } = await client.listTools()
|
||||
const serverTools: MCPTool[] = []
|
||||
tools.map((tool: any) => {
|
||||
const serverTool: MCPTool = {
|
||||
...tool,
|
||||
id: nanoid(),
|
||||
serverId: server.id,
|
||||
serverName: server.name
|
||||
}))
|
||||
}
|
||||
serverTools.push(serverTool)
|
||||
})
|
||||
CacheService.set(cacheKey, serverTools, 5 * 60 * 1000)
|
||||
return serverTools
|
||||
}
|
||||
|
||||
/**
|
||||
@ -144,11 +181,10 @@ class McpService {
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ server, name, args }: { server: MCPServer; name: string; args: any }
|
||||
): Promise<any> {
|
||||
await this.initClient(server)
|
||||
|
||||
try {
|
||||
Logger.info('[MCP] Calling:', server.name, name, args)
|
||||
const result = await this.client!.callTool({ name, arguments: args })
|
||||
const client = await this.initClient(server)
|
||||
const result = await client.callTool({ name, arguments: args })
|
||||
return result
|
||||
} catch (error) {
|
||||
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
|
||||
|
||||
2
src/preload/index.d.ts
vendored
2
src/preload/index.d.ts
vendored
@ -147,6 +147,8 @@ declare global {
|
||||
}
|
||||
mcp: {
|
||||
removeServer: (server: MCPServer) => Promise<void>
|
||||
restartServer: (server: MCPServer) => Promise<void>
|
||||
stopServer: (server: MCPServer) => Promise<void>
|
||||
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
|
||||
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
||||
|
||||
@ -121,6 +121,8 @@ const api = {
|
||||
},
|
||||
mcp: {
|
||||
removeServer: (server: MCPServer) => ipcRenderer.invoke('mcp:remove-server', server),
|
||||
restartServer: (server: MCPServer) => ipcRenderer.invoke('mcp:restart-server', server),
|
||||
stopServer: (server: MCPServer) => ipcRenderer.invoke('mcp:stop-server', server),
|
||||
listTools: (server: MCPServer) => ipcRenderer.invoke('mcp:list-tools', server),
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
|
||||
ipcRenderer.invoke('mcp:call-tool', { server, name, args }),
|
||||
|
||||
@ -1013,7 +1013,12 @@
|
||||
"updateSuccess": "Server updated successfully",
|
||||
"url": "URL",
|
||||
"editMcpJson": "Edit MCP Configuration",
|
||||
"installHelp": "Get Installation Help"
|
||||
"installHelp": "Get Installation Help",
|
||||
"tools": {
|
||||
"inputSchema": "Input Schema",
|
||||
"availableTools": "Available Tools",
|
||||
"noToolsAvailable": "No tools available"
|
||||
}
|
||||
},
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
|
||||
@ -1012,7 +1012,12 @@
|
||||
"32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください"
|
||||
},
|
||||
"editMcpJson": "MCP 設定を編集",
|
||||
"installHelp": "インストールヘルプを取得"
|
||||
"installHelp": "インストールヘルプを取得",
|
||||
"tools": {
|
||||
"inputSchema": "入力スキーマ",
|
||||
"availableTools": "利用可能なツール",
|
||||
"noToolsAvailable": "利用可能なツールはありません"
|
||||
}
|
||||
},
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
|
||||
@ -1011,8 +1011,13 @@
|
||||
"updateError": "Ошибка обновления сервера",
|
||||
"updateSuccess": "Сервер успешно обновлен",
|
||||
"url": "URL",
|
||||
"editMcpJson": "Редактировать MCP 配置",
|
||||
"installHelp": "Получить помощь по установке"
|
||||
"editMcpJson": "Редактировать MCP",
|
||||
"installHelp": "Получить помощь по установке",
|
||||
"tools": {
|
||||
"inputSchema": "входные параметры",
|
||||
"availableTools": "доступные инструменты",
|
||||
"noToolsAvailable": "нет доступных инструментов"
|
||||
}
|
||||
},
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
|
||||
@ -1013,7 +1013,12 @@
|
||||
"updateSuccess": "服务器更新成功",
|
||||
"url": "URL",
|
||||
"editMcpJson": "编辑 MCP 配置",
|
||||
"installHelp": "获取安装帮助"
|
||||
"installHelp": "获取安装帮助",
|
||||
"tools": {
|
||||
"inputSchema": "输入参数",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "没有可用工具"
|
||||
}
|
||||
},
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
|
||||
@ -1012,7 +1012,12 @@
|
||||
"updateSuccess": "伺服器更新成功",
|
||||
"url": "URL",
|
||||
"editMcpJson": "編輯 MCP 配置",
|
||||
"installHelp": "獲取安裝幫助"
|
||||
"installHelp": "獲取安裝幫助",
|
||||
"tools": {
|
||||
"inputSchema": "輸入參數",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "沒有可用工具"
|
||||
}
|
||||
},
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.grid_columns": "訊息網格展示列數",
|
||||
|
||||
@ -13,7 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
|
||||
const { mcpServers, activedMcpServers } = useMCPServers()
|
||||
const { activedMcpServers } = useMCPServers()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const dropdownRef = useRef<any>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
@ -25,14 +25,15 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
|
||||
}
|
||||
|
||||
// Check if all active servers are enabled
|
||||
const activeServers = mcpServers.filter((s) => s.isActive)
|
||||
|
||||
const anyEnable = activeServers.some((server) => enabledMCPs.some((enabledServer) => enabledServer.id === server.id))
|
||||
const anyEnable = activedMcpServers.some((server) =>
|
||||
enabledMCPs.some((enabledServer) => enabledServer.id === server.id)
|
||||
)
|
||||
|
||||
const enableAll = () => mcpServers.forEach(toggelEnableMCP)
|
||||
const enableAll = () => activedMcpServers.forEach(toggelEnableMCP)
|
||||
|
||||
const disableAll = () =>
|
||||
mcpServers.forEach((s) => {
|
||||
activedMcpServers.forEach((s) => {
|
||||
enabledMCPs.forEach((enabledServer) => {
|
||||
if (enabledServer.id === s.id) {
|
||||
toggelEnableMCP(s)
|
||||
@ -60,10 +61,8 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
|
||||
</div>
|
||||
</DropdownHeader>
|
||||
<DropdownBody>
|
||||
{mcpServers.length > 0 ? (
|
||||
mcpServers
|
||||
.filter((s) => s.isActive)
|
||||
.map((server) => (
|
||||
{activedMcpServers.length > 0 ? (
|
||||
activedMcpServers.map((server) => (
|
||||
<McpServerItems key={server.id} className="ant-dropdown-menu-item">
|
||||
<div className="server-info">
|
||||
<div className="server-name">{server.name}</div>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { Button, Flex, Form, Input, Radio, Switch } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
import MCPToolsSection from './McpTool'
|
||||
|
||||
interface Props {
|
||||
server: MCPServer
|
||||
@ -25,12 +26,14 @@ interface MCPFormValues {
|
||||
|
||||
const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
const { t } = useTranslation()
|
||||
const { deleteMCPServer } = useMCPServers()
|
||||
const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio')
|
||||
const [form] = Form.useForm<MCPFormValues>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isFormChanged, setIsFormChanged] = useState(false)
|
||||
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
||||
const { updateMCPServer } = useMCPServers()
|
||||
const [tools, setTools] = useState<MCPTool[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
@ -76,6 +79,30 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
type && setServerType(type)
|
||||
}, [form])
|
||||
|
||||
// Load tools on initial mount if server is active
|
||||
useEffect(() => {
|
||||
const fetchTools = async () => {
|
||||
if (server.isActive) {
|
||||
try {
|
||||
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),
|
||||
key: 'mcp-tools-error'
|
||||
})
|
||||
} finally {
|
||||
setLoadingServer(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchTools()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
// Save the form data
|
||||
const onSave = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@ -110,7 +137,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await window.api.mcp.listTools(mcpServer)
|
||||
await window.api.mcp.restartServer(mcpServer)
|
||||
updateMCPServer({ ...mcpServer, isActive: true })
|
||||
window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' })
|
||||
setLoading(false)
|
||||
@ -129,6 +156,23 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteMcpServer = useCallback(
|
||||
async (server: MCPServer) => {
|
||||
try {
|
||||
await window.api.mcp.removeServer(server)
|
||||
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'
|
||||
})
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[server, t]
|
||||
)
|
||||
|
||||
const onFormValuesChange = () => {
|
||||
setIsFormChanged(true)
|
||||
}
|
||||
@ -144,10 +188,14 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
const onToggleActive = async (active: boolean) => {
|
||||
await form.validateFields()
|
||||
setLoadingServer(server.id)
|
||||
const oldActiveState = server.isActive
|
||||
|
||||
try {
|
||||
if (active) {
|
||||
await window.api.mcp.listTools(server)
|
||||
const localTools = await window.api.mcp.listTools(server)
|
||||
setTools(localTools)
|
||||
} else {
|
||||
await window.api.mcp.stopServer(server)
|
||||
}
|
||||
updateMCPServer({ ...server, isActive: active })
|
||||
} catch (error: any) {
|
||||
@ -156,7 +204,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
content: formatError(error),
|
||||
centered: true
|
||||
})
|
||||
console.error('[MCP] Error toggling server active', error)
|
||||
updateMCPServer({ ...server, isActive: oldActiveState })
|
||||
} finally {
|
||||
setLoadingServer(null)
|
||||
}
|
||||
@ -177,6 +225,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
<Button type="primary" size="small" onClick={onSave} loading={loading} disabled={!isFormChanged}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
<Button danger type="primary" size="small" onClick={() => onDeleteMcpServer(server)} loading={loading}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
@ -185,7 +236,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
layout="vertical"
|
||||
onValuesChange={onFormValuesChange}
|
||||
style={{
|
||||
height: 'calc(100vh - var(--navbar-height) - 115px)',
|
||||
// height: 'calc(100vh - var(--navbar-height) - 315px)',
|
||||
overflowY: 'auto',
|
||||
width: 'calc(100% + 10px)',
|
||||
paddingRight: '10px'
|
||||
@ -237,6 +288,8 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
{server.isActive && <MCPToolsSection tools={tools} />}
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
)
|
||||
|
||||
132
src/renderer/src/pages/settings/MCPSettings/McpTool.tsx
Normal file
132
src/renderer/src/pages/settings/MCPSettings/McpTool.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { MCPTool } from '@renderer/types'
|
||||
import { Badge, Collapse, Descriptions, Empty, Flex, Tag, Tooltip, Typography } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const MCPToolsSection = ({ tools }: { tools: MCPTool[] }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Render tool properties from the input schema
|
||||
const renderToolProperties = (tool: MCPTool) => {
|
||||
if (!tool.inputSchema?.properties) return null
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return 'blue'
|
||||
case 'number':
|
||||
return 'green'
|
||||
case 'boolean':
|
||||
return 'purple'
|
||||
case 'object':
|
||||
return 'orange'
|
||||
case 'array':
|
||||
return 'cyan'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Typography.Title level={5}>{t('settings.mcp.tools.inputSchema')}:</Typography.Title>
|
||||
<Descriptions bordered size="small" column={1} style={{ marginTop: 8 }}>
|
||||
{Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => (
|
||||
<Descriptions.Item
|
||||
key={key}
|
||||
label={
|
||||
<Flex align="center" gap={8}>
|
||||
<Typography.Text strong>{key}</Typography.Text>
|
||||
{tool.inputSchema.required?.includes(key) && (
|
||||
<Tooltip title="Required field">
|
||||
<Tag color="red">Required</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
}>
|
||||
<Flex vertical gap={4}>
|
||||
<Flex align="center" gap={8}>
|
||||
{prop.type && (
|
||||
// <Typography.Text type="secondary">{prop.type} </Typography.Text>
|
||||
<Badge
|
||||
color={getTypeColor(prop.type)}
|
||||
text={<Typography.Text type="secondary">{prop.type}</Typography.Text>}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{prop.description && (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
|
||||
{prop.description}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
{prop.enum && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Typography.Text type="secondary">Allowed values: </Typography.Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
|
||||
{prop.enum.map((value: string, idx: number) => (
|
||||
<Tag key={idx}>{value}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<SectionTitle>{t('settings.mcp.tools.availableTools')}</SectionTitle>
|
||||
{tools.length > 0 ? (
|
||||
<Collapse bordered={false} ghost>
|
||||
{tools.map((tool) => (
|
||||
<Collapse.Panel
|
||||
key={tool.id}
|
||||
header={
|
||||
<Flex vertical align="flex-start" style={{ width: '100%' }}>
|
||||
<Flex align="center" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{tool.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 8, fontSize: '12px' }}>
|
||||
{tool.id}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
{tool.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||
{tool.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
}>
|
||||
<SelectableContent>{renderToolProperties(tool)}</SelectableContent>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
) : (
|
||||
<Empty description={t('settings.mcp.tools.noToolsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
const Section = styled.div`
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 8px;
|
||||
`
|
||||
|
||||
const SectionTitle = styled.h3`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text-secondary);
|
||||
`
|
||||
|
||||
const SelectableContent = styled.div`
|
||||
user-select: text;
|
||||
padding: 0 12px;
|
||||
`
|
||||
|
||||
export default MCPToolsSection
|
||||
@ -61,7 +61,7 @@ export function mcpToolsToOpenAITools(mcpTools: MCPTool[]): Array<ChatCompletion
|
||||
type: 'function',
|
||||
name: tool.name,
|
||||
function: {
|
||||
name: tool.serverId,
|
||||
name: tool.id,
|
||||
description: tool.description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
@ -79,9 +79,10 @@ export function openAIToolsToMcpTool(
|
||||
return undefined
|
||||
}
|
||||
|
||||
const tool = mcpTools.find((mcptool) => mcptool.serverId === llmTool.function.name)
|
||||
const tool = mcpTools.find((mcptool) => mcptool.id === llmTool.function.name)
|
||||
|
||||
if (!tool) {
|
||||
console.warn('No MCP Tool found for tool call:', llmTool)
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user