From e337c0132d1b75993e1479ae2e4bce14ac0610b2 Mon Sep 17 00:00:00 2001 From: Ji Wang Date: Thu, 18 Sep 2025 13:32:26 -0700 Subject: [PATCH 1/5] Add `azmcp sql server list` command and unit tests --- docs/azmcp-commands.md | 4 + docs/e2eTestPrompts.md | 2 + .../src/Commands/Server/ServerListCommand.cs | 104 +++++++ .../src/Commands/SqlJsonContext.cs | 1 + .../src/Options/Server/ServerListOptions.cs | 10 + .../src/Services/ISqlService.cs | 14 + .../src/Services/SqlService.cs | 74 +++++ tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs | 1 + .../Server/ServerListCommandTests.cs | 294 ++++++++++++++++++ 9 files changed, 504 insertions(+) create mode 100644 tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerListCommand.cs create mode 100644 tools/Azure.Mcp.Tools.Sql/src/Options/Server/ServerListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Server/ServerListCommandTests.cs diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index 73f455689..6c80cb47d 100644 --- a/docs/azmcp-commands.md +++ b/docs/azmcp-commands.md @@ -1009,6 +1009,10 @@ azmcp sql server firewall-rule list --subscription \ --resource-group \ --server +# List SQL servers in a resource group +azmcp sql server list --subscription \ + --resource-group + # Delete a SQL server azmcp sql server delete --subscription \ --resource-group \ diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md index 6eddf0e05..fbdb1cc6f 100644 --- a/docs/e2eTestPrompts.md +++ b/docs/e2eTestPrompts.md @@ -405,6 +405,8 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | azmcp_sql_server_firewall-rule_list | List all firewall rules for SQL server | | azmcp_sql_server_firewall-rule_list | Show me the firewall rules for SQL server | | azmcp_sql_server_firewall-rule_list | What firewall rules are configured for my SQL server ? | +| azmcp_sql_server_list | List all Azure SQL servers in resource group | +| azmcp_sql_server_list | Show me every SQL server available in resource group | | azmcp_sql_server_show | Show me the details of Azure SQL server in resource group | | azmcp_sql_server_show | Get the configuration details for SQL server | | azmcp_sql_server_show | Display the properties of SQL server | diff --git a/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerListCommand.cs b/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerListCommand.cs new file mode 100644 index 000000000..bf9eaf7fa --- /dev/null +++ b/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerListCommand.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine.Parsing; +using Azure; +using Azure.Mcp.Core.Commands; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.Sql.Models; +using Azure.Mcp.Tools.Sql.Options.Server; +using Azure.Mcp.Tools.Sql.Services; +using Microsoft.Extensions.Logging; + +namespace Azure.Mcp.Tools.Sql.Commands.Server; + +public sealed class ServerListCommand(ILogger logger) + : SubscriptionCommand +{ + private readonly ILogger _logger = logger; + private const string CommandTitle = "List SQL Servers"; + + public override string Name => "list"; + + public override string Description => + """ + Lists Azure SQL servers within a resource group including fully qualified domain name, state, + administrator login, and network access settings. Use this command to discover SQL servers, + audit configurations, or verify deployment targets. Equivalent to 'az sql server list'. + Required parameters: subscription ID and resource group name. + Returns: JSON array of SQL server objects with metadata, network configuration, and tags. + """; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = true, + OpenWorld = true, + ReadOnly = true, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + } + + protected override ServerListOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup = parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.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 + { + var sqlService = context.GetService(); + + var servers = await sqlService.ListServersAsync( + options.ResourceGroup!, + options.Subscription!, + options.RetryPolicy); + + context.Response.Results = ResponseResult.Create( + new ServerListResult(servers ?? []), + SqlJsonContext.Default.ServerListResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error listing SQL servers. ResourceGroup: {ResourceGroup}, Options: {@Options}", + options.ResourceGroup, + options); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + RequestFailedException reqEx when reqEx.Status == 403 => + $"Authorization failed listing SQL servers. Verify you have appropriate permissions. Details: {reqEx.Message}", + RequestFailedException reqEx when reqEx.Status == 404 => + "No SQL servers found for the specified resource group. Verify the resource group and subscription.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record ServerListResult(List Servers); +} diff --git a/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs b/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs index 7c4c547ce..acd410e4a 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs @@ -23,6 +23,7 @@ namespace Azure.Mcp.Tools.Sql.Commands; [JsonSerializable(typeof(FirewallRuleDeleteCommand.FirewallRuleDeleteResult))] [JsonSerializable(typeof(ServerCreateCommand.ServerCreateResult))] [JsonSerializable(typeof(ServerDeleteCommand.ServerDeleteResult))] +[JsonSerializable(typeof(ServerListCommand.ServerListResult))] [JsonSerializable(typeof(ServerShowCommand.ServerShowResult))] [JsonSerializable(typeof(ElasticPoolListCommand.ElasticPoolListResult))] [JsonSerializable(typeof(SqlDatabase))] diff --git a/tools/Azure.Mcp.Tools.Sql/src/Options/Server/ServerListOptions.cs b/tools/Azure.Mcp.Tools.Sql/src/Options/Server/ServerListOptions.cs new file mode 100644 index 000000000..47975c7e5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Sql/src/Options/Server/ServerListOptions.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.Sql.Options.Server; + +public class ServerListOptions : SubscriptionOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs b/tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs index 75f4377f6..c77b902ab 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs @@ -260,6 +260,20 @@ Task GetServerAsync( RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken = default); + /// + /// Lists SQL servers in a resource group. + /// + /// The name of the resource group + /// The subscription ID or name + /// Optional retry policy options + /// Cancellation token + /// A list of SQL servers + Task> ListServersAsync( + string resourceGroup, + string subscription, + RetryPolicyOptions? retryPolicy, + CancellationToken cancellationToken = default); + /// /// Deletes a SQL server. /// diff --git a/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs b/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs index fbec99fee..82d836e3c 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs @@ -700,6 +700,60 @@ public async Task GetServerAsync( } } + /// + /// Retrieves a list of SQL servers within a specific resource group. + /// + /// The name of the resource group containing the servers + /// The subscription ID or name + /// Optional retry policy configuration for resilient operations + /// Token to observe for cancellation requests + /// A list of SQL servers found in the specified resource group + /// Thrown when required parameters are null or empty + public async Task> ListServersAsync( + string resourceGroup, + string subscription, + RetryPolicyOptions? retryPolicy, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters(resourceGroup, subscription); + + try + { + var armClient = await CreateArmClientAsync(null, retryPolicy); + var subscriptionResource = armClient.GetSubscriptionResource(Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + + Azure.ResourceManager.Resources.ResourceGroupResource resourceGroupResource; + try + { + var response = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); + resourceGroupResource = response.Value; + } + catch (RequestFailedException reqEx) when (reqEx.Status == 404) + { + _logger.LogWarning(reqEx, + "Resource group not found when listing SQL servers. ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + resourceGroup, subscription); + return []; + } + + var servers = new List(); + + await foreach (var serverResource in resourceGroupResource.GetSqlServers().GetAllAsync(cancellationToken: cancellationToken)) + { + servers.Add(ConvertToSqlServerModel(serverResource)); + } + + return servers; + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error listing SQL servers. ResourceGroup: {ResourceGroup}, Subscription: {Subscription}", + resourceGroup, subscription); + throw; + } + } + public async Task DeleteServerAsync( string serverName, string resourceGroup, @@ -857,6 +911,26 @@ private static SqlDatabase ConvertToSqlDatabaseModel(JsonElement item) ); } + private static SqlServer ConvertToSqlServerModel(SqlServerResource serverResource) + { + ArgumentNullException.ThrowIfNull(serverResource); + + var data = serverResource.Data; + var tags = data.Tags?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? new Dictionary(); + + return new SqlServer( + Name: data.Name, + FullyQualifiedDomainName: data.FullyQualifiedDomainName, + Location: data.Location.ToString(), + ResourceGroup: data.Id.ResourceGroupName ?? "Unknown", + Subscription: data.Id.SubscriptionId ?? "Unknown", + AdministratorLogin: data.AdministratorLogin, + Version: data.Version, + State: data.State?.ToString(), + PublicNetworkAccess: data.PublicNetworkAccess?.ToString(), + Tags: tags.Count == 0 ? null : tags); + } + private static SqlServerEntraAdministrator ConvertToSqlServerEntraAdministratorModel(JsonElement item) { SqlServerAadAdministratorData? admin = SqlServerAadAdministratorData.FromJson(item); diff --git a/tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs b/tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs index 7e055f69d..acd341acc 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs @@ -42,6 +42,7 @@ public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactor server.AddCommand("create", new ServerCreateCommand(loggerFactory.CreateLogger())); server.AddCommand("delete", new ServerDeleteCommand(loggerFactory.CreateLogger())); + server.AddCommand("list", new ServerListCommand(loggerFactory.CreateLogger())); server.AddCommand("show", new ServerShowCommand(loggerFactory.CreateLogger())); var elasticPool = new CommandGroup("elastic-pool", "SQL elastic pool operations"); diff --git a/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Server/ServerListCommandTests.cs b/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Server/ServerListCommandTests.cs new file mode 100644 index 000000000..492e99ae3 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Server/ServerListCommandTests.cs @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine; +using Azure; +using Azure.Mcp.Core.Models.Command; +using Azure.Mcp.Tools.Sql.Commands.Server; +using Azure.Mcp.Tools.Sql.Models; +using Azure.Mcp.Tools.Sql.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.Sql.UnitTests.Server; + +public class ServerListCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly ISqlService _service; + private readonly ILogger _logger; + private readonly ServerListCommand _command; + private readonly CommandContext _context; + private readonly Command _commandDefinition; + + public ServerListCommandTests() + { + _service = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection(); + collection.AddSingleton(_service); + _serviceProvider = collection.BuildServiceProvider(); + + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + Assert.Contains("Lists Azure SQL servers", command.Description); + } + + [Fact] + public void CommandMetadata_IsConfiguredCorrectly() + { + var metadata = _command.Metadata; + Assert.False(metadata.Destructive); + Assert.True(metadata.Idempotent); + Assert.True(metadata.OpenWorld); + Assert.True(metadata.ReadOnly); + Assert.False(metadata.LocalRequired); + Assert.False(metadata.Secret); + } + + [Theory] + [InlineData("--subscription sub --resource-group rg", true)] + [InlineData("--subscription sub", false)] // Missing resource group + [InlineData("--resource-group rg", false)] // Missing subscription + [InlineData("", false)] // Missing all required parameters + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + var expectedServers = new List + { + new( + Name: "testserver1", + FullyQualifiedDomainName: "testserver1.database.windows.net", + Location: "East US", + ResourceGroup: "rg", + Subscription: "sub", + AdministratorLogin: "admin", + Version: "12.0", + State: "Ready", + PublicNetworkAccess: "Enabled", + Tags: new Dictionary()) + }; + + _service.ListServersAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(expectedServers); + } + + var context = new CommandContext(_serviceProvider); + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(shouldSucceed ? 200 : 400, response.Status); + if (shouldSucceed) + { + Assert.Equal("Success", response.Message); + } + else + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ListsServersSuccessfully() + { + // Arrange + var expectedServers = new List + { + new( + Name: "testserver1", + FullyQualifiedDomainName: "testserver1.database.windows.net", + Location: "East US", + ResourceGroup: "rg", + Subscription: "sub", + AdministratorLogin: "admin1", + Version: "12.0", + State: "Ready", + PublicNetworkAccess: "Enabled", + Tags: new Dictionary()), + new( + Name: "testserver2", + FullyQualifiedDomainName: "testserver2.database.windows.net", + Location: "West US", + ResourceGroup: "rg", + Subscription: "sub", + AdministratorLogin: "admin2", + Version: "12.0", + State: "Ready", + PublicNetworkAccess: "Disabled", + Tags: new Dictionary { { "Environment", "Test" } }) + }; + + _service.ListServersAsync( + "rg", + "sub", + Arg.Any(), + Arg.Any()) + .Returns(expectedServers); + + var context = new CommandContext(_serviceProvider); + var parseResult = _commandDefinition.Parse("--subscription sub --resource-group rg"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.Equal("Success", response.Message); + Assert.NotNull(response.Results); + + await _service.Received(1).ListServersAsync( + "rg", + "sub", + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_WithEmptyList_ReturnsSuccessfully() + { + // Arrange + var emptyServerList = new List(); + + _service.ListServersAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(emptyServerList); + + var context = new CommandContext(_serviceProvider); + var parseResult = _commandDefinition.Parse("--subscription sub --resource-group rg"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.Equal("Success", response.Message); + Assert.NotNull(response.Results); + + await _service.Received(1).ListServersAsync( + "rg", + "sub", + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_WhenServiceThrowsException_ReturnsErrorResponse() + { + // Arrange + _service.ListServersAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromException>(new Exception("Test error"))); + + var context = new CommandContext(_serviceProvider); + var parseResult = _commandDefinition.Parse("--subscription sub --resource-group rg"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.NotEqual(200, response.Status); + Assert.Contains("error", response.Message.ToLower()); + } + + [Fact] + public async Task ExecuteAsync_WhenAuthorizationFails_Returns403StatusCode() + { + // Arrange + var requestException = new RequestFailedException(403, "Forbidden: Insufficient permissions"); + + _service.ListServersAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromException>(requestException)); + + var context = new CommandContext(_serviceProvider); + var parseResult = _commandDefinition.Parse("--subscription sub --resource-group rg"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(403, response.Status); + Assert.Contains("authorization failed", response.Message.ToLower()); + Assert.Contains("verify you have appropriate permissions", response.Message.ToLower()); + } + + [Fact] + public async Task ExecuteAsync_WhenResourceGroupNotFound_Returns404StatusCode() + { + // Arrange + var requestException = new RequestFailedException(404, "Not Found: Resource group does not exist"); + + _service.ListServersAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromException>(requestException)); + + var context = new CommandContext(_serviceProvider); + var parseResult = _commandDefinition.Parse("--subscription sub --resource-group nonexistent-rg"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(404, response.Status); + Assert.Contains("no sql servers found", response.Message.ToLower()); + Assert.Contains("verify the resource group and subscription", response.Message.ToLower()); + } + + [Fact] + public async Task ExecuteAsync_WithGenericRequestFailedException_ReturnsOriginalMessage() + { + // Arrange + var requestException = new RequestFailedException(500, "Internal Server Error"); + + _service.ListServersAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromException>(requestException)); + + var context = new CommandContext(_serviceProvider); + var parseResult = _commandDefinition.Parse("--subscription sub --resource-group rg"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(500, response.Status); + Assert.StartsWith("Internal Server Error", response.Message); + } +} \ No newline at end of file From 9cc6494c06b5d60e8fa744c5f28dda57ed90ec2a Mon Sep 17 00:00:00 2001 From: Ji Wang Date: Thu, 18 Sep 2025 13:41:43 -0700 Subject: [PATCH 2/5] fix style issue --- .../Server/ServerListCommandTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Server/ServerListCommandTests.cs b/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Server/ServerListCommandTests.cs index 492e99ae3..a17dacd9a 100644 --- a/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Server/ServerListCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Server/ServerListCommandTests.cs @@ -291,4 +291,4 @@ public async Task ExecuteAsync_WithGenericRequestFailedException_ReturnsOriginal Assert.Equal(500, response.Status); Assert.StartsWith("Internal Server Error", response.Message); } -} \ No newline at end of file +} From eac09168d6c9771154425064d4edb539bcd00840 Mon Sep 17 00:00:00 2001 From: Ji Wang Date: Thu, 18 Sep 2025 13:58:55 -0700 Subject: [PATCH 3/5] update doc --- servers/Azure.Mcp.Server/CHANGELOG.md | 3 ++- servers/Azure.Mcp.Server/README.md | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index e2284df18..a7ebb576a 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -7,7 +7,8 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Features Added - Enhanced AKS nodepool information with comprehensive properties. [[#454](https://github.com/microsoft/mcp/issues/454)] -- Added support for updating Azure SQL databases via the command `azmcp_sql_db_update`. [#488](https://github.com/microsoft/mcp/issues/488) +- Added support for `azmcp sql server list` command to list SQL servers in a subscription and resource group. [[#503](https://github.com/microsoft/mcp/issues/503)] +- Added support for updating Azure SQL databases via the command `azmcp_sql_db_update`. [[#488](https://github.com/microsoft/mcp/issues/488)] ### Breaking Changes diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index 65d93633c..13e55c0d7 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -121,6 +121,8 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some ### 🗄️ Azure SQL Database +* "List all SQL servers in my subscription" +* "List all SQL servers in my resource group 'my-resource-group'" * "Show me details about my Azure SQL database 'mydb'" * "List all databases in my Azure SQL server 'myserver'" * "Update the performance tier of my Azure SQL database 'mydb'" @@ -333,6 +335,7 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some * List Microsoft Entra ID administrators for SQL servers * Create new SQL servers * Show details and properties of SQL servers +* List SQL servers in subscription or resource group * Delete SQL servers ### 💾 Azure Storage From 8602f91ef89ff6b360d9dbc77f5ec512c3e285f6 Mon Sep 17 00:00:00 2001 From: Ji Wang Date: Thu, 18 Sep 2025 14:23:11 -0700 Subject: [PATCH 4/5] refactor the list command --- .../src/Commands/Server/ServerListCommand.cs | 14 +++++++++----- .../src/Options/Server/ServerListOptions.cs | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerListCommand.cs b/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerListCommand.cs index bf9eaf7fa..6ba917d2f 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerListCommand.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerListCommand.cs @@ -4,9 +4,9 @@ using System.CommandLine.Parsing; using Azure; using Azure.Mcp.Core.Commands; -using Azure.Mcp.Core.Commands.Subscription; using Azure.Mcp.Core.Extensions; using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.Sql.Commands; using Azure.Mcp.Tools.Sql.Models; using Azure.Mcp.Tools.Sql.Options.Server; using Azure.Mcp.Tools.Sql.Services; @@ -15,9 +15,8 @@ namespace Azure.Mcp.Tools.Sql.Commands.Server; public sealed class ServerListCommand(ILogger logger) - : SubscriptionCommand + : BaseSqlCommand(logger) { - private readonly ILogger _logger = logger; private const string CommandTitle = "List SQL Servers"; public override string Name => "list"; @@ -45,14 +44,19 @@ public sealed class ServerListCommand(ILogger logger) protected override void RegisterOptions(Command command) { - base.RegisterOptions(command); + // Only register subscription and resource group options, not server option + // since we're listing all servers in the resource group + command.Options.Add(OptionDefinitions.Common.Subscription.AsRequired()); command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); } protected override ServerListOptions BindOptions(ParseResult parseResult) { - var options = base.BindOptions(parseResult); + var options = new ServerListOptions(); + options.Subscription = parseResult.GetValueOrDefault(OptionDefinitions.Common.Subscription.Name); options.ResourceGroup = parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + // Server property is inherited from BaseSqlOptions but not needed for listing + options.Server = null; return options; } diff --git a/tools/Azure.Mcp.Tools.Sql/src/Options/Server/ServerListOptions.cs b/tools/Azure.Mcp.Tools.Sql/src/Options/Server/ServerListOptions.cs index 47975c7e5..7b3123b6a 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Options/Server/ServerListOptions.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Options/Server/ServerListOptions.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.Sql.Options; namespace Azure.Mcp.Tools.Sql.Options.Server; -public class ServerListOptions : SubscriptionOptions +public class ServerListOptions : BaseSqlOptions { } From 7d2f740b3be62765073ac06dd30614edb98d0673 Mon Sep 17 00:00:00 2001 From: Ji Wang Date: Thu, 18 Sep 2025 14:24:29 -0700 Subject: [PATCH 5/5] Update tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs b/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs index 82d836e3c..12eabc24d 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs @@ -928,7 +928,7 @@ private static SqlServer ConvertToSqlServerModel(SqlServerResource serverResourc Version: data.Version, State: data.State?.ToString(), PublicNetworkAccess: data.PublicNetworkAccess?.ToString(), - Tags: tags.Count == 0 ? null : tags); + Tags: tags.Count > 0 ? tags : null); } private static SqlServerEntraAdministrator ConvertToSqlServerEntraAdministratorModel(JsonElement item)