Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Azure.Mcp.Core.Areas.Subscription.Models;

namespace Azure.Mcp.Core.Areas.Subscription.Commands;

[JsonSerializable(typeof(SubscriptionListCommand.SubscriptionListCommandResult))]
[JsonSerializable(typeof(SubscriptionInfo))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal partial class SubscriptionJsonContext : JsonSerializerContext
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Areas.Subscription.Models;
using Azure.Mcp.Core.Areas.Subscription.Options;
using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Services.Azure.Subscription;
Expand All @@ -21,7 +22,11 @@ public sealed class SubscriptionListCommand(ILogger<SubscriptionListCommand> log
public override string Name => "list";

public override string Description =>
"List all or current subscriptions for an account in Azure; returns subscriptionId, displayName, state, tenantId, and isDefault. Use for scope selection in governance, policy, access, cost management, or deployment.";
"List all Azure subscriptions for the current account. Returns subscriptionId, displayName, state, tenantId, and isDefault for each subscription. " +
"The isDefault field indicates the user's default subscription as resolved from the Azure CLI profile (configured via 'az account set') or, if not set there, from the AZURE_SUBSCRIPTION_ID environment variable. " +
"When the user has not specified a subscription, prefer the subscription where isDefault is true. " +
"If no default can be determined from either source and multiple subscriptions exist, ask the user which subscription to use.";

public override string Title => CommandTitle;

public override ToolMetadata Metadata => new()
Expand All @@ -48,8 +53,11 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
var subscriptionService = context.GetService<ISubscriptionService>();
var subscriptions = await subscriptionService.GetSubscriptions(options.Tenant, options.RetryPolicy, cancellationToken);

var defaultSubscriptionId = subscriptionService.GetDefaultSubscriptionId();
var subscriptionInfos = MapToSubscriptionInfos(subscriptions, defaultSubscriptionId);

context.Response.Results = ResponseResult.Create(
new SubscriptionListCommandResult(subscriptions),
new SubscriptionListCommandResult(subscriptionInfos),
SubscriptionJsonContext.Default.SubscriptionListCommandResult);
Comment on lines +56 to 61
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching the result payload from List<SubscriptionData> to List<SubscriptionInfo> changes the JSON contract for subscription list (it will no longer serialize any extra fields present on SubscriptionData). If existing clients might be consuming those additional fields, this is a breaking change; consider either wrapping SubscriptionData and adding an isDefault field, or explicitly documenting/changelogging the narrower output contract.

Copilot uses AI. Check for mistakes.
}
catch (Exception ex)
Expand All @@ -61,5 +69,26 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
return context.Response;
}

internal record SubscriptionListCommandResult(List<SubscriptionData> Subscriptions);
internal static List<SubscriptionInfo> MapToSubscriptionInfos(List<SubscriptionData> subscriptions, string? defaultSubscriptionId)
{
var hasDefault = !string.IsNullOrEmpty(defaultSubscriptionId);

var infos = subscriptions.Select(s => new SubscriptionInfo(
s.SubscriptionId,
s.DisplayName,
s.State?.ToString(),
s.TenantId?.ToString(),
hasDefault && s.SubscriptionId.Equals(defaultSubscriptionId, StringComparison.OrdinalIgnoreCase)
)).ToList();

// Sort so the default subscription appears first
if (hasDefault)
{
infos = [.. infos.OrderByDescending(s => s.IsDefault)];
}

return infos;
}

internal record SubscriptionListCommandResult(List<SubscriptionInfo> Subscriptions);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.Mcp.Core.Areas.Subscription.Models;

