diff --git a/web/src/app/home/plugins/mcp/MCPCardVO.ts b/web/src/app/home/plugins/mcp/MCPCardVO.ts index 45cca9ba..67330b61 100644 --- a/web/src/app/home/plugins/mcp/MCPCardVO.ts +++ b/web/src/app/home/plugins/mcp/MCPCardVO.ts @@ -4,7 +4,7 @@ export class MCPCardVO { name: string; mode: 'stdio' | 'sse'; enable: boolean; - status: 'connected' | 'disconnected' | 'error'; + status: 'connected' | 'disconnected' | 'error' | 'disabled'; tools: number; error?: string; config: MCPServerConfig; @@ -13,9 +13,14 @@ export class MCPCardVO { this.name = data.name; this.mode = data.mode; this.enable = data.enable; - this.status = data.status; + // 将后端返回的 "enabled" 状态映射为 "connected" + this.status = (data.status as string) === 'enabled' + ? 'connected' + : data.status; // tools可能是数组或数字 - this.tools = Array.isArray(data.tools) ? data.tools.length : (data.tools || 0); + this.tools = Array.isArray(data.tools) + ? data.tools.length + : data.tools || 0; this.error = data.error; this.config = data.config; } @@ -28,6 +33,8 @@ export class MCPCardVO { return 'text-gray-500'; case 'error': return 'text-red-600'; + case 'disabled': + return 'text-gray-400'; default: return 'text-gray-500'; } @@ -41,6 +48,8 @@ export class MCPCardVO { 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'; } diff --git a/web/src/app/home/plugins/mcp/MCPComponent.tsx b/web/src/app/home/plugins/mcp/MCPComponent.tsx index 63299b95..b89b743f 100644 --- a/web/src/app/home/plugins/mcp/MCPComponent.tsx +++ b/web/src/app/home/plugins/mcp/MCPComponent.tsx @@ -31,7 +31,7 @@ export interface MCPComponentRef { } // eslint-disable-next-line react/display-name -const MCPComponent = forwardRef((props, ref) => { +const MCPComponent = forwardRef((_props, ref) => { const { t } = useTranslation(); const [serverList, setServerList] = useState([]); const [modalOpen, setModalOpen] = useState(false); @@ -39,6 +39,9 @@ const MCPComponent = forwardRef((props, ref) => { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [serverToDelete, setServerToDelete] = useState(null); const [deleting, setDeleting] = useState(false); + const [autoTestTriggered, setAutoTestTriggered] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [testingServers, setTestingServers] = useState>(new Set()); useEffect(() => { initData(); @@ -46,22 +49,130 @@ const MCPComponent = forwardRef((props, ref) => { }, []); function initData() { - getServerList(); + getServerList(true); } - function getServerList() { + function getServerList(shouldAutoTest: boolean = false) { + console.log('[MCP] Fetching server list...'); httpClient .getMCPServers() .then((value) => { - setServerList(value.servers.map((server) => new MCPCardVO(server))); + const servers = value.servers.map((server) => new MCPCardVO(server)); + console.log( + '[MCP] Server list updated:', + servers.map((s) => ({ + name: s.name, + status: s.status, + tools: s.tools, + })), + ); + setServerList(servers); + + // 自动测试:仅在初始加载且还未触发过自动测试时执行 + if (shouldAutoTest && !autoTestTriggered && servers.length > 0) { + setAutoTestTriggered(true); + testAllServers(servers); + } }) .catch((error) => { toast.error(t('mcp.getServerListError') + error.message); }); } + async function testAllServers(servers: MCPCardVO[]) { + // 为每个服务器启动测试 + console.log('[MCP] Starting tests for all servers:', servers.length); + const testPromises = servers.map((server) => testServer(server.name)); + + // 等待所有测试完成 + try { + await Promise.all(testPromises); + console.log('[MCP] All tests completed, refreshing server list...'); + // 所有测试完成后,延迟1秒再刷新,确保后端状态已更新 + setTimeout(() => { + console.log('[MCP] Refreshing server list after tests'); + getServerList(false); + }, 1000); + } catch (err) { + console.error('[MCP] Some tests failed:', err); + // 即使有失败,也要刷新列表 + setTimeout(() => { + console.log('[MCP] Refreshing server list after test failures'); + getServerList(false); + }, 1000); + } + } + + function testServer(serverName: string): Promise { + return new Promise((resolve, reject) => { + // 标记为正在测试 + console.log(`[MCP] Starting test for server: ${serverName}`); + setTestingServers((prev) => new Set(prev).add(serverName)); + + httpClient + .testMCPServer(serverName) + .then((resp) => { + const taskId = resp.task_id; + console.log( + `[MCP] Test task created for ${serverName}, task_id: ${taskId}`, + ); + // 监控任务状态 + const interval = setInterval(() => { + httpClient + .getAsyncTask(taskId) + .then((taskResp) => { + if (taskResp.runtime.done) { + clearInterval(interval); + // 标记测试完成 + setTestingServers((prev) => { + const newSet = new Set(prev); + newSet.delete(serverName); + return newSet; + }); + + if (taskResp.runtime.exception) { + console.error( + `[MCP] Test failed for ${serverName}:`, + taskResp.runtime.exception, + ); + reject(new Error(taskResp.runtime.exception)); + } else { + console.log( + `[MCP] Test completed successfully for ${serverName}`, + ); + resolve(); + } + } + }) + .catch((err) => { + clearInterval(interval); + setTestingServers((prev) => { + const newSet = new Set(prev); + newSet.delete(serverName); + return newSet; + }); + console.error( + `[MCP] Error monitoring task for ${serverName}:`, + err, + ); + reject(err); + }); + }, 1000); + }) + .catch((err) => { + console.error(`[MCP] Failed to start test for ${serverName}:`, err); + setTestingServers((prev) => { + const newSet = new Set(prev); + newSet.delete(serverName); + return newSet; + }); + reject(err); + }); + }); + } + useImperativeHandle(ref, () => ({ - refreshServerList: getServerList, + refreshServerList: () => getServerList(false), createServer: () => { setSelectedServer(null); setModalOpen(true); @@ -99,7 +210,7 @@ const MCPComponent = forwardRef((props, ref) => { toast.error(t('mcp.deleteError') + taskResp.runtime.exception); } else { toast.success(t('mcp.deleteSuccess')); - getServerList(); + getServerList(false); } } }); @@ -128,13 +239,13 @@ const MCPComponent = forwardRef((props, ref) => { ) : (
- {serverList.map((vo, index) => { + {serverList.map((vo) => { return ( -
+
handleServerClick(vo)} - onRefresh={getServerList} + onRefresh={() => getServerList(false)} /> {/* 删除按钮 */} @@ -177,7 +288,7 @@ const MCPComponent = forwardRef((props, ref) => { isEdit={!!selectedServer} onFormSubmit={() => { setModalOpen(false); - getServerList(); + getServerList(false); }} onFormCancel={() => { setModalOpen(false); diff --git a/web/src/app/home/plugins/mcp/mcp-card/MCPCardComponent.tsx b/web/src/app/home/plugins/mcp/mcp-card/MCPCardComponent.tsx index c2d1dfcb..1dd5cb2e 100644 --- a/web/src/app/home/plugins/mcp/mcp-card/MCPCardComponent.tsx +++ b/web/src/app/home/plugins/mcp/mcp-card/MCPCardComponent.tsx @@ -1,5 +1,5 @@ import { MCPCardVO } from '@/app/home/plugins/mcp/MCPCardVO'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; @@ -21,6 +21,51 @@ export default function MCPCardComponent({ const [switchEnable, setSwitchEnable] = useState(true); const [testing, setTesting] = useState(false); const [toolsCount, setToolsCount] = useState(cardVO.tools); + const [status, setStatus] = useState(cardVO.status); + const [error, setError] = useState(cardVO.error); + + // 响应cardVO的变化,更新本地状态 + useEffect(() => { + console.log(`[MCPCard ${cardVO.name}] Status updated:`, { + status: cardVO.status, + tools: cardVO.tools, + error: cardVO.error, + }); + setStatus(cardVO.status); + setError(cardVO.error); + setToolsCount(cardVO.tools); + setEnabled(cardVO.enable); + }, [cardVO.name, cardVO.status, cardVO.error, cardVO.tools, cardVO.enable]); + + function getStatusColor(): string { + switch (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'; + } + } + + function getStatusIcon(): string { + switch (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'; + } + } function handleEnable(checked: boolean) { setSwitchEnable(false); @@ -55,23 +100,39 @@ export default function MCPCardComponent({ } else { // 解析测试结果获取工具数量 try { - let result: { - status?: string; - tools_count?: number; - tools_names_lists?: string[]; - error?: string; - }; + const rawResult = taskResp.runtime.result as + | string + | { + status?: string; + tools_count?: number; + tools_names_lists?: string[]; + error?: string; + } + | undefined; - const rawResult: any = taskResp.runtime.result; - if (typeof rawResult === 'string') { - result = JSON.parse(rawResult.replace(/'/g, '"')); - } else { - result = rawResult as typeof result; - } + if (rawResult) { + let result: { + status?: string; + tools_count?: number; + tools_names_lists?: string[]; + error?: string; + }; - if (result.tools_count !== undefined) { - setToolsCount(result.tools_count); - toast.success(t('mcp.testSuccess') + ` - ${result.tools_count} ${t('mcp.toolsFound')}`); + if (typeof rawResult === 'string') { + result = JSON.parse(rawResult.replace(/'/g, '"')); + } else { + result = rawResult; + } + + if (result.tools_count !== undefined) { + setToolsCount(result.tools_count); + toast.success( + t('mcp.testSuccess') + + ` - ${result.tools_count} ${t('mcp.toolsFound')}`, + ); + } else { + toast.success(t('mcp.testSuccess')); + } } else { toast.success(t('mcp.testSuccess')); } @@ -120,7 +181,7 @@ export default function MCPCardComponent({
-
- {cardVO.status === 'connected' && t('mcp.statusConnected')} - {cardVO.status === 'disconnected' && - t('mcp.statusDisconnected')} - {cardVO.status === 'error' && t('mcp.statusError')} +
+ {status === 'connected' && t('mcp.statusConnected')} + {status === 'disconnected' && t('mcp.statusDisconnected')} + {status === 'error' && t('mcp.statusError')} + {status === 'disabled' && t('mcp.statusDisabled')}
- {cardVO.error && ( + {error && (
- {cardVO.error} + {error}
)}
diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index c8252385..677c91ab 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -324,7 +324,7 @@ export interface MCPServer { mode: 'stdio' | 'sse'; enable: boolean; config: MCPServerConfig; - status: 'connected' | 'disconnected' | 'error'; + status: 'connected' | 'disconnected' | 'error' | 'disabled'; tools: MCPTool[]; error?: string; } diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 91d49597..82942deb 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -320,6 +320,7 @@ const zhHans = { statusConnected: '已连接', statusDisconnected: '未连接', statusError: '连接错误', + statusDisabled: '已禁用', serverStatus: '服务器状态', marketplace: 'MCP商店', searchServer: '搜索MCP服务器',