From d63719fb1487b52873528fef002d940191510e8a Mon Sep 17 00:00:00 2001 From: Mansur Besleney Date: Wed, 14 Jan 2026 15:56:15 +0300 Subject: [PATCH] Refactor MCP command handling and tool definition flow Introduces CommandLineArgsExtensions for MCP command detection, removes McpToolDefinitionValidator, and updates MCP tool definition fetching to require a successful server connection. Cleans up unused environment variables, adds mcp-config.json support, and simplifies tool validation and caching logic. These changes improve reliability and maintainability of MCP command execution and tool management. --- .../Abp/Cli/Args/CommandLineArgsExtensions.cs | 11 ++ .../Volo/Abp/Cli/CliConsts.cs | 2 - .../Volo/Abp/Cli/CliPaths.cs | 1 + .../Volo/Abp/Cli/CliService.cs | 6 +- .../Volo/Abp/Cli/Commands/CommandSelector.cs | 4 +- .../Volo/Abp/Cli/Commands/HelpCommand.cs | 4 +- .../Volo/Abp/Cli/Commands/McpCommand.cs | 80 +------- .../Commands/Services/McpHttpClientService.cs | 68 +++---- .../Cli/Commands/Services/McpServerService.cs | 1 + .../Services/McpToolDefinitionValidator.cs | 181 ------------------ .../Commands/Services/McpToolsCacheService.cs | 55 ++---- 11 files changed, 77 insertions(+), 336 deletions(-) create mode 100644 framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs delete mode 100644 framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolDefinitionValidator.cs diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs new file mode 100644 index 0000000000..e9ae5ba77c --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Cli.Commands; + +namespace Volo.Abp.Cli.Args; + +public static class CommandLineArgsExtensions +{ + public static bool IsMcpCommand(this CommandLineArgs args) + { + return args.IsCommand(McpCommand.Name); + } +} 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 4aaa111398..43436329fb 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 @@ -20,7 +20,6 @@ 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"; @@ -28,6 +27,5 @@ public static class CliConsts { public const string LatestCliVersionCheckDate = "LatestCliVersionCheckDate"; public const string McpToolsLastFetchDate = "McpToolsLastFetchDate"; - public const string McpServerUrl = "McpServerUrl"; } } 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 683770bb98..537c794c15 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 @@ -16,6 +16,7 @@ public static class CliPaths 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 string McpConfig => Path.Combine(Root, "mcp-config.json"); 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 c8c6ba52a1..52bbac8e43 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 @@ -64,7 +64,7 @@ public class CliService : ITransientDependency var commandLineArgs = CommandLineArgumentParser.Parse(args); var currentCliVersion = await CliVersionService.GetCurrentCliVersionAsync(); - var isMcpCommand = commandLineArgs.IsCommand("mcp"); + var isMcpCommand = commandLineArgs.IsMcpCommand(); // Don't print banner for MCP command to avoid corrupting stdout JSON-RPC stream if (!isMcpCommand) @@ -99,7 +99,7 @@ public class CliService : ITransientDependency catch (CliUsageException usageException) { // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream - if (commandLineArgs.IsCommand("mcp")) + if (commandLineArgs.IsMcpCommand()) { _mcpLogger.Error(McpLogSource, usageException.Message); } @@ -113,7 +113,7 @@ public class CliService : ITransientDependency { await _telemetryService.AddErrorActivityAsync(ex.Message); // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream - if (commandLineArgs.IsCommand("mcp")) + if (commandLineArgs.IsMcpCommand()) { _mcpLogger.Error(McpLogSource, "Fatal error", ex); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs index cf73aa21a7..e398a7747a 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using Volo.Abp.Cli.Args; @@ -18,7 +18,7 @@ public class CommandSelector : ICommandSelector, ITransientDependency public Type Select(CommandLineArgs commandLineArgs) { // Don't fall back to HelpCommand for MCP command to avoid corrupting stdout JSON-RPC stream - if (commandLineArgs.IsCommand("mcp")) + if (commandLineArgs.IsMcpCommand()) { return Options.Commands.GetOrDefault("mcp") ?? typeof(HelpCommand); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs index 43a36a8b74..b6c1cfba11 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -34,7 +34,7 @@ public class HelpCommand : IConsoleCommand, ITransientDependency { // Don't output help text for MCP command to avoid corrupting stdout JSON-RPC stream // If MCP command is being used, it should have been handled directly, not through HelpCommand - if (commandLineArgs.IsCommand("mcp")) + if (commandLineArgs.IsMcpCommand()) { // Silently return - MCP server should handle its own errors return Task.CompletedTask; 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 95500c17e0..ebf59e2ab6 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 @@ -65,14 +65,16 @@ public class McpCommand : IConsoleCommand, ITransientDependency await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsMcp); - // Check server health before starting + // Check server health before starting - fail if not reachable _mcpLogger.Info(LogSource, "Checking ABP.IO MCP Server connection..."); var isHealthy = await _mcpHttpClient.CheckServerHealthAsync(); if (!isHealthy) { - _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..."); + throw new CliUsageException( + "Could not connect to ABP.IO MCP Server. " + + "The MCP server requires a connection to fetch tool definitions. " + + "Please check your internet connection and try again."); } _mcpLogger.Info(LogSource, "Starting ABP MCP Server..."); @@ -141,15 +143,13 @@ public class McpCommand : IConsoleCommand, ITransientDependency private Task PrintConfigurationAsync() { - var abpCliPath = GetAbpCliExecutablePath(); - var config = new McpClientConfiguration { McpServers = new Dictionary { ["abp"] = new McpServerConfig { - Command = abpCliPath, + Command = "abp", Args = new List { "mcp" }, Env = new Dictionary() } @@ -167,74 +167,6 @@ public class McpCommand : IConsoleCommand, ITransientDependency return Task.CompletedTask; } - private string GetAbpCliExecutablePath() - { - var processPath = TryGetExecutablePathFromCurrentProcess(); - if (processPath != null) - { - return processPath; - } - - var environmentPath = TryGetExecutablePathFromEnvironmentPath(); - if (environmentPath != null) - { - return environmentPath; - } - - // Default to "abp" and let the system resolve it - return "abp"; - } - - private string TryGetExecutablePathFromCurrentProcess() - { - try - { - using (var process = Process.GetCurrentProcess()) - { - var processPath = process.MainModule?.FileName; - - if (!string.IsNullOrEmpty(processPath) && - Path.GetFileName(processPath).StartsWith("abp", StringComparison.OrdinalIgnoreCase)) - { - return processPath; - } - } - } - catch - { - // Ignore errors getting process path - } - - return null; - } - - private string TryGetExecutablePathFromEnvironmentPath() - { - var pathEnv = Environment.GetEnvironmentVariable("PATH"); - if (string.IsNullOrEmpty(pathEnv)) - { - return null; - } - - var paths = pathEnv.Split(Path.PathSeparator); - foreach (var path in paths) - { - var abpPath = Path.Combine(path, "abp.exe"); - if (File.Exists(abpPath)) - { - return abpPath; - } - - abpPath = Path.Combine(path, "abp"); - if (File.Exists(abpPath)) - { - return abpPath; - } - } - - return null; - } - public string GetUsageInfo() { var sb = new StringBuilder(); 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 6c3ba356af..c1314d7ea1 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 @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -9,15 +10,13 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Volo.Abp.Cli.Commands.Models; using Volo.Abp.Cli.Http; -using Volo.Abp.Cli.Memory; using Volo.Abp.DependencyInjection; +using Volo.Abp.IO; 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 @@ -27,53 +26,67 @@ public class McpHttpClientService : ITransientDependency public const string Unexpected = "The tool execution failed due to an unexpected error. Please try again later."; } + private const string LogSource = nameof(McpHttpClientService); + private readonly CliHttpClientFactory _httpClientFactory; private readonly ILogger _logger; private readonly IMcpLogger _mcpLogger; - private readonly MemoryService _memoryService; private readonly Lazy> _cachedServerUrlLazy; private List _validToolNames; + private bool _toolDefinitionsLoaded; public McpHttpClientService( CliHttpClientFactory httpClientFactory, ILogger logger, - IMcpLogger mcpLogger, - MemoryService memoryService) + IMcpLogger mcpLogger) { _httpClientFactory = httpClientFactory; _logger = logger; _mcpLogger = mcpLogger; - _memoryService = memoryService; _cachedServerUrlLazy = new Lazy>(GetMcpServerUrlInternalAsync); } private async Task GetMcpServerUrlAsync() { - return await _cachedServerUrlLazy.Value; + return "http://localhost:5100";//await _cachedServerUrlLazy.Value; } private async Task GetMcpServerUrlInternalAsync() { - // 1. Check environment variable (highest priority) - var envUrl = Environment.GetEnvironmentVariable(CliConsts.McpServerUrlEnvironmentVariable); - if (!string.IsNullOrWhiteSpace(envUrl)) - { - return envUrl.TrimEnd('/'); - } - - // 2. Check persisted setting - var persistedUrl = await _memoryService.GetAsync(CliConsts.MemoryKeys.McpServerUrl); - if (!string.IsNullOrWhiteSpace(persistedUrl)) + // Check config file + if (File.Exists(CliPaths.McpConfig)) { - return persistedUrl.TrimEnd('/'); + try + { + var json = await FileHelper.ReadAllTextAsync(CliPaths.McpConfig); + var config = JsonSerializer.Deserialize(json, JsonSerializerOptionsWeb); + if (!string.IsNullOrWhiteSpace(config?.ServerUrl)) + { + return config.ServerUrl.TrimEnd('/'); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to read MCP config file"); + } } - // 3. Return default + // Return default return CliConsts.DefaultMcpServerUrl; } + private class McpConfig + { + public string ServerUrl { get; set; } + } + public async Task CallToolAsync(string toolName, JsonElement arguments) { + if (!_toolDefinitionsLoaded) + { + throw new CliUsageException("Tool definitions have not been loaded yet. This is an internal error."); + } + // Validate toolName against whitelist to prevent malicious input if (_validToolNames != null && !_validToolNames.Contains(toolName)) { @@ -146,16 +159,6 @@ public class McpHttpClientService : ITransientDependency }, JsonSerializerOptionsWeb); } - private CliUsageException CreateToolDefinitionException(string userMessage) - { - return new CliUsageException($"Failed to fetch tool definitions: {userMessage}"); - } - - private CliUsageException CreateToolDefinitionException(string userMessage, Exception innerException) - { - return new CliUsageException($"Failed to fetch tool definitions: {userMessage}", innerException); - } - private string GetSanitizedHttpErrorMessage(HttpStatusCode statusCode) { return statusCode switch @@ -204,7 +207,7 @@ public class McpHttpClientService : ITransientDependency // Throw sanitized exception var errorMessage = GetSanitizedHttpErrorMessage(response.StatusCode); - throw CreateToolDefinitionException(errorMessage); + throw new CliUsageException($"Failed to fetch tool definitions: {errorMessage}"); } var responseContent = await response.Content.ReadAsStringAsync(); @@ -215,6 +218,7 @@ public class McpHttpClientService : ITransientDependency // Cache tool names for validation _validToolNames = tools.Select(t => t.Name).ToList(); + _toolDefinitionsLoaded = true; return tools; } @@ -253,7 +257,7 @@ public class McpHttpClientService : ITransientDependency _ => "An unexpected error occurred. Please try again later." }; - return CreateToolDefinitionException(userMessage, ex); + return new CliUsageException($"Failed to fetch tool definitions: {userMessage}", ex); } private class McpToolsResponse 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 30370b8bc6..c77443b1f1 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,6 +4,7 @@ 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; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolDefinitionValidator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolDefinitionValidator.cs deleted file mode 100644 index 49d8c6afc6..0000000000 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolDefinitionValidator.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; -using Volo.Abp.Cli.Commands.Models; -using Volo.Abp.DependencyInjection; - -namespace Volo.Abp.Cli.Commands.Services; - -public class McpToolDefinitionValidator : ITransientDependency -{ - private const int MaxToolNameLength = 100; - private const int MaxDescriptionLength = 2000; - private const string UnknownToolName = ""; - - private static readonly Regex ToolNameRegex = new Regex("^[a-zA-Z0-9_]+$", RegexOptions.Compiled); - private static readonly HashSet ValidTypeValues = new HashSet - { "string", "number", "boolean", "object", "array" }; - - private readonly ILogger _logger; - - public McpToolDefinitionValidator(ILogger logger) - { - _logger = logger; - } - - public List ValidateAndFilter(List tools) - { - if (tools == null || tools.Count == 0) - { - return new List(); - } - - var validTools = new List(); - - foreach (var tool in tools) - { - try - { - if (!IsValidTool(tool)) - { - continue; - } - - validTools.Add(tool); - } - catch (Exception ex) - { - _logger.LogWarning($"Error validating tool '{tool?.Name ?? UnknownToolName}': {ex.Message}"); - } - } - - if (validTools.Count < tools.Count) - { - _logger.LogWarning($"Filtered out {tools.Count - validTools.Count} invalid tool(s). {validTools.Count} valid tool(s) remaining."); - } - - return validTools; - } - - private bool IsValidTool(McpToolDefinition tool) - { - if (!IsValidToolName(tool)) - { - return false; - } - - if (!IsValidDescription(tool)) - { - return false; - } - - if (tool.InputSchema != null && !IsValidInputSchema(tool)) - { - return false; - } - - return true; - } - - private bool IsValidToolName(McpToolDefinition tool) - { - if (string.IsNullOrWhiteSpace(tool.Name)) - { - _logger.LogWarning($"Skipping tool with empty name"); - return false; - } - - if (tool.Name.Length > MaxToolNameLength) - { - _logger.LogWarning($"Skipping tool '{tool.Name}' with name exceeding {MaxToolNameLength} characters"); - return false; - } - - if (!ToolNameRegex.IsMatch(tool.Name)) - { - _logger.LogWarning($"Skipping tool with invalid name format: {tool.Name}"); - return false; - } - - return true; - } - - private bool IsValidDescription(McpToolDefinition tool) - { - if (string.IsNullOrWhiteSpace(tool.Description)) - { - _logger.LogWarning($"Skipping tool '{tool.Name}' with empty description"); - return false; - } - - if (tool.Description.Length > MaxDescriptionLength) - { - _logger.LogWarning($"Skipping tool '{tool.Name}' with description exceeding {MaxDescriptionLength} characters"); - return false; - } - - return true; - } - - private bool IsValidInputSchema(McpToolDefinition tool) - { - if (!ArePropertiesValid(tool)) - { - return false; - } - - if (!AreRequiredFieldsValid(tool)) - { - return false; - } - - return true; - } - - private bool ArePropertiesValid(McpToolDefinition tool) - { - if (tool.InputSchema.Properties == null) - { - return true; - } - - foreach (var property in tool.InputSchema.Properties) - { - if (string.IsNullOrWhiteSpace(property.Value?.Type) || - !ValidTypeValues.Contains(property.Value.Type)) - { - _logger.LogWarning($"Skipping tool '{tool.Name}' with invalid property type: {property.Value?.Type ?? UnknownToolName}"); - return false; - } - - if (property.Value.Description != null && property.Value.Description.Length > MaxDescriptionLength) - { - _logger.LogWarning($"Skipping tool '{tool.Name}' with property description exceeding {MaxDescriptionLength} characters"); - return false; - } - } - - return true; - } - - private bool AreRequiredFieldsValid(McpToolDefinition tool) - { - if (tool.InputSchema.Required == null || tool.InputSchema.Properties == null) - { - return true; - } - - foreach (var required in tool.InputSchema.Required) - { - if (!tool.InputSchema.Properties.ContainsKey(required)) - { - _logger.LogWarning($"Skipping tool '{tool.Name}' with required field '{required}' not in properties"); - return false; - } - } - - return true; - } -} - 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 741e30a840..2a24f585d2 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 @@ -20,20 +20,17 @@ public class McpToolsCacheService : ITransientDependency 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, IMcpLogger mcpLogger) { _mcpHttpClient = mcpHttpClient; _memoryService = memoryService; - _validator = validator; _logger = logger; _mcpLogger = mcpLogger; } @@ -51,45 +48,23 @@ public class McpToolsCacheService : ITransientDependency } // Cache is invalid or missing, fetch from server - try + _mcpLogger.Info(LogSource, "Fetching tool definitions from server..."); + var tools = await _mcpHttpClient.GetToolDefinitionsAsync(); + + // Validate that we got tools + if (tools == null || tools.Count == 0) { - _mcpLogger.Info(LogSource, "Fetching tool definitions from server..."); - var tools = await _mcpHttpClient.GetToolDefinitionsAsync(); - - // Validate and filter tool definitions - var validTools = _validator.ValidateAndFilter(tools); - - if (validTools.Count == 0) - { - _logger.LogWarning("No valid tool definitions received from server"); - _mcpLogger.Warning(LogSource, "No valid tool definitions received from server"); - return new List(); - } - - // Save validated tools to cache - await SaveToCacheAsync(validTools); - await _memoryService.SetAsync(CliConsts.MemoryKeys.McpToolsLastFetchDate, DateTime.Now.ToString(CultureInfo.InvariantCulture)); - - _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"); - _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) - { - _mcpLogger.Info(LogSource, "Using expired cache as fallback"); - return cachedTools; - } - - _mcpLogger.Warning(LogSource, "No cached data available, using empty tool list"); - return new List(); + throw new CliUsageException( + "Failed to fetch tool definitions from ABP.IO MCP Server. " + + "No tools available. The MCP server cannot start without tool definitions."); } + + // Save tools to cache + await SaveToCacheAsync(tools); + await _memoryService.SetAsync(CliConsts.MemoryKeys.McpToolsLastFetchDate, DateTime.Now.ToString(CultureInfo.InvariantCulture)); + + _mcpLogger.Info(LogSource, $"Successfully fetched and cached {tools.Count} tool definitions"); + return tools; } private async Task IsCacheValidAsync()