diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/RegistryToolLoader.cs b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/RegistryToolLoader.cs index db4bb463b..e5c20cf88 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/RegistryToolLoader.cs +++ b/core/Azure.Mcp.Core/src/Areas/Server/Commands/ToolLoading/RegistryToolLoader.cs @@ -162,6 +162,11 @@ private async Task InitializeAsync(CancellationToken cancellationToken) _logger.LogWarning("Failed to create client for provider {ProviderName}: {Error}", serverMetadata.Name, ex.Message); continue; } + catch (Exception ex) + { + _logger.LogWarning("Failed to start client for provider {ProviderName}: {Error}", serverMetadata.Name, ex.Message); + continue; + } if (mcpClient == null) { diff --git a/core/Azure.Mcp.Core/src/Areas/Server/Resources/registry.json b/core/Azure.Mcp.Core/src/Areas/Server/Resources/registry.json index 7b44adc49..50566dfd4 100644 --- a/core/Azure.Mcp.Core/src/Areas/Server/Resources/registry.json +++ b/core/Azure.Mcp.Core/src/Areas/Server/Resources/registry.json @@ -3,6 +3,12 @@ "documentation": { "url": "https://learn.microsoft.com/api/mcp", "description": "Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user's query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge." + }, + "azd": { + "type": "stdio", + "command": "azd", + "args": ["mcp", "start"], + "description": "Azure Developer CLI (azd) includes a suite of tools to help build, modernize, and manage applications on Azure. It simplifies the process of developing cloud applications by providing commands for project initialization, resource provisioning, deployment, and monitoring. Use this tool to streamline your Azure development workflow and manage your cloud resources efficiently." } } } diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 2da6841db..59555990b 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -2,10 +2,11 @@ The Azure MCP Server updates automatically by default whenever a new release comes out 🚀. We ship updates twice a week on Tuesdays and Thursdays 😊 -## 0.8.3 (unreleased) +## 0.8.3 (Unreleased) ### Features Added +- Added support for Azure Developer CLI (azd) MCP tools when azd CLI is installed locally - [[#566](https://github.com/microsoft/mcp/issues/566)] - Adds support to proxy MCP capabilities when child servers leverage sampling or elicitation. [[#581](https://github.com/microsoft/mcp/pull/581)] ## 0.8.2 (2025-09-25) diff --git a/tools/Azure.Mcp.Tools.Extension/src/Commands/AzdCommand.cs b/tools/Azure.Mcp.Tools.Extension/src/Commands/AzdCommand.cs deleted file mode 100644 index e5ed690b7..000000000 --- a/tools/Azure.Mcp.Tools.Extension/src/Commands/AzdCommand.cs +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Net; -using System.Reflection; -using System.Runtime.InteropServices; -using Azure.Mcp.Core.Commands; -using Azure.Mcp.Core.Extensions; -using Azure.Mcp.Core.Helpers; -using Azure.Mcp.Core.Services.Azure; -using Azure.Mcp.Core.Services.ProcessExecution; -using Azure.Mcp.Tools.Extension.Options; -using Microsoft.Extensions.Logging; - -namespace Azure.Mcp.Tools.Extension.Commands; - -public sealed class AzdCommand(ILogger logger, int processTimeoutSeconds = 300) : GlobalCommand() -{ - private const string CommandTitle = "Azure Developer CLI Command"; - private readonly ILogger _logger = logger; - private readonly int _processTimeoutSeconds = processTimeoutSeconds; - private static string? _cachedAzdPath; - - private readonly IEnumerable longRunningCommands = - [ - "provision", - "package", - "deploy", - "up", - "down", - ]; - - private static readonly string s_bestPracticesText = LoadBestPracticesText(); - - /// - /// Clears the cached Azure CLI path. Used for testing purposes. - /// - internal static void ClearCachedAzdPath() - { - _cachedAzdPath = null; - } - - private static string LoadBestPracticesText() - { - Assembly assembly = typeof(AzdCommand).Assembly; - string resourceName = EmbeddedResourceHelper.FindEmbeddedResource(assembly, "azd-best-practices.txt"); - return EmbeddedResourceHelper.ReadEmbeddedResource(assembly, resourceName); - } - - private static readonly string[] s_azdCliPaths = - [ - // Windows - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "Local", "Programs", "Azure Dev CLI"), - // Linux and MacOS - Path.Combine("usr", "local", "bin"), - ]; - - public override string Name => "azd"; - - public override string Description => - """ - Runs Azure Developer CLI (azd) commands. - Agents and LLM's must always run this tool with the 'learn' parameter and empty 'command' on first use to learn more about 'azd' best practices and usage patterns. - - This tool supports the following: - - List, search and show templates to start your project - - Create and initialize new projects and templates - - Show and manage azd configuration - - Show and manage environments and values - - Provision Azure resources - - Deploy applications - - Bring the whole project up and online - - Bring the whole project down and deallocate all Azure resources - - Setup CI/CD pipelines - - Monitor Azure applications - - Show information about the project and its resources - - Show and manage extensions and extension sources - - Show and manage templates and template sources - - If unsure about available commands or their parameters, run azd help or azd --help in the command to discover them. - """; - - public override string Title => CommandTitle; - - public override ToolMetadata Metadata => new() { Destructive = true, ReadOnly = false }; - - protected override void RegisterOptions(Command command) - { - base.RegisterOptions(command); - command.Options.Add(ExtensionOptionDefinitions.Azd.Command); - command.Options.Add(ExtensionOptionDefinitions.Azd.Cwd); - command.Options.Add(ExtensionOptionDefinitions.Azd.Environment); - command.Options.Add(ExtensionOptionDefinitions.Azd.Learn); - } - - protected override AzdOptions BindOptions(ParseResult parseResult) - { - var options = base.BindOptions(parseResult); - options.Command = parseResult.GetValueOrDefault(ExtensionOptionDefinitions.Azd.Command.Name); - options.Cwd = parseResult.GetValueOrDefault(ExtensionOptionDefinitions.Azd.Cwd.Name); - options.Environment = parseResult.GetValueOrDefault(ExtensionOptionDefinitions.Azd.Environment.Name); - options.Learn = parseResult.GetValueOrDefault(ExtensionOptionDefinitions.Azd.Learn.Name); - - return options; - } - - public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult) - { - if (!Validate(parseResult.CommandResult, context.Response).IsValid) - { - return context.Response; - } - - var options = BindOptions(parseResult); - - try - { - // If the agent is asking for help, return the best practices text - if (options.Learn && string.IsNullOrWhiteSpace(options.Command)) - { - context.Response.Message = s_bestPracticesText; - context.Response.Status = HttpStatusCode.OK; - return context.Response; - } - - ArgumentNullException.ThrowIfNull(options.Command); - ArgumentNullException.ThrowIfNull(options.Cwd); - - // Check if the command is a long-running command. The command can contain other flags. - // If is long running command return error message to the user. - if (longRunningCommands.Any(c => options.Command.StartsWith(c, StringComparison.OrdinalIgnoreCase))) - { - var terminalCommand = $"azd {options.Command}"; - - if (!options.Command.Contains("--cwd", StringComparison.OrdinalIgnoreCase)) - { - terminalCommand += $" --cwd {options.Cwd}"; - } - if (!options.Command.Contains("-e", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(options.Environment)) - { - terminalCommand += $" -e {options.Environment}"; - } - - context.Response.Status = HttpStatusCode.BadRequest; - context.Response.Message = - $""" - The requested command is a long-running command and is better suited to be run in a terminal. - Invoke the following command in a terminal window instead of using this tool so the user can see incremental progress. - - ```bash - {terminalCommand} - ``` - """; - - return context.Response; - } - - var command = options.Command; - - command += $" --cwd {options.Cwd}"; - - if (options.Environment is not null) - { - command += $" -e {options.Environment}"; - } - - // We need to always pass the --no-prompt flag to avoid prompting for user input and getting the process stuck - command += " --no-prompt"; - - var processService = context.GetService(); - processService.SetEnvironmentVariables(new Dictionary - { - ["AZURE_DEV_USER_AGENT"] = BaseAzureService.DefaultUserAgent, - }); - - var azdPath = FindAzdCliPath() ?? throw new FileNotFoundException("Azure Developer CLI executable not found in PATH or common installation locations. Please ensure Azure Developer CLI is installed."); - var result = await processService.ExecuteAsync(azdPath, command, _processTimeoutSeconds); - - if (string.IsNullOrWhiteSpace(result.Error) && result.ExitCode == 0) - { - return HandleSuccess(result, command, context.Response); - } - else - { - return HandleError(result, context.Response); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "An exception occurred executing command. Command: {Command}.", options.Command); - HandleException(context, ex); - } - - return context.Response; - } - - internal static string? FindAzdCliPath() - { - // Return cached path if available and still exists - if (!string.IsNullOrEmpty(_cachedAzdPath) && File.Exists(_cachedAzdPath)) - { - return _cachedAzdPath; - } - - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - // Add PATH environment directories followed by the common installation locations - // This will capture any custom AZD installations as well as standard installations. - var searchPaths = new List(); - if (Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) is { } pathDirs) - { - searchPaths.AddRange(pathDirs); - } - - searchPaths.AddRange(s_azdCliPaths); - - foreach (var dir in searchPaths.Where(d => !string.IsNullOrEmpty(d))) - { - if (isWindows) - { - var cmdPath = Path.Combine(dir, "azd.exe"); - if (File.Exists(cmdPath)) - { - _cachedAzdPath = cmdPath; - return cmdPath; - } - } - else - { - var fullPath = Path.Combine(dir, "azd"); - if (File.Exists(fullPath)) - { - _cachedAzdPath = fullPath; - return fullPath; - } - } - } - - return null; - } - - private static CommandResponse HandleSuccess(ProcessResult result, string command, CommandResponse response) - { - var contentResults = new List(); - if (!string.IsNullOrWhiteSpace(result.Output)) - { - contentResults.Add(result.Output); - } - - var commandArgs = command.Split(' '); - - // Help the user decide what the next steps might be based on the command they just executed - switch (commandArgs[0]) - { - case "init": - contentResults.Add( - "Most common commands after initializing a template are the following:\n" + - "- 'azd up': Provision and deploy the application.\n" + - "- 'azd provision': Provision the infrastructure resources.\n" + - "- 'azd deploy': Deploy the application code to the Azure infrastructure.\n" - ); - break; - - case "provision": - contentResults.Add( - "Most common commands after provisioning the application are the following:\n" + - "- 'azd deploy': Deploy the application code to the Azure infrastructure.\n" - ); - break; - - case "deploy": - contentResults.Add( - "Most common commands after deploying the application are the following:\n" + - "- 'azd monitor': Allows the user to monitor the running application and its resources.\n" + - "- 'azd show': Displays the current state of the application and its resources.\n" + - "- 'azd pipeline config': Allows the user to configure a CI/CD pipeline for the application.\n" - ); - break; - - case "up": - contentResults.Add( - "Most common commands after provisioning the application are the following:\n" + - "- 'azd pipeline' config: Allows the user to configure a CI/CD pipeline for the application.\n" + - "- 'azd monitor': Allows the user to monitor the running application and its resources.\n" - ); - break; - } - - response.Results = ResponseResult.Create(contentResults, ExtensionJsonContext.Default.ListString); - - return response; - } - - private static CommandResponse HandleError(ProcessResult result, CommandResponse response) - { - response.Status = HttpStatusCode.InternalServerError; - response.Message = result.Error; - - var contentResults = new List - { - result.Output - }; - - // Check for specific error messages and provide contextual help - if (result.Output.Contains("ERROR: no project exists")) - { - contentResults.Add( - "An azd project is required to run this command. Create a new project by calling 'azd init'" - ); - } - else if (result.Output.Contains("ERROR: infrastructure has not been provisioned.")) - { - contentResults.Add( - "The azd project has not been provisioned yet. Run 'azd provision' to create the Azure infrastructure resources." - ); - } - else if (result.Output.Contains("not logged in") || result.Output.Contains("fetching current principal")) - { - contentResults.Add( - "User is not logged in our auth token is expired. Login by calling 'azd auth login'." - ); - } - else if (result.Output.Contains("ERROR: no environment exists")) - { - contentResults.Add( - "An azd environment is required to run this command. Create a new environment by calling 'azd env new'\n" + - "- Prompt the user for the environment name if needed.\n" - ); - } - else if (result.Output.Contains("no default response for prompt")) - { - contentResults.Add( - """ - The command requires user input. Prompt the user for the required information. - - If missing Azure subscription use other tools to query and list available subscriptions for the user to select, then set the subscription ID (UUID) in the azd environment. - - If missing Azure location, use other tools to query and list available locations for the user to select, then set the location name in the azd environment. - - To set values in the azd environment use the command 'azd env set' command." - """ - ); - } - else if (result.Output.Contains("user denied delete confirmation")) - { - contentResults.Add( - """ - The command requires user confirmation to delete resources. Prompt the user for confirmation before proceeding. - - If the user confirms, re-run the command with the '--force' flag to bypass the confirmation prompt. - - To permanently delete the resources include the '--purge` flag - """ - ); - } - else - { - contentResults.Add( - """ - The command failed. Rerun the command with the '--help' flag to get more information about the command and its parameters. - After reviewing the help information, run the command again with updated parameters. - """ - ); - } - - response.Results = ResponseResult.Create(contentResults, ExtensionJsonContext.Default.ListString); - - return response; - } -} diff --git a/tools/Azure.Mcp.Tools.Extension/src/ExtensionSetup.cs b/tools/Azure.Mcp.Tools.Extension/src/ExtensionSetup.cs index dc6bb4c05..a99a10944 100644 --- a/tools/Azure.Mcp.Tools.Extension/src/ExtensionSetup.cs +++ b/tools/Azure.Mcp.Tools.Extension/src/ExtensionSetup.cs @@ -23,7 +23,6 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider) // Azure CLI and Azure Developer CLI tools are hidden // extension.AddCommand("az", new AzCommand(loggerFactory.CreateLogger())); - // extension.AddCommand("azd", new AzdCommand(loggerFactory.CreateLogger())); var azqr = serviceProvider.GetRequiredService(); extension.AddCommand(azqr.Name, azqr);