Browse Source

Introduce IMcpLogger for structured MCP logging

Added IMcpLogger interface and McpLogger implementation to provide structured logging for MCP operations, supporting log levels and file rotation. Replaced direct Console.Error logging with IMcpLogger in MCP-related services and commands. Log level is now configurable via the ABP_MCP_LOG_LEVEL environment variable, and logs are written to both file and stderr as appropriate.
pull/24677/head
Mansur Besleney 4 weeks ago
parent
commit
0bc6d1cdff
  1. 1
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs
  2. 3
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs
  3. 18
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs
  4. 20
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs
  5. 36
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/IMcpLogger.cs
  6. 33
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs
  7. 210
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs
  8. 30
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs
  9. 21
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs

1
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

3
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");
}

18
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<CliService> 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<CliService>.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
{

20
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<McpCommand> 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<McpCommand>.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

36
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;
/// <summary>
/// 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.
/// </summary>
public interface IMcpLogger
{
/// <summary>
/// Logs a debug message. Only written to file when log level is Debug.
/// </summary>
void Debug(string source, string message);
/// <summary>
/// Logs an informational message. Written to file when log level is Debug or Info.
/// </summary>
void Info(string source, string message);
/// <summary>
/// Logs a warning message. Written to file and stderr.
/// </summary>
void Warning(string source, string message);
/// <summary>
/// Logs an error message. Written to file and stderr.
/// </summary>
void Error(string source, string message);
/// <summary>
/// Logs an error message with exception details. Written to file and stderr.
/// </summary>
void Error(string source, string message, Exception exception);
}

33
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<McpHttpClientService> _logger;
private readonly IMcpLogger _mcpLogger;
private readonly MemoryService _memoryService;
private string _cachedServerUrl;
public McpHttpClientService(
CliHttpClientFactory httpClientFactory,
ILogger<McpHttpClientService> 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.");

210
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;
/// <summary>
/// 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
/// </summary>
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
}
}
/// <summary>
/// Log levels for MCP logging.
/// </summary>
public enum McpLogLevel
{
Debug = 0,
Info = 1,
Warning = 2,
Error = 3,
None = 4
}

30
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);

21
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<McpToolsCacheService> _logger;
private readonly IMcpLogger _mcpLogger;
public McpToolsCacheService(
McpHttpClientService mcpHttpClient,
MemoryService memoryService,
McpToolDefinitionValidator validator,
ILogger<McpToolsCacheService> logger)
ILogger<McpToolsCacheService> logger,
IMcpLogger mcpLogger)
{
_mcpHttpClient = mcpHttpClient;
_memoryService = memoryService;
_validator = validator;
_logger = logger;
_mcpLogger = mcpLogger;
}
public async Task<List<McpToolDefinition>> 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<McpToolDefinition>();
}
@ -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<McpToolDefinition>();
}
}

Loading…
Cancel
Save