diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs index 821a78cc14..4aaa111398 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs @@ -21,6 +21,7 @@ public static class CliConsts public static string AppSettingsSecretJsonFileName = "appsettings.secrets.json"; public const string McpServerUrlEnvironmentVariable = "ABP_MCP_SERVER_URL"; + public const string McpLogLevelEnvironmentVariable = "ABP_MCP_LOG_LEVEL"; public const string DefaultMcpServerUrl = "https://mcp.abp.io"; public static class MemoryKeys diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs index 8ddf99a24e..683770bb98 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text; @@ -15,6 +15,7 @@ public static class CliPaths public static string Build => Path.Combine(AbpRootPath, "build"); public static string Lic => Path.Combine(Path.GetTempPath(), Encoding.ASCII.GetString(new byte[] { 65, 98, 112, 76, 105, 99, 101, 110, 115, 101, 46, 98, 105, 110 })); public static string McpToolsCache => Path.Combine(Root, "mcp-tools.json"); + public static string McpLog => Path.Combine(Log, "mcp.log"); public static readonly string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp"); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs index 42b8c93219..c8c6ba52a1 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NuGet.Versioning; @@ -10,6 +10,7 @@ using System.Reflection; using System.Threading.Tasks; using Volo.Abp.Cli.Args; using Volo.Abp.Cli.Commands; +using Volo.Abp.Cli.Commands.Services; using Volo.Abp.Cli.Memory; using Volo.Abp.Cli.Version; using Volo.Abp.Cli.Utils; @@ -21,8 +22,11 @@ namespace Volo.Abp.Cli; public class CliService : ITransientDependency { + private const string McpLogSource = nameof(CliService); + private readonly MemoryService _memoryService; private readonly ITelemetryService _telemetryService; + private readonly IMcpLogger _mcpLogger; public ILogger Logger { get; set; } protected ICommandLineArgumentParser CommandLineArgumentParser { get; } protected ICommandSelector CommandSelector { get; } @@ -39,7 +43,8 @@ public class CliService : ITransientDependency ICmdHelper cmdHelper, MemoryService memoryService, CliVersionService cliVersionService, - ITelemetryService telemetryService) + ITelemetryService telemetryService, + IMcpLogger mcpLogger) { _memoryService = memoryService; CommandLineArgumentParser = commandLineArgumentParser; @@ -49,6 +54,7 @@ public class CliService : ITransientDependency CmdHelper = cmdHelper; CliVersionService = cliVersionService; _telemetryService = telemetryService; + _mcpLogger = mcpLogger; Logger = NullLogger.Instance; } @@ -92,10 +98,10 @@ public class CliService : ITransientDependency } catch (CliUsageException usageException) { - // For MCP command, write errors to stderr to avoid corrupting stdout JSON-RPC stream + // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream if (commandLineArgs.IsCommand("mcp")) { - await Console.Error.WriteLineAsync($"[MCP] Error: {usageException.Message}"); + _mcpLogger.Error(McpLogSource, usageException.Message); } else { @@ -106,10 +112,10 @@ public class CliService : ITransientDependency catch (Exception ex) { await _telemetryService.AddErrorActivityAsync(ex.Message); - // For MCP command, write errors to stderr to avoid corrupting stdout JSON-RPC stream + // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream if (commandLineArgs.IsCommand("mcp")) { - await Console.Error.WriteLineAsync($"[MCP] Fatal error: {ex.Message}"); + _mcpLogger.Error(McpLogSource, "Fatal error", ex); } else { diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs index 7463a8f5a7..d9dba2c0cc 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs @@ -20,12 +20,14 @@ namespace Volo.Abp.Cli.Commands; public class McpCommand : IConsoleCommand, ITransientDependency { + private const string LogSource = nameof(McpCommand); public const string Name = "mcp"; private readonly AuthService _authService; private readonly IApiKeyService _apiKeyService; private readonly McpServerService _mcpServerService; private readonly McpHttpClientService _mcpHttpClient; + private readonly IMcpLogger _mcpLogger; public ILogger Logger { get; set; } @@ -33,12 +35,14 @@ public class McpCommand : IConsoleCommand, ITransientDependency IApiKeyService apiKeyService, AuthService authService, McpServerService mcpServerService, - McpHttpClientService mcpHttpClient) + McpHttpClientService mcpHttpClient, + IMcpLogger mcpLogger) { _apiKeyService = apiKeyService; _authService = authService; _mcpServerService = mcpServerService; _mcpHttpClient = mcpHttpClient; + _mcpLogger = mcpLogger; Logger = NullLogger.Instance; } @@ -72,17 +76,17 @@ public class McpCommand : IConsoleCommand, ITransientDependency return; } - // Check server health before starting (log to stderr) - await Console.Error.WriteLineAsync("[MCP] Checking ABP.IO MCP Server connection..."); + // Check server health before starting + _mcpLogger.Info(LogSource, "Checking ABP.IO MCP Server connection..."); var isHealthy = await _mcpHttpClient.CheckServerHealthAsync(); if (!isHealthy) { - await Console.Error.WriteLineAsync("[MCP] Warning: Could not connect to ABP.IO MCP Server. The server might be offline."); - await Console.Error.WriteLineAsync("[MCP] Continuing to start local MCP server..."); + _mcpLogger.Warning(LogSource, "Could not connect to ABP.IO MCP Server. The server might be offline."); + _mcpLogger.Info(LogSource, "Continuing to start local MCP server..."); } - await Console.Error.WriteLineAsync("[MCP] Starting ABP MCP Server..."); + _mcpLogger.Info(LogSource, "Starting ABP MCP Server..."); var cts = new CancellationTokenSource(); ConsoleCancelEventHandler cancelHandler = null; @@ -90,7 +94,7 @@ public class McpCommand : IConsoleCommand, ITransientDependency cancelHandler = (sender, e) => { e.Cancel = true; - Console.Error.WriteLine("[MCP] Shutting down ABP MCP Server..."); + _mcpLogger.Info(LogSource, "Shutting down ABP MCP Server..."); try { @@ -114,7 +118,7 @@ public class McpCommand : IConsoleCommand, ITransientDependency } catch (Exception ex) { - await Console.Error.WriteLineAsync($"[MCP] Error running MCP server: {ex.Message}"); + _mcpLogger.Error(LogSource, "Error running MCP server", ex); throw; } finally diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/IMcpLogger.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/IMcpLogger.cs new file mode 100644 index 0000000000..f579420a1e --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/IMcpLogger.cs @@ -0,0 +1,36 @@ +using System; + +namespace Volo.Abp.Cli.Commands.Services; + +/// +/// Logger interface for MCP operations. +/// Writes detailed logs to file and critical messages (Warning/Error) to stderr. +/// Log level is controlled via ABP_MCP_LOG_LEVEL environment variable. +/// +public interface IMcpLogger +{ + /// + /// Logs a debug message. Only written to file when log level is Debug. + /// + void Debug(string source, string message); + + /// + /// Logs an informational message. Written to file when log level is Debug or Info. + /// + void Info(string source, string message); + + /// + /// Logs a warning message. Written to file and stderr. + /// + void Warning(string source, string message); + + /// + /// Logs an error message. Written to file and stderr. + /// + void Error(string source, string message); + + /// + /// Logs an error message with exception details. Written to file and stderr. + /// + void Error(string source, string message, Exception exception); +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs index 97a4ddab36..dc6bd84521 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs @@ -15,6 +15,7 @@ namespace Volo.Abp.Cli.Commands.Services; public class McpHttpClientService : ITransientDependency { + private const string LogSource = nameof(McpHttpClientService); private static readonly JsonSerializerOptions JsonSerializerOptionsWeb = new(JsonSerializerDefaults.Web); private static class ErrorMessages @@ -26,16 +27,19 @@ public class McpHttpClientService : ITransientDependency private readonly CliHttpClientFactory _httpClientFactory; private readonly ILogger _logger; + private readonly IMcpLogger _mcpLogger; private readonly MemoryService _memoryService; private string _cachedServerUrl; public McpHttpClientService( CliHttpClientFactory httpClientFactory, ILogger logger, + IMcpLogger mcpLogger, MemoryService memoryService) { _httpClientFactory = httpClientFactory; _logger = logger; + _mcpLogger = mcpLogger; _memoryService = memoryService; } @@ -87,8 +91,7 @@ public class McpHttpClientService : ITransientDependency if (!response.IsSuccessStatusCode) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] API call failed with status: {response.StatusCode}"); + _mcpLogger.Error(LogSource, $"API call failed with status: {response.StatusCode}"); // Return sanitized error message to client var errorMessage = GetSanitizedHttpErrorMessage(response.StatusCode); @@ -99,24 +102,21 @@ public class McpHttpClientService : ITransientDependency } catch (HttpRequestException ex) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] Network error calling tool '{toolName}': {ex.Message}"); + _mcpLogger.Error(LogSource, $"Network error calling tool '{toolName}'", ex); // Return sanitized error to client return CreateErrorResponse(ErrorMessages.NetworkConnectivity); } catch (TaskCanceledException ex) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] Timeout calling tool '{toolName}': {ex.Message}"); + _mcpLogger.Error(LogSource, $"Timeout calling tool '{toolName}'", ex); // Return sanitized error to client return CreateErrorResponse(ErrorMessages.Timeout); } catch (Exception ex) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] Unexpected error calling tool '{toolName}': {ex.Message}"); + _mcpLogger.Error(LogSource, $"Unexpected error calling tool '{toolName}'", ex); // Return generic sanitized error to client return CreateErrorResponse(ErrorMessages.Unexpected); @@ -152,7 +152,7 @@ public class McpHttpClientService : ITransientDependency HttpStatusCode.Forbidden => "Access denied. You do not have permission to use this tool.", HttpStatusCode.NotFound => "The requested tool could not be found. It may have been removed or is temporarily unavailable.", HttpStatusCode.BadRequest => "The tool request was invalid. Please check your input parameters and try again.", - HttpStatusCode.TooManyRequests => "Rate limit exceeded. Please wait a moment before trying again.", + (HttpStatusCode)429 => "Rate limit exceeded. Please wait a moment before trying again.", // TooManyRequests not available in .NET Standard 2.0 HttpStatusCode.ServiceUnavailable => "The service is temporarily unavailable. Please try again later.", HttpStatusCode.InternalServerError => "The tool execution encountered an internal error. Please try again later.", _ => "The tool execution failed. Please try again later." @@ -188,8 +188,7 @@ public class McpHttpClientService : ITransientDependency if (!response.IsSuccessStatusCode) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] Failed to fetch tool definitions with status: {response.StatusCode}"); + _mcpLogger.Error(LogSource, $"Failed to fetch tool definitions with status: {response.StatusCode}"); // Throw sanitized exception var errorMessage = GetSanitizedHttpErrorMessage(response.StatusCode); @@ -205,24 +204,21 @@ public class McpHttpClientService : ITransientDependency } catch (HttpRequestException ex) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] Network error fetching tool definitions: {ex.Message}"); + _mcpLogger.Error(LogSource, "Network error fetching tool definitions", ex); // Throw sanitized exception throw CreateToolDefinitionException("Network connectivity issue. Please check your internet connection and try again."); } catch (TaskCanceledException ex) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] Timeout fetching tool definitions: {ex.Message}"); + _mcpLogger.Error(LogSource, "Timeout fetching tool definitions", ex); // Throw sanitized exception throw CreateToolDefinitionException("Request timed out. Please try again."); } catch (JsonException ex) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] JSON parsing error: {ex.Message}"); + _mcpLogger.Error(LogSource, "JSON parsing error", ex); // Throw sanitized exception throw CreateToolDefinitionException("Invalid response format received."); @@ -234,8 +230,7 @@ public class McpHttpClientService : ITransientDependency } catch (Exception ex) { - // Log detailed error to stderr for debugging - await Console.Error.WriteLineAsync($"[MCP] Unexpected error fetching tool definitions: {ex.Message}"); + _mcpLogger.Error(LogSource, "Unexpected error fetching tool definitions", ex); // Throw sanitized exception throw CreateToolDefinitionException("An unexpected error occurred. Please try again later."); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs new file mode 100644 index 0000000000..dd3c0f2f78 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs @@ -0,0 +1,210 @@ +using System; +using System.IO; +using System.Text; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands.Services; + +/// +/// MCP logger implementation that writes to both file and stderr. +/// - All logs at or above the configured level are written to file +/// - Warning and Error logs are also written to stderr +/// - Log level is controlled via ABP_MCP_LOG_LEVEL environment variable +/// +public class McpLogger : IMcpLogger, ISingletonDependency +{ + private const long MaxLogFileSizeBytes = 5 * 1024 * 1024; // 5MB + private const string LogPrefix = "[MCP]"; + + private readonly object _fileLock = new(); + private readonly McpLogLevel _configuredLogLevel; + + public McpLogger() + { + _configuredLogLevel = GetConfiguredLogLevel(); + } + + public void Debug(string source, string message) + { + Log(McpLogLevel.Debug, source, message); + } + + public void Info(string source, string message) + { + Log(McpLogLevel.Info, source, message); + } + + public void Warning(string source, string message) + { + Log(McpLogLevel.Warning, source, message); + } + + public void Error(string source, string message) + { + Log(McpLogLevel.Error, source, message); + } + + public void Error(string source, string message, Exception exception) + { +#if DEBUG + var fullMessage = $"{message} | Exception: {exception.GetType().Name}: {exception.Message}"; +#else + var fullMessage = $"{message} | Exception: {exception.GetType().Name}"; +#endif + Log(McpLogLevel.Error, source, fullMessage); + } + + private void Log(McpLogLevel level, string source, string message) + { + if (_configuredLogLevel == McpLogLevel.None) + { + return; + } + + if (level < _configuredLogLevel) + { + return; + } + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + var levelStr = level.ToString().ToUpperInvariant(); + var formattedMessage = $"[{timestamp}][{levelStr}][{source}] {message}"; + + // Write to file (all levels at or above configured level) + WriteToFile(formattedMessage); + + // Write to stderr for Warning and Error levels + if (level >= McpLogLevel.Warning) + { + WriteToStderr(levelStr, message); + } + } + + private void WriteToFile(string formattedMessage) + { + try + { + lock (_fileLock) + { + EnsureLogDirectoryExists(); + RotateLogFileIfNeeded(); + + File.AppendAllText( + CliPaths.McpLog, + formattedMessage + Environment.NewLine, + Encoding.UTF8); + } + } + catch + { + // Silently ignore file write errors to not disrupt MCP operations + } + } + + private void WriteToStderr(string level, string message) + { + try + { + // Use synchronous write to avoid async issues in MCP context + Console.Error.WriteLine($"{LogPrefix}[{level}] {message}"); + } + catch + { + // Silently ignore stderr write errors + } + } + + private void EnsureLogDirectoryExists() + { + var directory = Path.GetDirectoryName(CliPaths.McpLog); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + } + + private void RotateLogFileIfNeeded() + { + try + { + if (!File.Exists(CliPaths.McpLog)) + { + return; + } + + var fileInfo = new FileInfo(CliPaths.McpLog); + if (fileInfo.Length < MaxLogFileSizeBytes) + { + return; + } + + var backupPath = CliPaths.McpLog + ".1"; + + // Delete old backup if exists + if (File.Exists(backupPath)) + { + File.Delete(backupPath); + } + + // Rename current log to backup + File.Move(CliPaths.McpLog, backupPath); + } + catch + { + // Silently ignore rotation errors + } + } + + private static McpLogLevel GetConfiguredLogLevel() + { +#if DEBUG + // In development builds, allow full control via environment variable + var envValue = Environment.GetEnvironmentVariable(CliConsts.McpLogLevelEnvironmentVariable); + + if (string.IsNullOrWhiteSpace(envValue)) + { + return McpLogLevel.Info; // Default level + } + + return envValue.ToLowerInvariant() switch + { + "debug" => McpLogLevel.Debug, + "info" => McpLogLevel.Info, + "warning" => McpLogLevel.Warning, + "error" => McpLogLevel.Error, + "none" => McpLogLevel.None, + _ => McpLogLevel.Info + }; +#else + // In release builds, restrict to Warning or higher (ignore env variable for Debug/Info) + var envValue = Environment.GetEnvironmentVariable(CliConsts.McpLogLevelEnvironmentVariable); + + if (string.IsNullOrWhiteSpace(envValue)) + { + return McpLogLevel.Info; // Default level + } + + return envValue.ToLowerInvariant() switch + { + "debug" => McpLogLevel.Info, // Cap Debug to Info + "info" => McpLogLevel.Info, + "warning" => McpLogLevel.Warning, + "error" => McpLogLevel.Error, + "none" => McpLogLevel.None, + _ => McpLogLevel.Info + }; +#endif + } +} + +/// +/// Log levels for MCP logging. +/// +public enum McpLogLevel +{ + Debug = 0, + Info = 1, + Warning = 2, + Error = 3, + None = 4 +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs index dff741d11a..c1938498a5 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; @@ -15,6 +14,8 @@ namespace Volo.Abp.Cli.Commands.Services; public class McpServerService : ITransientDependency { + private const string LogSource = nameof(McpServerService); + private static class ToolErrorMessages { public const string InvalidResponseFormat = "The tool execution completed but returned an invalid response format. Please try again."; @@ -23,26 +24,28 @@ public class McpServerService : ITransientDependency private readonly McpHttpClientService _mcpHttpClient; private readonly McpToolsCacheService _toolsCacheService; + private readonly IMcpLogger _mcpLogger; public McpServerService( McpHttpClientService mcpHttpClient, - McpToolsCacheService toolsCacheService) + McpToolsCacheService toolsCacheService, + IMcpLogger mcpLogger) { _mcpHttpClient = mcpHttpClient; _toolsCacheService = toolsCacheService; + _mcpLogger = mcpLogger; } public async Task RunAsync(CancellationToken cancellationToken = default) { - // Log to stderr to avoid corrupting stdout JSON-RPC stream - await Console.Error.WriteLineAsync("[MCP] Starting ABP MCP Server (stdio)"); + _mcpLogger.Info(LogSource, "Starting ABP MCP Server (stdio)"); var options = new McpServerOptions(); await RegisterAllToolsAsync(options); // Use NullLoggerFactory to prevent ModelContextProtocol library from logging to stdout - // All our logging goes to stderr via Console.Error + // All our logging goes to file and stderr via IMcpLogger var server = McpServer.Create( new StdioServerTransport("abp-mcp-server", NullLoggerFactory.Instance), options @@ -50,7 +53,7 @@ public class McpServerService : ITransientDependency await server.RunAsync(cancellationToken); - await Console.Error.WriteLineAsync("[MCP] ABP MCP Server stopped"); + _mcpLogger.Info(LogSource, "ABP MCP Server stopped"); } private async Task RegisterAllToolsAsync(McpServerOptions options) @@ -58,7 +61,7 @@ public class McpServerService : ITransientDependency // Get tool definitions from cache (or fetch from server) var toolDefinitions = await _toolsCacheService.GetToolDefinitionsAsync(); - await Console.Error.WriteLineAsync($"[MCP] Registering {toolDefinitions.Count} tools"); + _mcpLogger.Info(LogSource, $"Registering {toolDefinitions.Count} tools"); // Register each tool dynamically foreach (var toolDef in toolDefinitions) @@ -129,8 +132,7 @@ public class McpServerService : ITransientDependency JsonSerializer.SerializeToElement(inputSchema), async (context, cancellationToken) => { - // Log to stderr to avoid corrupting stdout JSON-RPC stream - await Console.Error.WriteLineAsync($"[MCP] Tool '{name}' called with arguments: {context.Params.Arguments}"); + _mcpLogger.Debug(LogSource, $"Tool '{name}' called with arguments: {context.Params.Arguments}"); try { @@ -151,11 +153,11 @@ public class McpServerService : ITransientDependency // Check if the HTTP client returned an error if (callToolResult.IsError == true) { - await Console.Error.WriteLineAsync($"[MCP] Tool '{name}' returned an error"); + _mcpLogger.Warning(LogSource, $"Tool '{name}' returned an error"); } else { - await Console.Error.WriteLineAsync($"[MCP] Tool '{name}' executed successfully"); + _mcpLogger.Debug(LogSource, $"Tool '{name}' executed successfully"); } return callToolResult; @@ -163,8 +165,8 @@ public class McpServerService : ITransientDependency } catch (Exception deserializeEx) { - await Console.Error.WriteLineAsync($"[MCP] Failed to deserialize response as CallToolResult: {deserializeEx.Message}"); - await Console.Error.WriteLineAsync($"[MCP] Response was: {resultJson.Substring(0, Math.Min(500, resultJson.Length))}"); + _mcpLogger.Error(LogSource, $"Failed to deserialize response as CallToolResult: {deserializeEx.Message}"); + _mcpLogger.Debug(LogSource, $"Response was: {resultJson.Substring(0, Math.Min(500, resultJson.Length))}"); } // Fallback: return error result if deserialization fails @@ -173,7 +175,7 @@ public class McpServerService : ITransientDependency catch (Exception ex) { // Log detailed error for debugging - await Console.Error.WriteLineAsync($"[MCP] Tool '{name}' execution failed with exception: {ex.Message}"); + _mcpLogger.Error(LogSource, $"Tool '{name}' execution failed", ex); // Return sanitized error to client return CreateErrorResult(ToolErrorMessages.UnexpectedError); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs index b8ca47bedd..ec5669d2e8 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs @@ -15,21 +15,26 @@ namespace Volo.Abp.Cli.Commands.Services; public class McpToolsCacheService : ITransientDependency { + private const string LogSource = nameof(McpToolsCacheService); + private readonly McpHttpClientService _mcpHttpClient; private readonly MemoryService _memoryService; private readonly McpToolDefinitionValidator _validator; private readonly ILogger _logger; + private readonly IMcpLogger _mcpLogger; public McpToolsCacheService( McpHttpClientService mcpHttpClient, MemoryService memoryService, McpToolDefinitionValidator validator, - ILogger logger) + ILogger logger, + IMcpLogger mcpLogger) { _mcpHttpClient = mcpHttpClient; _memoryService = memoryService; _validator = validator; _logger = logger; + _mcpLogger = mcpLogger; } public async Task> GetToolDefinitionsAsync() @@ -39,7 +44,7 @@ public class McpToolsCacheService : ITransientDependency var cachedTools = await LoadFromCacheAsync(); if (cachedTools != null) { - await Console.Error.WriteLineAsync("[MCP] Using cached tool definitions"); + _mcpLogger.Debug(LogSource, "Using cached tool definitions"); return cachedTools; } } @@ -47,7 +52,7 @@ public class McpToolsCacheService : ITransientDependency // Cache is invalid or missing, fetch from server try { - await Console.Error.WriteLineAsync("[MCP] Fetching tool definitions from server..."); + _mcpLogger.Info(LogSource, "Fetching tool definitions from server..."); var tools = await _mcpHttpClient.GetToolDefinitionsAsync(); // Validate and filter tool definitions @@ -56,7 +61,7 @@ public class McpToolsCacheService : ITransientDependency if (validTools.Count == 0) { _logger.LogWarning("No valid tool definitions received from server"); - await Console.Error.WriteLineAsync("[MCP] Warning: No valid tool definitions received from server"); + _mcpLogger.Warning(LogSource, "No valid tool definitions received from server"); return new List(); } @@ -64,24 +69,24 @@ public class McpToolsCacheService : ITransientDependency await SaveToCacheAsync(validTools); await _memoryService.SetAsync(CliConsts.MemoryKeys.McpToolsLastFetchDate, DateTime.Now.ToString(CultureInfo.InvariantCulture)); - await Console.Error.WriteLineAsync($"[MCP] Successfully fetched and cached {validTools.Count} tool definitions"); + _mcpLogger.Info(LogSource, $"Successfully fetched and cached {validTools.Count} tool definitions"); return validTools; } catch (Exception ex) { // Sanitize error message - use generic message for logger _logger.LogWarning("Failed to fetch tool definitions from server"); - await Console.Error.WriteLineAsync($"[MCP] Failed to fetch from server, attempting to use cached data..."); + _mcpLogger.Warning(LogSource, "Failed to fetch from server, attempting to use cached data..."); // Fall back to cache even if expired var cachedTools = await LoadFromCacheAsync(); if (cachedTools != null) { - await Console.Error.WriteLineAsync("[MCP] Using expired cache as fallback"); + _mcpLogger.Info(LogSource, "Using expired cache as fallback"); return cachedTools; } - await Console.Error.WriteLineAsync("[MCP] No cached data available, using empty tool list"); + _mcpLogger.Warning(LogSource, "No cached data available, using empty tool list"); return new List(); } }