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 && } +