This commit is contained in:
Bubu 2025-12-18 22:31:28 +08:00 committed by GitHub
commit de6b91ef1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 798 additions and 17 deletions

View File

@ -48,7 +48,8 @@ export default defineConfig([
'@eslint-react/no-unstable-context-value': 'off',
'@eslint-react/hooks-extra/prefer-use-state-lazy-initialization': 'off',
'@eslint-react/hooks-extra/no-unnecessary-use-prefix': 'off',
'@eslint-react/no-children-to-array': 'off'
'@eslint-react/no-children-to-array': 'off',
'@eslint-react/dom/no-unsafe-iframe-sandbox': 'off'
}
},
{

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "Available Tools",
"enable": "Enable Tool",
"execute": {
"button": "Execute",
"copied": "Copied to clipboard",
"copy": "Copy",
"copyFailed": "Failed to copy",
"error": {
"invalidJson": "Invalid JSON format: {{error}}",
"noToolOrServer": "Tool or server not found"
},
"execute": "Execute",
"label": "Execute",
"params": "Parameters (JSON)",
"paramsPlaceholder": "Enter JSON parameters...",
"result": {
"error": "Error Result",
"success": "Result",
"text": "Text"
},
"table": {
"name": "Name",
"value": "Value"
},
"title": "Execute Tool: {{name}}",
"tooltip": "Execute tool with custom parameters",
"view": {
"formatted": "Formatted",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "Allowed Values"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "可用工具",
"enable": "启用工具",
"execute": {
"button": "执行",
"copied": "已复制到剪贴板",
"copy": "复制",
"copyFailed": "复制失败",
"error": {
"invalidJson": "无效的 JSON 格式: {{error}}",
"noToolOrServer": "未找到工具或服务器"
},
"execute": "执行",
"label": "执行",
"params": "参数 (JSON)",
"paramsPlaceholder": "输入 JSON 参数...",
"result": {
"error": "错误结果",
"success": "结果",
"text": "文本"
},
"table": {
"name": "名称",
"value": "值"
},
"title": "执行工具: {{name}}",
"tooltip": "使用自定义参数执行工具",
"view": {
"formatted": "美化",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "允许的值"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "可用工具",
"enable": "啟用工具",
"execute": {
"button": "執行",
"copied": "已複製到剪貼板",
"copy": "複製",
"copyFailed": "複製失敗",
"error": {
"invalidJson": "無效的 JSON 格式: {{error}}",
"noToolOrServer": "未找到工具或伺服器"
},
"execute": "執行",
"label": "執行",
"params": "參數 (JSON)",
"paramsPlaceholder": "輸入 JSON 參數...",
"result": {
"error": "錯誤結果",
"success": "結果",
"text": "文字"
},
"table": {
"name": "名稱",
"value": "值"
},
"title": "執行工具: {{name}}",
"tooltip": "使用自訂參數執行工具",
"view": {
"formatted": "美化",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "允許的值"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "Verfügbare Tools",
"enable": "Tool aktivieren",
"execute": {
"button": "Ausführen",
"copied": "In Zwischenablage kopiert",
"copy": "Kopieren",
"copyFailed": "Kopieren fehlgeschlagen",
"error": {
"invalidJson": "Ungültiges JSON-Format: {{error}}",
"noToolOrServer": "Tool oder Server nicht gefunden"
},
"execute": "Ausführen",
"label": "Ausführen",
"params": "Parameter (JSON)",
"paramsPlaceholder": "JSON-Parameter eingeben...",
"result": {
"error": "Fehlerergebnis",
"success": "Ergebnis",
"text": "Text"
},
"table": {
"name": "Name",
"value": "Wert"
},
"title": "Tool ausführen: {{name}}",
"tooltip": "Tool mit benutzerdefinierten Parametern ausführen",
"view": {
"formatted": "Formatiert",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "Erlaubte Werte"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "Διαθέσιμα Εργαλεία",
"enable": "Ενεργοποίηση εργαλείου",
"execute": {
"button": "Εκτέλεση",
"copied": "Αντιγράφηκε στο πρόχειρο",
"copy": "Αντιγραφή",
"copyFailed": "Αποτυχία αντιγραφής",
"error": {
"invalidJson": "Μη έγκυρη μορφή JSON: {{error}}",
"noToolOrServer": "Εργαλείο ή διακομιστής δεν βρέθηκε"
},
"execute": "Εκτέλεση",
"label": "Εκτέλεση",
"params": "Παράμετροι (JSON)",
"paramsPlaceholder": "Εισαγάγετε παραμέτρους JSON...",
"result": {
"error": "Αποτέλεσμα σφάλματος",
"success": "Αποτέλεσμα",
"text": "Κείμενο"
},
"table": {
"name": "Όνομα",
"value": "Τιμή"
},
"title": "Εκτέλεση εργαλείου: {{name}}",
"tooltip": "Εκτέλεση εργαλείου με προσαρμοσμένες παραμέτρους",
"view": {
"formatted": "Μορφοποιημένο",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "Επιτρεπόμενες τιμές"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "Herramientas disponibles",
"enable": "Habilitar herramienta",
"execute": {
"button": "Ejecutar",
"copied": "Copiado al portapapeles",
"copy": "Copiar",
"copyFailed": "Error al copiar",
"error": {
"invalidJson": "Formato JSON inválido: {{error}}",
"noToolOrServer": "Herramienta o servidor no encontrado"
},
"execute": "Ejecutar",
"label": "Ejecutar",
"params": "Parámetros (JSON)",
"paramsPlaceholder": "Ingrese parámetros JSON...",
"result": {
"error": "Resultado de error",
"success": "Resultado",
"text": "Texto"
},
"table": {
"name": "Nombre",
"value": "Valor"
},
"title": "Ejecutar herramienta: {{name}}",
"tooltip": "Ejecutar herramienta con parámetros personalizados",
"view": {
"formatted": "Formateado",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "Valores permitidos"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "Outils disponibles",
"enable": "Activer l'outil",
"execute": {
"button": "Exécuter",
"copied": "Copié dans le presse-papiers",
"copy": "Copier",
"copyFailed": "Échec de la copie",
"error": {
"invalidJson": "Format JSON invalide: {{error}}",
"noToolOrServer": "Outil ou serveur introuvable"
},
"execute": "Exécuter",
"label": "Exécuter",
"params": "Paramètres (JSON)",
"paramsPlaceholder": "Entrez les paramètres JSON...",
"result": {
"error": "Résultat d'erreur",
"success": "Résultat",
"text": "Texte"
},
"table": {
"name": "Nom",
"value": "Valeur"
},
"title": "Exécuter l'outil: {{name}}",
"tooltip": "Exécuter l'outil avec des paramètres personnalisés",
"view": {
"formatted": "Formaté",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "Valeurs autorisées"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "利用可能なツール",
"enable": "ツールを有効にする",
"execute": {
"button": "実行",
"copied": "クリップボードにコピーしました",
"copy": "コピー",
"copyFailed": "コピーに失敗しました",
"error": {
"invalidJson": "無効なJSON形式: {{error}}",
"noToolOrServer": "ツールまたはサーバーが見つかりません"
},
"execute": "実行",
"label": "実行",
"params": "パラメータ (JSON)",
"paramsPlaceholder": "JSONパラメータを入力...",
"result": {
"error": "エラー結果",
"success": "結果",
"text": "テキスト"
},
"table": {
"name": "名前",
"value": "値"
},
"title": "ツールを実行: {{name}}",
"tooltip": "カスタムパラメータでツールを実行",
"view": {
"formatted": "フォーマット済み",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "許可された値"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "Ferramentas Disponíveis",
"enable": "Habilitar Ferramenta",
"execute": {
"button": "Executar",
"copied": "Copiado para a área de transferência",
"copy": "Copiar",
"copyFailed": "Falha ao copiar",
"error": {
"invalidJson": "Formato JSON inválido: {{error}}",
"noToolOrServer": "Ferramenta ou servidor não encontrado"
},
"execute": "Executar",
"label": "Executar",
"params": "Parâmetros (JSON)",
"paramsPlaceholder": "Digite os parâmetros JSON...",
"result": {
"error": "Resultado de erro",
"success": "Resultado",
"text": "Texto"
},
"table": {
"name": "Nome",
"value": "Valor"
},
"title": "Executar ferramenta: {{name}}",
"tooltip": "Executar ferramenta com parâmetros personalizados",
"view": {
"formatted": "Formatado",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "Valores permitidos"

View File

@ -4114,6 +4114,35 @@
},
"availableTools": "Доступные инструменты",
"enable": "Включить инструмент",
"execute": {
"button": "Выполнить",
"copied": "Скопировано в буфер обмена",
"copy": "Копировать",
"copyFailed": "Не удалось скопировать",
"error": {
"invalidJson": "Неверный формат JSON: {{error}}",
"noToolOrServer": "Инструмент или сервер не найден"
},
"execute": "Выполнить",
"label": "Выполнить",
"params": "Параметры (JSON)",
"paramsPlaceholder": "Введите параметры JSON...",
"result": {
"error": "Результат ошибки",
"success": "Результат",
"text": "Текст"
},
"table": {
"name": "Имя",
"value": "Значение"
},
"title": "Выполнить инструмент: {{name}}",
"tooltip": "Выполнить инструмент с пользовательскими параметрами",
"view": {
"formatted": "Форматированный",
"json": "JSON"
}
},
"inputSchema": {
"enum": {
"allowedValues": "Допустимые значения"

View File

@ -0,0 +1,442 @@
import 'katex/dist/katex.min.css'
import { loggerService } from '@logger'
import type { MCPServer, MCPTool } from '@renderer/types'
import { Button, Flex, Input, message, Modal, Space, Table, Typography } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { Code as CodeIcon, Copy, Play, Sparkles } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import rehypeKatex from 'rehype-katex'
import rehypeRaw from 'rehype-raw'
import remarkCjkFriendly from 'remark-cjk-friendly'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
const logger = loggerService.withContext('ExecuteToolModal')
// HTML 文档结构检测模式(在模块级别定义,避免重复创建)
const HTML_DOCUMENT_PATTERNS = [/<!DOCTYPE\s+html/i, /<html[\s>]/i, /<head[\s>]/i, /<body[\s>]/i]
// Markdown 检测模式(在模块级别定义,避免重复创建)
const MARKDOWN_PATTERNS = [
/^#{1,6}\s+.+$/m, // 标题
/^\s*[-*+]\s+.+$/m, // 列表
/^\s*\d+\.\s+.+$/m, // 有序列表
/\[.+\]\(.+\)/, // 链接
/!\[.+\]\(.+\)/, // 图片
/```[\s\S]*?```/, // 代码块
/`[^`]+`/, // 行内代码
/\*\*.*?\*\*/, // 粗体
/\*.*?\*/, // 斜体
/^>\s+.+$/m // 引用
]
/**
*
* @param text
* @returns 'json' | 'markdown' | 'html' | 'text'
*/
function detectContentType(text: string): 'json' | 'markdown' | 'html' | 'text' {
if (!text) return 'text'
// 检测 HTML 特征(优先检测,因为 HTML 可能包含其他格式)
// 检查是否包含完整的 HTML 文档结构或大量 HTML 标签
const hasHtmlDocument = HTML_DOCUMENT_PATTERNS.some((pattern) => pattern.test(text))
// 检查 HTML 标签数量
// 注意:每次调用时创建新的正则表达式实例,避免全局标志的状态污染
const htmlTags = text.match(/<[a-z][a-z0-9]*[\s>]/gi)
const htmlTagCount = htmlTags ? htmlTags.length : 0
// 如果包含 HTML 文档结构,或者有多个 HTML 标签,认为是 HTML
if (hasHtmlDocument || htmlTagCount >= 3) {
return 'html'
}
// 尝试解析为 JSON
try {
const parsed = JSON.parse(text)
if (typeof parsed === 'object' && parsed !== null) {
return 'json'
}
} catch {
// 不是 JSON
}
// 检测 Markdown 特征
const hasMarkdown = MARKDOWN_PATTERNS.some((pattern) => pattern.test(text))
if (hasMarkdown) {
return 'markdown'
}
return 'text'
}
interface ExecuteToolModalProps {
open: boolean
tool: MCPTool | null
server: MCPServer | null
onClose: () => void
}
interface TableData {
key: string
name: string
value: any
}
const ExecuteToolModal: React.FC<ExecuteToolModalProps> = ({ open, tool, server, onClose }) => {
const { t } = useTranslation()
const [paramsJson, setParamsJson] = useState('{}')
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<{ content: any[]; isError?: boolean } | null>(null)
const [viewMode, setViewMode] = useState<'json' | 'formatted'>('json')
// 初始化参数 JSON基于工具的 inputSchema
const initialParams = useMemo(() => {
if (!tool?.inputSchema?.properties) {
return '{}'
}
const params: Record<string, any> = {}
const properties = tool.inputSchema.properties
// 为每个属性生成默认值或示例
Object.keys(properties).forEach((key) => {
const prop = properties[key] as {
type?: string
default?: any
}
if (prop.type === 'string') {
params[key] = prop.default !== undefined ? prop.default : ''
} else if (prop.type === 'number') {
params[key] = prop.default !== undefined ? prop.default : 0
} else if (prop.type === 'boolean') {
params[key] = prop.default !== undefined ? prop.default : false
} else if (prop.type === 'array') {
params[key] = prop.default !== undefined ? prop.default : []
} else if (prop.type === 'object') {
params[key] = prop.default !== undefined ? prop.default : {}
}
})
return JSON.stringify(params, null, 2)
}, [tool])
// 当工具改变时,重置参数
// initialParams 是基于 tool 的 memoized 值,所以当 tool 改变时它会自动更新
// 因此不需要将 initialParams 包含在依赖数组中
useEffect(() => {
if (open && tool) {
setParamsJson(initialParams)
setResult(null)
setViewMode('json')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, tool])
// 获取主要文本内容
const mainTextContent = useMemo(() => {
if (!result || !result.content) return ''
// 查找第一个 text 类型的 content
const textContent = result.content.find((item) => item.type === 'text')
return textContent?.text || ''
}, [result])
// 获取格式化的内容类型
const formattedContentType = useMemo(() => {
return detectContentType(mainTextContent)
}, [mainTextContent])
// 验证 JSON 格式
const validateJson = (jsonStr: string): { valid: boolean; data?: any; error?: string } => {
try {
const data = JSON.parse(jsonStr)
return { valid: true, data }
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : 'Invalid JSON'
}
}
}
// 执行工具
const handleExecute = async () => {
if (!tool || !server) {
message.error(t('settings.mcp.tools.execute.error.noToolOrServer', 'Tool or server not found'))
return
}
// 验证 JSON
const validation = validateJson(paramsJson)
if (!validation.valid) {
message.error(
t('settings.mcp.tools.execute.error.invalidJson', 'Invalid JSON format: {{error}}', {
error: validation.error
})
)
return
}
setLoading(true)
setResult(null)
try {
logger.info(`Executing tool: ${tool.name}`, { params: validation.data })
const resp = await window.api.mcp.callTool({
server,
name: tool.name,
args: validation.data,
callId: `manual-${Date.now()}`
})
logger.info(`Tool executed successfully: ${tool.name}`, resp)
setResult(resp)
} catch (error) {
logger.error(`Error executing tool: ${tool.name}`, error as Error)
const errorMessage =
error instanceof Error ? error.message : typeof error === 'string' ? error : JSON.stringify(error)
setResult({
content: [
{
type: 'text',
text: errorMessage
}
],
isError: true
})
} finally {
setLoading(false)
}
}
// 复制结果
const handleCopy = () => {
if (!result) return
const resultText = JSON.stringify(result, null, 2)
navigator.clipboard.writeText(resultText).then(
() => {
message.success(t('settings.mcp.tools.execute.copied', 'Copied to clipboard'))
},
() => {
message.error(t('settings.mcp.tools.execute.copyFailed', 'Failed to copy'))
}
)
}
// 将结果转换为表格数据(仅当内容是 JSON 时)
const tableData: TableData[] = useMemo(() => {
if (!result || !result.content || formattedContentType !== 'json') return []
const data: TableData[] = []
try {
const parsed = JSON.parse(mainTextContent)
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
// 如果是对象,展开为多行
Object.entries(parsed).forEach(([key, value]) => {
data.push({
key: key,
name: key,
value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
})
})
} else if (Array.isArray(parsed)) {
// 如果是数组,显示索引和值
parsed.forEach((item, idx) => {
data.push({
key: `item-${idx}`,
name: String(idx),
value: typeof item === 'object' ? JSON.stringify(item, null, 2) : String(item)
})
})
}
} catch {
// 解析失败,返回空数组
}
return data
}, [result, formattedContentType, mainTextContent])
const tableColumns: ColumnsType<TableData> = [
{
title: t('settings.mcp.tools.execute.table.name', 'Name'),
dataIndex: 'name',
key: 'name',
width: 200
},
{
title: t('settings.mcp.tools.execute.table.value', 'Value'),
dataIndex: 'value',
key: 'value',
render: (value: string) => (
<Typography.Text
style={{
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
fontFamily: 'monospace',
fontSize: '12px',
maxWidth: '100%',
display: 'block'
}}>
{value}
</Typography.Text>
)
}
]
if (!tool || !server) {
return null
}
return (
<Modal
title={
<Flex align="center" gap={8}>
<Play size={16} />
<Typography.Text strong>
{t('settings.mcp.tools.execute.title', 'Execute Tool: {{name}}', { name: tool.name })}
</Typography.Text>
</Flex>
}
open={open}
onCancel={onClose}
width={900}
footer={[
<Button key="cancel" onClick={onClose}>
{t('common.cancel', 'Cancel')}
</Button>,
<Button key="execute" type="primary" loading={loading} onClick={handleExecute} icon={<Play size={14} />}>
{t('settings.mcp.tools.execute.execute', 'Execute')}
</Button>
]}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* 参数输入 */}
<div>
<Typography.Title level={5}>{t('settings.mcp.tools.execute.params', 'Parameters (JSON)')}</Typography.Title>
<Input.TextArea
value={paramsJson}
onChange={(e) => setParamsJson(e.target.value)}
rows={8}
placeholder={t('settings.mcp.tools.execute.paramsPlaceholder', 'Enter JSON parameters...')}
style={{ fontFamily: 'monospace', fontSize: '12px' }}
/>
</div>
{/* 结果显示 */}
{result && (
<div>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
<Typography.Title level={5} style={{ margin: 0, color: result.isError ? '#ff4d4f' : undefined }}>
{result.isError
? t('settings.mcp.tools.execute.result.error', 'Error Result')
: t('settings.mcp.tools.execute.result.success', 'Result')}
</Typography.Title>
<Space>
<Button.Group>
<Button
type={viewMode === 'json' ? 'primary' : 'default'}
icon={<CodeIcon size={14} />}
onClick={() => setViewMode('json')}
size="small">
{t('settings.mcp.tools.execute.view.json', 'JSON')}
</Button>
<Button
type={viewMode === 'formatted' ? 'primary' : 'default'}
icon={<Sparkles size={14} />}
onClick={() => setViewMode('formatted')}
size="small">
{t('settings.mcp.tools.execute.view.formatted', 'Formatted')}
</Button>
</Button.Group>
<Button icon={<Copy size={14} />} onClick={handleCopy} size="small">
{t('settings.mcp.tools.execute.copy', 'Copy')}
</Button>
</Space>
</Flex>
{viewMode === 'json' ? (
<Input.TextArea
value={JSON.stringify(result, null, 2)}
readOnly
rows={12}
style={{
fontFamily: 'monospace',
fontSize: '12px',
backgroundColor: result.isError ? '#fff1f0' : '#f6ffed',
borderColor: result.isError ? '#ffccc7' : '#b7eb8f'
}}
/>
) : (
<div
style={{
backgroundColor: result.isError ? '#fff1f0' : '#f6ffed',
border: `1px solid ${result.isError ? '#ffccc7' : '#b7eb8f'}`,
borderRadius: '4px',
padding: formattedContentType === 'html' ? '0' : '16px',
maxHeight: '500px',
overflow: formattedContentType === 'html' ? 'hidden' : 'auto',
position: 'relative'
}}>
{formattedContentType === 'html' ? (
<iframe
key={mainTextContent} // 强制重新创建 iframe 当内容改变时
srcDoc={mainTextContent}
title="HTML Preview"
sandbox="allow-scripts allow-same-origin allow-forms"
style={{
width: '100%',
height: '500px',
border: 'none',
display: 'block',
backgroundColor: 'white'
}}
/>
) : formattedContentType === 'json' && tableData.length > 0 ? (
<Table
columns={tableColumns}
dataSource={tableData}
pagination={false}
size="small"
scroll={{ y: 400 }}
style={{
backgroundColor: 'transparent'
}}
/>
) : formattedContentType === 'markdown' ? (
<div className="markdown" style={{ wordBreak: 'break-word' }}>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkCjkFriendly, remarkMath]}
rehypePlugins={[rehypeRaw, rehypeKatex]}>
{mainTextContent}
</ReactMarkdown>
</div>
) : (
<Typography.Paragraph
style={{
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
marginBottom: 0,
fontFamily: 'monospace',
fontSize: '12px'
}}>
{mainTextContent || JSON.stringify(result.content, null, 2)}
</Typography.Paragraph>
)}
</div>
)}
</div>
)}
</Space>
</Modal>
)
}
export default ExecuteToolModal

View File

@ -1,10 +1,13 @@
import type { MCPServer, MCPTool } from '@renderer/types'
import { isToolAutoApproved } from '@renderer/utils/mcp-tools'
import { Badge, Descriptions, Empty, Flex, Switch, Table, Tag, Tooltip, Typography } from 'antd'
import { Badge, Button, Descriptions, Empty, Flex, Switch, Table, Tag, Tooltip, Typography } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { Hammer, Info, Zap } from 'lucide-react'
import { Hammer, Info, Play, Zap } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ExecuteToolModal from './ExecuteToolModal'
interface MCPToolsSectionProps {
tools: MCPTool[]
server: MCPServer
@ -14,6 +17,8 @@ interface MCPToolsSectionProps {
const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: MCPToolsSectionProps) => {
const { t } = useTranslation()
const [executeModalOpen, setExecuteModalOpen] = useState(false)
const [selectedTool, setSelectedTool] = useState<MCPTool | null>(null)
// Check if a tool is enabled (not in the disabledTools array)
const isToolEnabled = (tool: MCPTool) => {
@ -30,6 +35,12 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
onToggleAutoApprove(tool, checked)
}
// Handle execute tool
const handleExecuteTool = (tool: MCPTool) => {
setSelectedTool(tool)
setExecuteModalOpen(true)
}
// Render tool properties from the input schema
const renderToolProperties = (tool: MCPTool) => {
if (!tool.inputSchema?.properties) return null
@ -102,6 +113,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
}
const columns: ColumnsType<MCPTool> = [
// 工具列表列定义:可用工具、启用工具、自动批准、执行工具
{
title: <Typography.Text strong>{t('settings.mcp.tools.availableTools')}</Typography.Text>,
dataIndex: 'name',
@ -141,7 +153,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
</Flex>
),
key: 'enable',
width: 150, // Fixed width might be good for alignment
width: 130, // Fixed width might be good for alignment
align: 'center',
render: (_, tool) => (
<Switch checked={isToolEnabled(tool)} onChange={(checked) => handleToggle(tool, checked)} size="small" />
@ -155,7 +167,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
</Flex>
),
key: 'autoApprove',
width: 150, // Fixed width
width: 130, // Fixed width
align: 'center',
render: (_, tool) => (
<Tooltip
@ -175,21 +187,57 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
/>
</Tooltip>
)
},
{
title: (
<Flex align="center" justify="center" gap={4}>
<Play size={14} color="green" />
<Typography.Text strong>{t('settings.mcp.tools.execute.label', 'Execute')}</Typography.Text>
</Flex>
),
key: 'execute',
width: 130,
align: 'center',
render: (_, tool) => (
<Tooltip title={t('settings.mcp.tools.execute.tooltip', 'Execute tool with custom parameters')}>
<Button
type="primary"
size="small"
icon={<Play size={12} />}
onClick={() => handleExecuteTool(tool)}
disabled={!isToolEnabled(tool)}>
{t('settings.mcp.tools.execute.button', 'Execute')}
</Button>
</Tooltip>
)
}
]
return tools.length > 0 ? (
<Table
rowKey="id"
columns={columns}
dataSource={tools}
pagination={false}
expandable={{
expandedRowRender: (tool) => renderToolProperties(tool)
}}
/>
) : (
<Empty description={t('settings.mcp.tools.noToolsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
return (
<>
{tools.length > 0 ? (
<Table
rowKey="id"
columns={columns}
dataSource={tools}
pagination={false}
expandable={{
expandedRowRender: (tool) => renderToolProperties(tool)
}}
/>
) : (
<Empty description={t('settings.mcp.tools.noToolsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
<ExecuteToolModal
open={executeModalOpen}
tool={selectedTool}
server={server}
onClose={() => {
setExecuteModalOpen(false)
setSelectedTool(null)
}}
/>
</>
)
}