/// <summary>
/// Represents an Azure subscription with default subscription indicator.
/// </summary>
/// <param name="SubscriptionId">The subscription ID.</param>
/// <param name="DisplayName">The display name of the subscription.</param>
/// <param name="State">The subscription state (e.g., Enabled, Disabled).</param>
/// <param name="TenantId">The tenant ID associated with the subscription.</param>
/// <param name="IsDefault">Whether this subscription is the default as configured via 'az account set' in the Azure CLI profile, or via the AZURE_SUBSCRIPTION_ID environment variable.</param>
internal record SubscriptionInfo(
string SubscriptionId,
string DisplayName,
string? State,
string? TenantId,
bool IsDefault);
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ protected override void RegisterOptions(Command command)
command.Options.Add(OptionDefinitions.Common.Subscription);
command.Validators.Add(commandResult =>
{
// Command-level validation for presence: allow either --subscription or AZURE_SUBSCRIPTION_ID
// This mirrors the prior behavior that preferred the explicit option but fell back to env var.
// Command-level validation for presence: allow either --subscription,
// Azure CLI profile default, or AZURE_SUBSCRIPTION_ID env var.
if (!CommandHelper.HasSubscriptionAvailable(commandResult))
{
commandResult.AddError("Missing Required options: --subscription");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ public interface ISubscriptionService
bool IsSubscriptionId(string subscription, string? tenant = null);
Task<string> GetSubscriptionIdByName(string subscriptionName, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default);
Task<string> GetSubscriptionNameById(string subscriptionId, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default);

/// <summary>
/// Gets the default subscription ID from the Azure CLI profile (~/.azure/azureProfile.json).
/// Falls back to the AZURE_SUBSCRIPTION_ID environment variable if the profile is unavailable.
/// </summary>
/// <returns>The default subscription ID if found, null otherwise.</returns>
string? GetDefaultSubscriptionId();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Helpers;
using Azure.Mcp.Core.Options;
using Azure.Mcp.Core.Services.Azure.Tenant;
using Azure.Mcp.Core.Services.Caching;
Expand Down Expand Up @@ -96,6 +97,12 @@ public async Task<string> GetSubscriptionNameById(string subscriptionId, string?
return subscription.DisplayName;
}

/// <inheritdoc/>
public string? GetDefaultSubscriptionId()
{
return CommandHelper.GetDefaultSubscription();
}

private async Task<string> GetSubscriptionId(string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken)
{
if (IsSubscriptionId(subscription))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Helpers;
using Xunit;

namespace Azure.Mcp.Core.UnitTests.Areas.Subscription;

public class AzureCliProfileHelperTests
{
[Fact]
public void ParseDefaultSubscriptionId_ValidProfile_ReturnsDefaultId()
{
var profileJson = """
{
"subscriptions": [
{
"id": "sub-1111-1111",
"name": "Subscription One",
"state": "Enabled",
"tenantId": "tenant-1111",
"isDefault": false
},
{
"id": "sub-2222-2222",
"name": "Subscription Two",
"state": "Enabled",
"tenantId": "tenant-2222",
"isDefault": true
},
{
"id": "sub-3333-3333",
"name": "Subscription Three",
"state": "Enabled",
"tenantId": "tenant-3333",
"isDefault": false
}
]
}
""";

var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson);

Assert.Equal("sub-2222-2222", result);
}

[Fact]
public void ParseDefaultSubscriptionId_NoDefaultInProfile_ReturnsNull()
{
var profileJson = """
{
"subscriptions": [
{
"id": "sub-1111-1111",
"name": "Subscription One",
"state": "Enabled",
"tenantId": "tenant-1111",
"isDefault": false
}
]
}
""";

var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson);

Assert.Null(result);
}

[Fact]
public void ParseDefaultSubscriptionId_EmptySubscriptions_ReturnsNull()
{
var profileJson = """
{
"subscriptions": []
}
""";

var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson);

Assert.Null(result);
}

[Fact]
public void ParseDefaultSubscriptionId_NoSubscriptionsProperty_ReturnsNull()
{
var profileJson = """
{
"installationId": "some-id"
}
""";

var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson);

Assert.Null(result);
}

[Fact]
public void ParseDefaultSubscriptionId_MissingIdOnDefault_ReturnsNull()
{
var profileJson = """
{
"subscriptions": [
{
"name": "Subscription One",
"isDefault": true
}
]
}
""";

var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson);

Assert.Null(result);
}

[Fact]
public void GetAzureProfilePath_ReturnsExpectedPath()
{
var result = AzureCliProfileHelper.GetAzureProfilePath();

// In a normal environment, user profile is available
Assert.NotNull(result);
Assert.Contains(".azure", result);
Assert.EndsWith("azureProfile.json", result);
}
}
Loading