Read default subscription from Azure CLI profile for all subscription-scoped commands#1974
Read default subscription from Azure CLI profile for all subscription-scoped commands#1974
Conversation
…cription to guide AI agent behavior The subscription_list tool now: - Returns a SubscriptionInfo model with isDefault field indicating the default subscription (based on AZURE_SUBSCRIPTION_ID environment variable) - Sorts subscriptions so the default appears first in the list - Updated description instructs AI agents to prefer the default subscription or ask the user when no default is set and multiple subscriptions exist Fixes #1079 Co-authored-by: ArthurMa1978 <20514459+ArthurMa1978@users.noreply.github.com>
Co-authored-by: ArthurMa1978 <20514459+ArthurMa1978@users.noreply.github.com>
…cription detection Read the default subscription from ~/.azure/azureProfile.json (set via 'az account set') as the primary source. Fall back to AZURE_SUBSCRIPTION_ID environment variable only if the profile is unavailable. - Add GetDefaultSubscriptionId() to ISubscriptionService interface - Implement ReadDefaultSubscriptionFromAzureProfile in SubscriptionService - Update SubscriptionListCommand to use service method instead of env var directly - Update tool description to reference 'az account set' - Add 6 new profile-reading unit tests - Update existing tests to use mocked GetDefaultSubscriptionId() Co-authored-by: ArthurMa1978 <20514459+ArthurMa1978@users.noreply.github.com>
Co-authored-by: ArthurMa1978 <20514459+ArthurMa1978@users.noreply.github.com>
… into CommandHelper for all subscription-scoped commands - Create AzureCliProfileHelper in Microsoft.Mcp.Core for reading default subscription from ~/.azure/azureProfile.json - Update CommandHelper.GetSubscription() and HasSubscriptionAvailable() to use Azure CLI profile as primary fallback (before AZURE_SUBSCRIPTION_ID env var) - Update SubscriptionService.GetDefaultSubscriptionId() to delegate to shared logic - Update SubscriptionCommand comments to reflect new behavior - Migrate SubscriptionServiceProfileTests to test AzureCliProfileHelper directly Co-authored-by: ArthurMa1978 <20514459+ArthurMa1978@users.noreply.github.com>
Co-authored-by: ArthurMa1978 <20514459+ArthurMa1978@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Updates subscription resolution across subscription-scoped commands to match Azure CLI behavior by reading the default subscription from ~/.azure/azureProfile.json (set via az account set) before falling back to AZURE_SUBSCRIPTION_ID, and surfaces that default selection in subscription list.
Changes:
- Added
AzureCliProfileHelperandCommandHelper.GetDefaultSubscription()to implement the default-subscription fallback chain. - Updated subscription option validation/resolution to use the shared default-subscription logic.
- Updated
subscription listto return anisDefaultindicator and prioritize the default subscription in the output.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| servers/Azure.Mcp.Server/docs/azmcp-commands.md | Updates CLI docs for subscription list to mention isDefault. |
| core/Microsoft.Mcp.Core/src/Helpers/CommandHelper.cs | Adds GetDefaultSubscription() and routes subscription resolution/validation through it. |
| core/Microsoft.Mcp.Core/src/Helpers/AzureCliProfileHelper.cs | New helper to parse Azure CLI profile JSON for the default subscription. |
| core/Azure.Mcp.Core/src/Services/Azure/Subscription/ISubscriptionService.cs | Adds GetDefaultSubscriptionId() to expose shared default-subscription resolution. |
| core/Azure.Mcp.Core/src/Services/Azure/Subscription/SubscriptionService.cs | Implements GetDefaultSubscriptionId() via CommandHelper. |
| core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand.cs | Updates validation comments to include the new fallback sources. |
| core/Azure.Mcp.Core/src/Areas/Subscription/Models/SubscriptionInfo.cs | Introduces a model that includes isDefault for subscription list output. |
| core/Azure.Mcp.Core/src/Areas/Subscription/Commands/SubscriptionListCommand.cs | Maps subscriptions to SubscriptionInfo, marks/sorts the default subscription, and updates description. |
| core/Azure.Mcp.Core/src/Areas/Subscription/Commands/SubscriptionJsonContext.cs | Adds source-gen serialization metadata for the new output model. |
| core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Subscription/SubscriptionListCommandTests.cs | Extends unit tests to cover default subscription marking/sorting and isDefault output. |
| core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Subscription/AzureCliProfileHelperTests.cs | Adds unit tests for parsing the default subscription from Azure CLI profile JSON. |
| public static bool HasSubscriptionAvailable(CommandResult commandResult) | ||
| { | ||
| var hasOption = commandResult.HasOptionResult(OptionDefinitions.Common.Subscription.Name); | ||
| var hasEnv = !string.IsNullOrEmpty(EnvironmentHelpers.GetAzureSubscriptionId()); | ||
| return hasOption || hasEnv; | ||
| var hasDefault = !string.IsNullOrEmpty(GetDefaultSubscription()); | ||
| return hasOption || hasDefault; |
There was a problem hiding this comment.
HasSubscriptionAvailable always calls GetDefaultSubscription() even when --subscription is present, which can trigger an unnecessary read/parse of ~/.azure/azureProfile.json on every command validation. Consider short-circuiting (return true when the option exists) and only checking defaults when the option is absent (and optionally check the env var first to avoid disk IO).
| public static string? GetSubscription(ParseResult parseResult) | ||
| { | ||
| // Get subscription from command line option or fallback to environment variable | ||
| // Get subscription from command line option or fallback to default subscription | ||
| var subscriptionValue = parseResult.GetValueOrDefault<string>(OptionDefinitions.Common.Subscription.Name); | ||
|
|
||
| var envSubscription = EnvironmentHelpers.GetAzureSubscriptionId(); | ||
| return (string.IsNullOrEmpty(subscriptionValue) || IsPlaceholder(subscriptionValue)) && !string.IsNullOrEmpty(envSubscription) | ||
| ? envSubscription | ||
| var defaultSubscription = GetDefaultSubscription(); | ||
| return (string.IsNullOrEmpty(subscriptionValue) || IsPlaceholder(subscriptionValue)) && !string.IsNullOrEmpty(defaultSubscription) | ||
| ? defaultSubscription | ||
| : subscriptionValue; |
There was a problem hiding this comment.
GetSubscription reads the default subscription unconditionally, even when a non-placeholder --subscription value is provided. To avoid unnecessary file IO/parsing, consider only calling GetDefaultSubscription() when subscriptionValue is null/empty or matches the placeholder check, and otherwise return the provided value directly.
| internal static string GetAzureProfilePath() | ||
| { | ||
| var azureDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".azure"); | ||
| return Path.Combine(azureDir, "azureProfile.json"); |
There was a problem hiding this comment.
GetAzureProfilePath() uses Environment.SpecialFolder.UserProfile; if that resolves to an empty string in some hosting environments, Path.Combine("", ".azure") yields a relative path and may accidentally read a local ./.azure/azureProfile.json. Consider guarding for an empty/unknown user profile and returning null/empty (and having GetDefaultSubscriptionId treat that as unavailable) to avoid unintended file reads.
| "The isDefault field indicates the user's default subscription as configured via 'az account set' in the Azure CLI profile. " + | ||
| "When the user has not specified a subscription, prefer the subscription where isDefault is true. " + | ||
| "If no default is set and multiple subscriptions exist, ask the user which subscription to use."; |
There was a problem hiding this comment.
The updated Description says isDefault reflects only the Azure CLI profile default set via az account set, but the implementation marks isDefault based on GetDefaultSubscriptionId() which can also come from AZURE_SUBSCRIPTION_ID. Consider updating the description to mention the env var fallback as well so callers interpret isDefault correctly.
| "The isDefault field indicates the user's default subscription as configured via 'az account set' in the Azure CLI profile. " + | |
| "When the user has not specified a subscription, prefer the subscription where isDefault is true. " + | |
| "If no default is set and multiple subscriptions exist, ask the user which subscription to use."; | |
| "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."; |
| var defaultSubscriptionId = subscriptionService.GetDefaultSubscriptionId(); | ||
| var subscriptionInfos = MapToSubscriptionInfos(subscriptions, defaultSubscriptionId); | ||
|
|
||
| context.Response.Results = ResponseResult.Create( | ||
| new SubscriptionListCommandResult(subscriptions), | ||
| new SubscriptionListCommandResult(subscriptionInfos), | ||
| SubscriptionJsonContext.Default.SubscriptionListCommandResult); |
There was a problem hiding this comment.
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.
CommandHelper.GetSubscription()— used by everySubscriptionCommand<T>-derived command (EventHubs, AKS, Storage, Cosmos, etc.) — only fell back toAZURE_SUBSCRIPTION_IDenv var when--subscriptionwasn't provided. It should read the default from~/.azure/azureProfile.json(set viaaz account set), matching Azure CLI behavior.Changes
AzureCliProfileHelperinMicrosoft.Mcp.Core— parses~/.azure/azureProfile.jsonfor the subscription withisDefault: true. CatchesJsonException | IOException | UnauthorizedAccessException | SecurityException.CommandHelper.GetDefaultSubscription()— new public method implementing the fallback chain: Azure CLI profile →AZURE_SUBSCRIPTION_IDenv varCommandHelper.GetSubscription()/HasSubscriptionAvailable()— now useGetDefaultSubscription()instead of only checking the env varSubscriptionService.GetDefaultSubscriptionId()— delegates toCommandHelper.GetDefaultSubscription()to share the same resolution logicSubscriptionListCommand— description updated to referenceaz account set; marks subscriptions withisDefaultusing the shared resolutionFallback chain
All
SubscriptionCommand<T>subclasses (60+ tool commands) inherit this behavior automatically throughCommandHelper.Invoking Livetests
Copilot submitted PRs are not trustworthy by default. Users with
writeaccess to the repo need to validate the contents of this PR before leaving a comment with the text/azp run mcp - pullrequest - live. This will trigger the necessary livetest workflows to complete required validation.🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.