From 96c84740dbe266bc9f5d6c97c714042854e4a9f1 Mon Sep 17 00:00:00 2001 From: Hyu Date: Fri, 3 Jul 2026 16:52:15 +0800 Subject: [PATCH] feat(mcp): add logs tab to MCP server detail panel (frontend) (#2310) * feat(mcp): add logs tab to MCP server detail panel - Add MCPLogs component mirroring PluginLogs functionality - Add getMcpServerLogs API method to BackendClient - Add logs tab to MCPForm detail panel (edit mode only) - Add i18n keys (tabLogs, logsLevelAll, logsRefresh, logsAutoRefresh, logsEmpty) to all 8 locales - Auto-refresh logs every 3s with toggle - Filter logs by level (ALL/DEBUG/INFO/WARNING/ERROR) * style: fix prettier formatting in MCPForm.tsx logs tab * style: fix prettier in i18n locale files --------- Co-authored-by: dadachann Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com> --- .../home/mcp/components/mcp-form/MCPForm.tsx | 6 + .../home/mcp/components/mcp-form/MCPLogs.tsx | 149 ++++++++++++++++++ web/src/app/infra/http/BackendClient.ts | 15 ++ web/src/i18n/locales/en-US.ts | 6 + web/src/i18n/locales/es-ES.ts | 6 + web/src/i18n/locales/ja-JP.ts | 5 + web/src/i18n/locales/ru-RU.ts | 6 + web/src/i18n/locales/th-TH.ts | 5 + web/src/i18n/locales/vi-VN.ts | 6 + web/src/i18n/locales/zh-Hans.ts | 5 + web/src/i18n/locales/zh-Hant.ts | 5 + 11 files changed, 214 insertions(+) create mode 100644 web/src/app/home/mcp/components/mcp-form/MCPLogs.tsx diff --git a/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx b/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx index 4b6188652..3f3d3a2bf 100644 --- a/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx +++ b/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx @@ -1207,6 +1207,9 @@ const MCPForm = forwardRef(function MCPForm( {resourcesTabLabel} + + {t('mcp.tabLogs')} + @@ -1235,6 +1238,9 @@ const MCPForm = forwardRef(function MCPForm( t={t} /> + + {persistedServerName && } + ) : ( runtimePanel diff --git a/web/src/app/home/mcp/components/mcp-form/MCPLogs.tsx b/web/src/app/home/mcp/components/mcp-form/MCPLogs.tsx new file mode 100644 index 000000000..16d2547a4 --- /dev/null +++ b/web/src/app/home/mcp/components/mcp-form/MCPLogs.tsx @@ -0,0 +1,149 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { httpClient } from '@/app/infra/http/HttpClient'; +import { useTranslation } from 'react-i18next'; +import { PluginLogEntry } from '@/app/infra/entities/plugin'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { RefreshCw } from 'lucide-react'; + +const LEVEL_OPTIONS = ['ALL', 'DEBUG', 'INFO', 'WARNING', 'ERROR'] as const; + +function levelClassName(level: string): string { + switch (level) { + case 'ERROR': + case 'CRITICAL': + return 'text-red-500'; + case 'WARNING': + return 'text-amber-500'; + case 'DEBUG': + return 'text-gray-400 dark:text-gray-500'; + default: + return 'text-gray-700 dark:text-gray-300'; + } +} + +export default function MCPLogs({ serverName }: { serverName: string }) { + const { t } = useTranslation(); + const [logs, setLogs] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [level, setLevel] = useState('ALL'); + const [autoRefresh, setAutoRefresh] = useState(true); + const scrollRef = useRef(null); + const atBottomRef = useRef(true); + + const fetchLogs = useCallback(() => { + setIsLoading(true); + httpClient + .getMcpServerLogs(serverName, 500, level === 'ALL' ? undefined : level) + .then((res) => { + setLogs(res.logs ?? []); + }) + .catch(() => { + setLogs([]); + }) + .finally(() => { + setIsLoading(false); + }); + }, [serverName, level]); + + useEffect(() => { + fetchLogs(); + }, [fetchLogs]); + + // Auto-refresh poll loop. + useEffect(() => { + if (!autoRefresh) return; + const timer = setInterval(fetchLogs, 3000); + return () => clearInterval(timer); + }, [autoRefresh, fetchLogs]); + + // Keep view pinned to bottom when the user is already at the bottom. + useEffect(() => { + const el = scrollRef.current; + if (el && atBottomRef.current) { + el.scrollTop = el.scrollHeight; + } + }, [logs]); + + function handleScroll() { + const el = scrollRef.current; + if (!el) return; + atBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40; + } + + return ( +
+
+ + +
+ + +
+
+ +
+ {logs.length === 0 ? ( +
+ {t('mcp.logsEmpty')} +
+ ) : ( + logs.map((entry, idx) => ( +
+ {entry.text} +
+ )) + )} +
+
+ ); +} diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index c4f8b6e3c..371b82942 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -671,6 +671,21 @@ export class BackendClient extends BaseHttpClient { ); } + public getMcpServerLogs( + serverName: string, + limit: number = 200, + level?: string, + ): Promise<{ logs: PluginLogEntry[] }> { + const params = new URLSearchParams(); + params.set('limit', String(limit)); + if (level) { + params.set('level', level); + } + return this.get( + `/api/v1/mcp/servers/${encodeURIComponent(serverName)}/logs?${params.toString()}`, + ); + } + public getPluginAssetURL( author: string, name: string, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index f30ba8536..1d2d58ae2 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -828,6 +828,12 @@ const enUS = { tabTools: 'Tools', tabResources: 'Resources', tabDocs: 'Docs', + tabLogs: 'Logs', + logsLevelAll: 'All levels', + logsRefresh: 'Refresh', + logsAutoRefresh: 'Auto refresh', + logsEmpty: + 'No logs yet. Runtime logs from the MCP server will appear here.', noReadme: 'No documentation available', parseResultFailed: 'Failed to parse test result', noResultReturned: 'Test returned no result', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 94a49efa7..a39c69c6e 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -842,6 +842,12 @@ const esES = { tabTools: 'Herramientas', tabResources: 'Recursos', tabDocs: 'Documentación', + tabLogs: 'Registros', + logsLevelAll: 'Todos los niveles', + logsRefresh: 'Actualizar', + logsAutoRefresh: 'Actualización automática', + logsEmpty: + 'Aún no hay registros. Los registros de ejecución del servidor MCP aparecerán aquí.', noReadme: 'No hay documentación disponible', parseResultFailed: 'Error al analizar el resultado de la prueba', noResultReturned: 'La prueba no devolvió resultados', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 5f171e8b1..0d6ca7363 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -834,6 +834,11 @@ const jaJP = { tabTools: 'ツール', tabResources: 'リソース', tabDocs: 'ドキュメント', + tabLogs: 'ログ', + logsLevelAll: 'すべてのレベル', + logsRefresh: '更新', + logsAutoRefresh: '自動更新', + logsEmpty: 'ログはありません。MCPサーバーの実行ログがここに表示されます。', noReadme: 'ドキュメントがありません', parseResultFailed: 'テスト結果の解析に失敗しました', noResultReturned: 'テスト結果が返されませんでした', diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index 5bdabf355..5c9a38791 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -839,6 +839,12 @@ const ruRU = { tabTools: 'Инструменты', tabResources: 'Ресурсы', tabDocs: 'Документация', + tabLogs: 'Журнал', + logsLevelAll: 'Все уровни', + logsRefresh: 'Обновить', + logsAutoRefresh: 'Автообновление', + logsEmpty: + 'Журналов пока нет. Здесь будут отображаться журналы выполнения MCP-сервера.', noReadme: 'Документация отсутствует', parseResultFailed: 'Не удалось разобрать результат теста', noResultReturned: 'Тест не вернул результат', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index 54bafcf9c..e714ef5e5 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -817,6 +817,11 @@ const thTH = { tabTools: 'เครื่องมือ', tabResources: 'ทรัพยากร', tabDocs: 'เอกสาร', + tabLogs: 'บันทึก', + logsLevelAll: 'ทุกระดับ', + logsRefresh: 'รีเฟรช', + logsAutoRefresh: 'รีเฟรชอัตโนมัติ', + logsEmpty: 'ยังไม่มีบันทึก บันทึกการทำงานของ MCP Server จะแสดงที่นี่', noReadme: 'ไม่มีเอกสาร', parseResultFailed: 'ไม่สามารถแยกวิเคราะห์ผลการทดสอบได้', noResultReturned: 'การทดสอบไม่ส่งผลลัพธ์กลับมา', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index 77d17040a..99a39b2c2 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -832,6 +832,12 @@ const viVN = { tabTools: 'Công cụ', tabResources: 'Tài nguyên', tabDocs: 'Tài liệu', + tabLogs: 'Nhật ký', + logsLevelAll: 'Tất cả cấp độ', + logsRefresh: 'Làm mới', + logsAutoRefresh: 'Tự động làm mới', + logsEmpty: + 'Chưa có nhật ký. Nhật ký chạy của MCP Server sẽ hiển thị ở đây.', noReadme: 'Không có tài liệu', parseResultFailed: 'Phân tích kết quả kiểm tra thất bại', noResultReturned: 'Kiểm tra không trả về kết quả', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 61cc70f57..972ec041d 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -794,6 +794,11 @@ const zhHans = { tabTools: '工具', tabResources: '资源', tabDocs: '文档', + tabLogs: '日志', + logsLevelAll: '全部级别', + logsRefresh: '刷新', + logsAutoRefresh: '自动刷新', + logsEmpty: '暂无日志。MCP 服务器的运行日志会显示在这里。', noReadme: '暂无文档', parseResultFailed: '解析测试结果失败', noResultReturned: '测试未返回结果', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 1f86d60ac..61ecc4ea2 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -793,6 +793,11 @@ const zhHant = { tabTools: '工具', tabResources: '資源', tabDocs: '文件', + tabLogs: '日誌', + logsLevelAll: '全部級別', + logsRefresh: '重新整理', + logsAutoRefresh: '自動重新整理', + logsEmpty: '暫無日誌。MCP 服務器的運行日誌會顯示在這裡。', noReadme: '暫無文件', parseResultFailed: '解析測試結果失敗', noResultReturned: '測試未返回結果',