perf: mcp server status checking logic

This commit is contained in:
Junyan Qin
2025-11-04 17:32:05 +08:00
parent 3ee7736361
commit 1afecf01e4
11 changed files with 182 additions and 90 deletions

View File

@@ -1,10 +1,10 @@
import { MCPServer } from '@/app/infra/entities/api';
import { MCPServer, MCPSessionStatus } from '@/app/infra/entities/api';
export class MCPCardVO {
name: string;
mode: 'stdio' | 'sse';
enable: boolean;
status: 'connected' | 'disconnected' | 'error' | 'disabled';
status: MCPSessionStatus;
tools: number;
error?: string;
@@ -15,45 +15,15 @@ export class MCPCardVO {
// Determine status from runtime_info
if (!data.runtime_info) {
this.status = 'disconnected';
this.status = MCPSessionStatus.ERROR;
this.tools = 0;
} else if (data.runtime_info.connected) {
this.status = 'connected';
} else if (data.runtime_info.status === MCPSessionStatus.CONNECTED) {
this.status = data.runtime_info.status;
this.tools = data.runtime_info.tool_count || 0;
} else {
this.status = 'error';
this.status = data.runtime_info.status;
this.tools = 0;
this.error = data.runtime_info.error_message;
}
}
getStatusColor(): string {
switch (this.status) {
case 'connected':
return 'text-green-600';
case 'disconnected':
return 'text-gray-500';
case 'error':
return 'text-red-600';
case 'disabled':
return 'text-gray-400';
default:
return 'text-gray-500';
}
}
getStatusIcon(): string {
switch (this.status) {
case 'connected':
return 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disconnected':
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'error':
return 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z';
case 'disabled':
return 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636';
default:
return 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z';
}
}
}

View File

