Compare commits
1 Commits
fix/openai
...
feat/custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ba7089bb |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "利用可能なプロンプト",
|
||||
|
||||
@@ -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": "Доступные подсказки",
|
||||
|
||||
@@ -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": "可用提示",
|
||||
|
||||
@@ -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": "可用提示",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]) => (
|
||||
<Descriptions.Item
|
||||
key={key}
|
||||
label={
|
||||
<Flex gap={4}>
|
||||
<Typography.Text strong>{key}</Typography.Text>
|
||||
{tool.inputSchema.required?.includes(key) && (
|
||||
<Tooltip title="Required field">
|
||||
<span style={{ color: '#f5222d' }}>*</span>
|
||||
</Tooltip>
|
||||
{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} align="center">
|
||||
<Typography.Text strong>{key}</Typography.Text>
|
||||
{tool.inputSchema.required?.includes(key) && (
|
||||
<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={8}>
|
||||
{prop.description && (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, fontSize: '13px' }}>
|
||||
{prop.description}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
</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>}
|
||||
/>
|
||||
|
||||
{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>
|
||||
)
|
||||
)}
|
||||
</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>
|
||||
))}
|
||||
|
||||
{prop.enum && (
|
||||
<div style={{ 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>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
)}
|
||||
</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>
|
||||
<Switch
|
||||
checked={isToolEnabled(tool)}
|
||||
onChange={(checked, event) => {
|
||||
event?.stopPropagation()
|
||||
handleToggle(tool, checked)
|
||||
}}
|
||||
/>
|
||||
<Space onClick={(e) => e.stopPropagation()} style={{ marginLeft: 10 }}>
|
||||
<Switch
|
||||
checked={isToolEnabled(tool)}
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 要执行的函数
|
||||
|
||||
Reference in New Issue
Block a user