From cffd9deab2eff33baea2a1e07c632c1d53fdb34d Mon Sep 17 00:00:00 2001 From: Pengfei Ni Date: Wed, 10 Sep 2025 15:15:18 +0800 Subject: [PATCH 1/3] Update CHANGELOG for rebase --- docs/azmcp-commands.md | 4 +- docs/e2eTestPrompts.md | 3 + servers/Azure.Mcp.Server/CHANGELOG.md | 10 +- servers/Azure.Mcp.Server/README.md | 1 + tools/Azure.Mcp.Tools.Aks/src/AksSetup.cs | 4 + .../src/Commands/AksJsonContext.cs | 3 + .../Commands/Nodepool/NodepoolListCommand.cs | 99 +++++++++++ .../src/Models/NodePool.cs | 38 +++++ .../Options/Nodepool/NodepoolListOptions.cs | 13 ++ .../src/Services/AksService.cs | 82 +++++++++ .../src/Services/IAksService.cs | 7 + .../NodepoolCommandTests.cs | 155 +++++++++++++++++ .../Nodepool/NodepoolListCommandTests.cs | 156 ++++++++++++++++++ 13 files changed, 573 insertions(+), 2 deletions(-) create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolListCommand.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Options/Nodepool/NodepoolListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolListCommandTests.cs diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index f2aae007bc..2d93f2a286 100644 --- a/docs/azmcp-commands.md +++ b/docs/azmcp-commands.md @@ -569,6 +569,9 @@ azmcp aks cluster get --subscription \ # List AKS clusters in a subscription azmcp aks cluster list --subscription + +# List AKS cluster's nodepools +azmcp aks nodepool list --subscription --resource-group --cluster ``` ### Azure Load Testing Operations @@ -1200,4 +1203,3 @@ The CLI returns structured JSON responses for errors, including: - Service availability issues - Authentication errors - diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md index 9613fc6e89..7ea3056dd7 100644 --- a/docs/e2eTestPrompts.md +++ b/docs/e2eTestPrompts.md @@ -200,6 +200,9 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | azmcp_aks_cluster_list | List all AKS clusters in my subscription | | azmcp_aks_cluster_list | Show me my Azure Kubernetes Service clusters | | azmcp_aks_cluster_list | What AKS clusters do I have? | +| azmcp_aks_nodepool_list | List nodepools for AKS cluster \ in \ | +| azmcp_aks_nodepool_list | Show me the nodepool list for AKS cluster \ in \ | +| azmcp_aks_nodepool_list | What nodepools do I have for AKS cluster \ in \ | ## Azure Load Testing diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 2abb7d3d8c..e3c0cc60b9 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -9,6 +9,7 @@ The Azure MCP Server updates automatically by default whenever a new release com - Added support for listing all Event Grid topics in a subscription via the command `azmcp_eventgrid_topic_list`. [[#43](https://github.com/microsoft/mcp/pull/43)] - Added support for retrieving knowledge index schema information in Azure AI Foundry projects via the command `azmcp_foundry_knowledge_index_schema`. [[#41](https://github.com/microsoft/mcp/pull/41)] - Added support for listing service health events in a subscription via the command `azmcp_resourcehealth_service-health-events_list`. [[#367](https://github.com/microsoft/mcp/pull/367)] +- Added nodepool list tool for AKS managed cluster: [[#360](https://github.com/microsoft/mcp/pull/360)] ### Breaking Changes @@ -28,11 +29,18 @@ The Azure MCP Server updates automatically by default whenever a new release com #### Dependency updates +<<<<<<< HEAD - Updated the following dependencies: - Azure.Identity: `1.14.0` → `1.15.0` [[#352](https://github.com/microsoft/mcp/pull/352)] - Azure.Identity.Broker: `1.2.0` → `1.3.0` [[#352](https://github.com/microsoft/mcp/pull/352)] - Microsoft.Azure.Cosmos.Aot: `0.1.1-preview.1` → `0.1.2-preview.1` [[#383](https://github.com/microsoft/mcp/pull/383)] - Updated the following dependencies to improve .NET Ahead-of-Time (AOT) compilation support: [[#363](https://github.com/microsoft/mcp/pull/363)] +======= +- Updated the following dependencies: [[#352](https://github.com/microsoft/mcp/pull/352)] + - Azure.Identity: `1.14.0` → `1.15.0` + - Azure.Identity.Broker: `1.2.0` → `1.3.0` +- Updated the following dependencies to improve .NET Ahead-of-Time (AOT) compilation support: +>>>>>>> 94163e4f (Update CHANGELOG for rebase) - Azure.ResourceManager.StorageCache: `1.3.1` → `1.3.2` ## 0.5.12 (2025-09-04) @@ -69,7 +77,7 @@ AOT- Added a verb to the namespace name for bestpractices [[#109](https://github #### Dependency Updates -- Updated the following dependencies to improve .NET Ahead-of-Time (AOT) compilation support: +- Updated the following dependencies to improve .NET Ahead-of-Time (AOT) compilation support: - Microsoft.Azure.Cosmos `3.51.0` → Microsoft.Azure.Cosmos.Aot `0.1.1-preview.1`. [[#37](https://github.com/microsoft/mcp/pull/37)] ## 0.5.8 (2025-08-21) diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index fc30fb4e9e..4c45d67bb6 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -53,6 +53,7 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some * "List my AKS clusters in my subscription" * "Show me all my Azure Kubernetes Service clusters" +* "List nodepools for AKS cluster in resource group " ### 📊 Azure Cosmos DB diff --git a/tools/Azure.Mcp.Tools.Aks/src/AksSetup.cs b/tools/Azure.Mcp.Tools.Aks/src/AksSetup.cs index 888c9dfc2c..e2b5e2e101 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/AksSetup.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/AksSetup.cs @@ -4,6 +4,7 @@ using Azure.Mcp.Core.Areas; using Azure.Mcp.Core.Commands; using Azure.Mcp.Tools.Aks.Commands.Cluster; +using Azure.Mcp.Tools.Aks.Commands.Nodepool; using Azure.Mcp.Tools.Aks.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -28,9 +29,12 @@ public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactor // Create AKS subgroups var cluster = new CommandGroup("cluster", "AKS cluster operations - Commands for listing and managing AKS clusters in your Azure subscription."); aks.AddSubGroup(cluster); + var nodepool = new CommandGroup("nodepool", "AKS node pool operations - Commands for listing and managing AKS node pools for an AKS cluster."); + aks.AddSubGroup(nodepool); // Register AKS commands cluster.AddCommand("list", new ClusterListCommand(loggerFactory.CreateLogger())); cluster.AddCommand("get", new ClusterGetCommand(loggerFactory.CreateLogger())); + nodepool.AddCommand("list", new NodepoolListCommand(loggerFactory.CreateLogger())); } } diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs index d88942a138..f733dc26de 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs @@ -3,11 +3,14 @@ using System.Text.Json.Serialization; using Azure.Mcp.Tools.Aks.Commands.Cluster; +using Azure.Mcp.Tools.Aks.Commands.Nodepool; namespace Azure.Mcp.Tools.Aks.Commands; [JsonSerializable(typeof(ClusterListCommand.ClusterListCommandResult))] [JsonSerializable(typeof(ClusterGetCommand.ClusterGetCommandResult))] [JsonSerializable(typeof(Models.Cluster))] +[JsonSerializable(typeof(NodepoolListCommand.NodepoolListCommandResult))] +[JsonSerializable(typeof(Models.NodePool))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] internal sealed partial class AksJsonContext : JsonSerializerContext; diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolListCommand.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolListCommand.cs new file mode 100644 index 0000000000..923aa01738 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolListCommand.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Commands; +using Azure.Mcp.Tools.Aks.Options; +using Azure.Mcp.Tools.Aks.Options.Nodepool; +using Azure.Mcp.Tools.Aks.Services; +using Microsoft.Extensions.Logging; + +namespace Azure.Mcp.Tools.Aks.Commands.Nodepool; + +public sealed class NodepoolListCommand(ILogger logger) : BaseAksCommand +{ + private const string CommandTitle = "List AKS Node Pools"; + private readonly ILogger _logger = logger; + + private readonly Option _clusterNameOption = AksOptionDefinitions.Cluster; + + public override string Name => "list"; + + public override string Description => + """ + List all node pools for a specific Azure Kubernetes Service (AKS) cluster. + Returns key node pool details including sizing, count, OS type, mode, and autoscaling settings. + """; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() { Destructive = false, ReadOnly = true }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + RequireResourceGroup(); + command.Options.Add(_clusterNameOption); + } + + protected override NodepoolListOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ClusterName = parseResult.GetValue(_clusterNameOption); + 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 aksService = context.GetService(); + var nodePools = await aksService.ListNodePools( + options.Subscription!, + options.ResourceGroup!, + options.ClusterName!, + options.Tenant, + options.RetryPolicy); + + context.Response.Results = nodePools?.Count > 0 ? + ResponseResult.Create( + new NodepoolListCommandResult(nodePools), + AksJsonContext.Default.NodepoolListCommandResult) : + null; + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error listing AKS node pools. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, ClusterName: {ClusterName}, Options: {@Options}", + options.Subscription, options.ResourceGroup, options.ClusterName, options); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + RequestFailedException reqEx when reqEx.Status == 404 => + "AKS cluster or node pools not found. Verify the cluster name, resource group, and subscription, and ensure you have access.", + RequestFailedException reqEx when reqEx.Status == 403 => + $"Authorization failed accessing AKS node pools. Details: {reqEx.Message}", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + protected override int GetStatusCode(Exception ex) => ex switch + { + RequestFailedException reqEx => reqEx.Status, + _ => base.GetStatusCode(ex) + }; + + internal record NodepoolListCommandResult(List NodePools); +} + diff --git a/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs b/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs new file mode 100644 index 0000000000..db200d91a4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.Aks.Models; + +public class NodePool +{ + /// Name of the node pool (agent pool). + public string? Name { get; set; } + + /// Number of nodes in the pool. + public int? NodeCount { get; set; } + + /// VM size of the agent nodes. + public string? NodeVmSize { get; set; } + + /// OS type of the node pool. + public string? OsType { get; set; } + + /// Pool mode (System/User). + public string? Mode { get; set; } + + /// Kubernetes/orchestrator version for the pool. + public string? OrchestratorVersion { get; set; } + + /// Whether cluster autoscaler is enabled for this pool. + public bool? EnableAutoScaling { get; set; } + + /// Minimum node count when autoscaling is enabled. + public int? MinCount { get; set; } + + /// Maximum node count when autoscaling is enabled. + public int? MaxCount { get; set; } + + /// Provisioning state of the node pool resource. + public string? ProvisioningState { get; set; } +} + diff --git a/tools/Azure.Mcp.Tools.Aks/src/Options/Nodepool/NodepoolListOptions.cs b/tools/Azure.Mcp.Tools.Aks/src/Options/Nodepool/NodepoolListOptions.cs new file mode 100644 index 0000000000..2d4ce00d54 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Options/Nodepool/NodepoolListOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.Aks.Options.Nodepool; + +public class NodepoolListOptions : BaseAksOptions +{ + [JsonPropertyName(AksOptionDefinitions.ClusterName)] + public string? ClusterName { get; set; } +} + diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs index c8817e2efd..013ba9835e 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs @@ -21,6 +21,7 @@ public sealed class AksService( private const string CacheGroup = "aks"; private const string AksClustersCacheKey = "clusters"; + private const string AksNodePoolsCacheKey = "nodepools"; private static readonly TimeSpan s_cacheDuration = TimeSpan.FromHours(1); public async Task> ListClusters( @@ -121,6 +122,68 @@ public async Task> ListClusters( } } + public async Task> ListNodePools( + string subscription, + string resourceGroup, + string clusterName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null) + { + ValidateRequiredParameters(subscription, resourceGroup, clusterName); + + // Create cache key + var cacheKey = string.IsNullOrEmpty(tenant) + ? $"{AksNodePoolsCacheKey}_{subscription}_{resourceGroup}_{clusterName}" + : $"{AksNodePoolsCacheKey}_{subscription}_{resourceGroup}_{clusterName}_{tenant}"; + + // Try to get from cache first + var cachedNodePools = await _cacheService.GetAsync>(CacheGroup, cacheKey, s_cacheDuration); + if (cachedNodePools != null) + { + return cachedNodePools; + } + + var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy); + var nodePools = new List(); + + try + { + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup); + if (resourceGroupResource?.Value == null) + { + return nodePools; + } + + var clusterResource = await resourceGroupResource.Value + .GetContainerServiceManagedClusters() + .GetAsync(clusterName); + + if (clusterResource?.Value == null) + { + return nodePools; + } + + await foreach (var agentPool in clusterResource.Value + .GetContainerServiceAgentPools() + .GetAllAsync()) + { + if (agentPool?.Data != null) + { + nodePools.Add(ConvertToNodePoolModel(agentPool)); + } + } + + // Cache the results + await _cacheService.SetAsync(CacheGroup, cacheKey, nodePools, s_cacheDuration); + } + catch (Exception ex) + { + throw new Exception($"Error retrieving AKS node pools for cluster '{clusterName}': {ex.Message}", ex); + } + + return nodePools; + } + private static Cluster ConvertToClusterModel(ContainerServiceManagedClusterResource clusterResource) { var data = clusterResource.Data; @@ -149,4 +212,23 @@ private static Cluster ConvertToClusterModel(ContainerServiceManagedClusterResou Tags = data.Tags?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; } + + private static NodePool ConvertToNodePoolModel(ContainerServiceAgentPoolResource agentPoolResource) + { + var data = agentPoolResource.Data; + + return new NodePool + { + Name = data.Name, + NodeCount = data.Count, + NodeVmSize = data.VmSize?.ToString(), + OsType = data.OSType?.ToString(), + Mode = data.Mode?.ToString(), + OrchestratorVersion = data.OrchestratorVersion, + EnableAutoScaling = data.EnableAutoScaling, + MinCount = data.MinCount, + MaxCount = data.MaxCount, + ProvisioningState = data.ProvisioningState?.ToString() + }; + } } diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs index d217455e7f..a48486d93d 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs @@ -19,4 +19,11 @@ Task> ListClusters( string resourceGroup, string? tenant = null, RetryPolicyOptions? retryPolicy = null); + + Task> ListNodePools( + string subscription, + string resourceGroup, + string clusterName, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null); } diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs new file mode 100644 index 0000000000..284ee3c715 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.Mcp.Tests; +using Azure.Mcp.Tests.Client; +using Azure.Mcp.Tests.Client.Helpers; +using Xunit; + +namespace Azure.Mcp.Tools.Aks.LiveTests; + +public sealed class NodepoolCommandTests(LiveTestFixture liveTestFixture, ITestOutputHelper output) + : CommandTestsBase(liveTestFixture, output), IClassFixture +{ + [Fact] + public async Task Should_list_nodepools_for_cluster() + { + // Get a real cluster to target + var listResult = await CallToolAsync( + "azmcp_aks_cluster_list", + new() + { + { "subscription", Settings.SubscriptionId } + }); + + var clusters = listResult.AssertProperty("clusters"); + Assert.True(clusters.GetArrayLength() > 0, "Expected at least one AKS cluster for testing nodepool list command"); + + var firstCluster = clusters.EnumerateArray().First(); + var clusterName = firstCluster.GetProperty("name").GetString()!; + var resourceGroupName = firstCluster.GetProperty("resourceGroupName").GetString()!; + + // List node pools for that cluster + var nodepoolResult = await CallToolAsync( + "azmcp_aks_nodepool_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", resourceGroupName }, + { "cluster", clusterName } + }); + + var nodePools = nodepoolResult.AssertProperty("nodePools"); + Assert.Equal(JsonValueKind.Array, nodePools.ValueKind); + Assert.True(nodePools.GetArrayLength() > 0, "Expected at least one node pool in the cluster"); + + // Validate a few common properties exist on each node pool + foreach (var pool in nodePools.EnumerateArray()) + { + Assert.Equal(JsonValueKind.Object, pool.ValueKind); + Assert.True(pool.TryGetProperty("name", out var nameProperty)); + Assert.False(string.IsNullOrEmpty(nameProperty.GetString())); + + if (pool.TryGetProperty("mode", out var modeProperty)) + { + Assert.False(string.IsNullOrEmpty(modeProperty.GetString())); + } + + if (pool.TryGetProperty("provisioningState", out var stateProperty)) + { + Assert.False(string.IsNullOrEmpty(stateProperty.GetString())); + } + } + } + + [Fact] + public async Task Should_handle_nonexistent_cluster_gracefully() + { + var result = await CallToolAsync( + "azmcp_aks_nodepool_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", "nonexistent-rg" }, + { "cluster", "nonexistent-cluster" } + }); + + // Should return runtime error details in results + Assert.True(result.HasValue); + var errorDetails = result.Value; + Assert.True(errorDetails.TryGetProperty("message", out _)); + Assert.True(errorDetails.TryGetProperty("type", out var typeProperty)); + Assert.Equal("Exception", typeProperty.GetString()); + } + + [Fact] + public async Task Should_validate_required_parameters() + { + // Missing cluster + var r1 = await CallToolAsync( + "azmcp_aks_nodepool_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", "rg" } + }); + Assert.False(r1.HasValue); + + // Missing resource-group + var r2 = await CallToolAsync( + "azmcp_aks_nodepool_list", + new() + { + { "subscription", Settings.SubscriptionId }, + { "cluster", "cluster" } + }); + Assert.False(r2.HasValue); + + // Missing subscription + var r3 = await CallToolAsync( + "azmcp_aks_nodepool_list", + new() + { + { "resource-group", "rg" }, + { "cluster", "cluster" } + }); + Assert.False(r3.HasValue); + } + + [Fact] + public async Task Should_handle_invalid_subscription_gracefully() + { + // Use obviously invalid subscription ID to ensure failure is surfaced + var result = await CallToolAsync( + "azmcp_aks_nodepool_list", + new() + { + { "subscription", "invalid-subscription" }, + { "resource-group", "rg" }, + { "cluster", "cluster" } + }); + + Assert.True(result.HasValue); + var errorDetails = result.Value; + Assert.True(errorDetails.TryGetProperty("message", out _)); + Assert.True(errorDetails.TryGetProperty("type", out var typeProperty)); + Assert.Equal("Exception", typeProperty.GetString()); + } + + [Fact] + public async Task Should_handle_empty_subscription_gracefully() + { + var result = await CallToolAsync( + "azmcp_aks_nodepool_list", + new() + { + { "subscription", "" }, + { "resource-group", "rg" }, + { "cluster", "cluster" } + }); + + // Should return validation error response with no results + Assert.False(result.HasValue); + } +} diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolListCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolListCommandTests.cs new file mode 100644 index 0000000000..2ab98da2f8 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolListCommandTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.Mcp.Core.Models.Command; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.Aks.Commands; +using Azure.Mcp.Tools.Aks.Commands.Nodepool; +using Azure.Mcp.Tools.Aks.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.Aks.UnitTests.Nodepool; + +public sealed class NodepoolListCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAksService _aksService; + private readonly ILogger _logger; + private readonly NodepoolListCommand _command; + + public NodepoolListCommandTests() + { + _aksService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection(); + collection.AddSingleton(_aksService); + _serviceProvider = collection.BuildServiceProvider(); + + _command = new(_logger); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Theory] + [InlineData("--subscription sub123 --resource-group rg1 --cluster c1", true)] + [InlineData("--subscription sub123 --resource-group rg1 --cluster c1 --tenant t1", true)] + [InlineData("--subscription sub123 --resource-group rg1", false)] // missing cluster + [InlineData("--subscription sub123 --cluster c1", false)] // missing rg + [InlineData("--resource-group rg1 --cluster c1", false)] // missing subscription + [InlineData("", false)] + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + var testNodePools = new List + { + new() { Name = "np1", NodeCount = 3, NodeVmSize = "Standard_DS2_v2" }, + new() { Name = "np2", NodeCount = 5, NodeVmSize = "Standard_D4s_v5" } + }; + _aksService.ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(testNodePools); + } + + var context = new CommandContext(_serviceProvider); + var parseResult = _command.GetCommand().Parse(args); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(shouldSucceed ? 200 : 400, response.Status); + if (shouldSucceed) + { + Assert.NotNull(response.Results); + Assert.Equal("Success", response.Message); + } + else + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + [Fact] + public async Task ExecuteAsync_ReturnsNodePoolsList() + { + // Arrange + var expectedNodePools = new List + { + new() { Name = "systempool", NodeCount = 3, NodeVmSize = "Standard_DS2_v2", Mode = "System" }, + new() { Name = "userpool", NodeCount = 5, NodeVmSize = "Standard_D4s_v5", Mode = "User" } + }; + _aksService.ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(expectedNodePools); + + var context = new CommandContext(_serviceProvider); + var parseResult = _command.GetCommand().Parse("--subscription sub123 --resource-group rg1 --cluster c1"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + + // Verify the mock was called + await _aksService.Received(1).ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AksJsonContext.Default.NodepoolListCommandResult); + + Assert.NotNull(result); + Assert.Equal(expectedNodePools.Count, result.NodePools.Count); + Assert.Equal(expectedNodePools[0].Name, result.NodePools[0].Name); + Assert.Equal(expectedNodePools[0].NodeCount, result.NodePools[0].NodeCount); + Assert.Equal(expectedNodePools[0].NodeVmSize, result.NodePools[0].NodeVmSize); + } + + [Fact] + public async Task ExecuteAsync_ReturnsNullWhenNoNodePools() + { + // Arrange + _aksService.ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new List()); + + var context = new CommandContext(_serviceProvider); + var parseResult = _command.GetCommand().Parse("--subscription sub123 --resource-group rg1 --cluster c1"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(200, response.Status); + Assert.Null(response.Results); + } + + [Fact] + public async Task ExecuteAsync_HandlesServiceErrors() + { + // Arrange + _aksService.ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromException>(new Exception("Test error"))); + + var context = new CommandContext(_serviceProvider); + var parseResult = _command.GetCommand().Parse("--subscription sub123 --resource-group rg1 --cluster c1"); + + // Act + var response = await _command.ExecuteAsync(context, parseResult); + + // Assert + Assert.Equal(500, response.Status); + Assert.Contains("Test error", response.Message); + Assert.Contains("troubleshooting", response.Message); + } +} From 4f6cafc24ef56b66f4c42196e051fcf8fddcc725 Mon Sep 17 00:00:00 2001 From: Pengfei Ni Date: Mon, 8 Sep 2025 18:37:52 +0800 Subject: [PATCH 2/3] Update README for the nodepool list tool --- servers/Azure.Mcp.Server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index 4c45d67bb6..bf59afcdc5 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -53,7 +53,7 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some * "List my AKS clusters in my subscription" * "Show me all my Azure Kubernetes Service clusters" -* "List nodepools for AKS cluster in resource group " +* "List the node pools for my AKS cluster 'my-aks-cluster' in the 'my-resource-group' resource group" ### 📊 Azure Cosmos DB From e016dcd8f49040784c281b5b3d044ae329bf4a79 Mon Sep 17 00:00:00 2001 From: Pengfei Ni Date: Wed, 10 Sep 2025 11:26:21 +0800 Subject: [PATCH 3/3] Fix the nodepool test definitions for AKS tool --- .../Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs index 284ee3c715..bf9780324d 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolCommandTests.cs @@ -4,13 +4,12 @@ using System.Text.Json; using Azure.Mcp.Tests; using Azure.Mcp.Tests.Client; -using Azure.Mcp.Tests.Client.Helpers; using Xunit; namespace Azure.Mcp.Tools.Aks.LiveTests; -public sealed class NodepoolCommandTests(LiveTestFixture liveTestFixture, ITestOutputHelper output) - : CommandTestsBase(liveTestFixture, output), IClassFixture +public sealed class NodepoolCommandTests(ITestOutputHelper output) + : CommandTestsBase(output) { [Fact] public async Task Should_list_nodepools_for_cluster()