@@ -1,9 +1,10 @@
'use client';
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import MCPCardComponent from '@/app/home/plugins/mcp-server/mcp-card/MCPCardComponent';
import { MCPCardVO } from '@/app/home/plugins/mcp-server/MCPCardVO';
import { useTranslation } from 'react-i18next';
import { MCPSessionStatus } from '@/app/infra/entities/api';
import { httpClient } from '@/app/infra/http/HttpClient';
@@ -16,11 +17,44 @@ export default function MCPComponent({
const { t } = useTranslation();
const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
const [loading, setLoading] = useState(false);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
fetchInstalledServers();
return () => {
// Cleanup: clear polling interval when component unmounts
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
};
}, []);
// Check if any server is connecting and start/stop polling accordingly
useEffect(() => {
const hasConnecting = installedServers.some(
(server) => server.status === MCPSessionStatus.CONNECTING,
);
if (hasConnecting && !pollingIntervalRef.current) {
// Start polling every 3 seconds
pollingIntervalRef.current = setInterval(() => {
fetchInstalledServers();
}, 3000);
} else if (!hasConnecting && pollingIntervalRef.current) {
// Stop polling when no server is connecting
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
}, [installedServers]);
function fetchInstalledServers() {
setLoading(true);
httpClient

View File

@@ -5,7 +5,8 @@ import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { RefreshCcw, Wrench, Ban, AlertCircle } from 'lucide-react';
import { RefreshCcw, Wrench, Ban, AlertCircle, Loader2 } from 'lucide-react';
import { MCPSessionStatus } from '@/app/infra/entities/api';
export default function MCPCardComponent({
cardVO,
@@ -110,7 +111,7 @@ export default function MCPCardComponent({
{t('mcp.statusDisabled')}
</div>
</div>
) : status === 'connected' ? (
) : status === MCPSessionStatus.CONNECTED ? (
// 连接成功 - 显示工具数量
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
<Wrench className="w-5 h-5" />
@@ -118,6 +119,14 @@ export default function MCPCardComponent({
{t('mcp.toolCount', { count: toolsCount })}
</div>
</div>
) : status === MCPSessionStatus.CONNECTING ? (
// 连接中 - 蓝色加载
<div className="flex flex-row items-center gap-[0.4rem]">
<Loader2 className="w-4 h-4 text-blue-500 dark:text-blue-400 animate-spin" />
<div className="text-sm text-blue-500 dark:text-blue-400 font-medium">
{t('mcp.connecting')}
</div>
</div>
) : (
// 连接失败 - 红色
<div className="flex flex-row items-center gap-[0.4rem]">

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Resolver, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -42,9 +42,10 @@ import {
MCPServerRuntimeInfo,
MCPTool,
MCPServer,
MCPSessionStatus,
} from '@/app/infra/entities/api';
// Status Display Component - 在测试中或连接失败时使用
// Status Display Component - 在测试中、连接中或连接失败时使用
function StatusDisplay({
testing,
runtimeInfo,
@@ -82,6 +83,35 @@ function StatusDisplay({
);
}
// 连接中
if (runtimeInfo.status === MCPSessionStatus.CONNECTING) {
return (
<div className="flex items-center gap-2 text-blue-600">
<svg
className="w-5 h-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<span className="font-medium">{t('mcp.connecting')}</span>
</div>
);
}
// 连接失败
return (
<div className="space-y-1">
@@ -201,6 +231,7 @@ export default function MCPFormDialog({
const [runtimeInfo, setRuntimeInfo] = useState<MCPServerRuntimeInfo | null>(
null,
);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
// Load server data when editing
useEffect(() => {
@@ -212,8 +243,48 @@ export default function MCPFormDialog({
setExtraArgs([]);
setRuntimeInfo(null);
}
// Cleanup polling interval when dialog closes
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
}, [open, isEditMode, serverName]);
// Poll for updates when runtime_info status is CONNECTING
useEffect(() => {
if (
!open ||
!isEditMode ||
!serverName ||
!runtimeInfo ||
runtimeInfo.status !== MCPSessionStatus.CONNECTING
) {
// Stop polling if conditions are not met
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
return;
}
// Start polling if not already running
if (!pollingIntervalRef.current) {
pollingIntervalRef.current = setInterval(() => {
loadServerForEdit(serverName);
}, 3000);
}
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
}, [open, isEditMode, serverName, runtimeInfo?.status]);
async function loadServerForEdit(serverName: string) {
try {
const resp = await httpClient.getMCPServer(serverName);
@@ -312,7 +383,7 @@ export default function MCPFormDialog({
taskResp.runtime.exception || t('mcp.unknownError');
toast.error(`${t('mcp.testError')}: ${errorMsg}`);
setRuntimeInfo({
connected: false,
status: MCPSessionStatus.ERROR,
error_message: errorMsg,
tool_count: 0,
tools: [],
@@ -387,7 +458,8 @@ export default function MCPFormDialog({
{isEditMode && runtimeInfo && (
<div className="mb-4 space-y-3">
{/* 测试中或连接失败时显示状态 */}
{(mcpTesting || !runtimeInfo.connected) && (
{(mcpTesting ||
runtimeInfo.status !== MCPSessionStatus.CONNECTED) && (
<div className="p-3 rounded-lg border">
<StatusDisplay
testing={mcpTesting}
@@ -399,7 +471,7 @@ export default function MCPFormDialog({
{/* 连接成功时只显示工具列表 */}
{!mcpTesting &&
runtimeInfo.connected &&
runtimeInfo.status === MCPSessionStatus.CONNECTED &&
runtimeInfo.tools?.length > 0 && (
<ToolsList tools={runtimeInfo.tools} />
)}

View File

@@ -325,8 +325,14 @@ export interface MCPServerExtraArgsSSE {
ssereadtimeout: number;
}
export enum MCPSessionStatus {
CONNECTING = 'connecting',
CONNECTED = 'connected',
ERROR = 'error',
}
export interface MCPServerRuntimeInfo {
connected: boolean;
status: MCPSessionStatus;
error_message: string;
tool_count: number;
tools: MCPTool[];

View File

@@ -311,6 +311,7 @@ const enUS = {
keyName: 'Key Name',
value: 'Value',
testing: 'Testing...',
connecting: 'Connecting...',
testSuccess: 'Connection test successful',
testFailed: 'Connection test failed: ',
testError: 'Connection test error',

View File

@@ -313,6 +313,7 @@ const jaJP = {
keyName: 'キー名',
value: '値',
testing: 'テスト中...',
connecting: '接続中...',
testSuccess: '接続テストに成功しました',
testFailed: '接続テストに失敗しました:',
testError: '接続テストエラー',

View File

@@ -299,6 +299,7 @@ const zhHans = {
keyName: '键名',
value: '值',
testing: '测试中...',
connecting: '连接中...',
testSuccess: '连接测试成功',
testFailed: '连接测试失败:',
testError: '连接测试出错',

View File

@@ -297,6 +297,7 @@ const zhHant = {
keyName: '鍵名',
value: '值',
testing: '測試中...',
connecting: '連接中...',
testSuccess: '連接測試成功',
testFailed: '連接測試失敗:',
testError: '連接測試出錯',