diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs
index 4af52fc34a..8500be36b4 100644
--- a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs
+++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs
@@ -4,6 +4,7 @@
using System.Text.Json.Serialization;
using Azure.Mcp.Tools.Aks.Commands.Cluster;
using Azure.Mcp.Tools.Aks.Commands.Nodepool;
+using Azure.Mcp.Tools.Aks.Services;
namespace Azure.Mcp.Tools.Aks.Commands;
@@ -46,5 +47,14 @@ namespace Azure.Mcp.Tools.Aks.Commands;
[JsonSerializable(typeof(Models.WorkloadAutoScalerProfile))]
[JsonSerializable(typeof(Models.WorkloadAutoScalerKeda))]
[JsonSerializable(typeof(Models.WorkloadAutoScalerVerticalPodAutoscaler))]
+[JsonSerializable(typeof(AksClusterResourceGraphResponse))]
+[JsonSerializable(typeof(AksClusterSkuJson))]
+[JsonSerializable(typeof(AksClusterPropertiesJson))]
+[JsonSerializable(typeof(AksPowerStateJson))]
+[JsonSerializable(typeof(AksClusterNetworkProfileJson))]
+[JsonSerializable(typeof(AksNetworkLoadBalancerProfileJson))]
+[JsonSerializable(typeof(AksManagedOutboundIPsJson))]
+[JsonSerializable(typeof(AksAddonProfileJson))]
+[JsonSerializable(typeof(AksAddonIdentityJson))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal sealed partial class AksJsonContext : JsonSerializerContext;
diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs
index 666b4a3ce3..a2dd81fb94 100644
--- a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs
+++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs
@@ -33,15 +33,6 @@ protected override void RegisterOptions(Command command)
base.RegisterOptions(command);
command.Options.Add(OptionDefinitions.Common.ResourceGroup);
command.Options.Add(AksOptionDefinitions.Cluster);
- command.Validators.Add(commandResults =>
- {
- var clusterName = commandResults.GetValueOrDefault(AksOptionDefinitions.Cluster);
- var resourceGroup = commandResults.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup);
- if (!string.IsNullOrEmpty(clusterName) && string.IsNullOrEmpty(resourceGroup))
- {
- commandResults.AddError("When specifying a cluster name, the --resource-group option is required.");
- }
- });
}
protected override ClusterGetOptions BindOptions(ParseResult parseResult)
diff --git a/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs b/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs
index 05063053bc..d8ec04bed8 100644
--- a/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs
+++ b/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs
@@ -155,7 +155,10 @@ public sealed class NodePoolNetworkProfile
public sealed class PortRange
{
+ [System.Text.Json.Serialization.JsonPropertyName("portStart")]
public int? StartPort { get; set; }
+
+ [System.Text.Json.Serialization.JsonPropertyName("portEnd")]
public int? EndPort { get; set; }
}
diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksClusterResourceGraphResponse.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksClusterResourceGraphResponse.cs
new file mode 100644
index 0000000000..e94457d440
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksClusterResourceGraphResponse.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+using Azure.Mcp.Tools.Aks.Models;
+
+namespace Azure.Mcp.Tools.Aks.Services;
+
+///
+/// Intermediate type that mirrors the Azure Resource Graph JSON structure for a managed cluster
+/// resource. Used only for deserialization; the public model is then
+/// populated from this type via AksService.MapToCluster.
+///
+internal sealed class AksClusterResourceGraphResponse
+{
+ public string? Id { get; set; }
+ public string? Name { get; set; }
+ public string? SubscriptionId { get; set; }
+
+ [JsonPropertyName("resourceGroup")]
+ public string? ResourceGroup { get; set; }
+
+ public string? Location { get; set; }
+ public Dictionary? Tags { get; set; }
+ public AksClusterSkuJson? Sku { get; set; }
+ public ResourceIdentity? Identity { get; set; }
+ public AksClusterPropertiesJson? Properties { get; set; }
+}
+
+internal sealed class AksClusterSkuJson
+{
+ public string? Name { get; set; }
+ public string? Tier { get; set; }
+}
+
+internal sealed class AksClusterPropertiesJson
+{
+ public string? KubernetesVersion { get; set; }
+ public string? ProvisioningState { get; set; }
+ public string? DnsPrefix { get; set; }
+ public string? Fqdn { get; set; }
+ public string? NodeResourceGroup { get; set; }
+ public string? SupportPlan { get; set; }
+
+ [JsonPropertyName("resourceUID")]
+ public string? ResourceUid { get; set; }
+
+ [JsonPropertyName("enableRBAC")]
+ public bool? EnableRbac { get; set; }
+
+ public bool? DisableLocalAccounts { get; set; }
+ public int? MaxAgentPools { get; set; }
+ public AksPowerStateJson? PowerState { get; set; }
+ public AksClusterNetworkProfileJson? NetworkProfile { get; set; }
+ public OidcIssuerProfile? OidcIssuerProfile { get; set; }
+ public AutoUpgradeProfile? AutoUpgradeProfile { get; set; }
+ public ClusterSecurityProfile? SecurityProfile { get; set; }
+ public ClusterStorageProfile? StorageProfile { get; set; }
+ public WorkloadAutoScalerProfile? WorkloadAutoScalerProfile { get; set; }
+ public Dictionary? AddonProfiles { get; set; }
+ public Dictionary? IdentityProfile { get; set; }
+ public List? AgentPoolProfiles { get; set; }
+}
+
+internal sealed class AksPowerStateJson
+{
+ public string? Code { get; set; }
+}
+
+///
+/// Intermediate network profile type. The public model
+/// has a flat ManagedOutboundIPCount, while the Resource Graph JSON nests this value
+/// under loadBalancerProfile.managedOutboundIPs.count, requiring a separate type.
+///
+internal sealed class AksClusterNetworkProfileJson
+{
+ public string? NetworkPlugin { get; set; }
+ public string? NetworkPluginMode { get; set; }
+ public string? NetworkPolicy { get; set; }
+ public string? NetworkDataplane { get; set; }
+ public string? LoadBalancerSku { get; set; }
+ public AksNetworkLoadBalancerProfileJson? LoadBalancerProfile { get; set; }
+ public string? PodCidr { get; set; }
+ public string? ServiceCidr { get; set; }
+ public string? DnsServiceIP { get; set; }
+ public string? OutboundType { get; set; }
+ public List? PodCidrs { get; set; }
+ public List? ServiceCidrs { get; set; }
+ public List? IpFamilies { get; set; }
+}
+
+internal sealed class AksNetworkLoadBalancerProfileJson
+{
+ public AksManagedOutboundIPsJson? ManagedOutboundIPs { get; set; }
+ public List? EffectiveOutboundIPs { get; set; }
+ public string? BackendPoolType { get; set; }
+}
+
+internal sealed class AksManagedOutboundIPsJson
+{
+ public int? Count { get; set; }
+}
+
+///
+/// Intermediate add-on profile type used to deserialize each entry in
+/// properties.addonProfiles before flattening to the config.* /
+/// identity.* key convention used by .
+///
+internal sealed class AksAddonProfileJson
+{
+ public bool? Enabled { get; set; }
+ public Dictionary? Config { get; set; }
+ public AksAddonIdentityJson? Identity { get; set; }
+}
+
+internal sealed class AksAddonIdentityJson
+{
+ public string? ClientId { get; set; }
+ public string? ObjectId { 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 7207106f80..dae5805c8c 100644
--- a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs
+++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs
@@ -1,9 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.Text.Json;
using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Core.Services.Azure.Subscription;
using Azure.Mcp.Core.Services.Azure.Tenant;
+using Azure.Mcp.Tools.Aks.Commands;
using Azure.Mcp.Tools.Aks.Models;
using Azure.ResourceManager.ContainerService;
using Azure.ResourceManager.ContainerService.Models;
@@ -17,7 +19,7 @@ public sealed class AksService(
ISubscriptionService subscriptionService,
ITenantService tenantService,
ICacheService cacheService,
- ILogger logger) : BaseAzureService(tenantService), IAksService
+ ILogger logger) : BaseAzureResourceService(subscriptionService, tenantService), IAksService
{
private readonly ISubscriptionService _subscriptionService = subscriptionService ?? throw new ArgumentNullException(nameof(subscriptionService));
private readonly ICacheService _cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
@@ -56,36 +58,16 @@ public async Task> GetClusters(
return cachedClusters;
}
- var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken);
-
- var clusters = new List();
- if (string.IsNullOrEmpty(resourceGroup))
- {
-
- await foreach (var cluster in subscriptionResource.GetContainerServiceManagedClustersAsync(cancellationToken))
- {
- if (cluster?.Data != null)
- {
- clusters.Add(ConvertToClusterModel(cluster));
- }
- }
- }
- else
- {
- var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken);
- if (resourceGroupResource?.Value == null)
- {
- return clusters;
- }
+ var result = await ExecuteResourceQueryAsync(
+ "Microsoft.ContainerService/managedClusters",
+ resourceGroup,
+ subscription,
+ retryPolicy,
+ ConvertToClusterFromJson,
+ tenant: tenant,
+ cancellationToken: cancellationToken);
- await foreach (var cluster in resourceGroupResource.Value.GetContainerServiceManagedClusters().GetAllAsync(cancellationToken))
- {
- if (cluster?.Data != null)
- {
- clusters.Add(ConvertToClusterModel(cluster));
- }
- }
- }
+ var clusters = result.Results;
// Cache the results
await _cacheService.SetAsync(CacheGroup, cacheKey, clusters, s_cacheDuration, cancellationToken);
@@ -94,12 +76,16 @@ public async Task> GetClusters(
}
else
{
- ValidateRequiredParameters((nameof(resourceGroup), resourceGroup), (nameof(clusterName), clusterName));
+ ValidateRequiredParameters((nameof(clusterName), clusterName));
// Create cache key
- var cacheKey = string.IsNullOrEmpty(tenant)
- ? CacheKeyBuilder.Build("cluster", subscription, resourceGroup!, clusterName)
- : CacheKeyBuilder.Build("cluster", subscription, resourceGroup!, clusterName, tenant);
+ var cacheKey = (string.IsNullOrEmpty(resourceGroup), string.IsNullOrEmpty(tenant)) switch
+ {
+ (true, true) => CacheKeyBuilder.Build("cluster", subscription, clusterName),
+ (false, true) => CacheKeyBuilder.Build("cluster", subscription, resourceGroup, clusterName),
+ (true, false) => CacheKeyBuilder.Build("cluster", subscription, clusterName, tenant),
+ (false, false) => CacheKeyBuilder.Build("cluster", subscription, resourceGroup, clusterName, tenant)
+ };
// Try to get from cache first
var cachedCluster = await _cacheService.GetAsync>(CacheGroup, cacheKey, s_cacheDuration, cancellationToken);
@@ -108,24 +94,22 @@ public async Task> GetClusters(
return cachedCluster;
}
- var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken);
- var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken);
-
- if (resourceGroupResource?.Value == null)
- {
- return [];
- }
-
- var clusterResource = await resourceGroupResource.Value
- .GetContainerServiceManagedClusters()
- .GetAsync(clusterName, cancellationToken);
-
- if (clusterResource?.Value?.Data == null)
+ var cluster = await ExecuteSingleResourceQueryAsync(
+ "Microsoft.ContainerService/managedClusters",
+ resourceGroup: resourceGroup,
+ subscription: subscription,
+ retryPolicy: retryPolicy,
+ converter: ConvertToClusterFromJson,
+ additionalFilter: $"name =~ '{EscapeKqlString(clusterName!)}'" ,
+ tenant: tenant,
+ cancellationToken: cancellationToken);
+
+ if (cluster == null)
{
return [];
}
- var clusters = new List() { ConvertToClusterModel(clusterResource.Value) };
+ var clusters = new List() { cluster };
// Cache the result
await _cacheService.SetAsync(CacheGroup, cacheKey, clusters, s_cacheDuration, cancellationToken);
@@ -242,176 +226,121 @@ public async Task> GetNodePools(
}
}
- private static Cluster ConvertToClusterModel(ContainerServiceManagedClusterResource clusterResource)
+ private static Cluster ConvertToClusterFromJson(JsonElement item)
+ {
+ var response = JsonSerializer.Deserialize(item, AksJsonContext.Default.AksClusterResourceGraphResponse);
+ return response is null ? new Cluster() : MapToCluster(response);
+ }
+
+ private static Cluster MapToCluster(AksClusterResourceGraphResponse response)
{
- var data = clusterResource.Data;
- var agentPool = data.AgentPoolProfiles?.FirstOrDefault();
+ var props = response.Properties;
+ var np = props?.NetworkProfile;
var cluster = new Cluster
{
- Id = clusterResource.Id.ToString(),
- Name = data.Name,
- SubscriptionId = clusterResource.Id.SubscriptionId,
- ResourceGroupName = clusterResource.Id.ResourceGroupName,
- Location = data.Location.ToString(),
- KubernetesVersion = data.KubernetesVersion,
- ProvisioningState = data.ProvisioningState?.ToString(),
- PowerState = data.PowerStateCode?.ToString(),
- DnsPrefix = data.DnsPrefix,
- Fqdn = data.Fqdn,
- NodeCount = agentPool?.Count,
- NodeVmSize = agentPool?.VmSize,
- IdentityType = data.Identity?.ManagedServiceIdentityType.ToString(),
- Identity = new()
- {
- Type = data.Identity?.ManagedServiceIdentityType.ToString(),
- PrincipalId = data.Identity?.PrincipalId?.ToString(),
- TenantId = data.Identity?.TenantId?.ToString()
- },
- EnableRbac = data.EnableRbac,
- NetworkPlugin = data.NetworkProfile?.NetworkPlugin?.ToString(),
- NetworkPolicy = data.NetworkProfile?.NetworkPolicy?.ToString(),
- ServiceCidr = data.NetworkProfile?.ServiceCidr,
- DnsServiceIP = data.NetworkProfile?.DnsServiceIP?.ToString(),
- SkuTier = data.Sku?.Tier?.ToString(),
- SkuName = data.Sku?.Name?.ToString(),
- NodeResourceGroup = data.NodeResourceGroup,
- MaxAgentPools = data.MaxAgentPools,
- SupportPlan = data.SupportPlan?.ToString(),
- NetworkProfile = new()
- {
- NetworkPlugin = data.NetworkProfile?.NetworkPlugin?.ToString(),
- NetworkPluginMode = data.NetworkProfile?.NetworkPluginMode?.ToString(),
- NetworkPolicy = data.NetworkProfile?.NetworkPolicy?.ToString(),
- NetworkDataplane = data.NetworkProfile?.NetworkDataplane?.ToString(),
- LoadBalancerSku = data.NetworkProfile?.LoadBalancerSku?.ToString(),
- LoadBalancerProfile = data.NetworkProfile?.LoadBalancerProfile is null ? null : new()
- {
- ManagedOutboundIPCount = data.NetworkProfile?.LoadBalancerProfile?.ManagedOutboundIPs?.Count,
- EffectiveOutboundIPs = data.NetworkProfile?.LoadBalancerProfile?.EffectiveOutboundIPs?.Select(e => new EffectiveOutboundIPReference() { Id = e.Id?.ToString() }).ToList(),
- BackendPoolType = data.NetworkProfile?.LoadBalancerProfile?.BackendPoolType?.ToString()
- },
- PodCidr = data.NetworkProfile?.PodCidr,
- ServiceCidr = data.NetworkProfile?.ServiceCidr,
- DnsServiceIP = data.NetworkProfile?.DnsServiceIP?.ToString(),
- OutboundType = data.NetworkProfile?.OutboundType?.ToString(),
- PodCidrs = data.NetworkProfile?.PodCidrs?.ToList(),
- ServiceCidrs = data.NetworkProfile?.ServiceCidrs?.ToList(),
- IpFamilies = data.NetworkProfile?.IPFamilies?.Select(f => f.ToString()).ToList()
- },
- WindowsProfile = data.WindowsProfile is null ? null : new() { AdminUsername = data.WindowsProfile.AdminUsername },
- ServicePrincipalProfile = data.ServicePrincipalProfile is null ? null : new() { ClientId = data.ServicePrincipalProfile.ClientId },
- AutoUpgradeProfile = data.AutoUpgradeProfile is null ? null : new()
- {
- UpgradeChannel = data.AutoUpgradeProfile.UpgradeChannel?.ToString(),
- NodeOSUpgradeChannel = data.AutoUpgradeProfile.NodeOSUpgradeChannel?.ToString()
- },
- // OIDC Issuer Profile
- OidcIssuerProfile = data.OidcIssuerProfile is null ? null : new()
- {
- Enabled = data.OidcIssuerProfile.IsEnabled,
- IssuerUrl = data.OidcIssuerProfile.IssuerUriInfo
- },
- AddonProfiles = data.AddonProfiles?.ToDictionary(
- kvp => kvp.Key,
- static kvp =>
- {
- IDictionary map = new Dictionary();
- if (kvp.Value != null)
- {
- if (kvp.Value.Config != null)
- {
- foreach (var c in kvp.Value.Config)
- {
- map[$"config.{c.Key}"] = c.Value;
- }
- }
- if (kvp.Value.Identity != null)
- {
- if (kvp.Value.Identity.ClientId != null)
- map.Add("identity.clientId", kvp.Value.Identity.ClientId.ToString()!);
- if (kvp.Value.Identity.ObjectId != null)
- map.Add("identity.objectId", kvp.Value.Identity.ObjectId.ToString()!);
- }
- }
- return map;
- }),
- IdentityProfile = data.IdentityProfile?.ToDictionary(
- kvp => kvp.Key,
- kvp => new ManagedIdentityReference
- {
- ResourceId = kvp.Value?.ResourceId?.ToString(),
- ClientId = kvp.Value?.ClientId?.ToString(),
- ObjectId = kvp.Value?.ObjectId?.ToString()
- }),
- DisableLocalAccounts = data.DisableLocalAccounts,
- // Security Profile
- SecurityProfile = data.SecurityProfile is null ? null : new()
- {
- AzureKeyVaultKms = data.SecurityProfile.AzureKeyVaultKms is null ? null : new()
- {
- Enabled = data.SecurityProfile.AzureKeyVaultKms.IsEnabled,
- KeyId = data.SecurityProfile.AzureKeyVaultKms.KeyId?.ToString()
- },
- Defender = data.SecurityProfile.Defender is null ? null : new()
- {
- LogAnalyticsWorkspaceResourceId = data.SecurityProfile.Defender.LogAnalyticsWorkspaceResourceId?.ToString(),
- SecurityMonitoring = new() { Enabled = data.SecurityProfile.Defender.IsSecurityMonitoringEnabled }
- },
- ImageCleaner = data.SecurityProfile.ImageCleaner is null ? null : new()
- {
- Enabled = data.SecurityProfile.ImageCleaner.IsEnabled,
- IntervalHours = data.SecurityProfile.ImageCleaner.IntervalHours
- },
- WorkloadIdentity = data.SecurityProfile.IsWorkloadIdentityEnabled is null
- ? null
- : new() { Enabled = data.SecurityProfile.IsWorkloadIdentityEnabled }
- },
- // Storage Profile
- StorageProfile = data.StorageProfile is null ? null : new()
- {
- BlobCSIDriver = data.StorageProfile.IsBlobCsiDriverEnabled is null ? null : new() { Enabled = data.StorageProfile.IsBlobCsiDriverEnabled },
- DiskCSIDriver = data.StorageProfile.IsDiskCsiDriverEnabled is null ? null : new() { Enabled = data.StorageProfile.IsDiskCsiDriverEnabled },
- FileCSIDriver = data.StorageProfile.IsFileCsiDriverEnabled is null ? null : new() { Enabled = data.StorageProfile.IsFileCsiDriverEnabled },
- SnapshotController = data.StorageProfile.IsSnapshotControllerEnabled is null ? null : new() { Enabled = data.StorageProfile.IsSnapshotControllerEnabled }
- },
- // Metrics profile (no 1.2.5 SDK match for our CostAnalysis model)
- MetricsProfile = null,
- // Node provisioning and bootstrap profiles are not exposed in SDK 1.2.5
- NodeProvisioningProfile = null,
- BootstrapProfile = null,
- // Workload Auto-scaler profile
- WorkloadAutoScalerProfile = data.WorkloadAutoScalerProfile is null ? null : new()
+ Id = response.Id,
+ Name = response.Name,
+ SubscriptionId = response.SubscriptionId,
+ ResourceGroupName = response.ResourceGroup,
+ Location = response.Location,
+ Tags = response.Tags,
+ SkuName = response.Sku?.Name,
+ SkuTier = response.Sku?.Tier,
+ IdentityType = response.Identity?.Type,
+ Identity = response.Identity,
+ KubernetesVersion = props?.KubernetesVersion,
+ ProvisioningState = props?.ProvisioningState,
+ DnsPrefix = props?.DnsPrefix,
+ Fqdn = props?.Fqdn,
+ NodeResourceGroup = props?.NodeResourceGroup,
+ SupportPlan = props?.SupportPlan,
+ ResourceUid = props?.ResourceUid,
+ EnableRbac = props?.EnableRbac,
+ DisableLocalAccounts = props?.DisableLocalAccounts,
+ MaxAgentPools = props?.MaxAgentPools,
+ PowerState = props?.PowerState?.Code,
+ NetworkPlugin = np?.NetworkPlugin,
+ NetworkPolicy = np?.NetworkPolicy,
+ ServiceCidr = np?.ServiceCidr,
+ DnsServiceIP = np?.DnsServiceIP,
+ NetworkProfile = MapToNetworkProfile(np),
+ OidcIssuerProfile = props?.OidcIssuerProfile,
+ AutoUpgradeProfile = props?.AutoUpgradeProfile,
+ SecurityProfile = props?.SecurityProfile,
+ StorageProfile = props?.StorageProfile,
+ WorkloadAutoScalerProfile = props?.WorkloadAutoScalerProfile,
+ AddonProfiles = MapAddonProfiles(props?.AddonProfiles),
+ IdentityProfile = props?.IdentityProfile,
+ AgentPoolProfiles = props?.AgentPoolProfiles,
+ NodeCount = props?.AgentPoolProfiles?.Count > 0 ? props.AgentPoolProfiles[0].Count : null,
+ NodeVmSize = props?.AgentPoolProfiles?.Count > 0 ? props.AgentPoolProfiles[0].VmSize : null,
+ };
+
+ return cluster;
+ }
+
+ private static ClusterNetworkProfile? MapToNetworkProfile(AksClusterNetworkProfileJson? np)
+ {
+ if (np is null)
+ return null;
+
+ ClusterNetworkLoadBalancerProfile? lbProfile = null;
+ if (np.LoadBalancerProfile is { } lb)
+ {
+ lbProfile = new()
{
- Keda = data.WorkloadAutoScalerProfile.IsKedaEnabled is null ? null : new() { Enabled = data.WorkloadAutoScalerProfile.IsKedaEnabled },
- VerticalPodAutoscaler = data.WorkloadAutoScalerProfile.IsVpaEnabled is null ? null : new() { Enabled = data.WorkloadAutoScalerProfile.IsVpaEnabled }
- },
- // AI toolchain operator profile not exposed in SDK 1.2.5
- AiToolchainOperatorProfile = null,
- // Unique resource UID
- ResourceUid = data.ResourceId,
- Tags = data.Tags?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
+ ManagedOutboundIPCount = lb.ManagedOutboundIPs?.Count,
+ EffectiveOutboundIPs = lb.EffectiveOutboundIPs,
+ BackendPoolType = lb.BackendPoolType,
+ };
+ }
+
+ return new()
+ {
+ NetworkPlugin = np.NetworkPlugin,
+ NetworkPluginMode = np.NetworkPluginMode,
+ NetworkPolicy = np.NetworkPolicy,
+ NetworkDataplane = np.NetworkDataplane,
+ LoadBalancerSku = np.LoadBalancerSku,
+ LoadBalancerProfile = lbProfile,
+ PodCidr = np.PodCidr,
+ ServiceCidr = np.ServiceCidr,
+ DnsServiceIP = np.DnsServiceIP,
+ OutboundType = np.OutboundType,
+ PodCidrs = np.PodCidrs,
+ ServiceCidrs = np.ServiceCidrs,
+ IpFamilies = np.IpFamilies,
};
+ }
- // Map agent pool profiles from the cluster resource data when available
- if (data.AgentPoolProfiles is not null)
+ private static IDictionary>? MapAddonProfiles(
+ Dictionary? addonProfiles)
+ {
+ if (addonProfiles is null)
+ return null;
+
+ var result = new Dictionary>();
+ foreach (var (name, addon) in addonProfiles)
{
- try
+ var map = new Dictionary();
+ if (addon.Config is not null)
{
- cluster.AgentPoolProfiles = data.AgentPoolProfiles.Select(ConvertToNodePoolModel).ToList();
+ foreach (var (key, value) in addon.Config)
+ map[$"config.{key}"] = value;
}
- catch
+ if (addon.Identity is not null)
{
- // If SDK shape differs, fall back to minimal projection
- cluster.AgentPoolProfiles = data.AgentPoolProfiles
- .Select(p => new NodePool { Name = p.Name, Count = p.Count, VmSize = p.VmSize?.ToString(), Mode = p.Mode?.ToString() })
- .ToList();
+ if (addon.Identity.ClientId is not null)
+ map["identity.clientId"] = addon.Identity.ClientId;
+ if (addon.Identity.ObjectId is not null)
+ map["identity.objectId"] = addon.Identity.ObjectId;
}
+ result[name] = map;
}
- return cluster;
+ return result.Count > 0 ? result : null;
}
-
private static NodePool ConvertToNodePoolModel(ContainerServiceAgentPoolResource agentPoolResource)
{
var data = agentPoolResource.Data;
diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/AksCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/AksCommandTests.cs
index 63b6a71f79..efc58fc6a0 100644
--- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/AksCommandTests.cs
+++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/AksCommandTests.cs
@@ -178,21 +178,36 @@ public async Task Should_get_specific_aks_cluster()
[Fact]
public async Task Should_handle_nonexistent_cluster_gracefully()
{
+ // First, get a list of clusters to find a resource group to test against
+ var listResult = await CallToolAsync(
+ "aks_cluster_get",
+ new()
+ {
+ { "subscription", Settings.SubscriptionId }
+ });
+
+ var clusters = listResult.AssertProperty("clusters");
+ Assert.True(clusters.GetArrayLength() > 0, "Expected at least one AKS cluster for testing get command");
+
+ // Get the first cluster's resource group
+ var firstCluster = clusters.EnumerateArray().First();
+ var resourceGroupName = RegisterOrRetrieveVariable("firstResourceGroupName", firstCluster.GetProperty("resourceGroupName").GetString()!);
+
+ // Attempt to get a non-existent cluster from that resource group
var result = await CallToolAsync(
"aks_cluster_get",
new()
{
{ "subscription", Settings.SubscriptionId },
- { "resource-group", "nonexistent-rg" },
+ { "resource-group", resourceGroupName },
{ "cluster", "nonexistent-cluster" }
});
- // Should return runtime error response with error details
+ // Should return list with zero clusters
Assert.True(result.HasValue);
- var errorDetails = result.Value;
- errorDetails.AssertProperty("message");
- var typeProperty = errorDetails.AssertProperty("type");
- Assert.Equal("RequestFailedException", typeProperty.GetString());
+ var results = result.Value;
+ var resultsClusters = results.AssertProperty("clusters");
+ Assert.True(resultsClusters.GetArrayLength() == 0, "Expected no clusters for nonexistent cluster request");
}
[Fact]
@@ -353,6 +368,7 @@ public async Task Should_handle_nonexistent_nodepool_gracefully()
var errorDetails = result.Value;
errorDetails.AssertProperty("message");
var typeProperty = errorDetails.AssertProperty("type");
+
Assert.Equal("RequestFailedException", typeProperty.GetString());
}
diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Cluster/ClusterGetCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Cluster/ClusterGetCommandTests.cs
index e99b43ceb5..ce5ddbe031 100644
--- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Cluster/ClusterGetCommandTests.cs
+++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Cluster/ClusterGetCommandTests.cs
@@ -26,9 +26,9 @@ public void Constructor_InitializesCommandCorrectly()
[Theory]
[InlineData("--subscription sub1 --resource-group rg1 --cluster cluster1", true)]
- [InlineData("--subscription sub1 --cluster cluster1", false)] // Missing resource-group
- [InlineData("--resource-group rg1 --cluster cluster1", false)] // Missing subscription
- [InlineData("", false)] // Missing all required options
+ [InlineData("--subscription sub1 --cluster cluster1", true)] // Resource group is optional with ARG queries
+ [InlineData("--resource-group rg1 --cluster cluster1", true)] // Subscription is no longer required
+ [InlineData("", true)] // Valid scenario now that resource querying is being utilized
public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed)
{
// Arrange
diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Nodepool/NodepoolGetCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Nodepool/NodepoolGetCommandTests.cs
index b54108c949..c89e69d767 100644
--- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Nodepool/NodepoolGetCommandTests.cs
+++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Nodepool/NodepoolGetCommandTests.cs
@@ -29,7 +29,7 @@ public void Constructor_InitializesCommandCorrectly()
[InlineData("--subscription sub123 --resource-group rg1 --cluster c1 --nodepool np1 --tenant t1", true)]
[InlineData("--subscription sub123 --resource-group rg1 --nodepool np1", false)] // missing cluster
[InlineData("--subscription sub123 --cluster c1 --nodepool np1", false)] // missing rg
- [InlineData("--resource-group rg1 --cluster c1 --nodepool np1", false)] // missing subscription
+ [InlineData("--resource-group rg1 --cluster c1 --nodepool np1", true)] // ARG querying doesn't require subscription
[InlineData("", false)]
public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed)
{