feat: add MCP server log viewer (#11826)

*  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 <kangfenmao@qq.com>
This commit is contained in:
LiuVaayne 2025-12-11 11:18:56 +08:00 committed by GitHub
parent 711f805a5b
commit 96085707ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 316 additions and 6 deletions

View File

@ -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',

View File

@ -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

View File

@ -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,

View File

@ -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) => {

View File

@ -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<string, Promise<Client>> = new Map()
private dxtService = new DxtService()
private activeToolCalls: Map<string, AbortController> = 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<Client> {
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)

View File

@ -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)
})
})

View File

@ -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<string, MCPServerLogEntry[]> = 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)
}
}

View File

@ -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<string | null> =>
ipcRenderer.invoke(IpcChannel.Mcp_GetServerVersion, server)
ipcRenderer.invoke(IpcChannel.Mcp_GetServerVersion, server),
getServerLogs: (server: MCPServer): Promise<MCPServerLogEntry[]> =>
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<string, any>, timeout?: number) =>

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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<string | null>(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 = () => {
<ServerName className="text-nowrap">{server?.name}</ServerName>
{serverVersion && <VersionBadge count={serverVersion} color="blue" />}
</Flex>
<Button size="small" onClick={() => setLogModalOpen(true)}>
{t('settings.mcp.logs', 'View Logs')}
</Button>
<Button
danger
icon={<DeleteIcon size={14} className="lucide-custom" />}
@ -770,6 +808,37 @@ const McpSettings: React.FC = () => {
/>
</SettingGroup>
</SettingContainer>
<Modal
title={t('settings.mcp.logs', 'Server Logs')}
open={logModalOpen}
onCancel={() => setLogModalOpen(false)}
footer={null}
width={720}
centered
transitionName="animation-move-down"
bodyStyle={{ maxHeight: '60vh', minHeight: '40vh', overflowY: 'auto' }}
afterOpenChange={(open) => {
if (open) {
fetchServerLogs()
}
}}>
<LogList>
{logs.length === 0 && <Text type="secondary">{t('settings.mcp.noLogs', 'No logs yet')}</Text>}
{logs.map((log, idx) => (
<LogItem key={`${log.timestamp}-${idx}`}>
<Flex gap={8} align="baseline">
<Timestamp>{new Date(log.timestamp).toLocaleTimeString()}</Timestamp>
<Tag color={mapLogLevelColor(log.level)}>{log.level}</Tag>
<Text>{log.message}</Text>
</Flex>
{log.data && (
<PreBlock>{typeof log.data === 'string' ? log.data : JSON.stringify(log.data, null, 2)}</PreBlock>
)}
</LogItem>
))}
</LogList>
</Modal>
</Container>
)
}
@ -792,6 +861,52 @@ const AdvancedSettingsButton = styled.div`
align-items: center;
`
const LogList = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
`
const LogItem = styled.div`
background: var(--color-bg-2, #1f1f1f);
color: var(--color-text-1, #e6e6e6);
border-radius: 8px;
padding: 10px 12px;
border: 1px solid var(--color-border, rgba(255, 255, 255, 0.08));
`
const Timestamp = styled.span`
color: var(--color-text-3, #9aa2b1);
font-size: 12px;
`
const PreBlock = styled.pre`
margin: 6px 0 0;
padding: 8px;
background: var(--color-bg-3, #111418);
color: var(--color-text-1, #e6e6e6);
border-radius: 6px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-word;
border: 1px solid var(--color-border, rgba(255, 255, 255, 0.08));
`
function mapLogLevelColor(level: MCPServerLogEntry['level']) {
switch (level) {
case 'error':
case 'stderr':
return 'red'
case 'warn':
return 'orange'
case 'info':
case 'stdout':
return 'blue'
default:
return 'default'
}
}
const VersionBadge = styled(Badge)`
.ant-badge-count {
background-color: var(--color-primary);