Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ public sealed class SubscriptionListCommand(ILogger<SubscriptionListCommand> log
{
private const string CommandTitle = "List Azure Subscriptions";
private readonly ILogger<SubscriptionListCommand> _logger = logger;
private readonly Option<int> _characterLimitOption = OptionDefinitions.Common.CharacterLimit;

public override string Name => "list";

public override string Description =>
$"""
List all Azure subscriptions accessible to your account. Optionally specify {OptionDefinitions.Common.TenantName}
and {OptionDefinitions.Common.AuthMethodName}. Results include subscription names and IDs, returned as a JSON array.
Use {OptionDefinitions.Common.CharacterLimitName} to limit response size.
""";

public override string Title => CommandTitle;
Expand All @@ -35,6 +37,19 @@ List all Azure subscriptions accessible to your account. Optionally specify {Opt
Secret = false
};

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(_characterLimitOption);
}

protected override SubscriptionListOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.CharacterLimit = parseResult.GetValue(_characterLimitOption);
return options;
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
{
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
Expand All @@ -49,11 +64,52 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
var subscriptionService = context.GetService<ISubscriptionService>();
var subscriptions = await subscriptionService.GetSubscriptions(options.Tenant, options.RetryPolicy);

context.Response.Results = subscriptions?.Count > 0
? ResponseResult.Create(
new SubscriptionListCommandResult(subscriptions),
SubscriptionJsonContext.Default.SubscriptionListCommandResult)
: null;
if (subscriptions?.Count > 0)
{
var result = new SubscriptionListCommandResult(subscriptions);

// Serialize to check character count
var json = System.Text.Json.JsonSerializer.Serialize(result, SubscriptionJsonContext.Default.SubscriptionListCommandResult);

if (json.Length <= options.CharacterLimit)
{
// Response is within limit
context.Response.Results = ResponseResult.Create(result, SubscriptionJsonContext.Default.SubscriptionListCommandResult);
context.Response.Message = $"All {subscriptions.Count} subscriptions returned ({json.Length} characters).";
}
else
{
// Response exceeds limit, truncate subscriptions
var truncatedSubscriptions = new List<SubscriptionData>();
var currentLength = 0;

foreach (var subscription in subscriptions)
{
var tempList = new List<SubscriptionData>(truncatedSubscriptions) { subscription };
var tempResult = new SubscriptionListCommandResult(tempList);
var tempJson = System.Text.Json.JsonSerializer.Serialize(tempResult, SubscriptionJsonContext.Default.SubscriptionListCommandResult);

if (tempJson.Length <= options.CharacterLimit)
{
truncatedSubscriptions.Add(subscription);
currentLength = tempJson.Length;
}
else
{
break;
}
}

var truncatedResult = new SubscriptionListCommandResult(truncatedSubscriptions);
context.Response.Results = ResponseResult.Create(truncatedResult, SubscriptionJsonContext.Default.SubscriptionListCommandResult);
context.Response.Message = $"Results truncated to {truncatedSubscriptions.Count} of {subscriptions.Count} subscriptions ({currentLength} characters). Increase --{OptionDefinitions.Common.CharacterLimitName} to see more results.";
}
}
else
{
context.Response.Results = null;
context.Response.Message = "No subscriptions found.";
}
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Azure.Mcp.Core.Models.Option;
using Azure.Mcp.Core.Options;

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

public class SubscriptionListOptions : GlobalOptions;
public class SubscriptionListOptions : GlobalOptions
{
/// <summary>
/// The maximum number of characters to return in the response. Defaults to 10000.
/// </summary>
[JsonPropertyName(OptionDefinitions.Common.CharacterLimitName)]
public int CharacterLimit { get; set; } = 10000;
}
11 changes: 11 additions & 0 deletions core/Azure.Mcp.Core/src/Models/Option/OptionDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ public static class Common
Description = "The name of the Azure resource group. This is a logical container for Azure resources.",
Required = false
};

public const string CharacterLimitName = "character-limit";

public static readonly Option<int> CharacterLimit = new(
$"--{CharacterLimitName}"
)
{
Description = "The maximum number of characters to return in the response. If the response exceeds this limit, it will be truncated with a status message.",
Required = false,
DefaultValueFactory = _ => 10000
};
}

public static class RetryPolicy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,178 @@ await _subscriptionService.Received(1).GetSubscriptions(
Arg.Any<RetryPolicyOptions>());
}

[Fact]
public async Task ExecuteAsync_WithCharacterLimit_ParsesAndBindsCorrectly()
{
// Arrange
var characterLimit = 5000;
var subscriptions = new List<SubscriptionData>
{
SubscriptionTestHelpers.CreateSubscriptionData("sub1", "Test Subscription 1")
};

_subscriptionService
.GetSubscriptions(Arg.Any<string>(), Arg.Any<RetryPolicyOptions>())
.Returns(subscriptions);

var args = _commandDefinition.Parse($"--character-limit {characterLimit}");

// Act
var result = await _command.ExecuteAsync(_context, args);

// Assert
Assert.NotNull(result);
Assert.Equal(200, result.Status);
Assert.NotNull(result.Results);
Assert.Contains("1 subscriptions returned", result.Message);
Assert.Contains("characters", result.Message);
}

