Compare commits

...

1 Commits

Author SHA1 Message Date
suyao
92ba7089bb feat: implement default parameter configuration for MCP tools
- Added functionality to apply default parameters to tool arguments in MCPService.
- Introduced UI components for managing tool parameter configurations in MCPSettings.
- Updated translations for new settings and tool configurations across multiple languages.
- Enhanced MCPToolsSection to allow editing and saving of tool parameter defaults.
2025-05-25 03:19:21 +08:00
11 changed files with 583 additions and 119 deletions

View File

@@ -450,6 +450,35 @@ class McpService {
return cachedListTools(server)
}
/**
* Apply default parameters to tool arguments
*/
private applyDefaultParameters(
toolName: string,
server: MCPServer,
providedArgs: Record<string, any> = {}
): Record<string, any> {
const toolConfig = server.customToolConfigs?.find((config) => config.toolName === toolName)
if (!toolConfig) {
return providedArgs
}
const mergedArgs = { ...providedArgs }
toolConfig.parameters.forEach((paramConfig) => {
if (
paramConfig.defaultValue !== undefined &&
paramConfig.defaultValue !== null &&
paramConfig.defaultValue !== ''
) {
mergedArgs[paramConfig.name] = paramConfig.defaultValue
}
})
return mergedArgs
}
/**
* Call a tool on an MCP server
*/
@@ -466,8 +495,13 @@ class McpService {
Logger.error('[MCP] args parse error', args)
}
}
Logger.info('[MCP] Calling with args:', server.name, name, args)
const mergedArgs = this.applyDefaultParameters(name, server, args || {})
Logger.info('[MCP] Calling with merged args:', server.name, name, mergedArgs)
const client = await this.initClient(server)
const result = await client.callTool({ name, arguments: args }, undefined, {
const result = await client.callTool({ name, arguments: mergedArgs }, undefined, {
timeout: server.timeout ? server.timeout * 1000 : 60000 // Default timeout of 1 minute
})
return result as MCPCallToolResponse

View File

@@ -45,7 +45,10 @@
"search.no_results": "No results found",
"sorting.title": "Sorting",
"settings": {
"title": "Agent Setting"
"title": "Agent Configuration",
"subscription": {
"title": "Agent Subscription Configuration"
}
},
"tag.agent": "Agent",
"tag.default": "Default",
@@ -57,50 +60,50 @@
"title": "Assistants",
"abbr": "Assistants",
"settings.title": "Assistant Settings",
"clear.content": "Clearing the topic will delete all topics and files in the assistant. Are you sure you want to continue?",
"clear.title": "Clear topics",
"clear.content": "Clearing topics will delete all topics and files under the assistant. Are you sure you want to continue?",
"clear.title": "Clear Topics",
"copy.title": "Copy Assistant",
"delete.content": "Deleting an assistant will delete all topics and files under the assistant. Are you sure you want to delete it?",
"delete.content": "Deleting an assistant will delete all topics and files under that assistant. Are you sure you want to continue?",
"delete.title": "Delete Assistant",
"edit.title": "Edit Assistant",
"save.success": "Saved successfully",
"save.title": "Save to agent",
"save.title": "Save to Agent",
"icon.type": "Assistant Icon",
"search": "Search assistants...",
"search": "Search Assistants",
"settings.mcp": "MCP Servers",
"settings.mcp.enableFirst": "Please enable this server in MCP Settings first",
"settings.mcp.title": "MCP Settings",
"settings.mcp.noServersAvailable": "No MCP servers available. Please add a server in settings.",
"settings.mcp.description": "Default enabled MCP servers",
"settings.default_model": "Default Model",
"settings.knowledge_base": "Knowledge Base Settings",
"settings.mcp": "MCP Servers",
"settings.mcp.enableFirst": "Enable this server in MCP settings first",
"settings.mcp.title": "MCP Settings",
"settings.mcp.noServersAvailable": "No MCP servers available. Add servers in settings",
"settings.mcp.description": "Default enabled MCP servers",
"settings.model": "Model Settings",
"settings.preset_messages": "Preset Messages",
"settings.prompt": "Prompt Settings",
"settings.reasoning_effort": "Reasoning effort",
"settings.reasoning_effort.off": "Off",
"settings.reasoning_effort.high": "Think harder",
"settings.reasoning_effort.low": "Think less",
"settings.reasoning_effort.medium": "Think normally",
"settings.reasoning_effort.default": "Default",
"settings.more": "Assistant Settings",
"settings.knowledge_base.recognition.tip": "The assistant will use the large model's intent recognition capability to determine whether to use the knowledge base for answering. This feature will depend on the model's capabilities",
"settings.knowledge_base.recognition": "Use Knowledge Base",
"settings.knowledge_base.recognition.off": "Force Search",
"settings.knowledge_base.recognition.tip": "The agent will use the model's intent recognition ability to determine if it needs to call the knowledge base for an answer. This feature will depend on the model's capabilities.",
"settings.knowledge_base.recognition": "Call Knowledge Base",
"settings.knowledge_base.recognition.off": "Force Retrieval",
"settings.knowledge_base.recognition.on": "Intent Recognition",
"settings.tool_use_mode": "Tool Use Mode",
"settings.tool_use_mode.function": "Function",
"settings.tool_use_mode.prompt": "Prompt",
"settings.model": "Model Settings",
"settings.preset_messages": "Preset Messages",
"settings.prompt": "Prompt Settings",
"settings.reasoning_effort": "Chain of Thought Length",
"settings.reasoning_effort.off": "Off",
"settings.reasoning_effort.low": "Imagine",
"settings.reasoning_effort.medium": "Consider",
"settings.reasoning_effort.high": "Ponder",
"settings.reasoning_effort.default": "Default",
"settings.more": "Assistant Settings",
"settings.regular_phrases": {
"title": "Regular Phrase",
"title": "Regular Phrases",
"add": "Add Phrase",
"edit": "Edit Phrase",
"delete": "Delete Phrase",
"deleteConfirm": "Are you sure to delete this phrase?",
"deleteConfirm": "Are you sure you want to delete this phrase?",
"titleLabel": "Title",
"titlePlaceholder": "Enter title",
"contentLabel": "Content",
"contentPlaceholder": "Please enter phrase content, support using variables, and press Tab to quickly locate the variable to modify. For example: \nHelp me plan a route from ${from} to ${to}, and send it to ${email}."
"contentPlaceholder": "Enter phrase content, supports variables, then press Tab to quickly navigate to variables for modification. E.g.:\nHelp me plan a route from ${from} to ${to}, then send to ${email}"
}
},
"auth": {
@@ -355,7 +358,7 @@
"add": "Add",
"advanced_settings": "Advanced Settings",
"and": "and",
"assistant": "Assistant",
"assistant": "Agent",
"avatar": "Avatar",
"back": "Back",
"cancel": "Cancel",
@@ -376,9 +379,9 @@
"edit": "Edit",
"expand": "Expand",
"collapse": "Collapse",
"footnote": "Reference content",
"footnotes": "References",
"fullscreen": "Entered fullscreen mode. Press F11 to exit",
"footnote": "Citation",
"footnotes": "Citations",
"fullscreen": "Entered fullscreen mode, press F11 to exit",
"knowledge_base": "Knowledge Base",
"language": "Language",
"loading": "Loading...",
@@ -392,7 +395,10 @@
"regenerate": "Regenerate",
"rename": "Rename",
"reset": "Reset",
"required": "REQUIRED",
"allowed_values": "Allowed values",
"save": "Save",
"unsaved_changes": "You have unsaved changes",
"search": "Search",
"select": "Select",
"selectedMessages": "Selected {{count}} messages",
@@ -821,7 +827,7 @@
"style_type": "Style",
"rendering_speed": "Rendering Speed",
"learn_more": "Learn More",
"paint_course":"tutorial",
"paint_course": "tutorial",
"prompt_placeholder_edit": "Enter your image description, text drawing uses \"double quotes\" to wrap",
"proxy_required": "Currently, you need to open a proxy to view the generated images, it will be supported in the future",
"image_file_required": "Please upload an image first",
@@ -1373,7 +1379,13 @@
"inputSchema": "Input Schema",
"availableTools": "Available Tools",
"noToolsAvailable": "No tools available",
"loadError": "Get tools Error"
"loadError": "Get tools Error",
"configureDefaults": "Configure Default Parameters",
"configureDefaultsDescription": "Configure default values for tool parameters. When enabled, these values will be automatically used if the AI model doesn't provide them.",
"defaultValue": "Default Value",
"configSaved": "Tool configuration saved successfully",
"hasDefaults": "DEFAULTS",
"hasDefaultTooltip": "This tool has configured default parameters."
},
"prompts": {
"availablePrompts": "Available Prompts",

View File

@@ -50,7 +50,10 @@
"tag.system": "システム",
"title": "エージェント",
"settings": {
"title": "エージェント設定"
"title": "エージェント設定",
"subscription": {
"title": "エージェントサブスクリプション設定"
}
}
},
"assistants": {
@@ -406,7 +409,10 @@
"pinyin.asc": "ピンインで昇順ソート",
"pinyin.desc": "ピンインで降順ソート"
},
"no_results": "検索結果なし"
"no_results": "検索結果なし",
"required": "必須",
"allowed_values": "許可された値",
"unsaved_changes": "未保存の変更があります"
},
"docs": {
"title": "ドキュメント"
@@ -821,7 +827,7 @@
"style_type": "スタイル",
"learn_more": "詳しくはこちら",
"prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します",
"paint_course":"チュートリアル",
"paint_course": "チュートリアル",
"proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です",
"image_file_required": "画像を先にアップロードしてください",
"image_file_retry": "画像を先にアップロードしてください",
@@ -1369,7 +1375,13 @@
"inputSchema": "入力スキーマ",
"availableTools": "利用可能なツール",
"noToolsAvailable": "利用可能なツールなし",
"loadError": "ツール取得エラー"
"loadError": "ツール取得エラー",
"configureDefaults": "デフォルトパラメーターを設定",
"configureDefaultsDescription": "ツールパラメーターのデフォルト値を設定します。有効にすると、AIモデルが提供しない場合、これらの値が自動的に使用されます。",
"defaultValue": "デフォルト値",
"configSaved": "ツール設定が正常に保存されました",
"hasDefaults": "デフォルトあり",
"hasDefaultTooltip": "このツールには、設定済みのデフォルトパラメーターがあります。"
},
"prompts": {
"availablePrompts": "利用可能なプロンプト",

View File

@@ -50,7 +50,10 @@
"agent": "Экспорт агента"
},
"settings": {
"title": "Настройки агента"
"title": "Настройки агента",
"subscription": {
"title": "Конфигурация подписки агента"
}
}
},
"assistants": {
@@ -406,7 +409,10 @@
"pinyin.asc": "Сортировать по пиньинь (А-Я)",
"pinyin.desc": "Сортировать по пиньинь (Я-А)"
},
"no_results": "Результатов не найдено"
"no_results": "Результатов не найдено",
"required": "Обязательно",
"allowed_values": "Допустимые значения",
"unsaved_changes": "У вас есть несохраненные изменения"
},
"docs": {
"title": "Документация"
@@ -822,7 +828,7 @@
"rendering_speed": "Скорость рендеринга",
"learn_more": "Узнать больше",
"prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
"paint_course":"Руководство / Учебник",
"paint_course": "Руководство / Учебник",
"proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение",
"image_file_required": "Пожалуйста, сначала загрузите изображение",
"image_file_retry": "Пожалуйста, сначала загрузите изображение",
@@ -888,7 +894,6 @@
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов",
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
},
"rendering_speed": "Скорость рендеринга",
"text_desc_required": "Пожалуйста, сначала введите описание изображения"
},
"prompts": {
@@ -1370,7 +1375,13 @@
"inputSchema": "Схема ввода",
"availableTools": "Доступные инструменты",
"noToolsAvailable": "Нет доступных инструментов",
"loadError": "Ошибка получения инструментов"
"loadError": "Ошибка получения инструментов",
"configureDefaults": "Настроить параметры по умолчанию",
"configureDefaultsDescription": "Настройте значения по умолчанию для параметров инструмента. Когда эта функция включена, эти значения будут использоваться автоматически, если их не предоставит AI модель.",
"defaultValue": "Значение по умолчанию",
"configSaved": "Конфигурация инструмента успешно сохранена",
"hasDefaults": "ЗНАЧЕНИЯ ПО УМОЛЧАНИЮ",
"hasDefaultTooltip": "Для этого инструмента настроены параметры по умолчанию."
},
"prompts": {
"availablePrompts": "Доступные подсказки",

View File

@@ -50,7 +50,10 @@
"tag.system": "系统",
"title": "智能体",
"settings": {
"title": "智能体配置"
"title": "智能体配置",
"subscription": {
"title": "智能体订阅配置"
}
}
},
"assistants": {
@@ -392,7 +395,10 @@
"regenerate": "重新生成",
"rename": "重命名",
"reset": "重置",
"required": "必填",
"allowed_values": "允许的值",
"save": "保存",
"unsaved_changes": "您有未保存的更改",
"search": "搜索",
"select": "选择",
"selectedMessages": "选中 {{count}} 条消息",
@@ -821,7 +827,7 @@
"style_type": "风格",
"rendering_speed": "渲染速度",
"learn_more": "了解更多",
"paint_course":"教程",
"paint_course": "教程",
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹",
"proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连",
"image_file_required": "请先上传图片",
@@ -1373,7 +1379,13 @@
"inputSchema": "输入模式",
"availableTools": "可用工具",
"noToolsAvailable": "无可用工具",
"loadError": "获取工具失败"
"loadError": "获取工具失败",
"configureDefaults": "配置默认参数",
"configureDefaultsDescription": "配置工具参数的默认值。启用后如果AI模型没有提供这些参数将自动使用这些默认值。",
"defaultValue": "默认值",
"configSaved": "工具配置保存成功",
"hasDefaults": "有默认值",
"hasDefaultTooltip": "此工具已配置默认参数。"
},
"prompts": {
"availablePrompts": "可用提示",

View File

@@ -50,7 +50,10 @@
"tag.system": "系統",
"title": "智慧代理人",
"settings": {
"title": "智慧代理人設定"
"title": "智慧代理人設定",
"subscription": {
"title": "智慧代理人訂閱設定"
}
}
},
"assistants": {
@@ -406,7 +409,10 @@
"pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序"
},
"no_results": "沒有結果"
"no_results": "沒有結果",
"required": "必填",
"allowed_values": "允許的值",
"unsaved_changes": "您有未儲存的變更"
},
"docs": {
"title": "說明文件"
@@ -594,12 +600,11 @@
"citations": "引用內容",
"copied": "已複製!",
"copy.failed": "複製失敗",
"copy.success": "複製",
"copy.success": "複製成功",
"delete.confirm.title": "刪除確認",
"delete.confirm.content": "確認刪除選中的 {{count}} 條訊息嗎?",
"delete.failed": "刪除失敗",
"delete.success": "刪除成功",
"copy.success": "複製成功",
"empty_url": "無法下載圖片,可能是提示詞包含敏感內容或違禁詞彙",
"error.chunk_overlap_too_large": "分段重疊不能大於分段大小",
"error.dimension_too_large": "內容尺寸過大",
@@ -822,7 +827,7 @@
"style_type": "風格",
"learn_more": "了解更多",
"prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹",
"paint_course":"教程",
"paint_course": "教程",
"proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連",
"image_file_required": "請先上傳圖片",
"image_file_retry": "請重新上傳圖片",
@@ -1373,7 +1378,13 @@
"inputSchema": "輸入模式",
"availableTools": "可用工具",
"noToolsAvailable": "無可用工具",
"loadError": "獲取工具失敗"
"loadError": "獲取工具失敗",
"configureDefaults": "配置預設參數",
"configureDefaultsDescription": "配置工具參數的預設值。啟用後,如果 AI 模型未提供這些值,將自動使用。",
"defaultValue": "預設值",
"configSaved": "工具配置已成功保存",
"hasDefaults": "預設值已配置",
"hasDefaultTooltip": "此工具已配置預設參數。"
},
"prompts": {
"availablePrompts": "可用提示",

View File

@@ -22,10 +22,7 @@ const AgentsSubscribeUrlSettings: FC = () => {
return (
<SettingGroup theme={theme}>
<SettingTitle>
{t('agents.tag.agent')}
{t('settings.websearch.subscribe_add')}
</SettingTitle>
<SettingTitle>{t('agents.settings.subscription.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.websearch.subscribe_url')}</SettingRowTitle>

View File

@@ -2,7 +2,8 @@ import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { MCPPrompt, MCPResource, MCPServer, MCPTool, MCPToolConfig, MCPToolParameterConfig } from '@renderer/types'
import { isEmpty } from '@renderer/utils'
import { formatMcpError } from '@renderer/utils/error'
import { Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd'
import TextArea from 'antd/es/input/TextArea'
@@ -398,6 +399,42 @@ const McpSettings: React.FC = () => {
[server, updateMCPServer]
)
// Handle updating tool parameter configuration
const handleUpdateToolConfig = useCallback(
async (toolName: string, parameterConfig: MCPToolParameterConfig[]) => {
let customToolConfigs = [...(server.customToolConfigs || [])]
const existingConfigIndex = customToolConfigs.findIndex((config) => config.toolName === toolName)
const newToolConfig: MCPToolConfig = {
toolName,
parameters: parameterConfig
}
if (existingConfigIndex >= 0) {
customToolConfigs[existingConfigIndex] = newToolConfig
} else {
customToolConfigs.push(newToolConfig)
}
customToolConfigs = customToolConfigs.filter((config) =>
config.parameters.some((param) => !isEmpty(param.defaultValue))
)
const updatedServer = {
...server,
customToolConfigs
}
updateMCPServer(updatedServer)
window.message.success({
content: t('settings.mcp.tools.configSaved'),
key: 'mcp-tool-config'
})
},
[server, updateMCPServer, t]
)
const tabs = [
{
key: 'settings',
@@ -592,7 +629,14 @@ const McpSettings: React.FC = () => {
{
key: 'tools',
label: t('settings.mcp.tabs.tools'),
children: <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />
children: (
<MCPToolsSection
tools={tools}
server={server}
onToggleTool={handleToggleTool}
onUpdateToolConfig={handleUpdateToolConfig}
/>
)
},
{
key: 'prompts',

View File

@@ -1,5 +1,21 @@
import { MCPServer, MCPTool } from '@renderer/types'
import { Badge, Collapse, Descriptions, Empty, Flex, Switch, Tag, Tooltip, Typography } from 'antd'
import { CloseOutlined, SaveOutlined, SettingOutlined } from '@ant-design/icons'
import { MCPServer, MCPTool, MCPToolParameterConfig } from '@renderer/types'
import { isEmpty } from '@renderer/utils'
import {
Button,
Collapse,
Descriptions,
Empty,
Flex,
Input,
InputNumber,
Space,
Switch,
Tag,
Tooltip,
Typography
} from 'antd'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -7,22 +23,211 @@ interface MCPToolsSectionProps {
tools: MCPTool[]
server: MCPServer
onToggleTool: (tool: MCPTool, enabled: boolean) => void
onUpdateToolConfig?: (toolName: string, config: MCPToolParameterConfig[]) => void
}
const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps) => {
const MCPToolsSection = ({ tools, server, onToggleTool, onUpdateToolConfig }: MCPToolsSectionProps) => {
const { t } = useTranslation()
const [editingToolName, setEditingToolName] = useState<string | null>(null)
const [editableToolParams, setEditableToolParams] = useState<MCPToolParameterConfig[]>([])
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
// Effect to reset editable params when editingToolName changes or server config changes
useEffect(() => {
if (editingToolName) {
const tool = tools.find((t) => t.name === editingToolName)
if (tool) {
initializeEditableParams(tool)
} else {
setEditingToolName(null)
}
} else {
setEditableToolParams([])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editingToolName, server.customToolConfigs, tools])
// Check if a tool is enabled (not in the disabledTools array)
const isToolEnabled = (tool: MCPTool) => {
return !server.disabledTools?.includes(tool.name)
}
// Handle tool toggle
const handleToggle = (tool: MCPTool, checked: boolean) => {
onToggleTool(tool, checked)
}
// Render tool properties from the input schema
const getToolConfig = (toolName: string): MCPToolParameterConfig[] => {
const toolConfig = server.customToolConfigs?.find((config) => config.toolName === toolName)
return toolConfig?.parameters || []
}
const getDefaultValueForType = (type: string): any => {
switch (type) {
case 'string':
return ''
case 'number':
return 0
case 'boolean':
return false
case 'array':
return []
case 'object':
return {}
default:
return ''
}
}
const initializeEditableParams = (tool: MCPTool) => {
const currentConfig = getToolConfig(tool.name)
const initialConfig: MCPToolParameterConfig[] = []
if (tool.inputSchema?.properties) {
Object.entries(tool.inputSchema.properties).forEach(([paramName, paramDef]: [string, any]) => {
const existingConfig = currentConfig.find((c) => c.name === paramName)
initialConfig.push({
name: paramName,
defaultValue: existingConfig?.defaultValue ?? getDefaultValueForType(paramDef.type),
description: paramDef.description || ''
})
})
}
setEditableToolParams(initialConfig)
}
const handleEditToolParams = (tool: MCPTool) => {
if (editingToolName === tool.name) {
// If already editing this tool, cancel editing
setEditingToolName(null)
setEditableToolParams([])
setHasUnsavedChanges(false)
} else {
setEditingToolName(tool.name)
setHasUnsavedChanges(false)
// initializeEditableParams will be called by the useEffect
}
}
const handleSaveToolParams = (toolName: string) => {
if (onUpdateToolConfig) {
onUpdateToolConfig(toolName, editableToolParams)
}
setEditingToolName(null)
setEditableToolParams([])
setHasUnsavedChanges(false)
}
const handleCancelEditToolParams = () => {
setEditingToolName(null)
setEditableToolParams([])
setHasUnsavedChanges(false)
}
const updateParameterConfig = (index: number, field: keyof MCPToolParameterConfig, value: any) => {
const newConfig = [...editableToolParams]
newConfig[index] = { ...newConfig[index], [field]: value }
setEditableToolParams(newConfig)
setHasUnsavedChanges(true)
}
const handleParameterBlur = (index: number, field: keyof MCPToolParameterConfig, value: any) => {
updateParameterConfig(index, field, value)
}
const renderParameterInput = (param: MCPToolParameterConfig, index: number, paramDef: any) => {
const { type } = paramDef
switch (type) {
case 'number':
return (
<InputNumber
value={param.defaultValue}
onChange={(value) => updateParameterConfig(index, 'defaultValue', value)}
onBlur={(e) => {
const value = e.target.value ? Number(e.target.value) : undefined
handleParameterBlur(index, 'defaultValue', value)
}}
style={{ width: '100%' }}
placeholder="Enter default number"
/>
)
case 'boolean':
return (
<Switch
checked={param.defaultValue}
onChange={(checked) => {
updateParameterConfig(index, 'defaultValue', checked)
// Switch 组件没有 onBlur直接在 onChange 中处理
handleParameterBlur(index, 'defaultValue', checked)
}}
/>
)
case 'array':
return (
<Input.TextArea
value={Array.isArray(param.defaultValue) ? param.defaultValue.join('\n') : ''}
onChange={(e) => {
const lines = e.target.value.split('\n').filter((line) => line.trim())
updateParameterConfig(index, 'defaultValue', lines)
}}
onBlur={(e) => {
const lines = e.target.value.split('\n').filter((line) => line.trim())
handleParameterBlur(index, 'defaultValue', lines)
}}
placeholder="Enter array items (one per line)"
rows={3}
/>
)
default: // 'string' and 'object' (as JSON string for simplicity here)
if (type === 'object') {
return (
<Input.TextArea
value={
typeof param.defaultValue === 'object'
? JSON.stringify(param.defaultValue, null, 2)
: param.defaultValue
}
onChange={(e) => {
try {
const val = e.target.value
// Attempt to parse if it's meant to be an object, otherwise store as string
if (val.trim().startsWith('{') || val.trim().startsWith('[')) {
updateParameterConfig(index, 'defaultValue', JSON.parse(val))
} else {
updateParameterConfig(index, 'defaultValue', val)
}
} catch (error) {
// If JSON is invalid while typing, keep the string value
updateParameterConfig(index, 'defaultValue', e.target.value)
}
}}
onBlur={(e) => {
try {
const val = e.target.value
if (val.trim().startsWith('{') || val.trim().startsWith('[')) {
handleParameterBlur(index, 'defaultValue', JSON.parse(val))
} else {
handleParameterBlur(index, 'defaultValue', val)
}
} catch (error) {
// If JSON is invalid, keep the string value
handleParameterBlur(index, 'defaultValue', e.target.value)
}
}}
placeholder="Enter default JSON object or string"
rows={3}
/>
)
}
return (
<Input
value={param.defaultValue}
onChange={(e) => updateParameterConfig(index, 'defaultValue', e.target.value)}
onBlur={(e) => handleParameterBlur(index, 'defaultValue', e.target.value)}
placeholder="Enter default value"
/>
)
}
}
const renderToolProperties = (tool: MCPTool) => {
if (!tool.inputSchema?.properties) return null
@@ -43,52 +248,123 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
}
}
const toolConfig = getToolConfig(tool.name)
const isEditingThisTool = editingToolName === tool.name
return (
<div style={{ marginTop: 12 }}>
<Typography.Title level={5}>{t('settings.mcp.tools.inputSchema')}:</Typography.Title>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
<Typography.Title level={5} style={{ margin: 0 }}>
{t('settings.mcp.tools.inputSchema')}:
</Typography.Title>
<Tooltip title={t('settings.mcp.tools.configureDefaults')}>
<Button
type={isEditingThisTool ? 'primary' : 'text'}
icon={<SettingOutlined />}
size="small"
onClick={() => handleEditToolParams(tool)}
/>
</Tooltip>
</Flex>
<Descriptions bordered size="small" column={1} style={{ marginTop: 8 }}>
{Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => (
{Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => {
const paramDefFromSchema = tool.inputSchema?.properties?.[key] as any
const currentParamSetting = toolConfig.find((c) => c.name === key)
const hasDefaultValue = !isEmpty(currentParamSetting?.defaultValue)
const editableParam = isEditingThisTool ? editableToolParams.find((p) => p.name === key) : null
return (
<Descriptions.Item
key={key}
label={
<Flex gap={4}>
<Flex gap={4} align="center">
<Typography.Text strong>{key}</Typography.Text>
{tool.inputSchema.required?.includes(key) && (
<Tooltip title="Required field">
<span style={{ color: '#f5222d' }}>*</span>
</Tooltip>
<Tag color="red" style={{ margin: 0 }}>
{t('common.required')}
</Tag>
)}
{prop.type && (
<Tag color={getTypeColor(prop.type)} style={{ margin: 0 }}>
{prop.type}
</Tag>
)}
</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>
<Flex vertical gap={8}>
{prop.description && (
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, fontSize: '13px' }}>
{prop.description}
</Typography.Paragraph>
)}
{isEditingThisTool && editableParam && paramDefFromSchema ? (
<Flex
align="center"
gap={8}
style={{
marginTop: 8,
border: '1px solid var(--color-border-secondary)',
borderRadius: 6,
backgroundColor: 'var(--color-background)'
}}>
<Typography.Text style={{ fontWeight: 500, marginBottom: 0, flexShrink: 0 }}>
{t('settings.mcp.tools.defaultValue')}:
</Typography.Text>
{renderParameterInput(
editableParam,
editableToolParams.findIndex((p) => p.name === key),
paramDefFromSchema
)}
</Flex>
) : (
hasDefaultValue && (
<div style={{ marginTop: 4 }}>
<Typography.Text type="secondary">{t('settings.mcp.tools.defaultValue')}: </Typography.Text>
<Tag color="geekblue">{JSON.stringify(currentParamSetting?.defaultValue)}</Tag>
</div>
)
)}
{prop.enum && (
<div style={{ marginTop: 4 }}>
<Typography.Text type="secondary">Allowed values: </Typography.Text>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
<Typography.Text type="secondary">{t('common.allowed_values')}: </Typography.Text>
<Flex wrap="wrap" gap={4} style={{ marginTop: 4 }}>
{prop.enum.map((value: string, idx: number) => (
<Tag key={idx}>{value}</Tag>
))}
</div>
</Flex>
</div>
)}
</Flex>
</Descriptions.Item>
))}
)
})}
</Descriptions>
{isEditingThisTool && (
<Flex justify="space-between" align="center" style={{ marginTop: 16 }}>
<Flex align="center" gap={8}>
{hasUnsavedChanges && (
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
{t('common.unsaved_changes')}
</Typography.Text>
)}
</Flex>
<Flex gap={8}>
<Button icon={<CloseOutlined />} onClick={handleCancelEditToolParams}>
{t('common.cancel')}
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => handleSaveToolParams(tool.name)}
disabled={!hasUnsavedChanges}>
{t('common.save')}
</Button>
</Flex>
</Flex>
)}
</div>
)
}
@@ -97,35 +373,65 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
<Section>
<SectionTitle>{t('settings.mcp.tools.availableTools')}</SectionTitle>
{tools.length > 0 ? (
<Collapse bordered={false} ghost>
<Collapse bordered={false} ghost accordion>
{tools.map((tool) => (
<Collapse.Panel
key={tool.id}
header={
<Flex justify="space-between" align="center" style={{ width: '100%' }}>
<Flex vertical align="flex-start">
<Flex vertical align="flex-start" style={{ flexGrow: 1, maxWidth: 'calc(100% - 60px)' }}>
<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 strong style={{ marginRight: 8 }}>
{tool.name}
</Typography.Text>
<Typography.Text
type="secondary"
style={{
fontSize: '12px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
({tool.id})
</Typography.Text>
{server.customToolConfigs
?.find((c) => c.toolName === tool.name)
?.parameters.some((p) => !isEmpty(p.defaultValue)) && (
<Tooltip title={t('settings.mcp.tools.hasDefaultTooltip')}>
<Tag color="blue" style={{ marginLeft: 'auto', marginRight: 8, flexShrink: 0 }}>
{t('settings.mcp.tools.hasDefaults')}
</Tag>
</Tooltip>
)}
</Flex>
{tool.description && (
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
{tool.description.length > 100 ? `${tool.description.substring(0, 100)}...` : tool.description}
<Typography.Text
type="secondary"
style={{
fontSize: '13px',
marginTop: 4,
width: '100%',
whiteSpace: 'normal',
wordBreak: 'break-word'
}}>
{tool.description.length > 150 ? `${tool.description.substring(0, 150)}...` : tool.description}
</Typography.Text>
)}
</Flex>
<Space onClick={(e) => e.stopPropagation()} style={{ marginLeft: 10 }}>
<Switch
checked={isToolEnabled(tool)}
onChange={(checked, event) => {
event?.stopPropagation()
onChange={(checked) => {
handleToggle(tool, checked)
}}
/>
</Space>
</Flex>
}>
<SelectableContent>{renderToolProperties(tool)}</SelectableContent>
<SelectableContent
onClick={(e) => e.stopPropagation() /* Prevent collapse toggle when clicking content */}>
{renderToolProperties(tool)}
</SelectableContent>
</Collapse.Panel>
))}
</Collapse>
@@ -150,7 +456,7 @@ const SectionTitle = styled.h3`
const SelectableContent = styled.div`
user-select: text;
padding: 0 12px;
padding: 0 12px 12px 12px;
`
export default MCPToolsSection

View File

@@ -518,6 +518,19 @@ export interface MCPConfigSample {
env?: Record<string, string> | undefined
}
// MCP工具自定义参数配置接口
export interface MCPToolParameterConfig {
name: string
defaultValue: any
description?: string
}
// MCP工具配置接口
export interface MCPToolConfig {
toolName: string
parameters: MCPToolParameterConfig[]
}
export interface MCPServer {
id: string
name: string
@@ -530,6 +543,7 @@ export interface MCPServer {
env?: Record<string, string>
isActive: boolean
disabledTools?: string[] // List of tool names that are disabled for this server
customToolConfigs?: MCPToolConfig[]
configSample?: MCPConfigSample
headers?: Record<string, string> // Custom headers to be sent with requests to this server
searchKey?: string

View File

@@ -4,6 +4,17 @@ import { ModalFuncProps } from 'antd/es/modal/interface'
// @ts-ignore next-line`
import { v4 as uuidv4 } from 'uuid'
export function isEmpty(value: any) {
return (
value === null ||
value === undefined ||
value === '' ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && Object.keys(value).length === 0) ||
(typeof value === 'number' && value === 0)
)
}
/**
* 异步执行一个函数。
* @param fn 要执行的函数