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:
LiuVaayne 2025-03-29 07:16:59 +08:00 committed by GitHub
parent d3584d2d39
commit 3f40cc28ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 328 additions and 76 deletions

View File

@ -264,6 +264,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// Register MCP handlers // Register MCP handlers
ipcMain.handle('mcp:remove-server', mcpService.removeServer) 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:list-tools', mcpService.listTools)
ipcMain.handle('mcp:call-tool', mcpService.callTool) ipcMain.handle('mcp:call-tool', mcpService.callTool)
ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo) ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo)

View File

@ -5,12 +5,14 @@ import { getBinaryName, getBinaryPath } from '@main/utils/process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.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 { app } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import { CacheService } from './CacheService'
class McpService { class McpService {
private client: Client | null = null
private clients: Map<string, Client> = new Map() private clients: Map<string, Client> = new Map()
private getServerKey(server: MCPServer): string { private getServerKey(server: MCPServer): string {
@ -29,25 +31,30 @@ class McpService {
this.callTool = this.callTool.bind(this) this.callTool = this.callTool.bind(this)
this.closeClient = this.closeClient.bind(this) this.closeClient = this.closeClient.bind(this)
this.removeServer = this.removeServer.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) const serverKey = this.getServerKey(server)
// Check if we already have a client for this server configuration // Check if we already have a client for this server configuration
const existingClient = this.clients.get(serverKey) const existingClient = this.clients.get(serverKey)
if (existingClient) { if (existingClient) {
this.client = existingClient // Check if the existing client is still connected
return const pingResult = await existingClient.ping()
} Logger.info(`[MCP] Ping result for ${server.name}:`, pingResult)
// If the ping fails, remove the client from the cache
// If there's an existing client for a different server, close it // and create a new one
if (this.client) { if (!pingResult) {
await this.closeClient() this.clients.delete(serverKey)
} else {
return existingClient
}
} }
// Create new client instance for each connection // 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 || [])] const args = [...(server.args || [])]
@ -95,46 +102,76 @@ class McpService {
throw new Error('Either baseUrl or command must be provided') 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 // 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}`) Logger.info(`[MCP] Activated server: ${server.name}`)
return client
} catch (error: any) { } catch (error: any) {
Logger.error(`[MCP] Error activating server ${server.name}:`, error) Logger.error(`[MCP] Error activating server ${server.name}:`, error)
throw error throw error
} }
} }
async closeClient() { async closeClient(serverKey: string) {
if (this.client) { const client = this.clients.get(serverKey)
if (client) {
// Remove the client from the cache // Remove the client from the cache
for (const [key, client] of this.clients.entries()) { await client.close()
if (client === this.client) { Logger.info(`[MCP] Closed server: ${serverKey}`)
this.clients.delete(key) this.clients.delete(serverKey)
break } 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) { async removeServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
await this.closeClient() const serverKey = this.getServerKey(server)
this.clients.delete(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) { async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
await this.initClient(server) const client = await this.initClient(server)
const { tools } = await this.client!.listTools() const cacheKey = `mcp:list_tool:${server.id}`
return tools.map((tool) => ({ if (CacheService.has(cacheKey)) {
...tool, Logger.info(`[MCP] Tools from ${server.name} loaded from cache`)
serverId: server.id, const cachedTools = CacheService.get<MCPTool[]>(cacheKey)
serverName: server.name 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, _: Electron.IpcMainInvokeEvent,
{ server, name, args }: { server: MCPServer; name: string; args: any } { server, name, args }: { server: MCPServer; name: string; args: any }
): Promise<any> { ): Promise<any> {
await this.initClient(server)
try { try {
Logger.info('[MCP] Calling:', server.name, name, args) 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 return result
} catch (error) { } catch (error) {
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error) Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)

View File

@ -147,6 +147,8 @@ declare global {
} }
mcp: { mcp: {
removeServer: (server: MCPServer) => Promise<void> removeServer: (server: MCPServer) => Promise<void>
restartServer: (server: MCPServer) => Promise<void>
stopServer: (server: MCPServer) => Promise<void>
listTools: (server: MCPServer) => Promise<MCPTool[]> listTools: (server: MCPServer) => Promise<MCPTool[]>
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any> callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }> getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>

View File

@ -121,6 +121,8 @@ const api = {
}, },
mcp: { mcp: {
removeServer: (server: MCPServer) => ipcRenderer.invoke('mcp:remove-server', server), 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), listTools: (server: MCPServer) => ipcRenderer.invoke('mcp:list-tools', server),
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
ipcRenderer.invoke('mcp:call-tool', { server, name, args }), ipcRenderer.invoke('mcp:call-tool', { server, name, args }),

View File

@ -1013,7 +1013,12 @@
"updateSuccess": "Server updated successfully", "updateSuccess": "Server updated successfully",
"url": "URL", "url": "URL",
"editMcpJson": "Edit MCP Configuration", "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.divider": "Show divider between messages",
"messages.grid_columns": "Message grid display columns", "messages.grid_columns": "Message grid display columns",

View File

@ -1012,7 +1012,12 @@
"32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください" "32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください"
}, },
"editMcpJson": "MCP 設定を編集", "editMcpJson": "MCP 設定を編集",
"installHelp": "インストールヘルプを取得" "installHelp": "インストールヘルプを取得",
"tools": {
"inputSchema": "入力スキーマ",
"availableTools": "利用可能なツール",
"noToolsAvailable": "利用可能なツールはありません"
}
}, },
"messages.divider": "メッセージ間に区切り線を表示", "messages.divider": "メッセージ間に区切り線を表示",
"messages.grid_columns": "メッセージグリッドの表示列数", "messages.grid_columns": "メッセージグリッドの表示列数",

View File

@ -1011,8 +1011,13 @@
"updateError": "Ошибка обновления сервера", "updateError": "Ошибка обновления сервера",
"updateSuccess": "Сервер успешно обновлен", "updateSuccess": "Сервер успешно обновлен",
"url": "URL", "url": "URL",
"editMcpJson": "Редактировать MCP 配置", "editMcpJson": "Редактировать MCP",
"installHelp": "Получить помощь по установке" "installHelp": "Получить помощь по установке",
"tools": {
"inputSchema": "входные параметры",
"availableTools": "доступные инструменты",
"noToolsAvailable": "нет доступных инструментов"
}
}, },
"messages.divider": "Показывать разделитель между сообщениями", "messages.divider": "Показывать разделитель между сообщениями",
"messages.grid_columns": "Количество столбцов сетки сообщений", "messages.grid_columns": "Количество столбцов сетки сообщений",

View File

@ -1013,7 +1013,12 @@
"updateSuccess": "服务器更新成功", "updateSuccess": "服务器更新成功",
"url": "URL", "url": "URL",
"editMcpJson": "编辑 MCP 配置", "editMcpJson": "编辑 MCP 配置",
"installHelp": "获取安装帮助" "installHelp": "获取安装帮助",
"tools": {
"inputSchema": "输入参数",
"availableTools": "可用工具",
"noToolsAvailable": "没有可用工具"
}
}, },
"messages.divider": "消息分割线", "messages.divider": "消息分割线",
"messages.grid_columns": "消息网格展示列数", "messages.grid_columns": "消息网格展示列数",

View File

@ -1012,7 +1012,12 @@
"updateSuccess": "伺服器更新成功", "updateSuccess": "伺服器更新成功",
"url": "URL", "url": "URL",
"editMcpJson": "編輯 MCP 配置", "editMcpJson": "編輯 MCP 配置",
"installHelp": "獲取安裝幫助" "installHelp": "獲取安裝幫助",
"tools": {
"inputSchema": "輸入參數",
"availableTools": "可用工具",
"noToolsAvailable": "沒有可用工具"
}
}, },
"messages.divider": "訊息間顯示分隔線", "messages.divider": "訊息間顯示分隔線",
"messages.grid_columns": "訊息網格展示列數", "messages.grid_columns": "訊息網格展示列數",

View File

@ -13,7 +13,7 @@ interface Props {
} }
const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton }) => { const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
const { mcpServers, activedMcpServers } = useMCPServers() const { activedMcpServers } = useMCPServers()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<any>(null) const dropdownRef = useRef<any>(null)
const menuRef = useRef<HTMLDivElement>(null) const menuRef = useRef<HTMLDivElement>(null)
@ -25,14 +25,15 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
} }
// Check if all active servers are enabled // 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 = () => const disableAll = () =>
mcpServers.forEach((s) => { activedMcpServers.forEach((s) => {
enabledMCPs.forEach((enabledServer) => { enabledMCPs.forEach((enabledServer) => {
if (enabledServer.id === s.id) { if (enabledServer.id === s.id) {
toggelEnableMCP(s) toggelEnableMCP(s)
@ -60,27 +61,25 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
</div> </div>
</DropdownHeader> </DropdownHeader>
<DropdownBody> <DropdownBody>
{mcpServers.length > 0 ? ( {activedMcpServers.length > 0 ? (
mcpServers activedMcpServers.map((server) => (
.filter((s) => s.isActive) <McpServerItems key={server.id} className="ant-dropdown-menu-item">
.map((server) => ( <div className="server-info">
<McpServerItems key={server.id} className="ant-dropdown-menu-item"> <div className="server-name">{server.name}</div>
<div className="server-info"> {server.description && (
<div className="server-name">{server.name}</div> <Tooltip title={server.description} placement="bottom">
{server.description && ( <div className="server-description">{truncateText(server.description)}</div>
<Tooltip title={server.description} placement="bottom"> </Tooltip>
<div className="server-description">{truncateText(server.description)}</div> )}
</Tooltip> {server.baseUrl && <div className="server-url">{server.baseUrl}</div>}
)} </div>
{server.baseUrl && <div className="server-url">{server.baseUrl}</div>} <Switch
</div> size="small"
<Switch checked={enabledMCPs.some((s) => s.id === server.id)}
size="small" onChange={() => toggelEnableMCP(server)}
checked={enabledMCPs.some((s) => s.id === server.id)} />
onChange={() => toggelEnableMCP(server)} </McpServerItems>
/> ))
</McpServerItems>
))
) : ( ) : (
<div className="ant-dropdown-menu-item-group"> <div className="ant-dropdown-menu-item-group">
<div className="ant-dropdown-menu-item no-results">{t('settings.mcp.noServers')}</div> <div className="ant-dropdown-menu-item no-results">{t('settings.mcp.noServers')}</div>

View File

@ -1,12 +1,13 @@
import { useMCPServers } from '@renderer/hooks/useMCPServers' 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 { Button, Flex, Form, Input, Radio, Switch } from 'antd'
import TextArea from 'antd/es/input/TextArea' 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 { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
import MCPToolsSection from './McpTool'
interface Props { interface Props {
server: MCPServer server: MCPServer
@ -25,12 +26,14 @@ interface MCPFormValues {
const McpSettings: React.FC<Props> = ({ server }) => { const McpSettings: React.FC<Props> = ({ server }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { deleteMCPServer } = useMCPServers()
const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio') const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio')
const [form] = Form.useForm<MCPFormValues>() const [form] = Form.useForm<MCPFormValues>()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [isFormChanged, setIsFormChanged] = useState(false) const [isFormChanged, setIsFormChanged] = useState(false)
const [loadingServer, setLoadingServer] = useState<string | null>(null) const [loadingServer, setLoadingServer] = useState<string | null>(null)
const { updateMCPServer } = useMCPServers() const { updateMCPServer } = useMCPServers()
const [tools, setTools] = useState<MCPTool[]>([])
useEffect(() => { useEffect(() => {
if (server) { if (server) {
@ -76,6 +79,30 @@ const McpSettings: React.FC<Props> = ({ server }) => {
type && setServerType(type) type && setServerType(type)
}, [form]) }, [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 () => { const onSave = async () => {
setLoading(true) setLoading(true)
try { try {
@ -110,7 +137,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
} }
try { try {
await window.api.mcp.listTools(mcpServer) await window.api.mcp.restartServer(mcpServer)
updateMCPServer({ ...mcpServer, isActive: true }) updateMCPServer({ ...mcpServer, isActive: true })
window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' }) window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' })
setLoading(false) 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 = () => { const onFormValuesChange = () => {
setIsFormChanged(true) setIsFormChanged(true)
} }
@ -144,10 +188,14 @@ const McpSettings: React.FC<Props> = ({ server }) => {
const onToggleActive = async (active: boolean) => { const onToggleActive = async (active: boolean) => {
await form.validateFields() await form.validateFields()
setLoadingServer(server.id) setLoadingServer(server.id)
const oldActiveState = server.isActive
try { try {
if (active) { 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 }) updateMCPServer({ ...server, isActive: active })
} catch (error: any) { } catch (error: any) {
@ -156,7 +204,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
content: formatError(error), content: formatError(error),
centered: true centered: true
}) })
console.error('[MCP] Error toggling server active', error) updateMCPServer({ ...server, isActive: oldActiveState })
} finally { } finally {
setLoadingServer(null) setLoadingServer(null)
} }
@ -177,6 +225,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
<Button type="primary" size="small" onClick={onSave} loading={loading} disabled={!isFormChanged}> <Button type="primary" size="small" onClick={onSave} loading={loading} disabled={!isFormChanged}>
{t('common.save')} {t('common.save')}
</Button> </Button>
<Button danger type="primary" size="small" onClick={() => onDeleteMcpServer(server)} loading={loading}>
{t('common.delete')}
</Button>
</Flex> </Flex>
</SettingTitle> </SettingTitle>
<SettingDivider /> <SettingDivider />
@ -185,7 +236,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
layout="vertical" layout="vertical"
onValuesChange={onFormValuesChange} onValuesChange={onFormValuesChange}
style={{ style={{
height: 'calc(100vh - var(--navbar-height) - 115px)', // height: 'calc(100vh - var(--navbar-height) - 315px)',
overflowY: 'auto', overflowY: 'auto',
width: 'calc(100% + 10px)', width: 'calc(100% + 10px)',
paddingRight: '10px' paddingRight: '10px'
@ -237,6 +288,8 @@ const McpSettings: React.FC<Props> = ({ server }) => {
</> </>
)} )}
</Form> </Form>
{server.isActive && <MCPToolsSection tools={tools} />}
</SettingGroup> </SettingGroup>
</SettingContainer> </SettingContainer>
) )

View 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

View File

@ -61,7 +61,7 @@ export function mcpToolsToOpenAITools(mcpTools: MCPTool[]): Array<ChatCompletion
type: 'function', type: 'function',
name: tool.name, name: tool.name,
function: { function: {
name: tool.serverId, name: tool.id,
description: tool.description, description: tool.description,
parameters: { parameters: {
type: 'object', type: 'object',
@ -79,9 +79,10 @@ export function openAIToolsToMcpTool(
return undefined return undefined
} }
const tool = mcpTools.find((mcptool) => mcptool.serverId === llmTool.function.name) const tool = mcpTools.find((mcptool) => mcptool.id === llmTool.function.name)
if (!tool) { if (!tool) {
console.warn('No MCP Tool found for tool call:', llmTool)
return undefined return undefined
} }