Ver Fonte

fix: prevent MCP operations from blocking chat interface

Kadxy há 10 meses atrás
pai
commit
bfeea4ed49
4 ficheiros alterados com 132 adições e 127 exclusões
  1. 5 0
      app/components/mcp-market.module.scss
  2. 41 66
      app/components/mcp-market.tsx
  3. 70 59
      app/mcp/actions.ts
  4. 16 2
      app/mcp/types.ts

+ 5 - 0
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;

+ 41 - 66
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<string, any> = {};
-        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<string, any> = {};
-      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: (
+        <span className={clsx(styles["server-status"], styles["initializing"])}>
+          Initializing
+        </span>
+      ),
       paused: (
         <span className={clsx(styles["server-status"], styles["stopped"])}>
           Stopped
@@ -517,10 +486,11 @@ export function McpMarketPage() {
         const statusPriority: Record<string, number> = {
           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)
           );
         }
 

+ 70 - 59
app/mcp/actions.ts

@@ -24,40 +24,54 @@ const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
 const clientsMap = new Map<string, McpClientData>();
 
 // 获取客户端状态
-export async function getClientStatus(
-  clientId: string,
-): Promise<ServerStatusResponse> {
-  const status = clientsMap.get(clientId);
+export async function getClientsStatus(): Promise<
+  Record<string, ServerStatusResponse>
+> {
   const config = await getMcpConfigFromFile();
-  const serverConfig = config.mcpServers[clientId];
+  const result: Record<string, ServerStatusResponse> = {};
 
-  // 如果配置中不存在该服务器
-  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<boolean> {
+export async function resumeMcpServer(clientId: string): Promise<void> {
   try {
     const currentConfig = await getMcpConfigFromFile();
     const serverConfig = currentConfig.mcpServers[clientId];
@@ -233,10 +257,6 @@ export async function resumeMcpServer(clientId: string): Promise<boolean> {
         },
       };
       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<boolean> {
         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<void> {
   }
 }
 
-// 重新初始化单个客户端
-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;

+ 16 - 2
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;