diff --git a/.gitignore b/.gitignore
index a386e86320..d3bc52cfb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -328,4 +328,4 @@ deploy/_run_all_log.txt
# No commit yarn.lock files in the subfolders of templates directory
templates/**/yarn.lock
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Logs/logs.txt
-templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json
+templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 45b24e171e..5d05c21e02 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -30,6 +30,7 @@
+
@@ -195,4 +196,4 @@
-
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
index 078d1aa6cc..23cb5c2b33 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
+++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
@@ -14,6 +14,7 @@
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
index 8ff8ad3206..a188137ea2 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
@@ -79,6 +79,7 @@ public class AbpCliCoreModule : AbpModule
options.Commands[ClearDownloadCacheCommand.Name] = typeof(ClearDownloadCacheCommand);
options.Commands[RecreateInitialMigrationCommand.Name] = typeof(RecreateInitialMigrationCommand);
options.Commands[GenerateRazorPage.Name] = typeof(GenerateRazorPage);
+ options.Commands[McpCommand.Name] = typeof(McpCommand);
options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Pro");
options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Lite");
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 78a36fe329..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
@@ -1,4 +1,4 @@
-namespace Volo.Abp.Cli;
+namespace Volo.Abp.Cli;
public static class CliConsts
{
@@ -20,8 +20,12 @@ public static class CliConsts
public static string AppSettingsSecretJsonFileName = "appsettings.secrets.json";
+ public const string McpLogLevelEnvironmentVariable = "ABP_MCP_LOG_LEVEL";
+ public const string DefaultMcpServerUrl = "https://mcp.abp.io";
+
public static class MemoryKeys
{
public const string LatestCliVersionCheckDate = "LatestCliVersionCheckDate";
+ public const string McpToolsLastFetchDate = "McpToolsLastFetchDate";
}
}
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 d47987b220..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
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Text;
@@ -14,6 +14,9 @@ public static class CliPaths
public static string Memory => Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!, "memory.bin");
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 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 063ceddebf..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
@@ -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,19 +54,27 @@ public class CliService : ITransientDependency
CmdHelper = cmdHelper;
CliVersionService = cliVersionService;
_telemetryService = telemetryService;
+ _mcpLogger = mcpLogger;
Logger = NullLogger.Instance;
}
public async Task RunAsync(string[] args)
{
- var currentCliVersion = await CliVersionService.GetCurrentCliVersionAsync();
- Logger.LogInformation($"ABP CLI {currentCliVersion}");
-
var commandLineArgs = CommandLineArgumentParser.Parse(args);
+ var currentCliVersion = await CliVersionService.GetCurrentCliVersionAsync();
+
+ var isMcpCommand = commandLineArgs.IsMcpCommand();
+
+ // Don't print banner for MCP command to avoid corrupting stdout JSON-RPC stream
+ if (!isMcpCommand)
+ {
+ Logger.LogInformation($"ABP CLI {currentCliVersion}");
+ }
#if !DEBUG
- if (!commandLineArgs.Options.ContainsKey("skip-cli-version-check"))
+ // Skip version check for MCP command to avoid corrupting stdout JSON-RPC stream
+ if (!isMcpCommand && !commandLineArgs.Options.ContainsKey("skip-cli-version-check"))
{
await CheckCliVersionAsync(currentCliVersion);
}
@@ -85,13 +98,29 @@ public class CliService : ITransientDependency
}
catch (CliUsageException usageException)
{
- Logger.LogWarning(usageException.Message);
+ // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream
+ if (commandLineArgs.IsMcpCommand())
+ {
+ _mcpLogger.Error(McpLogSource, usageException.Message);
+ }
+ else
+ {
+ Logger.LogWarning(usageException.Message);
+ }
Environment.ExitCode = 1;
}
catch (Exception ex)
{
await _telemetryService.AddErrorActivityAsync(ex.Message);
- Logger.LogException(ex);
+ // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream
+ if (commandLineArgs.IsMcpCommand())
+ {
+ _mcpLogger.Error(McpLogSource, "Fatal error", ex);
+ }
+ else
+ {
+ Logger.LogException(ex);
+ }
throw;
}
finally
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 9bfdcfac5c..a409614712 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;
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 cc13e9d187..c051f6d455 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;
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
new file mode 100644
index 0000000000..ebf59e2ab6
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Volo.Abp.Cli.Args;
+using Volo.Abp.Cli.Auth;
+using Volo.Abp.Cli.Commands.Models;
+using Volo.Abp.Cli.Commands.Services;
+using Volo.Abp.Cli.Licensing;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
+
+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;
+ private readonly ITelemetryService _telemetryService;
+
+ public ILogger Logger { get; set; }
+
+ public McpCommand(
+ IApiKeyService apiKeyService,
+ AuthService authService,
+ McpServerService mcpServerService,
+ McpHttpClientService mcpHttpClient,
+ IMcpLogger mcpLogger,
+ ITelemetryService telemetryService)
+ {
+ _apiKeyService = apiKeyService;
+ _authService = authService;
+ _mcpServerService = mcpServerService;
+ _mcpHttpClient = mcpHttpClient;
+ _mcpLogger = mcpLogger;
+ _telemetryService = telemetryService;
+ Logger = NullLogger.Instance;
+ }
+
+ public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
+ {
+ await ValidateLicenseAsync();
+
+ var option = commandLineArgs.Target;
+
+ if (!string.IsNullOrEmpty(option) && option.Equals("get-config", StringComparison.OrdinalIgnoreCase))
+ {
+ await PrintConfigurationAsync();
+ return;
+ }
+
+ await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsMcp);
+
+ // 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)
+ {
+ 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...");
+
+ var cts = new CancellationTokenSource();
+
+ ConsoleCancelEventHandler cancelHandler = (sender, e) =>
+ {
+ e.Cancel = true;
+ _mcpLogger.Info(LogSource, "Shutting down ABP MCP Server...");
+
+ try
+ {
+ cts.Cancel();
+ }
+ catch (ObjectDisposedException)
+ {
+ // CTS already disposed
+ }
+ };
+
+ Console.CancelKeyPress += cancelHandler;
+
+ try
+ {
+ await _mcpServerService.RunAsync(cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when Ctrl+C is pressed
+ }
+ catch (Exception ex)
+ {
+ _mcpLogger.Error(LogSource, "Error running MCP server", ex);
+ throw;
+ }
+ finally
+ {
+ Console.CancelKeyPress -= cancelHandler;
+ cts.Dispose();
+ }
+ }
+
+ private async Task ValidateLicenseAsync()
+ {
+ var loginInfo = await _authService.GetLoginInfoAsync();
+
+ if (string.IsNullOrEmpty(loginInfo?.Organization))
+ {
+ throw new CliUsageException("Please log in with your account!");
+ }
+
+ var licenseResult = await _apiKeyService.GetApiKeyOrNullAsync();
+
+ if (licenseResult == null || !licenseResult.HasActiveLicense)
+ {
+ var errorMessage = licenseResult?.ErrorMessage ?? "No active license found.";
+ throw new CliUsageException(errorMessage);
+ }
+
+ if (licenseResult.LicenseEndTime.HasValue && licenseResult.LicenseEndTime.Value < DateTime.UtcNow)
+ {
+ throw new CliUsageException("Your license has expired. Please renew your license to use the MCP server.");
+ }
+ }
+
+ private Task PrintConfigurationAsync()
+ {
+ var config = new McpClientConfiguration
+ {
+ McpServers = new Dictionary
+ {
+ ["abp"] = new McpServerConfig
+ {
+ Command = "abp",
+ Args = new List { "mcp" },
+ Env = new Dictionary()
+ }
+ }
+ };
+
+ var json = JsonSerializer.Serialize(config, new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ Console.WriteLine(json);
+
+ return Task.CompletedTask;
+ }
+
+ public string GetUsageInfo()
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("");
+ sb.AppendLine("Usage:");
+ sb.AppendLine("");
+ sb.AppendLine(" abp mcp [options]");
+ sb.AppendLine("");
+ sb.AppendLine("Options:");
+ sb.AppendLine("");
+ sb.AppendLine(" (start the local MCP server)");
+ sb.AppendLine("get-config (print MCP client configuration as JSON)");
+ sb.AppendLine("");
+ sb.AppendLine("Examples:");
+ sb.AppendLine("");
+ sb.AppendLine(" abp mcp");
+ sb.AppendLine(" abp mcp get-config");
+ sb.AppendLine("");
+
+ return sb.ToString();
+ }
+
+ public static string GetShortDescription()
+ {
+ return "Runs the local MCP server and outputs client configuration for AI tool integration.";
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpClientConfiguration.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpClientConfiguration.cs
new file mode 100644
index 0000000000..60b21cbb33
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpClientConfiguration.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Volo.Abp.Cli.Commands.Models;
+
+public class McpClientConfiguration
+{
+ [JsonPropertyName("mcpServers")]
+ public Dictionary McpServers { get; set; } = new();
+}
+
+public class McpServerConfig
+{
+ [JsonPropertyName("command")]
+ public string Command { get; set; }
+
+ [JsonPropertyName("args")]
+ public List Args { get; set; } = new();
+
+ [JsonPropertyName("env")]
+ public Dictionary Env { get; set; }
+}
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpToolDefinition.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpToolDefinition.cs
new file mode 100644
index 0000000000..f59776bc0a
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpToolDefinition.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Text.Json;
+
+namespace Volo.Abp.Cli.Commands.Models;
+
+public class McpToolDefinition
+{
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public McpToolInputSchema InputSchema { get; set; }
+ public JsonElement? OutputSchema { get; set; }
+}
+
+public class McpToolInputSchema
+{
+ public string Type { get; set; } = "object";
+ public Dictionary Properties { get; set; }
+ public List Required { get; set; }
+}
+
+public class McpToolProperty
+{
+ public string Type { get; set; }
+ public string Description { get; set; }
+}
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpMcpServerTool.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpMcpServerTool.cs
new file mode 100644
index 0000000000..6858cf7571
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpMcpServerTool.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+namespace Volo.Abp.Cli.Commands.Services;
+
+internal class AbpMcpServerTool : McpServerTool
+{
+ private readonly string _name;
+ private readonly string _description;
+ private readonly JsonElement _inputSchema;
+ private readonly JsonElement? _outputSchema;
+ private readonly Func, CancellationToken, ValueTask> _handler;
+
+ public AbpMcpServerTool(
+ string name,
+ string description,
+ JsonElement inputSchema,
+ JsonElement? outputSchema,
+ Func, CancellationToken, ValueTask> handler)
+ {
+ _name = name;
+ _description = description;
+ _inputSchema = inputSchema;
+ _outputSchema = outputSchema;
+ _handler = handler;
+ }
+
+ public override Tool ProtocolTool => new Tool
+ {
+ Name = _name,
+ Description = _description,
+ InputSchema = _inputSchema,
+ OutputSchema = _outputSchema
+ };
+
+ public override IReadOnlyList