[Fact]
public async Task ExecuteAsync_ResponseWithinCharacterLimit_ReturnsAllSubscriptions()
{
// Arrange
var characterLimit = 10000; // Large limit
var subscriptions = new List<SubscriptionData>
{
SubscriptionTestHelpers.CreateSubscriptionData("sub1", "Subscription 1"),
SubscriptionTestHelpers.CreateSubscriptionData("sub2", "Subscription 2")
};

_subscriptionService
.GetSubscriptions(Arg.Any<string>(), Arg.Any<RetryPolicyOptions>())
.Returns(subscriptions);

var args = _commandDefinition.Parse($"--character-limit {characterLimit}");

// Act
var result = await _command.ExecuteAsync(_context, args);

// Assert
Assert.NotNull(result);
Assert.Equal(200, result.Status);
Assert.NotNull(result.Results);

var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(result.Results));
var subscriptionsArray = jsonDoc.RootElement.GetProperty("subscriptions");

Assert.Equal(2, subscriptionsArray.GetArrayLength());
Assert.Contains("All 2 subscriptions returned", result.Message);
Assert.Contains("characters", result.Message);
}

[Fact]
public async Task ExecuteAsync_ResponseExceedsCharacterLimit_TruncatesSubscriptions()
{
// Arrange
var characterLimit = 100; // Very small limit to force truncation
var subscriptions = SubscriptionTestHelpers.CreateTestSubscriptions(10); // Create 10 subscriptions

_subscriptionService
.GetSubscriptions(Arg.Any<string>(), Arg.Any<RetryPolicyOptions>())
.Returns(subscriptions);

var args = _commandDefinition.Parse($"--character-limit {characterLimit}");

// Act
var result = await _command.ExecuteAsync(_context, args);

// Assert
Assert.NotNull(result);
Assert.Equal(200, result.Status);
Assert.NotNull(result.Results);

var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(result.Results));
var subscriptionsArray = jsonDoc.RootElement.GetProperty("subscriptions");

// Should have fewer than 10 subscriptions due to truncation
Assert.True(subscriptionsArray.GetArrayLength() < 10);
Assert.Contains("Results truncated", result.Message);
Assert.Contains("of 10 subscriptions", result.Message);
Assert.Contains("Increase --character-limit", result.Message);
}

[Fact]
public async Task ExecuteAsync_EmptySubscriptionListWithCharacterLimit_ReturnsAppropriateMessage()
{
// Arrange
var characterLimit = 5000;
_subscriptionService
.GetSubscriptions(Arg.Any<string>(), Arg.Any<RetryPolicyOptions>())
.Returns([]);

var args = _commandDefinition.Parse($"--character-limit {characterLimit}");

// Act
var result = await _command.ExecuteAsync(_context, args);

// Assert
Assert.NotNull(result);
Assert.Equal(200, result.Status);
Assert.Null(result.Results);
Assert.Equal("No subscriptions found.", result.Message);
}

[Fact]
public async Task ExecuteAsync_DefaultCharacterLimit_UsesDefaultValue()
{
// Arrange
var subscriptions = new List<SubscriptionData>
{
SubscriptionTestHelpers.CreateSubscriptionData("sub1", "Test Subscription")
};

_subscriptionService
.GetSubscriptions(Arg.Any<string>(), Arg.Any<RetryPolicyOptions>())
.Returns(subscriptions);

var args = _commandDefinition.Parse(""); // No character-limit specified

// Act
var result = await _command.ExecuteAsync(_context, args);

// Assert
Assert.NotNull(result);
Assert.Equal(200, result.Status);
Assert.NotNull(result.Results);
// Should use default behavior (10000 characters is the default)
Assert.Contains("1 subscriptions returned", result.Message);
}

[Fact]
public async Task ExecuteAsync_VerySmallCharacterLimit_HandlesGracefully()
{
// Arrange - Set a very small character limit that might not even fit one subscription
var characterLimit = 50;
var subscriptions = new List<SubscriptionData>
{
SubscriptionTestHelpers.CreateSubscriptionData("sub1", "Test")
};

_subscriptionService
.GetSubscriptions(Arg.Any<string>(), Arg.Any<RetryPolicyOptions>())
.Returns(subscriptions);

var args = _commandDefinition.Parse($"--character-limit {characterLimit}");

// Act
var result = await _command.ExecuteAsync(_context, args);

// Assert
Assert.NotNull(result);
Assert.Equal(200, result.Status);

// Should either return empty results or truncated results, but handle it gracefully
if (result.Results != null)
{
var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(result.Results));
var subscriptionsArray = jsonDoc.RootElement.GetProperty("subscriptions");

// Should have 0 or 1 subscription due to very small limit
Assert.True(subscriptionsArray.GetArrayLength() <= 1);
}

Assert.NotNull(result.Message);
}

}
1 change: 1 addition & 0 deletions servers/Azure.Mcp.Server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 `--character-limit` parameter to `azmcp_subscription_list` command to control response size. This parameter allows users to limit the number of characters returned in the response, with automatic truncation and informative status messages when the limit is exceeded. Default limit is 10,000 characters.

### Breaking Changes

Expand Down
Loading