From bfeea4ed4996c103d5ee36a908d6726e82472300 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sun, 19 Jan 2025 01:02:01 +0800 Subject: [PATCH] fix: prevent MCP operations from blocking chat interface --- app/components/mcp-market.module.scss | 5 + app/components/mcp-market.tsx | 107 ++++++++------------- app/mcp/actions.ts | 129 ++++++++++++++------------ app/mcp/types.ts | 18 +++- 4 files changed, 132 insertions(+), 127 deletions(-) diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss index f5c8c0ccae3..283436c7f84 100644 --- a/app/components/mcp-market.module.scss +++ b/app/components/mcp-market.module.scss @@ -167,6 +167,11 @@ background-color: #6b7280; } + &.initializing { + background-color: #f59e0b; + animation: pulse 1.5s infinite; + } + .error-message { margin-left: 4px; font-size: 12px; diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index 98211ceddbf..235f63b1ca3 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -13,7 +13,7 @@ import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; import { addMcpServer, - getClientStatus, + getClientsStatus, getClientTools, getMcpConfigFromFile, isMcpEnabled, @@ -71,6 +71,23 @@ export function McpMarketPage() { checkMcpStatus(); }, [navigate]); + // 添加状态轮询 + useEffect(() => { + if (!mcpEnabled || !config) return; + + const updateStatuses = async () => { + const statuses = await getClientsStatus(); + setClientStatuses(statuses); + }; + + // 立即执行一次 + updateStatuses(); + // 每 1000ms 轮询一次 + const timer = setInterval(updateStatuses, 1000); + + return () => clearInterval(timer); + }, [mcpEnabled, config]); + // 加载预设服务器 useEffect(() => { const loadPresetServers = async () => { @@ -103,10 +120,7 @@ export function McpMarketPage() { setConfig(config); // 获取所有客户端的状态 - const statuses: Record = {}; - for (const clientId of Object.keys(config.mcpServers)) { - statuses[clientId] = await getClientStatus(clientId); - } + const statuses = await getClientsStatus(); setClientStatuses(statuses); } catch (error) { console.error("Failed to load initial state:", error); @@ -165,7 +179,6 @@ export function McpMarketPage() { const preset = presetServers.find((s) => s.id === editingServerId); if (!preset || !preset.configSchema || !editingServerId) return; - // 先关闭模态框 const savingServerId = editingServerId; setEditingServerId(undefined); @@ -200,31 +213,8 @@ export function McpMarketPage() { ...(Object.keys(env).length > 0 ? { env } : {}), }; - // 检查是否是新增还是编辑 - const isNewServer = !isServerAdded(savingServerId); - - // 如果是编辑现有服务器,保持原有状态 - if (!isNewServer) { - const currentConfig = await getMcpConfigFromFile(); - const currentStatus = currentConfig.mcpServers[savingServerId]?.status; - if (currentStatus) { - serverConfig.status = currentStatus; - } - } - - // 更新配置并初始化新服务器 const newConfig = await addMcpServer(savingServerId, serverConfig); setConfig(newConfig); - - // 只有新增的服务器才需要获取状态(因为会自动启动) - if (isNewServer) { - const status = await getClientStatus(savingServerId); - setClientStatuses((prev) => ({ - ...prev, - [savingServerId]: status, - })); - } - showToast("Server configuration updated successfully"); } catch (error) { showToast( @@ -277,11 +267,8 @@ export function McpMarketPage() { setConfig(newConfig); // 更新状态 - const status = await getClientStatus(preset.id); - setClientStatuses((prev) => ({ - ...prev, - [preset.id]: status, - })); + const statuses = await getClientsStatus(); + setClientStatuses(statuses); } finally { updateLoadingState(preset.id, null); } @@ -298,11 +285,6 @@ export function McpMarketPage() { updateLoadingState(id, "Stopping server..."); const newConfig = await pauseMcpServer(id); setConfig(newConfig); - - setClientStatuses((prev) => ({ - ...prev, - [id]: { status: "paused", errorMsg: null }, - })); showToast("Server stopped successfully"); } catch (error) { showToast("Failed to stop server"); @@ -316,19 +298,7 @@ export function McpMarketPage() { const restartServer = async (id: string) => { try { updateLoadingState(id, "Starting server..."); - - const success = await resumeMcpServer(id); - const status = await getClientStatus(id); - setClientStatuses((prev) => ({ - ...prev, - [id]: status, - })); - - if (success) { - showToast("Server started successfully"); - } else { - throw new Error("Failed to start server"); - } + await resumeMcpServer(id); } catch (error) { showToast( error instanceof Error @@ -347,14 +317,7 @@ export function McpMarketPage() { updateLoadingState("all", "Restarting all servers..."); const newConfig = await restartAllClients(); setConfig(newConfig); - - const statuses: Record = {}; - for (const clientId of Object.keys(newConfig.mcpServers)) { - statuses[clientId] = await getClientStatus(clientId); - } - setClientStatuses(statuses); - - showToast("Successfully restarted all clients"); + showToast("Restarting all clients"); } catch (error) { showToast("Failed to restart clients"); console.error(error); @@ -452,6 +415,12 @@ export function McpMarketPage() { const statusMap = { undefined: null, // 未配置/未找到不显示 + // 添加初始化状态 + initializing: ( + + Initializing + + ), paused: ( Stopped @@ -517,10 +486,11 @@ export function McpMarketPage() { const statusPriority: Record = { error: 0, // Highest priority for error status active: 1, // Second for active - starting: 2, // Starting - stopping: 3, // Stopping - paused: 4, // Paused - undefined: 5, // Lowest priority for undefined + initializing: 2, // Initializing + starting: 3, // Starting + stopping: 4, // Stopping + paused: 5, // Paused + undefined: 6, // Lowest priority for undefined }; // Get actual status (including loading status) @@ -529,6 +499,11 @@ export function McpMarketPage() { const operationType = getOperationStatusType(loading); return operationType === "default" ? status : operationType; } + + if (status === "initializing" && !loading) { + return "active"; + } + return status; }; @@ -538,8 +513,8 @@ export function McpMarketPage() { // 首先按状态排序 if (aEffectiveStatus !== bEffectiveStatus) { return ( - (statusPriority[aEffectiveStatus] ?? 5) - - (statusPriority[bEffectiveStatus] ?? 5) + (statusPriority[aEffectiveStatus] ?? 6) - + (statusPriority[bEffectiveStatus] ?? 6) ); } diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index 7d4b5b661dd..b4611d93409 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -24,40 +24,54 @@ const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); const clientsMap = new Map(); // 获取客户端状态 -export async function getClientStatus( - clientId: string, -): Promise { - const status = clientsMap.get(clientId); +export async function getClientsStatus(): Promise< + Record +> { const config = await getMcpConfigFromFile(); - const serverConfig = config.mcpServers[clientId]; + const result: Record = {}; - // 如果配置中不存在该服务器 - if (!serverConfig) { - return { status: "undefined", errorMsg: null }; - } + for (const clientId of Object.keys(config.mcpServers)) { + const status = clientsMap.get(clientId); + const serverConfig = config.mcpServers[clientId]; - // 如果服务器配置为暂停状态 - if (serverConfig.status === "paused") { - return { status: "paused", errorMsg: null }; - } + if (!serverConfig) { + result[clientId] = { status: "undefined", errorMsg: null }; + continue; + } - // 如果 clientsMap 中没有记录 - if (!status) { - return { status: "undefined", errorMsg: null }; - } + if (serverConfig.status === "paused") { + result[clientId] = { status: "paused", errorMsg: null }; + continue; + } - // 如果有错误 - if (status.errorMsg) { - return { status: "error", errorMsg: status.errorMsg }; - } + if (!status) { + result[clientId] = { status: "undefined", errorMsg: null }; + continue; + } - // 如果客户端正常运行 - if (status.client) { - return { status: "active", errorMsg: null }; + if ( + status.client === null && + status.tools === null && + status.errorMsg === null + ) { + result[clientId] = { status: "initializing", errorMsg: null }; + continue; + } + + if (status.errorMsg) { + result[clientId] = { status: "error", errorMsg: status.errorMsg }; + continue; + } + + if (status.client) { + result[clientId] = { status: "active", errorMsg: null }; + continue; + } + + result[clientId] = { status: "error", errorMsg: "Client not found" }; } - // 如果客户端不存在 - return { status: "error", errorMsg: "Client not found" }; + return result; } // 获取客户端工具 @@ -96,22 +110,32 @@ async function initializeSingleClient( } logger.info(`Initializing client [${clientId}]...`); - try { - const client = await createClient(clientId, serverConfig); - const tools = await listTools(client); - logger.info( - `Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`, - ); - clientsMap.set(clientId, { client, tools, errorMsg: null }); - logger.success(`Client [${clientId}] initialized successfully`); - } catch (error) { - clientsMap.set(clientId, { - client: null, - tools: null, - errorMsg: error instanceof Error ? error.message : String(error), + + // 先设置初始化状态 + clientsMap.set(clientId, { + client: null, + tools: null, + errorMsg: null, // null 表示正在初始化 + }); + + // 异步初始化 + createClient(clientId, serverConfig) + .then(async (client) => { + const tools = await listTools(client); + logger.info( + `Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`, + ); + clientsMap.set(clientId, { client, tools, errorMsg: null }); + logger.success(`Client [${clientId}] initialized successfully`); + }) + .catch((error) => { + clientsMap.set(clientId, { + client: null, + tools: null, + errorMsg: error instanceof Error ? error.message : String(error), + }); + logger.error(`Failed to initialize client [${clientId}]: ${error}`); }); - logger.error(`Failed to initialize client [${clientId}]: ${error}`); - } } // 初始化系统 @@ -184,7 +208,7 @@ export async function pauseMcpServer(clientId: string) { ...currentConfig.mcpServers, [clientId]: { ...serverConfig, - status: "paused" as const, + status: "paused", }, }, }; @@ -205,7 +229,7 @@ export async function pauseMcpServer(clientId: string) { } // 恢复服务器 -export async function resumeMcpServer(clientId: string): Promise { +export async function resumeMcpServer(clientId: string): Promise { try { const currentConfig = await getMcpConfigFromFile(); const serverConfig = currentConfig.mcpServers[clientId]; @@ -233,10 +257,6 @@ export async function resumeMcpServer(clientId: string): Promise { }, }; await updateMcpConfig(newConfig); - - // 再次确认状态 - const status = await getClientStatus(clientId); - return status.status === "active"; } catch (error) { const currentConfig = await getMcpConfigFromFile(); const serverConfig = currentConfig.mcpServers[clientId]; @@ -254,7 +274,7 @@ export async function resumeMcpServer(clientId: string): Promise { errorMsg: error instanceof Error ? error.message : String(error), }); logger.error(`Failed to initialize client [${clientId}]: ${error}`); - return false; + throw error; } } catch (error) { logger.error(`Failed to resume server [${clientId}]: ${error}`); @@ -297,6 +317,7 @@ export async function restartAllClients() { await removeClient(client.client); } } + // 清空状态 clientsMap.clear(); @@ -350,21 +371,11 @@ async function updateMcpConfig(config: McpConfigData): Promise { } } -// 重新初始化单个客户端 -export async function reinitializeClient(clientId: string) { - const config = await getMcpConfigFromFile(); - const serverConfig = config.mcpServers[clientId]; - if (!serverConfig) { - throw new Error(`Server config not found for client ${clientId}`); - } - await initializeSingleClient(clientId, serverConfig); -} - // 检查 MCP 是否启用 export async function isMcpEnabled() { try { const serverConfig = getServerSideConfig(); - return !!serverConfig.enableMcp; + return serverConfig.enableMcp; } catch (error) { logger.error(`Failed to check MCP status: ${error}`); return false; diff --git a/app/mcp/types.ts b/app/mcp/types.ts index 85e94f3b8a6..45d1d979a98 100644 --- a/app/mcp/types.ts +++ b/app/mcp/types.ts @@ -73,7 +73,16 @@ export interface ListToolsResponse { }; } -export type McpClientData = McpActiveClient | McpErrorClient; +export type McpClientData = + | McpActiveClient + | McpErrorClient + | McpInitializingClient; + +interface McpInitializingClient { + client: null; + tools: null; + errorMsg: null; +} interface McpActiveClient { client: Client; @@ -88,7 +97,12 @@ interface McpErrorClient { } // 服务器状态类型 -export type ServerStatus = "undefined" | "active" | "paused" | "error"; +export type ServerStatus = + | "undefined" + | "active" + | "paused" + | "error" + | "initializing"; export interface ServerStatusResponse { status: ServerStatus;