fix: prevent MCP operations from blocking chat interface

This commit is contained in:
Kadxy 2025-01-19 01:02:01 +08:00
parent bc71ae247b
commit bfeea4ed49
4 changed files with 136 additions and 131 deletions

View File

@ -167,6 +167,11 @@
background-color: #6b7280; background-color: #6b7280;
} }
&.initializing {
background-color: #f59e0b;
animation: pulse 1.5s infinite;
}
.error-message { .error-message {
margin-left: 4px; margin-left: 4px;
font-size: 12px; font-size: 12px;

View File

@ -13,7 +13,7 @@ import { useNavigate } from "react-router-dom";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
addMcpServer, addMcpServer,
getClientStatus, getClientsStatus,
getClientTools, getClientTools,
getMcpConfigFromFile, getMcpConfigFromFile,
isMcpEnabled, isMcpEnabled,
@ -71,6 +71,23 @@ export function McpMarketPage() {
checkMcpStatus(); checkMcpStatus();
}, [navigate]); }, [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(() => { useEffect(() => {
const loadPresetServers = async () => { const loadPresetServers = async () => {
@ -103,10 +120,7 @@ export function McpMarketPage() {
setConfig(config); setConfig(config);
// 获取所有客户端的状态 // 获取所有客户端的状态
const statuses: Record<string, any> = {}; const statuses = await getClientsStatus();
for (const clientId of Object.keys(config.mcpServers)) {
statuses[clientId] = await getClientStatus(clientId);
}
setClientStatuses(statuses); setClientStatuses(statuses);
} catch (error) { } catch (error) {
console.error("Failed to load initial state:", error); console.error("Failed to load initial state:", error);
@ -165,7 +179,6 @@ export function McpMarketPage() {
const preset = presetServers.find((s) => s.id === editingServerId); const preset = presetServers.find((s) => s.id === editingServerId);
if (!preset || !preset.configSchema || !editingServerId) return; if (!preset || !preset.configSchema || !editingServerId) return;
// 先关闭模态框
const savingServerId = editingServerId; const savingServerId = editingServerId;
setEditingServerId(undefined); setEditingServerId(undefined);
@ -200,31 +213,8 @@ export function McpMarketPage() {
...(Object.keys(env).length > 0 ? { env } : {}), ...(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); const newConfig = await addMcpServer(savingServerId, serverConfig);
setConfig(newConfig); setConfig(newConfig);
// 只有新增的服务器才需要获取状态(因为会自动启动)
if (isNewServer) {
const status = await getClientStatus(savingServerId);
setClientStatuses((prev) => ({
...prev,
[savingServerId]: status,
}));
}
showToast("Server configuration updated successfully"); showToast("Server configuration updated successfully");
} catch (error) { } catch (error) {
showToast( showToast(
@ -277,11 +267,8 @@ export function McpMarketPage() {
setConfig(newConfig); setConfig(newConfig);
// 更新状态 // 更新状态
const status = await getClientStatus(preset.id); const statuses = await getClientsStatus();
setClientStatuses((prev) => ({ setClientStatuses(statuses);
...prev,
[preset.id]: status,
}));
} finally { } finally {
updateLoadingState(preset.id, null); updateLoadingState(preset.id, null);
} }
@ -298,11 +285,6 @@ export function McpMarketPage() {
updateLoadingState(id, "Stopping server..."); updateLoadingState(id, "Stopping server...");
const newConfig = await pauseMcpServer(id); const newConfig = await pauseMcpServer(id);
setConfig(newConfig); setConfig(newConfig);
setClientStatuses((prev) => ({
...prev,
[id]: { status: "paused", errorMsg: null },
}));
showToast("Server stopped successfully"); showToast("Server stopped successfully");
} catch (error) { } catch (error) {
showToast("Failed to stop server"); showToast("Failed to stop server");
@ -316,19 +298,7 @@ export function McpMarketPage() {
const restartServer = async (id: string) => { const restartServer = async (id: string) => {
try { try {
updateLoadingState(id, "Starting server..."); updateLoadingState(id, "Starting server...");
await resumeMcpServer(id);
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");
}
} catch (error) { } catch (error) {
showToast( showToast(
error instanceof Error error instanceof Error
@ -347,14 +317,7 @@ export function McpMarketPage() {
updateLoadingState("all", "Restarting all servers..."); updateLoadingState("all", "Restarting all servers...");
const newConfig = await restartAllClients(); const newConfig = await restartAllClients();
setConfig(newConfig); setConfig(newConfig);
showToast("Restarting all clients");
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");
} catch (error) { } catch (error) {
showToast("Failed to restart clients"); showToast("Failed to restart clients");
console.error(error); console.error(error);
@ -452,6 +415,12 @@ export function McpMarketPage() {
const statusMap = { const statusMap = {
undefined: null, // 未配置/未找到不显示 undefined: null, // 未配置/未找到不显示
// 添加初始化状态
initializing: (
<span className={clsx(styles["server-status"], styles["initializing"])}>
Initializing
</span>
),
paused: ( paused: (
<span className={clsx(styles["server-status"], styles["stopped"])}> <span className={clsx(styles["server-status"], styles["stopped"])}>
Stopped Stopped
@ -517,10 +486,11 @@ export function McpMarketPage() {
const statusPriority: Record<string, number> = { const statusPriority: Record<string, number> = {
error: 0, // Highest priority for error status error: 0, // Highest priority for error status
active: 1, // Second for active active: 1, // Second for active
starting: 2, // Starting initializing: 2, // Initializing
stopping: 3, // Stopping starting: 3, // Starting
paused: 4, // Paused stopping: 4, // Stopping
undefined: 5, // Lowest priority for undefined paused: 5, // Paused
undefined: 6, // Lowest priority for undefined
}; };
// Get actual status (including loading status) // Get actual status (including loading status)
@ -529,6 +499,11 @@ export function McpMarketPage() {
const operationType = getOperationStatusType(loading); const operationType = getOperationStatusType(loading);
return operationType === "default" ? status : operationType; return operationType === "default" ? status : operationType;
} }
if (status === "initializing" && !loading) {
return "active";
}
return status; return status;
}; };
@ -538,8 +513,8 @@ export function McpMarketPage() {
// 首先按状态排序 // 首先按状态排序
if (aEffectiveStatus !== bEffectiveStatus) { if (aEffectiveStatus !== bEffectiveStatus) {
return ( return (
(statusPriority[aEffectiveStatus] ?? 5) - (statusPriority[aEffectiveStatus] ?? 6) -
(statusPriority[bEffectiveStatus] ?? 5) (statusPriority[bEffectiveStatus] ?? 6)
); );
} }

View File

@ -24,40 +24,54 @@ const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
const clientsMap = new Map<string, McpClientData>(); const clientsMap = new Map<string, McpClientData>();
// 获取客户端状态 // 获取客户端状态
export async function getClientStatus( export async function getClientsStatus(): Promise<
clientId: string, Record<string, ServerStatusResponse>
): Promise<ServerStatusResponse> { > {
const status = clientsMap.get(clientId);
const config = await getMcpConfigFromFile(); const config = await getMcpConfigFromFile();
const serverConfig = config.mcpServers[clientId]; const result: Record<string, ServerStatusResponse> = {};
// 如果配置中不存在该服务器 for (const clientId of Object.keys(config.mcpServers)) {
if (!serverConfig) { const status = clientsMap.get(clientId);
return { status: "undefined", errorMsg: null }; const serverConfig = config.mcpServers[clientId];
if (!serverConfig) {
result[clientId] = { status: "undefined", errorMsg: null };
continue;
}
if (serverConfig.status === "paused") {
result[clientId] = { status: "paused", errorMsg: null };
continue;
}
if (!status) {
result[clientId] = { status: "undefined", errorMsg: null };
continue;
}
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 result;
if (serverConfig.status === "paused") {
return { status: "paused", errorMsg: null };
}
// 如果 clientsMap 中没有记录
if (!status) {
return { status: "undefined", errorMsg: null };
}
// 如果有错误
if (status.errorMsg) {
return { status: "error", errorMsg: status.errorMsg };
}
// 如果客户端正常运行
if (status.client) {
return { status: "active", errorMsg: null };
}
// 如果客户端不存在
return { status: "error", errorMsg: "Client not found" };
} }
// 获取客户端工具 // 获取客户端工具
@ -96,22 +110,32 @@ async function initializeSingleClient(
} }
logger.info(`Initializing client [${clientId}]...`); logger.info(`Initializing client [${clientId}]...`);
try {
const client = await createClient(clientId, serverConfig); // 先设置初始化状态
const tools = await listTools(client); clientsMap.set(clientId, {
logger.info( client: null,
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`, tools: null,
); errorMsg: null, // null 表示正在初始化
clientsMap.set(clientId, { client, tools, errorMsg: null }); });
logger.success(`Client [${clientId}] initialized successfully`);
} catch (error) { // 异步初始化
clientsMap.set(clientId, { createClient(clientId, serverConfig)
client: null, .then(async (client) => {
tools: null, const tools = await listTools(client);
errorMsg: error instanceof Error ? error.message : String(error), 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, ...currentConfig.mcpServers,
[clientId]: { [clientId]: {
...serverConfig, ...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 { try {
const currentConfig = await getMcpConfigFromFile(); const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId]; const serverConfig = currentConfig.mcpServers[clientId];
@ -233,10 +257,6 @@ export async function resumeMcpServer(clientId: string): Promise<boolean> {
}, },
}; };
await updateMcpConfig(newConfig); await updateMcpConfig(newConfig);
// 再次确认状态
const status = await getClientStatus(clientId);
return status.status === "active";
} catch (error) { } catch (error) {
const currentConfig = await getMcpConfigFromFile(); const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId]; 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), errorMsg: error instanceof Error ? error.message : String(error),
}); });
logger.error(`Failed to initialize client [${clientId}]: ${error}`); logger.error(`Failed to initialize client [${clientId}]: ${error}`);
return false; throw error;
} }
} catch (error) { } catch (error) {
logger.error(`Failed to resume server [${clientId}]: ${error}`); logger.error(`Failed to resume server [${clientId}]: ${error}`);
@ -297,6 +317,7 @@ export async function restartAllClients() {
await removeClient(client.client); await removeClient(client.client);
} }
} }
// 清空状态 // 清空状态
clientsMap.clear(); 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 是否启用 // 检查 MCP 是否启用
export async function isMcpEnabled() { export async function isMcpEnabled() {
try { try {
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
return !!serverConfig.enableMcp; return serverConfig.enableMcp;
} catch (error) { } catch (error) {
logger.error(`Failed to check MCP status: ${error}`); logger.error(`Failed to check MCP status: ${error}`);
return false; return false;

View File

@ -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 { interface McpActiveClient {
client: Client; 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 { export interface ServerStatusResponse {
status: ServerStatus; status: ServerStatus;