mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
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:
parent
711f805a5b
commit
96085707ce
@ -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',
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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)
|
||||
|
||||
29
src/main/services/__tests__/ServerLogBuffer.test.ts
Normal file
29
src/main/services/__tests__/ServerLogBuffer.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
36
src/main/services/mcp/ServerLogBuffer.ts
Normal file
36
src/main/services/mcp/ServerLogBuffer.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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) =>
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user