diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index ae91322045..a0b86a8b1c 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -764,7 +764,7 @@ "invoking": "Invoking", "pending": "Pending", "preview": "Preview", - "raw": "Raw" + "autoApproveEnabled": "Auto-approve enabled for this tool" }, "topic.added": "New topic added", "upgrade.success.button": "Restart", @@ -1888,7 +1888,14 @@ "availableTools": "Available Tools", "inputSchema": "Input Schema", "loadError": "Get tools Error", - "noToolsAvailable": "No tools available" + "noToolsAvailable": "No tools available", + "enable": "Enable Tool", + "autoApprove": "Auto Approve", + "autoApprove.tooltip.howToEnable": "Enable the tool first to use auto-approve", + "autoApprove.tooltip.enabled": "Tool will run automatically without confirmation", + "autoApprove.tooltip.disabled": "Tool will require manual approval before running", + "autoApprove.tooltip.confirm": "Are you sure you want to run this MCP tool?", + "run": "Run" }, "type": "Type", "types": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index ae497704e7..48bff57e22 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -764,7 +764,7 @@ "invoking": "呼び出し中", "pending": "保留中", "preview": "プレビュー", - "raw": "生データ" + "autoApproveEnabled": "このツールは自動承認が有効になっています" }, "topic.added": "新しいトピックが追加されました", "upgrade.success.button": "再起動", @@ -1888,7 +1888,14 @@ "availableTools": "利用可能なツール", "inputSchema": "入力スキーマ", "loadError": "ツール取得エラー", - "noToolsAvailable": "利用可能なツールなし" + "noToolsAvailable": "利用可能なツールなし", + "enable": "ツールを有効にする", + "autoApprove": "自動承認", + "autoApprove.tooltip.howToEnable": "ツールを有効にしてから自動承認を使用できます", + "autoApprove.tooltip.enabled": "ツールは承認なしで自動実行されます", + "autoApprove.tooltip.disabled": "ツールは実行前に手動承認が必要です", + "autoApprove.tooltip.confirm": "このMCPツールを実行してもよろしいですか?", + "run": "実行" }, "type": "タイプ", "types": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 3b905943d3..69c2776806 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -764,7 +764,7 @@ "invoking": "Вызов", "pending": "Ожидание", "preview": "Предпросмотр", - "raw": "Исходный" + "autoApproveEnabled": "Для этого инструмента включен автоматический одобрен" }, "topic.added": "Новый топик добавлен", "upgrade.success.button": "Перезапустить", @@ -1888,7 +1888,14 @@ "availableTools": "Доступные инструменты", "inputSchema": "Схема ввода", "loadError": "Ошибка получения инструментов", - "noToolsAvailable": "Нет доступных инструментов" + "noToolsAvailable": "Нет доступных инструментов", + "enable": "Включить инструмент", + "autoApprove": "Автоматическое одобрение", + "autoApprove.tooltip.howToEnable": "Включите инструмент, чтобы использовать автоматическое одобрение", + "autoApprove.tooltip.enabled": "Инструмент будет автоматически выполняться без подтверждения", + "autoApprove.tooltip.disabled": "Инструмент будет требовать ручное одобрение перед выполнением", + "autoApprove.tooltip.confirm": "Вы уверены, что хотите выполнить этот инструмент MCP?", + "run": "Выполнить" }, "type": "Тип", "types": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 0edba54568..0816efed0b 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -764,7 +764,7 @@ "invoking": "调用中", "pending": "等待中", "preview": "预览", - "raw": "原始" + "autoApproveEnabled": "此工具已启用自动批准" }, "topic.added": "话题添加成功", "upgrade.success.button": "重启", @@ -1888,7 +1888,14 @@ "availableTools": "可用工具", "inputSchema": "输入模式", "loadError": "获取工具失败", - "noToolsAvailable": "无可用工具" + "noToolsAvailable": "无可用工具", + "enable": "启用工具", + "autoApprove": "自动批准", + "autoApprove.tooltip.howToEnable": "启用工具后才能使用自动批准", + "autoApprove.tooltip.enabled": "工具将自动运行而无需批准", + "autoApprove.tooltip.disabled": "工具运行前需要手动批准", + "autoApprove.tooltip.confirm": "是否运行该MCP工具?", + "run": "运行" }, "type": "类型", "types": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 7e2b69c930..291cddcdb7 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -764,7 +764,7 @@ "invoking": "調用中", "pending": "等待中", "preview": "預覽", - "raw": "原始碼" + "autoApproveEnabled": "此工具已啟用自動批准" }, "topic.added": "新話題已新增", "upgrade.success.button": "重新啟動", @@ -1888,7 +1888,14 @@ "availableTools": "可用工具", "inputSchema": "輸入模式", "loadError": "獲取工具失敗", - "noToolsAvailable": "無可用工具" + "noToolsAvailable": "無可用工具", + "enable": "啟用工具", + "autoApprove": "自動批准", + "autoApprove.tooltip.howToEnable": "啟用工具後才能使用自動批准", + "autoApprove.tooltip.enabled": "工具將自動運行而無需批准", + "autoApprove.tooltip.disabled": "工具運行前需要手動批准", + "autoApprove.tooltip.confirm": "是否運行該MCP工具?", + "run": "運行" }, "type": "類型", "types": { diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/MessageTools.tsx index 7b7e13a245..75359731a8 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTools.tsx @@ -1,13 +1,15 @@ import { CheckOutlined, CloseOutlined, LoadingOutlined, WarningOutlined } from '@ant-design/icons' import { useCodeStyle } from '@renderer/context/CodeStyleProvider' +import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useSettings } from '@renderer/hooks/useSettings' import type { ToolMessageBlock } from '@renderer/types/newMessage' +import { isToolAutoApproved } from '@renderer/utils/mcp-tools' import { cancelToolAction, confirmToolAction } from '@renderer/utils/userConfirmation' -import { Collapse, message as antdMessage, Tooltip } from 'antd' +import { Button, Collapse, ConfigProvider, Dropdown, Flex, message as antdMessage, Tooltip } from 'antd' import { message } from 'antd' import Logger from 'electron-log/renderer' -import { PauseCircle } from 'lucide-react' -import { FC, memo, useEffect, useMemo, useState } from 'react' +import { ChevronDown, ChevronRight, CirclePlay, CircleX, PauseCircle, ShieldCheck } from 'lucide-react' +import { FC, memo, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -15,11 +17,15 @@ interface Props { block: ToolMessageBlock } +const COUNTDOWN_TIME = 30 + const MessageTools: FC = ({ block }) => { const [activeKeys, setActiveKeys] = useState([]) const [copiedMap, setCopiedMap] = useState>({}) + const [countdown, setCountdown] = useState(COUNTDOWN_TIME) const { t } = useTranslation() const { messageFont, fontSize } = useSettings() + const { mcpServers, updateMCPServer } = useMCPServers() const toolResponse = block.metadata?.rawMcpToolResponse @@ -29,6 +35,32 @@ const MessageTools: FC = ({ block }) => { const isInvoking = status === 'invoking' const isDone = status === 'done' + const timer = useRef(null) + useEffect(() => { + if (!isPending) return + + if (countdown > 0) { + timer.current = setTimeout(() => { + console.log('countdown', countdown) + setCountdown((prev) => prev - 1) + }, 1000) + } else if (countdown === 0) { + confirmToolAction(id) + } + + return () => { + if (timer.current) { + clearTimeout(timer.current) + } + } + }, [countdown, id, isPending]) + + const cancelCountdown = () => { + if (timer.current) { + clearTimeout(timer.current) + } + } + const argsString = useMemo(() => { if (toolResponse?.arguments) { return JSON.stringify(toolResponse.arguments, null, 2) @@ -67,10 +99,12 @@ const MessageTools: FC = ({ block }) => { } const handleConfirmTool = () => { + cancelCountdown() confirmToolAction(id) } const handleCancelTool = () => { + cancelCountdown() cancelToolAction(id) } @@ -90,6 +124,76 @@ const MessageTools: FC = ({ block }) => { } } + const handleAutoApprove = async () => { + cancelCountdown() + + if (!tool || !tool.name) { + return + } + + const server = mcpServers.find((s) => s.id === tool.serverId) + if (!server) { + return + } + + let disabledAutoApproveTools = [...(server.disabledAutoApproveTools || [])] + + // Remove tool from disabledAutoApproveTools to enable auto-approve + disabledAutoApproveTools = disabledAutoApproveTools.filter((name) => name !== tool.name) + + const updatedServer = { + ...server, + disabledAutoApproveTools + } + + updateMCPServer(updatedServer) + + // Also confirm the current tool + confirmToolAction(id) + + message.success({ + content: t('message.tools.autoApproveEnabled', 'Auto-approve enabled for this tool'), + key: 'auto-approve' + }) + } + + const renderStatusIndicator = (status: string, hasError: boolean) => { + let label = '' + let icon: React.ReactNode | null = null + switch (status) { + case 'pending': + label = t('message.tools.pending', 'Awaiting Approval') + icon = + break + case 'invoking': + label = t('message.tools.invoking') + icon = + break + case 'cancelled': + label = t('message.tools.cancelled') + icon = + break + case 'done': + if (hasError) { + label = t('message.tools.error') + icon = + } else { + label = t('message.tools.completed') + icon = + } + break + default: + label = '' + icon = null + } + return ( + + {label} + {icon} + + ) + } + // Format tool responses for collapse items const getCollapseItems = () => { const items: { key: string; label: React.ReactNode; children: React.ReactNode }[] = [] @@ -104,108 +208,33 @@ const MessageTools: FC = ({ block }) => { label: ( - {tool.name} - - {(() => { - switch (status) { - case 'pending': - return ( - <> - {t('message.tools.pending')} - - - ) - case 'invoking': - return ( - <> - {t('message.tools.invoking')} - - - ) - case 'cancelled': - return ( - <> - {t('message.tools.cancelled')} - - - ) - case 'done': - if (hasError) { - return ( - <> - {t('message.tools.error')} - - - ) - } else { - return ( - <> - {t('message.tools.completed')} - - - ) - } - default: - return '' - } - })()} - + + {tool.serverName}: {tool.name} + {isToolAutoApproved(tool) && ( + + + + )} + - {isPending && ( - <> - - { - e.stopPropagation() - handleCancelTool() - }} - aria-label={t('common.cancel')}> - - - - - { - e.stopPropagation() - handleConfirmTool() - }} - aria-label={t('common.confirm')}> - - - - - )} - {isInvoking && toolResponse?.id && ( - + + {renderStatusIndicator(status, hasError)} + + {!isPending && !isInvoking && ( + { e.stopPropagation() - handleAbortTool() + copyContent(JSON.stringify(result, null, 2), id) }} - aria-label={t('chat.input.pause')}> - + aria-label={t('common.copy')}> + {!copiedMap[id] && } + {copiedMap[id] && } )} - {isDone && response && ( - <> - - { - e.stopPropagation() - copyContent(JSON.stringify(result, null, 2), id) - }} - aria-label={t('common.copy')}> - {!copiedMap[id] && } - {copiedMap[id] && } - - - - )} ), @@ -231,16 +260,91 @@ const MessageTools: FC = ({ block }) => { } return ( - - - + + + + ( + + )} + /> + {(isPending || isInvoking) && ( + + + {isPending ? t('settings.mcp.tools.autoApprove.tooltip.confirm') : t('message.tools.invoking')} + + + + {isPending && ( + + )} + {isInvoking && toolResponse?.id ? ( + + ) : ( + } + onClick={() => { + handleConfirmTool() + }} + menu={{ + items: [ + { + key: 'autoApprove', + label: t('settings.mcp.tools.autoApprove'), + onClick: () => { + handleAutoApprove() + } + } + ] + }}> + + + {t('settings.mcp.tools.run', 'Run')} ({countdown}s) + + + )} + + + )} + + + ) } @@ -265,29 +369,64 @@ const CollapsedContent: FC<{ isExpanded: boolean; resultString: string }> = ({ i return } -const CollapseContainer = styled(Collapse)` +const ToolContentWrapper = styled.div` + padding: 1px; + background-color: var(--color-background-soft); border-radius: 8px; + overflow: hidden; +` + +const ActionsBar = styled.div` + padding: 8px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +` + +const ActionLabel = styled.div` + flex: 1; + font-size: 14px; + color: var(--color-text-2); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const ActionButtonsGroup = styled.div` + display: flex; + gap: 10px; +` + +const CountdownText = styled.span` + width: 65px; + text-align: left; +` + +const StyledDropdownButton = styled(Dropdown.Button)` + .ant-btn-group { + border-radius: 6px; + } +` + +const ExpandIcon = styled(ChevronRight)<{ $isActive?: boolean }>` + transition: transform 0.2s; + transform: ${({ $isActive }) => ($isActive ? 'rotate(90deg)' : 'rotate(0deg)')}; +` + +const CollapseContainer = styled(Collapse)` + --status-color-warning: var(--color-warning, #faad14); + --status-color-invoking: var(--color-primary); + --status-color-error: var(--color-error, #ff4d4f); + --status-color-success: var(--color-success, green); + border-radius: 7px; border: none; + background-color: var(--color-background); overflow: hidden; .ant-collapse-header { - background-color: var(--color-bg-2); - transition: background-color 0.2s; - display: flex; - align-items: center; - .ant-collapse-expand-icon { - height: 100% !important; - } - .ant-collapse-arrow { - height: 28px !important; - svg { - width: 14px; - height: 14px; - } - } - &:hover { - background-color: var(--color-bg-3); - } + padding: 8px 10px !important; + align-items: center !important; } .ant-collapse-content-box { @@ -297,11 +436,7 @@ const CollapseContainer = styled(Collapse)` const ToolContainer = styled.div` margin-top: 10px; - margin-bottom: 12px; - border: 1px solid var(--color-border); - background-color: var(--color-bg-2); - border-radius: 8px; - overflow: hidden; + margin-bottom: 10px; ` const MarkdownContainer = styled.div` @@ -319,7 +454,6 @@ const MessageTitleLabel = styled.div` align-items: center; justify-content: space-between; width: 100%; - min-height: 26px; gap: 10px; padding: 0; margin-left: 4px; @@ -332,7 +466,7 @@ const TitleContent = styled.div` gap: 8px; ` -const ToolName = styled.span` +const ToolName = styled(Flex)` color: var(--color-text); font-weight: 500; font-size: 13px; @@ -342,29 +476,30 @@ const StatusIndicator = styled.span<{ status: string; hasError?: boolean }>` color: ${(props) => { switch (props.status) { case 'pending': - return 'var(--color-text-2)' + return 'var(--status-color-warning)' case 'invoking': - return 'var(--color-primary)' + return 'var(--status-color-invoking)' case 'cancelled': - return 'var(--color-error, #ff4d4f)' // Assuming cancelled should also be an error color + return 'var(--status-color-error)' case 'done': - return props.hasError ? 'var(--color-error, #ff4d4f)' : 'var(--color-success, #52c41a)' + return props.hasError ? 'var(--status-color-error)' : 'var(--status-color-success)' default: return 'var(--color-text)' } }}; font-size: 11px; + font-weight: ${(props) => (props.status === 'pending' ? '600' : '400')}; display: flex; align-items: center; - opacity: 0.85; - border-left: 1px solid var(--color-border); + opacity: ${(props) => (props.status === 'pending' ? '1' : '0.85')}; padding-left: 12px; ` const ActionButtonsContainer = styled.div` display: flex; - gap: 8px; + gap: 6px; margin-left: auto; + align-items: center; ` const ActionButton = styled.button` diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index 5bc5455180..6307a9b1b6 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -443,6 +443,33 @@ const McpSettings: React.FC = () => { [server, updateMCPServer] ) + // Handle toggling auto-approve for a tool + const handleToggleAutoApprove = useCallback( + async (tool: MCPTool, autoApprove: boolean) => { + let disabledAutoApproveTools = [...(server.disabledAutoApproveTools || [])] + + if (autoApprove) { + disabledAutoApproveTools = disabledAutoApproveTools.filter((name) => name !== tool.name) + } else { + // Add tool to disabledTools if it's being disabled + if (!disabledAutoApproveTools.includes(tool.name)) { + disabledAutoApproveTools.push(tool.name) + } + } + + // Update the server with new disabledTools + const updatedServer = { + ...server, + disabledAutoApproveTools + } + + // Save the updated server configuration + // await window.api.mcp.updateServer(updatedServer) + updateMCPServer(updatedServer) + }, + [server, updateMCPServer] + ) + const tabs = [ { key: 'settings', @@ -649,7 +676,14 @@ const McpSettings: React.FC = () => { { key: 'tools', label: t('settings.mcp.tabs.tools'), - children: + children: ( + + ) }, { key: 'prompts', diff --git a/src/renderer/src/pages/settings/MCPSettings/McpTool.tsx b/src/renderer/src/pages/settings/MCPSettings/McpTool.tsx index 788db5b77e..724eccb290 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpTool.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpTool.tsx @@ -1,15 +1,18 @@ import { MCPServer, MCPTool } from '@renderer/types' -import { Badge, Collapse, Descriptions, Empty, Flex, Switch, Tag, Tooltip, Typography } from 'antd' +import { isToolAutoApproved } from '@renderer/utils/mcp-tools' +import { Badge, Descriptions, Empty, Flex, Switch, Table, Tag, Tooltip, Typography } from 'antd' +import { ColumnsType } from 'antd/es/table' +import { Hammer, Info, Zap } from 'lucide-react' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' interface MCPToolsSectionProps { tools: MCPTool[] server: MCPServer onToggleTool: (tool: MCPTool, enabled: boolean) => void + onToggleAutoApprove: (tool: MCPTool, autoApprove: boolean) => void } -const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps) => { +const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: MCPToolsSectionProps) => { const { t } = useTranslation() // Check if a tool is enabled (not in the disabledTools array) @@ -22,6 +25,11 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps) onToggleTool(tool, checked) } + // Handle auto-approve toggle + const handleAutoApproveToggle = (tool: MCPTool, checked: boolean) => { + onToggleAutoApprove(tool, checked) + } + // Render tool properties from the input schema const renderToolProperties = (tool: MCPTool) => { if (!tool.inputSchema?.properties) return null @@ -43,114 +51,144 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps) } } + // {t('settings.mcp.tools.inputSchema')}: return ( -
- {t('settings.mcp.tools.inputSchema')}: - - {Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => ( - - {key} - {tool.inputSchema.required?.includes(key) && ( - - * - - )} - - }> - - - {prop.type && ( - // {prop.type} - {prop.type}} - /> - )} - - {prop.description && ( - - {prop.description} - - )} - {prop.enum && ( -
- Allowed values: -
- {prop.enum.map((value: string, idx: number) => ( - {value} - ))} -
-
+ + {Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => ( + + {key} + {tool.inputSchema.required?.includes(key) && ( + + * + )}
-
- ))} -
-
+ }> + + + {prop.type && ( + // {prop.type} + {prop.type}} + /> + )} + + {prop.description && ( + + {prop.description} + + )} + {prop.enum && ( +
+ Allowed values: +
+ {prop.enum.map((value: string, idx: number) => ( + {value} + ))} +
+
+ )} +
+ + ))} + ) } + const columns: ColumnsType = [ + { + title: {t('settings.mcp.tools.availableTools')}, + dataIndex: 'name', + key: 'name', + filters: tools.map((tool) => ({ + text: tool.name, + value: tool.name + })), + onFilter: (value, record) => record.name === value, + filterSearch: true, + render: (_, tool) => ( + + + {tool.name} + + + + + {tool.description && ( + + {tool.description.length > 100 ? `${tool.description.substring(0, 100)}...` : tool.description} + + )} + + ) + }, + { + title: ( + + + {t('settings.mcp.tools.enable')} + + ), + key: 'enable', + width: 150, // Fixed width might be good for alignment + align: 'center', + render: (_, tool) => ( + handleToggle(tool, checked)} size="small" /> + ) + }, + { + title: ( + + + {t('settings.mcp.tools.autoApprove')} + + ), + key: 'autoApprove', + width: 150, // Fixed width + align: 'center', + render: (_, tool) => ( + + handleAutoApproveToggle(tool, checked)} + size="small" + /> + + ) + } + ] + return ( -
- {t('settings.mcp.tools.availableTools')} + <> {tools.length > 0 ? ( - - {tools.map((tool) => ( - - - - {tool.name} - - {tool.id} - - - {tool.description && ( - - {tool.description.length > 100 ? `${tool.description.substring(0, 100)}...` : tool.description} - - )} - - { - event?.stopPropagation() - handleToggle(tool, checked) - }} - /> - - }> - {renderToolProperties(tool)} - - ))} - + renderToolProperties(tool) + }} + /> ) : ( )} - + ) } -const Section = styled.div` - margin-top: 8px; - padding-top: 8px; -` - -const SectionTitle = styled.h3` - font-size: 14px; - font-weight: 500; - margin-bottom: 8px; - color: var(--color-text-secondary); -` - -const SelectableContent = styled.div` - user-select: text; - padding: 0 12px; -` - export default MCPToolsSection diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 1d4ed79ca4..a8e0047fa1 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -624,6 +624,7 @@ export interface MCPServer { env?: Record isActive: boolean disabledTools?: string[] // List of tool names that are disabled for this server + disabledAutoApproveTools?: string[] // Whether to auto-approve tools for this server configSample?: MCPConfigSample headers?: Record // Custom headers to be sent with requests to this server searchKey?: string diff --git a/src/renderer/src/utils/mcp-tools.ts b/src/renderer/src/utils/mcp-tools.ts index 4b7a2e4735..05180e3303 100644 --- a/src/renderer/src/utils/mcp-tools.ts +++ b/src/renderer/src/utils/mcp-tools.ts @@ -27,7 +27,7 @@ import { } from 'openai/resources' import { CompletionsParams } from '../aiCore/middleware/schemas' -import { requestToolConfirmation } from './userConfirmation' +import { confirmSameNameTools, requestToolConfirmation, setToolIdToNameMapping } from './userConfirmation' const MCP_AUTO_INSTALL_SERVER_NAME = '@cherry/mcp-auto-install' const EXTRA_SCHEMA_KEYS = ['schema', 'headers'] @@ -461,6 +461,11 @@ export function getMcpServerByTool(tool: MCPTool) { return servers.find((s) => s.id === tool.serverId) } +export function isToolAutoApproved(tool: MCPTool, server?: MCPServer): boolean { + const effectiveServer = server ?? getMcpServerByTool(tool) + return effectiveServer ? !effectiveServer.disabledAutoApproveTools?.includes(tool.name) : false +} + export function parseToolUse(content: string, mcpTools: MCPTool[], startIdx: number = 0): ToolUseResponse[] { if (!content || !mcpTools || mcpTools.length === 0) { return [] @@ -576,7 +581,22 @@ export async function parseAndCallTools( const pendingPromises: Promise[] = [] curToolResponses.forEach((toolResponse) => { - const confirmationPromise = requestToolConfirmation(toolResponse.id, abortSignal) + const server = getMcpServerByTool(toolResponse.tool) + const isAutoApproveEnabled = isToolAutoApproved(toolResponse.tool, server) + let confirmationPromise: Promise + if (isAutoApproveEnabled) { + confirmationPromise = Promise.resolve(true) + } else { + setToolIdToNameMapping(toolResponse.id, toolResponse.tool.name) + + confirmationPromise = requestToolConfirmation(toolResponse.id, abortSignal).then((confirmed) => { + if (confirmed && server) { + // 自动确认其他同名的待确认工具 + confirmSameNameTools(toolResponse.tool.name) + } + return confirmed + }) + } const processingPromise = confirmationPromise .then(async (confirmed) => { @@ -700,16 +720,9 @@ export async function parseAndCallTools( pendingPromises.push(processingPromise) }) - Logger.info( - `🔧 [MCP] Waiting for tool confirmations:`, - curToolResponses.map((t) => t.id) - ) - // 等待所有工具处理完成(但每个工具的状态已经实时更新) await Promise.all(pendingPromises) - Logger.info(`🔧 [MCP] All tools processed. Confirmed tools: ${confirmedTools.length}`) - return { toolResults, confirmedToolResponses: confirmedTools } } diff --git a/src/renderer/src/utils/userConfirmation.ts b/src/renderer/src/utils/userConfirmation.ts index 9b23f0466a..b9c88886c5 100644 --- a/src/renderer/src/utils/userConfirmation.ts +++ b/src/renderer/src/utils/userConfirmation.ts @@ -84,3 +84,24 @@ export function getPendingToolIds(): string[] { export function isToolPending(toolId: string): boolean { return toolConfirmResolvers.has(toolId) } + +const toolIdToNameMap = new Map() + +export function setToolIdToNameMapping(toolId: string, toolName: string): void { + toolIdToNameMap.set(toolId, toolName) +} + +export function confirmSameNameTools(confirmedToolName: string): void { + const toolIdsToConfirm: string[] = [] + + for (const [toolId, toolName] of toolIdToNameMap.entries()) { + if (toolName === confirmedToolName && toolConfirmResolvers.has(toolId)) { + toolIdsToConfirm.push(toolId) + } + } + + toolIdsToConfirm.forEach((toolId) => { + confirmToolAction(toolId) + toolIdToNameMap.delete(toolId) + }) +}