Browse Source

Refactor MCP logging and tool initialization logic

Refactored McpLogger to use ILogger and Serilog for file logging, removing manual file handling and rotation. Enhanced debug logging in McpHttpClientService and added explicit tool name initialization from cache. Updated Program.cs to use a separate log file for MCP mode. Improved error logging in McpServerService for tool execution failures.
pull/24677/head
Mansur Besleney 3 weeks ago
parent
commit
fc3c21ade4
  1. 15
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs
  2. 106
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs
  3. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs
  4. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs
  5. 23
      framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs

15
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs

@ -15,7 +15,7 @@ using Volo.Abp.IO;
namespace Volo.Abp.Cli.Commands.Services; namespace Volo.Abp.Cli.Commands.Services;
public class McpHttpClientService : ITransientDependency public class McpHttpClientService : ISingletonDependency
{ {
private static readonly JsonSerializerOptions JsonSerializerOptionsWeb = new(JsonSerializerDefaults.Web); private static readonly JsonSerializerOptions JsonSerializerOptionsWeb = new(JsonSerializerDefaults.Web);
@ -80,8 +80,17 @@ public class McpHttpClientService : ITransientDependency
public string ServerUrl { get; set; } public string ServerUrl { get; set; }
} }
public void InitializeToolNames(List<McpToolDefinition> tools)
{
_validToolNames = tools.Select(t => t.Name).ToList();
_toolDefinitionsLoaded = true;
_mcpLogger.Debug(LogSource, $"Initialized tool names from cache. Count={tools.Count}, Instance={GetHashCode()}");
}
public async Task<string> CallToolAsync(string toolName, JsonElement arguments) public async Task<string> CallToolAsync(string toolName, JsonElement arguments)
{ {
_mcpLogger.Debug(LogSource, $"CallToolAsync called for '{toolName}'. _toolDefinitionsLoaded={_toolDefinitionsLoaded}, Instance={GetHashCode()}");
if (!_toolDefinitionsLoaded) if (!_toolDefinitionsLoaded)
{ {
throw new CliUsageException("Tool definitions have not been loaded yet. This is an internal error."); throw new CliUsageException("Tool definitions have not been loaded yet. This is an internal error.");
@ -193,6 +202,8 @@ public class McpHttpClientService : ITransientDependency
public async Task<List<McpToolDefinition>> GetToolDefinitionsAsync() public async Task<List<McpToolDefinition>> GetToolDefinitionsAsync()
{ {
_mcpLogger.Debug(LogSource, $"GetToolDefinitionsAsync called. Instance={GetHashCode()}");
var baseUrl = await GetMcpServerUrlAsync(); var baseUrl = await GetMcpServerUrlAsync();
var url = $"{baseUrl}/tools"; var url = $"{baseUrl}/tools";
@ -220,6 +231,8 @@ public class McpHttpClientService : ITransientDependency
_validToolNames = tools.Select(t => t.Name).ToList(); _validToolNames = tools.Select(t => t.Name).ToList();
_toolDefinitionsLoaded = true; _toolDefinitionsLoaded = true;
_mcpLogger.Debug(LogSource, $"Tool definitions loaded successfully. _toolDefinitionsLoaded={_toolDefinitionsLoaded}, Tool count={tools.Count}, Instance={GetHashCode()}");
return tools; return tools;
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)

106
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs

@ -1,26 +1,25 @@
using System; using System;
using System.IO; using Microsoft.Extensions.Logging;
using System.Text;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands.Services; namespace Volo.Abp.Cli.Commands.Services;
/// <summary> /// <summary>
/// MCP logger implementation that writes to both file and stderr. /// MCP logger implementation that writes to both file (via Serilog) and stderr.
/// - All logs at or above the configured level are written to file /// - All logs at or above the configured level are written to file via ILogger
/// - Warning and Error logs are also written to stderr /// - Warning and Error logs are also written to stderr
/// - Log level is controlled via ABP_MCP_LOG_LEVEL environment variable /// - Log level is controlled via ABP_MCP_LOG_LEVEL environment variable
/// </summary> /// </summary>
public class McpLogger : IMcpLogger, ISingletonDependency public class McpLogger : IMcpLogger, ISingletonDependency
{ {
private const long MaxLogFileSizeBytes = 5 * 1024 * 1024; // 5MB
private const string LogPrefix = "[MCP]"; private const string LogPrefix = "[MCP]";
private readonly object _fileLock = new(); private readonly ILogger<McpLogger> _logger;
private readonly McpLogLevel _configuredLogLevel; private readonly McpLogLevel _configuredLogLevel;
public McpLogger() public McpLogger(ILogger<McpLogger> logger)
{ {
_logger = logger;
_configuredLogLevel = GetConfiguredLogLevel(); _configuredLogLevel = GetConfiguredLogLevel();
} }
@ -56,48 +55,34 @@ public class McpLogger : IMcpLogger, ISingletonDependency
private void Log(McpLogLevel level, string source, string message) private void Log(McpLogLevel level, string source, string message)
{ {
if (_configuredLogLevel == McpLogLevel.None) if (_configuredLogLevel == McpLogLevel.None || level < _configuredLogLevel)
{ {
return; return;
} }
if (level < _configuredLogLevel) var mcpFormattedMessage = $"{LogPrefix}[{source}] {message}";
{
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) // File logging via Serilog
WriteToFile(formattedMessage); switch (level)
// Write to stderr for Warning and Error levels
if (level >= McpLogLevel.Warning)
{ {
WriteToStderr(levelStr, message); case McpLogLevel.Debug:
_logger.LogDebug(mcpFormattedMessage);
break;
case McpLogLevel.Info:
_logger.LogInformation(mcpFormattedMessage);
break;
case McpLogLevel.Warning:
_logger.LogWarning(mcpFormattedMessage);
break;
case McpLogLevel.Error:
_logger.LogError(mcpFormattedMessage);
break;
} }
}
private void WriteToFile(string formattedMessage) // Stderr output for MCP protocol (Warning/Error only)
{ if (level >= McpLogLevel.Warning)
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 WriteToStderr(level.ToString().ToUpperInvariant(), message);
} }
} }
@ -114,47 +99,6 @@ public class McpLogger : IMcpLogger, ISingletonDependency
} }
} }
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() private static McpLogLevel GetConfiguredLogLevel()
{ {
var envValue = Environment.GetEnvironmentVariable(CliConsts.McpLogLevelEnvironmentVariable); var envValue = Environment.GetEnvironmentVariable(CliConsts.McpLogLevelEnvironmentVariable);

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs

@ -161,7 +161,7 @@ public class McpServerService : ITransientDependency
} }
catch (Exception ex) catch (Exception ex)
{ {
_mcpLogger.Error(LogSource, $"Tool '{toolName}' execution failed", ex); _mcpLogger.Error(LogSource, $"Tool '{toolName}' execution failed '{ex.Message}'", ex);
return CreateErrorResult(ToolErrorMessages.UnexpectedError); return CreateErrorResult(ToolErrorMessages.UnexpectedError);
} }
} }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs

