Browse Source

Refactor MCP command handling and code organization

Removed special-case handling for MCP commands in CommandSelector and HelpCommand to simplify logic. In McpHttpClientService, reorganized private methods and classes for better code structure and maintainability. Made ConvertProperties static in McpServerService and clarified comments regarding MCP JSON schema requirements.
pull/24677/head
Mansur Besleney 2 weeks ago
parent
commit
a2a6cec9c6
  1. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs
  2. 8
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs
  3. 145
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs
  4. 4
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs

6
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs

@ -17,12 +17,6 @@ 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.IsMcpCommand())
{
return Options.Commands.GetOrDefault("mcp") ?? typeof(HelpCommand);
}
if (commandLineArgs.Command.IsNullOrWhiteSpace())
{
return typeof(HelpCommand);

8
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs

@ -32,14 +32,6 @@ public class HelpCommand : IConsoleCommand, ITransientDependency
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
// 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.IsMcpCommand())
{
// Silently return - MCP server should handle its own errors
return Task.CompletedTask;
}
if (string.IsNullOrWhiteSpace(commandLineArgs.Target))
{
Logger.LogInformation(GetUsageInfo());

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

@ -19,13 +19,6 @@ public class McpHttpClientService : ISingletonDependency
{
private static readonly JsonSerializerOptions JsonSerializerOptionsWeb = new(JsonSerializerDefaults.Web);
private static class ErrorMessages
{
public const string NetworkConnectivity = "The tool execution failed due to a network connectivity issue. Please check your internet connection and try again.";
public const string Timeout = "The tool execution timed out. The operation took too long to complete. Please try again.";
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;
@ -46,40 +39,6 @@ public class McpHttpClientService : ISingletonDependency
_cachedServerUrlLazy = new Lazy<Task<string>>(GetMcpServerUrlInternalAsync);
}
private async Task<string> GetMcpServerUrlAsync()
{
return await _cachedServerUrlLazy.Value;
}
private async Task<string> GetMcpServerUrlInternalAsync()
{
// Check config file
if (File.Exists(CliPaths.McpConfig))
{
try
{
var json = await FileHelper.ReadAllTextAsync(CliPaths.McpConfig);
var config = JsonSerializer.Deserialize<McpConfig>(json, JsonSerializerOptionsWeb);
if (!string.IsNullOrWhiteSpace(config?.ServerUrl))
{
return config.ServerUrl.TrimEnd('/');
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read MCP config file");
}
}
// Return default
return CliConsts.DefaultMcpServerUrl;
}
private class McpConfig
{
public string ServerUrl { get; set; }
}
public void InitializeToolNames(List<McpToolDefinition> tools)
{
_validToolNames = tools.Select(t => t.Name).ToList();
@ -152,37 +111,6 @@ public class McpHttpClientService : ISingletonDependency
}
}
private string CreateErrorResponse(string errorMessage)
{
return JsonSerializer.Serialize(new
{
content = new[]
{
new
{
type = "text",
text = errorMessage
}
},
isError = true
}, JsonSerializerOptionsWeb);
}
private string GetSanitizedHttpErrorMessage(HttpStatusCode statusCode)
{
return statusCode switch
{
HttpStatusCode.Unauthorized => "Authentication failed. Please ensure you are logged in with a valid account.",
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)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."
};
}
public async Task<bool> CheckServerHealthAsync()
{
var baseUrl = await GetMcpServerUrlAsync();
@ -258,6 +186,66 @@ public class McpHttpClientService : ISingletonDependency
}
}
private async Task<string> GetMcpServerUrlAsync()
{
return await _cachedServerUrlLazy.Value;
}
private async Task<string> GetMcpServerUrlInternalAsync()
{
// Check config file
if (File.Exists(CliPaths.McpConfig))
{
try
{
var json = await FileHelper.ReadAllTextAsync(CliPaths.McpConfig);
var config = JsonSerializer.Deserialize<McpConfig>(json, JsonSerializerOptionsWeb);
if (!string.IsNullOrWhiteSpace(config?.ServerUrl))
{
return config.ServerUrl.TrimEnd('/');
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read MCP config file");
}
}
// Return default
return CliConsts.DefaultMcpServerUrl;
}
private string CreateErrorResponse(string errorMessage)
{
return JsonSerializer.Serialize(new
{
content = new[]
{
new
{
type = "text",
text = errorMessage
}
},
isError = true
}, JsonSerializerOptionsWeb);
}
private string GetSanitizedHttpErrorMessage(HttpStatusCode statusCode)
{
return statusCode switch
{
HttpStatusCode.Unauthorized => "Authentication failed. Please ensure you are logged in with a valid account.",
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)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."
};
}
private CliUsageException CreateHttpExceptionWithInner(Exception ex, string context)
{
_mcpLogger.Error(LogSource, context, ex);
@ -273,9 +261,20 @@ public class McpHttpClientService : ISingletonDependency
return new CliUsageException($"Failed to fetch tool definitions: {userMessage}", ex);
}
private static class ErrorMessages
{
public const string NetworkConnectivity = "The tool execution failed due to a network connectivity issue. Please check your internet connection and try again.";
public const string Timeout = "The tool execution timed out. The operation took too long to complete. Please try again.";
public const string Unexpected = "The tool execution failed due to an unexpected error. Please try again later.";
}
private class McpConfig
{
public string ServerUrl { get; set; }
}
private class McpToolsResponse
{
public List<McpToolDefinition> Tools { get; set; }
}
}

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

@ -74,7 +74,7 @@ public class McpServerService : ITransientDependency
private void RegisterToolFromDefinition(McpServerOptions options, McpToolDefinition toolDef)
{
// Convert McpToolDefinition to the input schema format expected by MCP
// Build input schema with lowercase keys as required by MCP JSON Schema format
var inputSchemaObject = new Dictionary<string, object>
{
["type"] = "object",
@ -85,7 +85,7 @@ public class McpServerService : ITransientDependency
RegisterTool(options, toolDef.Name, toolDef.Description, inputSchemaObject, toolDef.OutputSchema);
}
private Dictionary<string, object> ConvertProperties(Dictionary<string, McpToolProperty> properties)
private static Dictionary<string, object> ConvertProperties(Dictionary<string, McpToolProperty> properties)
{
if (properties == null)
{

Loading…
Cancel
Save