mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-02 10:29:02 +08:00
feat: long run mcp (#8499)
* feat(MCPService, MCPSettings, MessageTools): enhance long-running server support and UI integration - Added support for long-running server configurations in MCPService, allowing for timeout adjustments based on server settings. - Introduced a new `longRunning` property in MCPSettings to manage server behavior and UI elements accordingly. - Integrated a ProgressBar component in MessageTools to visually represent progress for long-running operations, improving user experience. * refactor(IpcChannel, MCPService, MessageTools): remove progress IPC channel and integrate progress handling - Removed the `Mcp_SetProgress` channel from `IpcChannel` and its associated handlers in `ipc.ts` and `preload/index.ts`. - Integrated progress handling directly in `McpService` to send progress updates to the main window. - Updated `MessageTools` to display progress using Ant Design's `Progress` component, enhancing the user interface for long-running operations. - Deleted the `ProgressBar` component as its functionality has been replaced by the new progress handling approach. * feat(MCPService): add maxTotalTimeout configuration for long-running operations - Introduced a new `maxTotalTimeout` property in MCPService to define a maximum timeout duration for long-running server operations, enhancing control over server behavior based on the `longRunning` setting. * refactor(MCPService): remove unused notification handler for cancelled operations * Removed the CancelledNotificationHandler from MCPService to streamline notification handling and improve code clarity. * Updated MessageTools component to simplify rendering logic for status indicators, enhancing readability and maintainability.
This commit is contained in:
parent
08c5f82a04
commit
26bd9203e1
@ -77,7 +77,6 @@ export enum IpcChannel {
|
||||
Mcp_ServersUpdated = 'mcp:servers-updated',
|
||||
Mcp_CheckConnectivity = 'mcp:check-connectivity',
|
||||
Mcp_UploadDxt = 'mcp:upload-dxt',
|
||||
Mcp_SetProgress = 'mcp:set-progress',
|
||||
Mcp_AbortTool = 'mcp:abort-tool',
|
||||
Mcp_GetServerVersion = 'mcp:get-server-version',
|
||||
|
||||
|
||||
@ -575,9 +575,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
|
||||
ipcMain.handle(IpcChannel.Mcp_AbortTool, mcpService.abortTool)
|
||||
ipcMain.handle(IpcChannel.Mcp_GetServerVersion, mcpService.getServerVersion)
|
||||
ipcMain.handle(IpcChannel.Mcp_SetProgress, (_, progress: number) => {
|
||||
mainWindow.webContents.send('mcp-progress', progress)
|
||||
})
|
||||
|
||||
// DXT upload handler
|
||||
ipcMain.handle(IpcChannel.Mcp_UploadDxt, async (event, fileBuffer: ArrayBuffer, fileName: string) => {
|
||||
|
||||
@ -46,6 +46,7 @@ import DxtService from './DxtService'
|
||||
import { CallBackServer } from './mcp/oauth/callback'
|
||||
import { McpOAuthClientProvider } from './mcp/oauth/provider'
|
||||
import getLoginShellEnvironment from './mcp/shell-env'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
// Generic type for caching wrapped functions
|
||||
type CachedFunction<T extends unknown[], R> = (...args: T) => Promise<R>
|
||||
@ -440,6 +441,10 @@ class McpService {
|
||||
// Set up progress notification handler
|
||||
client.setNotificationHandler(ProgressNotificationSchema, async (notification) => {
|
||||
logger.debug(`Progress notification received for server: ${server.name}`, notification.params)
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('mcp-progress', notification.params.progress / (notification.params.total || 1))
|
||||
}
|
||||
})
|
||||
|
||||
// Set up cancelled notification handler
|
||||
@ -614,7 +619,7 @@ class McpService {
|
||||
|
||||
const callToolFunc = async ({ server, name, args }: CallToolArgs) => {
|
||||
try {
|
||||
logger.debug(`Calling: ${server.name} ${name} ${JSON.stringify(args)} callId: ${toolCallId}`)
|
||||
logger.debug(`Calling: ${server.name} ${name} ${JSON.stringify(args)} callId: ${toolCallId}`, server)
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = JSON.parse(args)
|
||||
@ -629,9 +634,12 @@ class McpService {
|
||||
const result = await client.callTool({ name, arguments: args }, undefined, {
|
||||
onprogress: (process) => {
|
||||
logger.debug(`Progress: ${process.progress / (process.total || 1)}`)
|
||||
window.api.mcp.setProgress(process.progress / (process.total || 1))
|
||||
},
|
||||
timeout: server.timeout ? server.timeout * 1000 : 60000, // Default timeout of 1 minute
|
||||
timeout: server.timeout ? server.timeout * 1000 : 60000, // Default timeout of 1 minute,
|
||||
// 需要服务端支持: https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#timeouts
|
||||
// Need server side support: https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#timeouts
|
||||
resetTimeoutOnProgress: server.longRunning,
|
||||
maxTotalTimeout: server.longRunning ? 10 * 60 * 1000 : undefined,
|
||||
signal: this.activeToolCalls.get(toolCallId)?.signal
|
||||
})
|
||||
return result as MCPCallToolResponse
|
||||
|
||||
@ -292,7 +292,6 @@ const api = {
|
||||
return ipcRenderer.invoke(IpcChannel.Mcp_UploadDxt, buffer, file.name)
|
||||
},
|
||||
abortTool: (callId: string) => ipcRenderer.invoke(IpcChannel.Mcp_AbortTool, callId),
|
||||
setProgress: (progress: number) => ipcRenderer.invoke(IpcChannel.Mcp_SetProgress, progress),
|
||||
getServerVersion: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_GetServerVersion, server)
|
||||
},
|
||||
python: {
|
||||
|
||||
@ -6,7 +6,18 @@ 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 { Button, Collapse, ConfigProvider, Dropdown, Flex, message as antdMessage, Modal, Tabs, Tooltip } from 'antd'
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
ConfigProvider,
|
||||
Dropdown,
|
||||
Flex,
|
||||
message as antdMessage,
|
||||
Modal,
|
||||
Progress,
|
||||
Tabs,
|
||||
Tooltip
|
||||
} from 'antd'
|
||||
import { message } from 'antd'
|
||||
import { ChevronDown, ChevronRight, CirclePlay, CircleX, PauseCircle, ShieldCheck } from 'lucide-react'
|
||||
import { FC, memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@ -29,6 +40,7 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
const { messageFont, fontSize } = useSettings()
|
||||
const { mcpServers, updateMCPServer } = useMCPServers()
|
||||
const [expandedResponse, setExpandedResponse] = useState<{ content: string; title: string } | null>(null)
|
||||
const [progress, setProgress] = useState<number>(0)
|
||||
|
||||
const toolResponse = block.metadata?.rawMcpToolResponse
|
||||
|
||||
@ -58,6 +70,19 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
}
|
||||
}, [countdown, id, isPending])
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = window.electron.ipcRenderer.on(
|
||||
'mcp-progress',
|
||||
(_event: Electron.IpcRendererEvent, value: number) => {
|
||||
setProgress(value)
|
||||
}
|
||||
)
|
||||
return () => {
|
||||
setProgress(0)
|
||||
removeListener()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const cancelCountdown = () => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
@ -221,9 +246,11 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
</ToolName>
|
||||
</TitleContent>
|
||||
<ActionButtonsContainer>
|
||||
<StatusIndicator status={status} hasError={hasError}>
|
||||
{renderStatusIndicator(status, hasError)}
|
||||
</StatusIndicator>
|
||||
{progress > 0 ? (
|
||||
<Progress type="circle" size={14} percent={Number((progress * 100)?.toFixed(0))} />
|
||||
) : (
|
||||
renderStatusIndicator(status, hasError)
|
||||
)}
|
||||
<Tooltip title={t('common.expand')} mouseEnterDelay={0.5}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
|
||||
@ -31,6 +31,7 @@ interface MCPFormValues {
|
||||
env?: string
|
||||
isActive: boolean
|
||||
headers?: string
|
||||
longRunning?: boolean
|
||||
timeout?: number
|
||||
|
||||
provider?: string
|
||||
@ -155,6 +156,7 @@ const McpSettings: React.FC = () => {
|
||||
command: server.command || '',
|
||||
registryUrl: server.registryUrl || '',
|
||||
isActive: server.isActive,
|
||||
longRunning: server.longRunning,
|
||||
timeout: server.timeout,
|
||||
args: server.args ? server.args.join('\n') : '',
|
||||
env: server.env
|
||||
@ -271,6 +273,7 @@ const McpSettings: React.FC = () => {
|
||||
registryUrl: values.registryUrl,
|
||||
searchKey: server.searchKey,
|
||||
timeout: values.timeout || server.timeout,
|
||||
longRunning: values.longRunning,
|
||||
// Preserve existing advanced properties if not set in the form
|
||||
provider: values.provider || server.provider,
|
||||
providerUrl: values.providerUrl || server.providerUrl,
|
||||
@ -630,6 +633,9 @@ const McpSettings: React.FC = () => {
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item name="longRunning" label={t('settings.mcp.longRunning', 'Long Running')} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="timeout"
|
||||
label={t('settings.mcp.timeout', 'Timeout')}
|
||||
|
||||
@ -658,6 +658,7 @@ export interface MCPServer {
|
||||
providerUrl?: string // URL of the MCP server in provider's website or documentation
|
||||
logoUrl?: string // URL of the MCP server's logo
|
||||
tags?: string[] // List of tags associated with this server
|
||||
longRunning?: boolean // Whether the server is long running
|
||||
timeout?: number // Timeout in seconds for requests to this server, default is 60 seconds
|
||||
dxtVersion?: string // Version of the DXT package
|
||||
dxtPath?: string // Path where the DXT package was extracted
|
||||
|
||||
Loading…
Reference in New Issue
Block a user