@ -43,6 +43,8 @@ public class McpToolsCacheService : ITransientDependency
if (cachedTools != null) if (cachedTools != null)
{ {
_mcpLogger.Debug(LogSource, "Using cached tool definitions"); _mcpLogger.Debug(LogSource, "Using cached tool definitions");
// Initialize the HTTP client's tool names list from cache
_mcpHttpClient.InitializeToolNames(cachedTools);
return cachedTools; return cachedTools;
} }
} }

23
framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs

@ -1,4 +1,4 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
@ -15,7 +15,7 @@ public class Program
Console.OutputEncoding = System.Text.Encoding.UTF8; Console.OutputEncoding = System.Text.Encoding.UTF8;
var loggerOutputTemplate = "{Message:lj}{NewLine}{Exception}"; var loggerOutputTemplate = "{Message:lj}{NewLine}{Exception}";
Log.Logger = new LoggerConfiguration() var config = new LoggerConfiguration()
.MinimumLevel.Information() .MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning) .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning)
@ -26,10 +26,21 @@ public class Program
#else #else
.MinimumLevel.Override("Volo.Abp.Cli", LogEventLevel.Information) .MinimumLevel.Override("Volo.Abp.Cli", LogEventLevel.Information)
#endif #endif
.Enrich.FromLogContext() .Enrich.FromLogContext();
.WriteTo.File(Path.Combine(CliPaths.Log, "abp-cli-logs.txt"), outputTemplate: loggerOutputTemplate)
.WriteTo.Console(theme: AnsiConsoleTheme.Sixteen, outputTemplate: loggerOutputTemplate) if (args.Length > 0 && args[0] == "mcp")
.CreateLogger(); {
Log.Logger = config
.WriteTo.File(Path.Combine(CliPaths.Log, "abp-cli-mcp-logs.txt"), outputTemplate: loggerOutputTemplate)
.CreateLogger();
}
else
{
Log.Logger = config
.WriteTo.File(Path.Combine(CliPaths.Log, "abp-cli-logs.txt"), outputTemplate: loggerOutputTemplate)
.WriteTo.Console(theme: AnsiConsoleTheme.Sixteen, outputTemplate: loggerOutputTemplate)
.CreateLogger();
}
using (var application = AbpApplicationFactory.Create<AbpCliModule>( using (var application = AbpApplicationFactory.Create<AbpCliModule>(
options => options =>

Loading…
Cancel
Save