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:
SuYao 2025-07-26 11:08:32 +08:00 committed by GitHub
parent 08c5f82a04
commit 26bd9203e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 49 additions and 12 deletions

View File

@ -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',

View File

@ -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) => {

View File

@ -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

View File

@ -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: {

View File

@ -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"

View File

@ -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')}

View File

@ -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