From 96085707cea18cd6c9b234dc827b332f70a58a26 Mon Sep 17 00:00:00 2001 From: LiuVaayne <10231735+vaayne@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:18:56 +0800 Subject: [PATCH] feat: add MCP server log viewer (#11826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: add MCP server log viewer * 🧹 chore: format files * 🐛 fix: resolve MCP log viewer type errors * 🧹 chore: sync i18n for MCP log viewer * 💄 fix: improve MCP log modal contrast in dark mode * 🌐 fix: translate MCP log viewer strings Add translations for noLogs and viewLogs keys in: - German (de-de) - Greek (el-gr) - Spanish (es-es) - French (fr-fr) - Japanese (ja-jp) - Portuguese (pt-pt) - Russian (ru-ru) * 🌐 fix: update MCP log viewer translations and key references Added "logs" key to various language files and updated references in the MCP settings component to improve consistency across translations. This includes updates for English, Chinese (Simplified and Traditional), German, Greek, Spanish, French, Japanese, Portuguese, and Russian. --------- Co-authored-by: kangfenmao --- packages/shared/IpcChannel.ts | 2 + packages/shared/config/types.ts | 8 ++ src/main/index.ts | 2 +- src/main/ipc.ts | 2 + src/main/services/MCPService.ts | 94 +++++++++++++- .../__tests__/ServerLogBuffer.test.ts | 29 +++++ src/main/services/mcp/ServerLogBuffer.ts | 36 ++++++ src/preload/index.ts | 12 +- src/renderer/src/i18n/locales/en-us.json | 2 + src/renderer/src/i18n/locales/zh-cn.json | 2 + src/renderer/src/i18n/locales/zh-tw.json | 2 + src/renderer/src/i18n/translate/de-de.json | 2 + src/renderer/src/i18n/translate/el-gr.json | 2 + src/renderer/src/i18n/translate/es-es.json | 2 + src/renderer/src/i18n/translate/fr-fr.json | 2 + src/renderer/src/i18n/translate/ja-jp.json | 2 + src/renderer/src/i18n/translate/pt-pt.json | 2 + src/renderer/src/i18n/translate/ru-ru.json | 2 + .../settings/MCPSettings/McpSettings.tsx | 117 +++++++++++++++++- 19 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 src/main/services/__tests__/ServerLogBuffer.test.ts create mode 100644 src/main/services/mcp/ServerLogBuffer.ts diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 1c61745a60..88e7ae85d5 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -90,6 +90,8 @@ export enum IpcChannel { Mcp_AbortTool = 'mcp:abort-tool', Mcp_GetServerVersion = 'mcp:get-server-version', Mcp_Progress = 'mcp:progress', + Mcp_GetServerLogs = 'mcp:get-server-logs', + Mcp_ServerLog = 'mcp:server-log', // Python Python_Execute = 'python:execute', diff --git a/packages/shared/config/types.ts b/packages/shared/config/types.ts index 8fba6399f8..7dff53c753 100644 --- a/packages/shared/config/types.ts +++ b/packages/shared/config/types.ts @@ -23,6 +23,14 @@ export type MCPProgressEvent = { progress: number // 0-1 range } +export type MCPServerLogEntry = { + timestamp: number + level: 'debug' | 'info' | 'warn' | 'error' | 'stderr' | 'stdout' + message: string + data?: any + source?: string +} + export type WebviewKeyEvent = { webviewId: number key: string diff --git a/src/main/index.ts b/src/main/index.ts index 56750e6b61..3588a370ff 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -19,8 +19,8 @@ import { agentService } from './services/agents' import { apiServerService } from './services/ApiServerService' import { appMenuService } from './services/AppMenuService' import { configManager } from './services/ConfigManager' -import mcpService from './services/MCPService' import { nodeTraceService } from './services/NodeTraceService' +import mcpService from './services/MCPService' import powerMonitorService from './services/PowerMonitorService' import { CHERRY_STUDIO_PROTOCOL, diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 444ca5fb8e..714292c67e 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -765,6 +765,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity) ipcMain.handle(IpcChannel.Mcp_AbortTool, mcpService.abortTool) ipcMain.handle(IpcChannel.Mcp_GetServerVersion, mcpService.getServerVersion) + ipcMain.handle(IpcChannel.Mcp_GetServerLogs, mcpService.getServerLogs) + ipcMain.handle(IpcChannel.Mcp_GetServerLogs, mcpService.getServerLogs) // DXT upload handler ipcMain.handle(IpcChannel.Mcp_UploadDxt, async (event, fileBuffer: ArrayBuffer, fileName: string) => { diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 3925376226..f9b43f039d 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -33,6 +33,7 @@ import { import { nanoid } from '@reduxjs/toolkit' import { HOME_CHERRY_DIR } from '@shared/config/constant' import type { MCPProgressEvent } from '@shared/config/types' +import type { MCPServerLogEntry } from '@shared/config/types' import { IpcChannel } from '@shared/IpcChannel' import { defaultAppHeaders } from '@shared/utils' import { @@ -56,6 +57,7 @@ import { CacheService } from './CacheService' import DxtService from './DxtService' import { CallBackServer } from './mcp/oauth/callback' import { McpOAuthClientProvider } from './mcp/oauth/provider' +import { ServerLogBuffer } from './mcp/ServerLogBuffer' import { windowService } from './WindowService' // Generic type for caching wrapped functions @@ -142,6 +144,7 @@ class McpService { private pendingClients: Map> = new Map() private dxtService = new DxtService() private activeToolCalls: Map = new Map() + private serverLogs = new ServerLogBuffer(200) constructor() { this.initClient = this.initClient.bind(this) @@ -172,6 +175,19 @@ class McpService { }) } + private emitServerLog(server: MCPServer, entry: MCPServerLogEntry) { + const serverKey = this.getServerKey(server) + this.serverLogs.append(serverKey, entry) + const mainWindow = windowService.getMainWindow() + if (mainWindow) { + mainWindow.webContents.send(IpcChannel.Mcp_ServerLog, { ...entry, serverId: server.id }) + } + } + + public getServerLogs(_: Electron.IpcMainInvokeEvent, server: MCPServer): MCPServerLogEntry[] { + return this.serverLogs.get(this.getServerKey(server)) + } + async initClient(server: MCPServer): Promise { const serverKey = this.getServerKey(server) @@ -366,9 +382,25 @@ class McpService { } const stdioTransport = new StdioClientTransport(transportOptions) - stdioTransport.stderr?.on('data', (data) => - getServerLogger(server).debug(`Stdio stderr`, { data: data.toString() }) - ) + stdioTransport.stderr?.on('data', (data) => { + const msg = data.toString() + getServerLogger(server).debug(`Stdio stderr`, { data: msg }) + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'stderr', + message: msg.trim(), + source: 'stdio' + }) + }) + ;(stdioTransport as any).stdout?.on('data', (data: any) => { + const msg = data.toString() + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'stdout', + message: msg.trim(), + source: 'stdio' + }) + }) return stdioTransport } else { throw new Error('Either baseUrl or command must be provided') @@ -436,6 +468,13 @@ class McpService { } } + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'info', + message: 'Server connected', + source: 'client' + }) + // Store the new client in the cache this.clients.set(serverKey, client) @@ -446,9 +485,22 @@ class McpService { this.clearServerCache(serverKey) logger.debug(`Activated server: ${server.name}`) + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'info', + message: 'Server activated', + source: 'client' + }) return client } catch (error) { getServerLogger(server).error(`Error activating server ${server.name}`, error as Error) + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'error', + message: `Error activating server: ${(error as Error)?.message}`, + data: redactSensitive(error), + source: 'client' + }) throw error } } finally { @@ -506,6 +558,16 @@ class McpService { // Set up logging message notification handler client.setNotificationHandler(LoggingMessageNotificationSchema, async (notification) => { logger.debug(`Message from server ${server.name}:`, notification.params) + const msg = notification.params?.message + if (msg) { + this.emitServerLog(server, { + timestamp: Date.now(), + level: (notification.params?.level as MCPServerLogEntry['level']) || 'info', + message: typeof msg === 'string' ? msg : JSON.stringify(msg), + data: redactSensitive(notification.params?.data), + source: notification.params?.logger || 'server' + }) + } }) getServerLogger(server).debug(`Set up notification handlers`) @@ -540,6 +602,7 @@ class McpService { this.clients.delete(serverKey) // Clear all caches for this server this.clearServerCache(serverKey) + this.serverLogs.remove(serverKey) } else { logger.warn(`No client found for server`, { serverKey }) } @@ -548,6 +611,12 @@ class McpService { async stopServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) { const serverKey = this.getServerKey(server) getServerLogger(server).debug(`Stopping server`) + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'info', + message: 'Stopping server', + source: 'client' + }) await this.closeClient(serverKey) } @@ -574,6 +643,12 @@ class McpService { async restartServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) { getServerLogger(server).debug(`Restarting server`) const serverKey = this.getServerKey(server) + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'info', + message: 'Restarting server', + source: 'client' + }) await this.closeClient(serverKey) // Clear cache before restarting to ensure fresh data this.clearServerCache(serverKey) @@ -606,9 +681,22 @@ class McpService { // Attempt to list tools as a way to check connectivity await client.listTools() getServerLogger(server).debug(`Connectivity check successful`) + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'info', + message: 'Connectivity check successful', + source: 'connectivity' + }) return true } catch (error) { getServerLogger(server).error(`Connectivity check failed`, error as Error) + this.emitServerLog(server, { + timestamp: Date.now(), + level: 'error', + message: `Connectivity check failed: ${(error as Error).message}`, + data: redactSensitive(error), + source: 'connectivity' + }) // Close the client if connectivity check fails to ensure a clean state for the next attempt const serverKey = this.getServerKey(server) await this.closeClient(serverKey) diff --git a/src/main/services/__tests__/ServerLogBuffer.test.ts b/src/main/services/__tests__/ServerLogBuffer.test.ts new file mode 100644 index 0000000000..0b7abe91e8 --- /dev/null +++ b/src/main/services/__tests__/ServerLogBuffer.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' + +import { ServerLogBuffer } from '../mcp/ServerLogBuffer' + +describe('ServerLogBuffer', () => { + it('keeps a bounded number of entries per server', () => { + const buffer = new ServerLogBuffer(3) + const key = 'srv' + + buffer.append(key, { timestamp: 1, level: 'info', message: 'a' }) + buffer.append(key, { timestamp: 2, level: 'info', message: 'b' }) + buffer.append(key, { timestamp: 3, level: 'info', message: 'c' }) + buffer.append(key, { timestamp: 4, level: 'info', message: 'd' }) + + const logs = buffer.get(key) + expect(logs).toHaveLength(3) + expect(logs[0].message).toBe('b') + expect(logs[2].message).toBe('d') + }) + + it('isolates entries by server key', () => { + const buffer = new ServerLogBuffer(5) + buffer.append('one', { timestamp: 1, level: 'info', message: 'a' }) + buffer.append('two', { timestamp: 2, level: 'info', message: 'b' }) + + expect(buffer.get('one')).toHaveLength(1) + expect(buffer.get('two')).toHaveLength(1) + }) +}) diff --git a/src/main/services/mcp/ServerLogBuffer.ts b/src/main/services/mcp/ServerLogBuffer.ts new file mode 100644 index 0000000000..01c45f373f --- /dev/null +++ b/src/main/services/mcp/ServerLogBuffer.ts @@ -0,0 +1,36 @@ +export type MCPServerLogEntry = { + timestamp: number + level: 'debug' | 'info' | 'warn' | 'error' | 'stderr' | 'stdout' + message: string + data?: any + source?: string +} + +/** + * Lightweight ring buffer for per-server MCP logs. + */ +export class ServerLogBuffer { + private maxEntries: number + private logs: Map = new Map() + + constructor(maxEntries = 200) { + this.maxEntries = maxEntries + } + + append(serverKey: string, entry: MCPServerLogEntry) { + const list = this.logs.get(serverKey) ?? [] + list.push(entry) + if (list.length > this.maxEntries) { + list.splice(0, list.length - this.maxEntries) + } + this.logs.set(serverKey, list) + } + + get(serverKey: string): MCPServerLogEntry[] { + return [...(this.logs.get(serverKey) ?? [])] + } + + remove(serverKey: string) { + this.logs.delete(serverKey) + } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index a357f59f00..654e727cc6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -5,6 +5,7 @@ import type { SpanContext } from '@opentelemetry/api' import type { TerminalConfig, UpgradeChannel } from '@shared/config/constant' import type { LogLevel, LogSourceWithContext } from '@shared/config/logger' import type { FileChangeEvent, WebviewKeyEvent } from '@shared/config/types' +import type { MCPServerLogEntry } from '@shared/config/types' import { IpcChannel } from '@shared/IpcChannel' import type { Notification } from '@types' import type { @@ -372,7 +373,16 @@ const api = { }, abortTool: (callId: string) => ipcRenderer.invoke(IpcChannel.Mcp_AbortTool, callId), getServerVersion: (server: MCPServer): Promise => - ipcRenderer.invoke(IpcChannel.Mcp_GetServerVersion, server) + ipcRenderer.invoke(IpcChannel.Mcp_GetServerVersion, server), + getServerLogs: (server: MCPServer): Promise => + ipcRenderer.invoke(IpcChannel.Mcp_GetServerLogs, server), + onServerLog: (callback: (log: MCPServerLogEntry & { serverId?: string }) => void) => { + const listener = (_event: Electron.IpcRendererEvent, log: MCPServerLogEntry & { serverId?: string }) => { + callback(log) + } + ipcRenderer.on(IpcChannel.Mcp_ServerLog, listener) + return () => ipcRenderer.off(IpcChannel.Mcp_ServerLog, listener) + } }, python: { execute: (script: string, context?: Record, timeout?: number) => diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 4ebc57cb9b..1e8c854a12 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "Failed to save JSON configuration.", "jsonSaveSuccess": "JSON configuration has been saved.", "logoUrl": "Logo URL", + "logs": "Logs", "longRunning": "Long Running Mode", "longRunningTooltip": "When enabled, the server supports long-running tasks. When receiving progress notifications, the timeout will be reset and the maximum execution time will be extended to 10 minutes.", "marketplaces": "Marketplaces", @@ -3931,6 +3932,7 @@ "name": "Name", "newServer": "MCP Server", "noDescriptionAvailable": "No description available", + "noLogs": "No logs yet", "noServers": "No servers configured", "not_support": "Model not supported", "npx_list": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8829bfe08e..9c2cea8d68 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "保存 JSON 配置失败", "jsonSaveSuccess": "JSON 配置已保存", "logoUrl": "标志网址", + "logs": "日志", "longRunning": "长时间运行模式", "longRunningTooltip": "启用后,服务器支持长时间任务,接收到进度通知时会重置超时计时器,并延长最大超时时间至10分钟", "marketplaces": "市场", @@ -3931,6 +3932,7 @@ "name": "名称", "newServer": "MCP 服务器", "noDescriptionAvailable": "暂无描述", + "noLogs": "暂无日志", "noServers": "未配置服务器", "not_support": "模型不支持", "npx_list": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 1a036a29e1..d497ba82ac 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "保存 JSON 配置失敗", "jsonSaveSuccess": "JSON 配置已儲存", "logoUrl": "標誌網址", + "logs": "日誌", "longRunning": "長時間運行模式", "longRunningTooltip": "啟用後,伺服器支援長時間任務,接收到進度通知時會重置超時計時器,並延長最大超時時間至10分鐘", "marketplaces": "市場", @@ -3931,6 +3932,7 @@ "name": "名稱", "newServer": "MCP 伺服器", "noDescriptionAvailable": "描述不存在", + "noLogs": "暫無日誌", "noServers": "未設定伺服器", "not_support": "不支援此模型", "npx_list": { diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 1300fbf6c7..e11976cd8b 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "JSON-Konfiguration speichern fehlgeschlagen", "jsonSaveSuccess": "JSON-Konfiguration erfolgreich gespeichert", "logoUrl": "Logo-URL", + "logs": "Protokolle", "longRunning": "Lang laufender Modus", "longRunningTooltip": "Nach Aktivierung unterstützt der Server lange Aufgaben. Wenn ein Fortschrittsbenachrichtigung empfangen wird, wird der Timeout-Timer zurückgesetzt und die maximale Timeout-Zeit auf 10 Minuten verlängert", "marketplaces": "Marktplätze", @@ -3931,6 +3932,7 @@ "name": "Name", "newServer": "MCP-Server", "noDescriptionAvailable": "Keine Beschreibung", + "noLogs": "Noch keine Protokolle", "noServers": "Server nicht konfiguriert", "not_support": "Modell nicht unterstützt", "npx_list": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 535a36489e..3bcd43fc6e 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "Αποτυχία αποθήκευσης της διαμορφωτικής ρύθμισης JSON", "jsonSaveSuccess": "Η διαμορφωτική ρύθμιση JSON αποθηκεύτηκε επιτυχώς", "logoUrl": "URL Λογότυπου", + "logs": "Αρχεία καταγραφής", "longRunning": "Μακροχρόνια λειτουργία", "longRunningTooltip": "Όταν ενεργοποιηθεί, ο διακομιστής υποστηρίζει μακροχρόνιες εργασίες, επαναφέρει το χρονικό όριο μετά από λήψη ειδοποίησης προόδου και επεκτείνει το μέγιστο χρονικό όριο σε 10 λεπτά.", "marketplaces": "Αγορές", @@ -3931,6 +3932,7 @@ "name": "Όνομα", "newServer": "Διακομιστής MCP", "noDescriptionAvailable": "Δεν υπάρχει διαθέσιμη περιγραφή", + "noLogs": "Δεν υπάρχουν αρχεία καταγραφής ακόμα", "noServers": "Δεν έχουν ρυθμιστεί διακομιστές", "not_support": "Το μοντέλο δεν υποστηρίζεται", "npx_list": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 43d5919f00..5ccfe83b51 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "Fallo al guardar la configuración JSON", "jsonSaveSuccess": "Configuración JSON guardada exitosamente", "logoUrl": "URL del logotipo", + "logs": "Registros", "longRunning": "Modo de ejecución prolongada", "longRunningTooltip": "Una vez habilitado, el servidor admite tareas de larga duración, reinicia el temporizador de tiempo de espera al recibir notificaciones de progreso y amplía el tiempo máximo de espera hasta 10 minutos.", "marketplaces": "Mercados", @@ -3931,6 +3932,7 @@ "name": "Nombre", "newServer": "Servidor MCP", "noDescriptionAvailable": "Sin descripción disponible por ahora", + "noLogs": "Aún no hay registros", "noServers": "No se han configurado servidores", "not_support": "El modelo no es compatible", "npx_list": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index ef7db8b3b3..f84fe2b775 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "Échec de la sauvegarde de la configuration JSON", "jsonSaveSuccess": "Configuration JSON sauvegardée", "logoUrl": "Адрес логотипа", + "logs": "Journaux", "longRunning": "Mode d'exécution prolongée", "longRunningTooltip": "Une fois activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de temporisation à la réception des notifications de progression, et prolonge le délai d'expiration maximal à 10 minutes.", "marketplaces": "Places de marché", @@ -3931,6 +3932,7 @@ "name": "Nom", "newServer": "Сервер MCP", "noDescriptionAvailable": "Aucune description disponible pour le moment", + "noLogs": "Aucun journal pour le moment", "noServers": "Aucun serveur configuré", "not_support": "Модель не поддерживается", "npx_list": { diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 42c50c8827..0aea0f7dc6 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "JSON設定の保存に失敗しました", "jsonSaveSuccess": "JSON設定が保存されました。", "logoUrl": "ロゴURL", + "logs": "ログ", "longRunning": "長時間運行モード", "longRunningTooltip": "このオプションを有効にすると、サーバーは長時間のタスクをサポートします。進行状況通知を受信すると、タイムアウトがリセットされ、最大実行時間が10分に延長されます。", "marketplaces": "マーケットプレイス", @@ -3931,6 +3932,7 @@ "name": "名前", "newServer": "MCP サーバー", "noDescriptionAvailable": "説明がありません", + "noLogs": "ログはまだありません", "noServers": "サーバーが設定されていません", "not_support": "モデルはサポートされていません", "npx_list": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index bc84fc99b1..a28cf2a820 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "Falha ao salvar configuração JSON", "jsonSaveSuccess": "Configuração JSON salva com sucesso", "logoUrl": "URL do Logotipo", + "logs": "Registros", "longRunning": "Modo de execução prolongada", "longRunningTooltip": "Quando ativado, o servidor suporta tarefas de longa duração, redefinindo o temporizador de tempo limite ao receber notificações de progresso e estendendo o tempo máximo de tempo limite para 10 minutos.", "marketplaces": "Mercados", @@ -3931,6 +3932,7 @@ "name": "Nome", "newServer": "Servidor MCP", "noDescriptionAvailable": "Nenhuma descrição disponível no momento", + "noLogs": "Ainda sem registos", "noServers": "Nenhum servidor configurado", "not_support": "Modelo Não Suportado", "npx_list": { diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 109bffb9b2..3597d963e0 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -3912,6 +3912,7 @@ "jsonSaveError": "Не удалось сохранить конфигурацию JSON", "jsonSaveSuccess": "JSON конфигурация сохранена", "logoUrl": "URL логотипа", + "logs": "Журналы", "longRunning": "Длительный режим работы", "longRunningTooltip": "Включив эту опцию, сервер будет поддерживать длительные задачи. При получении уведомлений о ходе выполнения будет сброшен тайм-аут и максимальное время выполнения будет увеличено до 10 минут.", "marketplaces": "Торговые площадки", @@ -3931,6 +3932,7 @@ "name": "Имя", "newServer": "MCP сервер", "noDescriptionAvailable": "Описание отсутствует", + "noLogs": "Логов пока нет", "noServers": "Серверы не настроены", "not_support": "Модель не поддерживается", "npx_list": { diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index c48ae8f794..30b7b45f30 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -9,8 +9,9 @@ import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription' import type { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types' import { parseKeyValueString } from '@renderer/utils/env' import { formatMcpError } from '@renderer/utils/error' +import type { MCPServerLogEntry } from '@shared/config/types' import type { TabsProps } from 'antd' -import { Badge, Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd' +import { Badge, Button, Flex, Form, Input, Modal, Radio, Select, Switch, Tabs, Tag, Typography } from 'antd' import TextArea from 'antd/es/input/TextArea' import { ChevronDown, SaveIcon } from 'lucide-react' import React, { useCallback, useEffect, useState } from 'react' @@ -88,8 +89,11 @@ const McpSettings: React.FC = () => { const [showAdvanced, setShowAdvanced] = useState(false) const [serverVersion, setServerVersion] = useState(null) + const [logModalOpen, setLogModalOpen] = useState(false) + const [logs, setLogs] = useState<(MCPServerLogEntry & { serverId?: string })[]>([]) const { theme } = useTheme() + const { Text } = Typography const navigate = useNavigate() @@ -234,12 +238,43 @@ const McpSettings: React.FC = () => { } } + const fetchServerLogs = async () => { + try { + const history = await window.api.mcp.getServerLogs(server) + setLogs(history) + } catch (error) { + logger.warn('Failed to load server logs', error as Error) + } + } + + useEffect(() => { + const unsubscribe = window.api.mcp.onServerLog((log) => { + if (log.serverId && log.serverId !== server.id) return + setLogs((prev) => { + const merged = [...prev, log] + if (merged.length > 200) { + return merged.slice(merged.length - 200) + } + return merged + }) + }) + + return () => { + unsubscribe?.() + } + }, [server.id]) + + useEffect(() => { + setLogs([]) + }, [server.id]) + useEffect(() => { if (server.isActive) { fetchTools() fetchPrompts() fetchResources() fetchServerVersion() + fetchServerLogs() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [server.id, server.isActive]) @@ -736,6 +771,9 @@ const McpSettings: React.FC = () => { {server?.name} {serverVersion && } +