diff --git a/Directory.Packages.props b/Directory.Packages.props
index 176a5cb12d..37953169b0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -62,8 +62,7 @@
-
+
@@ -95,4 +94,4 @@
-
+
\ No newline at end of file
diff --git a/core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand.cs b/core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand.cs
index 145e492379..eb4ef99d2e 100644
--- a/core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand.cs
+++ b/core/Azure.Mcp.Core/src/Commands/Subscription/SubscriptionCommand.cs
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.CommandLine.Parsing;
using System.Diagnostics.CodeAnalysis;
+using Azure.Mcp.Core.Helpers;
using Azure.Mcp.Core.Models.Option;
using Azure.Mcp.Core.Options;
@@ -20,9 +22,7 @@ protected override void RegisterOptions(Command command)
// This mirrors the prior behavior that preferred the explicit option but fell back to env var.
command.Validators.Add(commandResult =>
{
- var hasOption = commandResult.HasOptionResult(OptionDefinitions.Common.Subscription.Name);
- var hasEnv = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID"));
- if (!hasOption && !hasEnv)
+ if (!HasSubscriptionAvailable(commandResult))
{
commandResult.AddError("Missing Required options: --subscription");
}
@@ -36,7 +36,7 @@ protected override TOptions BindOptions(ParseResult parseResult)
// Get subscription from command line option or fallback to environment variable
var subscriptionValue = parseResult.GetValueOrDefault(OptionDefinitions.Common.Subscription.Name);
- var envSubscription = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID");
+ var envSubscription = EnvironmentHelpers.GetAzureSubscriptionId();
options.Subscription = (string.IsNullOrEmpty(subscriptionValue)
|| IsPlaceholder(subscriptionValue))
&& !string.IsNullOrEmpty(envSubscription)
@@ -46,6 +46,18 @@ protected override TOptions BindOptions(ParseResult parseResult)
return options;
}
+ ///
+ /// Checks if a subscription is available either from the command option or AZURE_SUBSCRIPTION_ID environment variable.
+ ///
+ /// The command result to check for the subscription option.
+ /// True if a subscription is available, false otherwise.
+ protected static bool HasSubscriptionAvailable(CommandResult commandResult)
+ {
+ var hasOption = commandResult.HasOptionResult(OptionDefinitions.Common.Subscription);
+ var hasEnv = !string.IsNullOrEmpty(EnvironmentHelpers.GetAzureSubscriptionId());
+ return hasOption || hasEnv;
+ }
+
private static bool IsPlaceholder(string value)
=> value.Contains("subscription") || value.Contains("default");
}
diff --git a/core/Azure.Mcp.Core/src/Helpers/EnvironmentVariableHelpers.cs b/core/Azure.Mcp.Core/src/Helpers/EnvironmentVariableHelpers.cs
index 0af31764a5..05f10bc7c8 100644
--- a/core/Azure.Mcp.Core/src/Helpers/EnvironmentVariableHelpers.cs
+++ b/core/Azure.Mcp.Core/src/Helpers/EnvironmentVariableHelpers.cs
@@ -5,6 +5,8 @@ namespace Azure.Mcp.Core.Helpers
{
public static class EnvironmentHelpers
{
+ private const string AzureSubscriptionIdEnvironmentVariable = "AZURE_SUBSCRIPTION_ID";
+
public static bool GetEnvironmentVariableAsBool(string envVarName)
{
return Environment.GetEnvironmentVariable(envVarName) switch
@@ -16,5 +18,24 @@ public static bool GetEnvironmentVariableAsBool(string envVarName)
_ => false
};
}
+
+ ///
+ /// Gets the Azure subscription ID from the AZURE_SUBSCRIPTION_ID environment variable.
+ ///
+ /// The subscription ID if available, null otherwise.
+ public static string? GetAzureSubscriptionId()
+ {
+ return Environment.GetEnvironmentVariable(AzureSubscriptionIdEnvironmentVariable);
+ }
+
+ ///
+ /// Sets the AZURE_SUBSCRIPTION_ID environment variable.
+ /// This method is primarily intended for testing scenarios.
+ ///
+ /// The subscription ID to set, or null to clear the variable.
+ public static void SetAzureSubscriptionId(string? subscriptionId)
+ {
+ Environment.SetEnvironmentVariable(AzureSubscriptionIdEnvironmentVariable, subscriptionId);
+ }
}
}
diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Subscription/SubscriptionCommandTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Subscription/SubscriptionCommandTests.cs
index d263160b97..95c48f24b8 100644
--- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Subscription/SubscriptionCommandTests.cs
+++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Subscription/SubscriptionCommandTests.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.CommandLine;
+using Azure.Mcp.Core.Helpers;
using Azure.Mcp.Core.Models.Command;
using Azure.Mcp.Core.Options;
using Azure.Mcp.Tools.Storage.Commands.Account;
@@ -40,9 +41,8 @@ public SubscriptionCommandTests()
public void Validate_WithEnvironmentVariableOnly_PassesValidation()
{
// Arrange
- var originalValue = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID");
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", "env-subs");
-
+ var originalValue = EnvironmentHelpers.GetAzureSubscriptionId();
+ EnvironmentHelpers.SetAzureSubscriptionId("env-subs");
try
{
var parseResult = _commandDefinition.Parse([]);
@@ -53,7 +53,7 @@ public void Validate_WithEnvironmentVariableOnly_PassesValidation()
finally
{
// Cleanup
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", originalValue);
+ EnvironmentHelpers.SetAzureSubscriptionId(originalValue);
}
}
@@ -61,8 +61,8 @@ public void Validate_WithEnvironmentVariableOnly_PassesValidation()
public async Task ExecuteAsync_WithEnvironmentVariableOnly_CallsServiceWithCorrectSubscription()
{
// Arrange
- var originalValue = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID");
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", "env-subs");
+ var originalValue = EnvironmentHelpers.GetAzureSubscriptionId();
+ EnvironmentHelpers.SetAzureSubscriptionId("env-subs");
try
{
@@ -97,7 +97,7 @@ public async Task ExecuteAsync_WithEnvironmentVariableOnly_CallsServiceWithCorre
finally
{
// Cleanup
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", originalValue);
+ EnvironmentHelpers.SetAzureSubscriptionId(originalValue);
}
}
@@ -105,8 +105,8 @@ public async Task ExecuteAsync_WithEnvironmentVariableOnly_CallsServiceWithCorre
public async Task ExecuteAsync_WithBothOptionAndEnvironmentVariable_PrefersOption()
{
// Arrange
- var originalValue = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID");
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", "env-subs");
+ var originalValue = EnvironmentHelpers.GetAzureSubscriptionId();
+ EnvironmentHelpers.SetAzureSubscriptionId("env-subs");
try
{
@@ -146,7 +146,7 @@ public async Task ExecuteAsync_WithBothOptionAndEnvironmentVariable_PrefersOptio
finally
{
// Cleanup
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", originalValue);
+ EnvironmentHelpers.SetAzureSubscriptionId(originalValue);
}
}
}
diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md
index ebc09224e9..550df944c0 100644
--- a/docs/azmcp-commands.md
+++ b/docs/azmcp-commands.md
@@ -478,6 +478,13 @@ azmcp deploy plan get --workspace-folder \
# List all Event Grid topics in a subscription or resource group
azmcp eventgrid topic list --subscription \
[--resource-group ]
+
+
+# List all Event Grid subscriptions in a subscription, resource group, or topic
+azmcp eventgrid subscription list --subscription \
+ [--resource-group ] \
+ [--topic ]
+ [--location ]
```
### Azure Function App Operations
diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md
index e0eb067e08..1558e8e182 100644
--- a/docs/e2eTestPrompts.md
+++ b/docs/e2eTestPrompts.md
@@ -146,6 +146,13 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
| azmcp_eventgrid_topic_list | Show me the Event Grid topics in my subscription |
| azmcp_eventgrid_topic_list | List all Event Grid topics in subscription |
| azmcp_eventgrid_topic_list | List all Event Grid topics in resource group in subscription |
+| azmcp_eventgrid_subscription_list | Show me all Event Grid subscriptions for topic |
+| azmcp_eventgrid_subscription_list | List Event Grid subscriptions for topic in subscription |
+| azmcp_eventgrid_subscription_list | List Event Grid subscriptions for topic in resource group |
+| azmcp_eventgrid_subscription_list | Show all Event Grid subscriptions in my subscription |
+| azmcp_eventgrid_subscription_list | List all Event Grid subscriptions in subscription |
+| azmcp_eventgrid_subscription_list | Show Event Grid subscriptions in resource group in subscription |
+| azmcp_eventgrid_subscription_list | List Event Grid subscriptions for subscription in location |
## Azure Function App
diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md
index e1de2943ab..a5f2a75a48 100644
--- a/servers/Azure.Mcp.Server/README.md
+++ b/servers/Azure.Mcp.Server/README.md
@@ -98,7 +98,11 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
* "List all Event Grid topics in subscription 'my-subscription'"
* "Show me the Event Grid topics in my subscription"
-* "List all Event Grid topics in resource group 'my-resource-group' in my subscription"
+* "List all Event Grid topics in resource group 'my-resourcegroup' in my subscription"
+* "List Event Grid subscriptions for topic 'my-topic' in resource group 'my-resourcegroup'"
+* "List Event Grid subscriptions for topic 'my-topic' in subscription 'my-subscription'"
+* "List Event Grid Subscriptions in subscription 'my-subscription'"
+* "List Event Grid subscriptions for topic 'my-topic' in location 'my-location'"
### ⚡ Azure Managed Lustre
@@ -211,6 +215,8 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
* List Event Grid topics in subscription or resource group
* View topic configuration and status information
* Access endpoint and key details for event publishing
+* List Event Grid subscriptions with filtering by topic name, resource group, and location
+* View subscription details including destination endpoints and retry policies
### 🧮 Azure Foundry
diff --git a/tools/Azure.Mcp.Tools.Acr/tests/Azure.Mcp.Tools.Acr.UnitTests/Registry/RegistryListCommandTests.cs b/tools/Azure.Mcp.Tools.Acr/tests/Azure.Mcp.Tools.Acr.UnitTests/Registry/RegistryListCommandTests.cs
index cc9ab6712b..7b50206205 100644
--- a/tools/Azure.Mcp.Tools.Acr/tests/Azure.Mcp.Tools.Acr.UnitTests/Registry/RegistryListCommandTests.cs
+++ b/tools/Azure.Mcp.Tools.Acr/tests/Azure.Mcp.Tools.Acr.UnitTests/Registry/RegistryListCommandTests.cs
@@ -3,6 +3,7 @@
using System.CommandLine;
using System.Text.Json;
+using Azure.Mcp.Core.Helpers;
using Azure.Mcp.Core.Models.Command;
using Azure.Mcp.Core.Options;
using Azure.Mcp.Tools.Acr.Commands;
@@ -52,7 +53,7 @@ public void Constructor_InitializesCommandCorrectly()
public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed)
{
// Ensure environment variable fallback does not interfere with validation tests
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", null);
+ EnvironmentHelpers.SetAzureSubscriptionId(null);
// Arrange
if (shouldSucceed)
{
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs
index 0a0a96b08d..301ad71ac6 100644
--- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs
@@ -2,13 +2,16 @@
// Licensed under the MIT License.
using System.Text.Json.Serialization;
+using Azure.Mcp.Tools.EventGrid.Commands.Subscription;
using Azure.Mcp.Tools.EventGrid.Commands.Topic;
namespace Azure.Mcp.Tools.EventGrid.Commands;
+[JsonSerializable(typeof(SubscriptionListCommand.SubscriptionListCommandResult))]
[JsonSerializable(typeof(TopicListCommand.TopicListCommandResult))]
+[JsonSerializable(typeof(EventGridSubscriptionInfo))]
[JsonSerializable(typeof(EventGridTopicInfo))]
-[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
internal sealed partial class EventGridJsonContext : JsonSerializerContext
{
}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs
new file mode 100644
index 0000000000..f348a6109c
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs
@@ -0,0 +1,176 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine.Parsing;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Core.Models.Option;
+using Azure.Mcp.Core.Options;
+using Azure.Mcp.Tools.EventGrid.Models;
+using Azure.Mcp.Tools.EventGrid.Options;
+using Azure.Mcp.Tools.EventGrid.Options.Subscription;
+using Azure.Mcp.Tools.EventGrid.Services;
+
+namespace Azure.Mcp.Tools.EventGrid.Commands.Subscription;
+
+public sealed class SubscriptionListCommand(ILogger logger) : BaseEventGridCommand
+{
+ private const string CommandTitle = "List Event Grid Subscriptions";
+ private readonly ILogger _logger = logger;
+
+ public override string Name => "list";
+
+ public override string Description =>
+ """
+ List event subscriptions for topics with filtering and endpoint configuration. This tool shows all active
+ subscriptions including webhook endpoints, event filters, and delivery retry policies. Returns subscription
+ details as JSON array. Requires either --topic (bare topic name) OR --subscription. If only --topic is provided
+ the tool searches all accessible subscriptions for a topic with that name. Optional --resource-group/--location
+ may only be used alongside --subscription or --topic.
+ """;
+
+ public override string Title => CommandTitle;
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = true,
+ ReadOnly = true,
+ LocalRequired = false,
+ Secret = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(OptionDefinitions.Common.ResourceGroup);
+ command.Options.Add(EventGridOptionDefinitions.TopicName);
+ command.Options.Add(EventGridOptionDefinitions.Location);
+ }
+
+ protected override SubscriptionListOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name);
+ options.TopicName = parseResult.GetValueOrDefault(EventGridOptionDefinitions.TopicName.Name);
+ options.Location = parseResult.GetValueOrDefault(EventGridOptionDefinitions.Location.Name);
+ return options;
+ }
+
+ public override ValidationResult Validate(CommandResult commandResult, CommandResponse? commandResponse = null)
+ {
+ // Skip the base validation that requires subscription and implement custom validation
+ var result = new ValidationResult { IsValid = true };
+
+ var hasSubscription = HasSubscriptionAvailable(commandResult);
+ var hasTopicOption = commandResult.HasOptionResult(EventGridOptionDefinitions.TopicName);
+ var hasRg = commandResult.HasOptionResult(OptionDefinitions.Common.ResourceGroup);
+ var hasLocation = commandResult.HasOptionResult(EventGridOptionDefinitions.Location);
+
+ // Either topic or subscription is mandatory
+ if (!hasSubscription && !hasTopicOption)
+ {
+ result.IsValid = false;
+ result.ErrorMessage = "Either --subscription or --topic is required.";
+
+ if (commandResponse != null)
+ {
+ commandResponse.Status = 400;
+ commandResponse.Message = result.ErrorMessage;
+ }
+ }
+ // Location and resource-group can only be used with subscription or topic
+ else if ((hasRg || hasLocation) && !hasSubscription && !hasTopicOption)
+ {
+ result.IsValid = false;
+ result.ErrorMessage = "Either --subscription or --topic is required when using --resource-group or --location.";
+
+ if (commandResponse != null)
+ {
+ commandResponse.Status = 400;
+ commandResponse.Message = result.ErrorMessage;
+ }
+ }
+
+ return result;
+ }
+
+ public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult)
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ var options = BindOptions(parseResult);
+
+ var hasSubscription = !string.IsNullOrWhiteSpace(options.Subscription);
+ var hasTopic = !string.IsNullOrWhiteSpace(options.TopicName);
+
+ // Bare topic name without subscription triggers cross-subscription search
+ bool crossSubscriptionSearch = !hasSubscription && hasTopic;
+
+ try
+ {
+ var eventGridService = context.GetService();
+
+ if (crossSubscriptionSearch)
+ {
+ // Iterate all subscriptions and aggregate
+ var subscriptionService = context.GetService();
+ var allSubs = await subscriptionService.GetSubscriptions(null, options.RetryPolicy);
+ var aggregate = new List();
+ foreach (var sub in allSubs)
+ {
+ try
+ {
+ var found = await eventGridService.GetSubscriptionsAsync(
+ sub.SubscriptionId,
+ options.ResourceGroup,
+ options.TopicName, // bare name
+ options.Location,
+ options.Tenant,
+ options.RetryPolicy);
+ if (found?.Count > 0)
+ {
+ aggregate.AddRange(found);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed searching topic '{Topic}' in subscription '{Sub}'. Continuing.", options.TopicName, sub.SubscriptionId);
+ continue;
+ }
+ }
+ context.Response.Results = ResponseResult.Create(
+ new SubscriptionListCommandResult(aggregate),
+ EventGridJsonContext.Default.SubscriptionListCommandResult);
+ }
+ else
+ {
+ var subscriptions = await eventGridService.GetSubscriptionsAsync(
+ options.Subscription!,
+ options.ResourceGroup,
+ options.TopicName,
+ options.Location,
+ options.Tenant,
+ options.RetryPolicy);
+
+ context.Response.Results = ResponseResult.Create(
+ new SubscriptionListCommandResult(subscriptions ?? []),
+ EventGridJsonContext.Default.SubscriptionListCommandResult);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex,
+ "Error listing Event Grid subscriptions. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, TopicName: {TopicName}, Location: {Location}, Options: {@Options}",
+ options.Subscription, options.ResourceGroup, options.TopicName, options.Location, options);
+ HandleException(context, ex);
+ }
+
+ return context.Response;
+ }
+
+ internal record SubscriptionListCommandResult(List Subscriptions);
+}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs
index 0bbf7a0da2..5c31a26fa7 100644
--- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs
@@ -3,6 +3,7 @@
using Azure.Mcp.Core.Extensions;
using Azure.Mcp.Core.Models.Option;
+using Azure.Mcp.Tools.EventGrid.Models;
using Azure.Mcp.Tools.EventGrid.Options.Topic;
using Azure.Mcp.Tools.EventGrid.Services;
@@ -37,7 +38,7 @@ Returns topic information as JSON array. Requires subscription.
protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
- command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsOptional());
+ command.Options.Add(OptionDefinitions.Common.ResourceGroup);
}
protected override TopicListOptions BindOptions(ParseResult parseResult)
@@ -62,9 +63,12 @@ public override async Task ExecuteAsync(CommandContext context,
var topics = await eventGridService.GetTopicsAsync(
options.Subscription!,
options.ResourceGroup,
+ options.Tenant,
options.RetryPolicy);
- context.Response.Results = ResponseResult.Create(new(topics ?? []), EventGridJsonContext.Default.TopicListCommandResult);
+ context.Response.Results = ResponseResult.Create(
+ new TopicListCommandResult(topics ?? []),
+ EventGridJsonContext.Default.TopicListCommandResult);
}
catch (Exception ex)
{
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs b/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs
index 76281e760e..4e1e985cad 100644
--- a/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using Azure.Mcp.Core.Areas;
+using Azure.Mcp.Tools.EventGrid.Commands.Subscription;
using Azure.Mcp.Tools.EventGrid.Commands.Topic;
using Azure.Mcp.Tools.EventGrid.Services;
using Microsoft.Extensions.DependencyInjection;
@@ -27,7 +28,14 @@ public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactor
var topics = new CommandGroup("topic", "Event Grid topic operations - Commands for managing Event Grid topics and their configurations.");
eventGrid.AddSubGroup(topics);
+ // Subscriptions subgroup
+ var subscriptions = new CommandGroup("subscription", "Event Grid subscription operations - Commands for managing event subscriptions with filtering and endpoint configuration.");
+ eventGrid.AddSubGroup(subscriptions);
+
// Register Topic commands
topics.AddCommand("list", new TopicListCommand(loggerFactory.CreateLogger()));
+
+ // Register Subscription commands
+ subscriptions.AddCommand("list", new SubscriptionListCommand(loggerFactory.CreateLogger()));
}
}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Models/EventGridSubscriptionInfo.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Models/EventGridSubscriptionInfo.cs
new file mode 100644
index 0000000000..7881da1f45
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/Models/EventGridSubscriptionInfo.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+
+namespace Azure.Mcp.Tools.EventGrid.Models;
+
+public record EventGridSubscriptionInfo(
+ [property: JsonPropertyName("name")] string Name,
+ [property: JsonPropertyName("type")] string Type,
+ [property: JsonPropertyName("endpointType")] string? EndpointType,
+ [property: JsonPropertyName("endpointUrl")] string? EndpointUrl,
+ [property: JsonPropertyName("provisioningState")] string? ProvisioningState,
+ [property: JsonPropertyName("deadLetterDestination")] string? DeadLetterDestination,
+ [property: JsonPropertyName("filter")] string? Filter,
+ [property: JsonPropertyName("maxDeliveryAttempts")] int? MaxDeliveryAttempts,
+ [property: JsonPropertyName("eventTimeToLiveInMinutes")] int? EventTimeToLiveInMinutes,
+ [property: JsonPropertyName("createdDateTime")] string? CreatedDateTime,
+ [property: JsonPropertyName("updatedDateTime")] string? UpdatedDateTime
+);
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs
index a94d03a7a7..c51bc62339 100644
--- a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs
@@ -5,4 +5,20 @@ namespace Azure.Mcp.Tools.EventGrid.Options;
public static class EventGridOptionDefinitions
{
+ public const string TopicNameParam = "topic";
+ public const string LocationParam = "location";
+
+ public static readonly Option TopicName = new(
+ $"--{TopicNameParam}"
+ )
+ {
+ Description = "The name of the Event Grid topic."
+ };
+
+ public static readonly Option Location = new(
+ $"--{LocationParam}"
+ )
+ {
+ Description = "The Azure region to filter resources by (e.g., 'eastus', 'westus2')."
+ };
}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Options/Subscription/SubscriptionListOptions.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Options/Subscription/SubscriptionListOptions.cs
new file mode 100644
index 0000000000..6041fef566
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/Subscription/SubscriptionListOptions.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.EventGrid.Options.Subscription;
+
+public class SubscriptionListOptions : BaseEventGridOptions
+{
+ public string? TopicName { get; set; }
+ public string? Location { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs
index ffbfccb602..9d83eebd89 100644
--- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs
@@ -1,21 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.Diagnostics.CodeAnalysis;
+using Azure.Mcp.Core.Options;
+using Azure.ResourceManager;
using Azure.ResourceManager.EventGrid;
+using Azure.ResourceManager.EventGrid.Models;
+using Azure.ResourceManager.Resources;
namespace Azure.Mcp.Tools.EventGrid.Services;
-public class EventGridService(ISubscriptionService subscriptionService, ITenantService tenantService)
+public class EventGridService(ISubscriptionService subscriptionService, ITenantService tenantService, ILogger logger)
: BaseAzureService(tenantService), IEventGridService
{
private readonly ISubscriptionService _subscriptionService = subscriptionService ?? throw new ArgumentNullException(nameof(subscriptionService));
+ private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
public async Task> GetTopicsAsync(
string subscription,
string? resourceGroup = null,
+ string? tenant = null,
RetryPolicyOptions? retryPolicy = null)
{
- var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy);
+ var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy);
var topics = new List();
if (!string.IsNullOrEmpty(resourceGroup))
@@ -41,6 +48,222 @@ public async Task> GetTopicsAsync(
return topics;
}
+ public async Task> GetSubscriptionsAsync(
+ string subscription,
+ string? resourceGroup = null,
+ string? topicName = null,
+ string? location = null,
+ string? tenant = null,
+ RetryPolicyOptions? retryPolicy = null)
+ {
+ var subscriptions = new List();
+
+ try
+ {
+ var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy);
+
+ // If specific topic is requested, get subscriptions for that topic only
+ if (!string.IsNullOrEmpty(topicName))
+ {
+ await GetSubscriptionsForSpecificTopic(subscriptionResource, resourceGroup, topicName, location, subscriptions);
+ }
+ else
+ {
+ // Get subscriptions from all topics in the subscription or resource group
+ await GetSubscriptionsFromAllTopics(subscriptionResource, resourceGroup, location, subscriptions);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log the actual error instead of swallowing it
+ throw new InvalidOperationException($"Failed to retrieve EventGrid subscriptions: {ex.Message}", ex);
+ }
+
+ return subscriptions;
+ }
+
+ private async Task GetSubscriptionsForSpecificTopic(
+ SubscriptionResource subscriptionResource,
+ string? resourceGroup,
+ string topicName,
+ string? location,
+ List subscriptions)
+ {
+ try
+ {
+ // Find the specific custom topic first
+ var topic = await FindTopic(subscriptionResource, resourceGroup, topicName);
+ if (topic != null)
+ {
+ await AddSubscriptionsFromTopic(topic.Data.Location.ToString(), location, subscriptions, topic.GetTopicEventSubscriptions().GetAllAsync());
+ return; // Found custom topic, no need to check system topics
+ }
+
+ // If not found in custom topics, check system topics
+ var systemTopic = await FindSystemTopic(subscriptionResource, resourceGroup, topicName);
+ if (systemTopic != null)
+ {
+ await AddSubscriptionsFromSystemTopic(systemTopic.Data.Location.ToString(), location, subscriptions, systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync());
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log and re-throw to preserve error information
+ throw new InvalidOperationException($"Failed to get subscriptions for topic '{topicName}': {ex.Message}", ex);
+ }
+ }
+
+ private async Task GetSubscriptionsFromAllTopics(
+ SubscriptionResource subscriptionResource,
+ string? resourceGroup,
+ string? location,
+ List subscriptions)
+ {
+ try
+ {
+ if (!string.IsNullOrEmpty(resourceGroup))
+ {
+ // Get topics from specific resource group and their subscriptions
+ var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup);
+
+ // Check custom topics
+ await foreach (var topic in resourceGroupResource.Value.GetEventGridTopics().GetAllAsync())
+ {
+ try
+ {
+ await AddSubscriptionsFromTopic(topic.Data.Location.ToString(), location, subscriptions, topic.GetTopicEventSubscriptions().GetAllAsync());
+ }
+ catch (Exception ex)
+ {
+ // Continue with other topics if one fails - individual topic access errors
+ // shouldn't block the entire operation since we're aggregating from multiple topics
+ _logger.LogWarning(ex, "Failed to get subscriptions for topic '{TopicName}'. Continuing with other topics.", topic.Data.Name);
+ continue;
+ }
+ } // Also check system topics in the resource group
+ await foreach (var systemTopic in resourceGroupResource.Value.GetSystemTopics().GetAllAsync())
+ {
+ try
+ {
+ await AddSubscriptionsFromSystemTopic(systemTopic.Data.Location.ToString(), location, subscriptions, systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync());
+ }
+ catch (Exception ex)
+ {
+ // Continue with other system topics if one fails - individual system topic access errors
+ // shouldn't block the entire operation since we're aggregating from multiple topics
+ _logger.LogWarning(ex, "Failed to get subscriptions for system topic '{SystemTopicName}'. Continuing with other topics.", systemTopic.Data.Name);
+ continue;
+ }
+ }
+ }
+ else
+ {
+ // Get topics from all resource groups and their subscriptions
+ await foreach (var topic in subscriptionResource.GetEventGridTopicsAsync())
+ {
+ try
+ {
+ await AddSubscriptionsFromTopic(topic.Data.Location.ToString(), location, subscriptions, topic.GetTopicEventSubscriptions().GetAllAsync());
+ }
+ catch (Exception ex)
+ {
+ // Continue with other topics if one fails - individual topic access errors
+ // shouldn't block the entire operation since we're aggregating from multiple topics
+ _logger.LogWarning(ex, "Failed to get subscriptions for topic '{TopicName}'. Continuing with other topics.", topic.Data.Name);
+ continue;
+ }
+ }
+
+ // Also check system topics across all resource groups
+ await foreach (var systemTopic in subscriptionResource.GetSystemTopicsAsync())
+ {
+ try
+ {
+ await AddSubscriptionsFromSystemTopic(systemTopic.Data.Location.ToString(), location, subscriptions, systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync());
+ }
+ catch (Exception ex)
+ {
+ // Continue with other system topics if one fails - individual system topic access errors
+ // shouldn't block the entire operation since we're aggregating from multiple topics
+ _logger.LogWarning(ex, "Failed to get subscriptions for system topic '{SystemTopicName}'. Continuing with other topics.", systemTopic.Data.Name);
+ continue;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log and re-throw to preserve error information
+ throw new InvalidOperationException($"Failed to get subscriptions from all topics in resource group '{resourceGroup}': {ex.Message}", ex);
+ }
+ }
+
+ private async Task FindTopic(
+ SubscriptionResource subscriptionResource,
+ string? resourceGroup,
+ string topicName)
+ {
+ if (!string.IsNullOrEmpty(resourceGroup))
+ {
+ // Search in specific resource group
+ var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup);
+
+ await foreach (var topic in resourceGroupResource.Value.GetEventGridTopics().GetAllAsync())
+ {
+ if (topic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase))
+ {
+ return topic;
+ }
+ }
+ }
+ else
+ {
+ // Search in all resource groups
+ await foreach (var topic in subscriptionResource.GetEventGridTopicsAsync())
+ {
+ if (topic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase))
+ {
+ return topic;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private async Task FindSystemTopic(
+ SubscriptionResource subscriptionResource,
+ string? resourceGroup,
+ string topicName)
+ {
+ if (!string.IsNullOrEmpty(resourceGroup))
+ {
+ // Search in specific resource group
+ var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup);
+
+ await foreach (var systemTopic in resourceGroupResource.Value.GetSystemTopics().GetAllAsync())
+ {
+ if (systemTopic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase))
+ {
+ return systemTopic;
+ }
+ }
+ }
+ else
+ {
+ // Search in all resource groups
+ await foreach (var systemTopic in subscriptionResource.GetSystemTopicsAsync())
+ {
+ if (systemTopic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase))
+ {
+ return systemTopic;
+ }
+ }
+ }
+
+ return null;
+ }
+
private static EventGridTopicInfo CreateTopicInfo(EventGridTopicData topicData)
{
return new EventGridTopicInfo(
@@ -51,4 +274,95 @@ private static EventGridTopicInfo CreateTopicInfo(EventGridTopicData topicData)
PublicNetworkAccess: topicData.PublicNetworkAccess?.ToString(),
InputSchema: topicData.InputSchema?.ToString());
}
+
+ private static EventGridSubscriptionInfo CreateSubscriptionInfo(EventGridSubscriptionData subscriptionData)
+ {
+ string? endpointType = null;
+ string? endpointUrl = null;
+
+ // Extract endpoint information based on destination type
+ if (subscriptionData.Destination != null)
+ {
+ // Extract both endpoint type and URL using type-safe pattern matching
+ (endpointType, endpointUrl) = subscriptionData.Destination switch
+ {
+ WebHookEventSubscriptionDestination webhook => ("WebHook", webhook.Endpoint?.ToString()),
+ AzureFunctionEventSubscriptionDestination azureFunction => ("AzureFunction", azureFunction.ResourceId?.ToString()),
+ EventHubEventSubscriptionDestination eventHub => ("EventHub", eventHub.ResourceId?.ToString()),
+ HybridConnectionEventSubscriptionDestination hybridConnection => ("HybridConnection", hybridConnection.ResourceId?.ToString()),
+ NamespaceTopicEventSubscriptionDestination namespaceTopic => ("NamespaceTopic", namespaceTopic.ResourceId?.ToString()),
+ PartnerEventSubscriptionDestination partner => ("Partner", partner.ResourceId),
+ ServiceBusQueueEventSubscriptionDestination serviceBusQueue => ("ServiceBusQueue", serviceBusQueue.ResourceId?.ToString()),
+ ServiceBusTopicEventSubscriptionDestination serviceBusTopic => ("ServiceBusTopic", serviceBusTopic.ResourceId?.ToString()),
+ StorageQueueEventSubscriptionDestination storageQueue => ("StorageQueue", storageQueue.ResourceId?.ToString()),
+ MonitorAlertEventSubscriptionDestination => ("MonitorAlert", null), // No endpoint property
+ _ => (subscriptionData.Destination.GetType().Name, null) // Unknown or future destination types - fallback to full type name
+ };
+ }
+
+ // Extract filter information
+ string? filterInfo = null;
+ if (subscriptionData.Filter != null)
+ {
+ var filterDetails = new List();
+
+ if (subscriptionData.Filter.SubjectBeginsWith != null)
+ filterDetails.Add($"SubjectBeginsWith: {subscriptionData.Filter.SubjectBeginsWith}");
+
+ if (subscriptionData.Filter.SubjectEndsWith != null)
+ filterDetails.Add($"SubjectEndsWith: {subscriptionData.Filter.SubjectEndsWith}");
+
+ if (subscriptionData.Filter.IncludedEventTypes?.Any() == true)
+ filterDetails.Add($"EventTypes: {string.Join(", ", subscriptionData.Filter.IncludedEventTypes)}");
+
+ if (subscriptionData.Filter.IsSubjectCaseSensitive.HasValue)
+ filterDetails.Add($"CaseSensitive: {subscriptionData.Filter.IsSubjectCaseSensitive}");
+
+ filterInfo = filterDetails.Any() ? string.Join("; ", filterDetails) : null;
+ }
+
+ return new EventGridSubscriptionInfo(
+ Name: subscriptionData.Name,
+ Type: subscriptionData.ResourceType.ToString(),
+ EndpointType: endpointType,
+ EndpointUrl: endpointUrl,
+ ProvisioningState: subscriptionData.ProvisioningState?.ToString(),
+ DeadLetterDestination: subscriptionData.DeadLetterDestination?.ToString(),
+ Filter: filterInfo,
+ MaxDeliveryAttempts: subscriptionData.RetryPolicy?.MaxDeliveryAttempts,
+ EventTimeToLiveInMinutes: subscriptionData.RetryPolicy?.EventTimeToLiveInMinutes,
+ CreatedDateTime: subscriptionData.SystemData?.CreatedOn?.ToString("yyyy-MM-ddTHH:mm:ssZ"),
+ UpdatedDateTime: subscriptionData.SystemData?.LastModifiedOn?.ToString("yyyy-MM-ddTHH:mm:ssZ")
+ );
+ }
+
+ private async Task AddSubscriptionsFromTopic(
+ string topicLocation,
+ string? locationFilter,
+ List subscriptions,
+ IAsyncEnumerable subscriptionCollection)
+ {
+ if (string.IsNullOrEmpty(locationFilter) || string.Equals(topicLocation, locationFilter, StringComparison.OrdinalIgnoreCase))
+ {
+ await foreach (var subscription in subscriptionCollection)
+ {
+ subscriptions.Add(CreateSubscriptionInfo(subscription.Data));
+ }
+ }
+ }
+
+ private async Task AddSubscriptionsFromSystemTopic(
+ string topicLocation,
+ string? locationFilter,
+ List subscriptions,
+ IAsyncEnumerable subscriptionCollection)
+ {
+ if (string.IsNullOrEmpty(locationFilter) || string.Equals(topicLocation, locationFilter, StringComparison.OrdinalIgnoreCase))
+ {
+ await foreach (var subscription in subscriptionCollection)
+ {
+ subscriptions.Add(CreateSubscriptionInfo(subscription.Data));
+ }
+ }
+ }
}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs
index 3fe6b2bc63..f956d66a05 100644
--- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs
+++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using Azure.Mcp.Tools.EventGrid.Models;
+
namespace Azure.Mcp.Tools.EventGrid.Services;
public interface IEventGridService
@@ -8,5 +10,14 @@ public interface IEventGridService
Task> GetTopicsAsync(
string subscription,
string? resourceGroup = null,
+ string? tenant = null,
+ RetryPolicyOptions? retryPolicy = null);
+
+ Task> GetSubscriptionsAsync(
+ string subscription,
+ string? resourceGroup = null,
+ string? topicName = null,
+ string? location = null,
+ string? tenant = null,
RetryPolicyOptions? retryPolicy = null);
}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.LiveTests/EventGridCommandTests.cs b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.LiveTests/EventGridCommandTests.cs
index 20151c95f8..99249b9289 100644
--- a/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.LiveTests/EventGridCommandTests.cs
+++ b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.LiveTests/EventGridCommandTests.cs
@@ -43,4 +43,35 @@ public async Task Should_list_eventgrid_topics_by_subscription_and_resource_grou
Assert.Equal(JsonValueKind.Array, topics.ValueKind);
// Note: topics array might be empty if no Event Grid topics exist in the resource group
}
+
+ [Fact]
+ public async Task Should_list_eventgrid_subscriptions_by_subscription()
+ {
+ var result = await CallToolAsync(
+ "azmcp_eventgrid_subscription_list",
+ new()
+ {
+ { "subscription", Settings.SubscriptionId }
+ });
+
+ var subscriptions = result.AssertProperty("subscriptions");
+ Assert.Equal(JsonValueKind.Array, subscriptions.ValueKind);
+ // Note: subscriptions array might be empty if no Event Grid subscriptions exist in the subscription
+ }
+
+ [Fact]
+ public async Task Should_list_eventgrid_subscriptions_by_subscription_and_resource_group()
+ {
+ var result = await CallToolAsync(
+ "azmcp_eventgrid_subscription_list",
+ new()
+ {
+ { "subscription", Settings.SubscriptionId },
+ { "resource-group", Settings.ResourceGroupName }
+ });
+
+ var subscriptions = result.AssertProperty("subscriptions");
+ Assert.Equal(JsonValueKind.Array, subscriptions.ValueKind);
+ // Note: subscriptions array might be empty if no Event Grid subscriptions exist in the resource group
+ }
}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Subscription/SubscriptionListCommandTests.cs b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Subscription/SubscriptionListCommandTests.cs
new file mode 100644
index 0000000000..240d575327
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Subscription/SubscriptionListCommandTests.cs
@@ -0,0 +1,253 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Azure.Core;
+using Azure.Mcp.Core.Models.Command;
+using Azure.Mcp.Core.Options;
+using Azure.Mcp.Tools.EventGrid.Commands.Subscription;
+using Azure.Mcp.Tools.EventGrid.Services;
+using Azure.ResourceManager.Models;
+using Azure.ResourceManager.Resources;
+using Azure.ResourceManager.Resources.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+using NSubstitute.ExceptionExtensions;
+using Xunit;
+
+namespace Azure.Mcp.Tools.EventGrid.UnitTests.Subscription;
+
+[Trait("Area", "EventGrid")]
+public class SubscriptionListCommandTests
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IEventGridService _eventGridService;
+ private readonly Azure.Mcp.Core.Services.Azure.Subscription.ISubscriptionService _subscriptionService;
+ private readonly ILogger _logger;
+ private readonly SubscriptionListCommand _command;
+ private readonly CommandContext _context;
+ private readonly Command _commandDefinition;
+
+ public SubscriptionListCommandTests()
+ {
+ _eventGridService = Substitute.For();
+ _subscriptionService = Substitute.For();
+ _logger = Substitute.For>();
+
+ var collection = new ServiceCollection()
+ .AddSingleton(_eventGridService)
+ .AddSingleton(_subscriptionService);
+ _serviceProvider = collection.BuildServiceProvider();
+ _command = new(_logger);
+ _context = new(_serviceProvider);
+ _commandDefinition = _command.GetCommand();
+ }
+
+ [Fact]
+ public void Constructor_InitializesCommandCorrectly()
+ {
+ var command = _command.GetCommand();
+ Assert.Equal("list", command.Name);
+ Assert.NotNull(command.Description);
+ Assert.NotEmpty(command.Description);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_NoParameters_ReturnsSubscriptions()
+ {
+ // Arrange
+ var subscription = "sub123";
+ var expectedSubscriptions = new List
+ {
+ new("subscription1", "Microsoft.EventGrid/eventSubscriptions", "WebHook", "https://example.com/webhook1", "Succeeded", null, null, 30, 1440, "2023-01-01T00:00:00Z", "2023-01-02T00:00:00Z"),
+ new("subscription2", "Microsoft.EventGrid/eventSubscriptions", "StorageQueue", "https://storage.queue.core.windows.net/myqueue", "Succeeded", null, null, 10, 720, "2023-01-03T00:00:00Z", "2023-01-04T00:00:00Z")
+ };
+
+ _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(expectedSubscriptions));
+
+ var args = _commandDefinition.Parse(["--subscription", subscription]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Results);
+
+ var json = JsonSerializer.Serialize(response.Results);
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ var result = JsonSerializer.Deserialize(json, options);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result!.Subscriptions);
+ Assert.Equal(expectedSubscriptions.Count, result.Subscriptions!.Count);
+ Assert.Equal(expectedSubscriptions.Select(s => s.Name), result.Subscriptions.Select(s => s.Name));
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithTopicNameFilter_FiltersCorrectly()
+ {
+ // Arrange
+ var subscription = "sub123";
+ var resourceGroup = "test-rg";
+ var topicName = "test-topic";
+ var expectedSubscriptions = new List
+ {
+ new("filtered-subscription", "Microsoft.EventGrid/eventSubscriptions", "WebHook", "https://example.com/webhook", "Succeeded", null, null, 30, 1440, "2023-01-01T00:00:00Z", "2023-01-02T00:00:00Z")
+ };
+
+ _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Is(resourceGroup), Arg.Is(topicName), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(expectedSubscriptions));
+
+ var args = _commandDefinition.Parse(["--subscription", subscription, "--resource-group", resourceGroup, "--topic", topicName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Results);
+
+ var json = JsonSerializer.Serialize(response.Results);
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ var result = JsonSerializer.Deserialize(json, options);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result!.Subscriptions);
+ Assert.Single(result.Subscriptions);
+ Assert.Equal("filtered-subscription", result.Subscriptions.First().Name);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ReturnsNull_WhenNoSubscriptions()
+ {
+ // Arrange
+ var subscription = "sub123";
+
+ _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(new List()));
+
+ var args = _commandDefinition.Parse(["--subscription", subscription]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Results);
+
+ var json = JsonSerializer.Serialize(response.Results);
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ var result = JsonSerializer.Deserialize(json, options);
+ Assert.NotNull(result);
+ Assert.NotNull(result.Subscriptions);
+ Assert.Empty(result.Subscriptions);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithLocationFilter_FiltersCorrectly()
+ {
+ // Arrange
+ var subscription = "sub123";
+ var location = "eastus";
+ var expectedSubscriptions = new List
+ {
+ new("location-filtered-subscription", "Microsoft.EventGrid/eventSubscriptions", "WebHook", "https://example.com/webhook", "Succeeded", null, null, 30, 1440, "2023-01-01T00:00:00Z", "2023-01-02T00:00:00Z")
+ };
+
+ _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Is(location), Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(expectedSubscriptions));
+
+ var args = _commandDefinition.Parse(["--subscription", subscription, "--location", location]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotNull(response.Results);
+
+ var json = JsonSerializer.Serialize(response.Results);
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+ var result = JsonSerializer.Deserialize(json, options);
+
+ Assert.NotNull(result);
+ Assert.NotNull(result!.Subscriptions);
+ Assert.Single(result.Subscriptions);
+ Assert.Equal("location-filtered-subscription", result.Subscriptions.First().Name);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_HandlesServiceErrors()
+ {
+ // Arrange
+ _eventGridService.GetSubscriptionsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Task.FromException>(new Exception("Test error")));
+
+ var parseResult = _commandDefinition.Parse(["--subscription", "sub"]);
+
+ // 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);
+ }
+
+
+
+
+ [Theory]
+ [InlineData("--subscription sub", true)]
+ [InlineData("--subscription sub --topic my-topic", true)]
+ [InlineData("--subscription sub --resource-group rg", true)]
+ [InlineData("--topic my-topic", true)] // Cross-subscription search - valid with topic alone
+ [InlineData("", false)]
+ [InlineData("--location eastus", false)]
+ [InlineData("--resource-group rg", false)]
+ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed)
+ {
+ // Arrange
+ if (shouldSucceed)
+ {
+ _eventGridService.GetSubscriptionsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(new List
+ {
+ new("subscription1", "Microsoft.EventGrid/eventSubscriptions", "WebHook", "https://example.com/webhook1", "Succeeded", null, null, 30, 1440, "2023-01-01T00:00:00Z", "2023-01-02T00:00:00Z")
+ });
+
+ // Set up subscription service for cross-subscription search scenario
+ _subscriptionService.GetSubscriptions(Arg.Any(), Arg.Any())
+ .Returns(new List());
+ }
+
+ var parseResult = _commandDefinition.Parse(args);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, parseResult);
+
+ // Assert
+ Assert.Equal(shouldSucceed ? 200 : 400, response.Status);
+ if (shouldSucceed)
+ {
+ Assert.NotNull(response.Results);
+ }
+ else
+ {
+ Assert.Contains("required", response.Message.ToLower());
+ }
+ }
+
+
+
+ private class SubscriptionListResult
+ {
+ [JsonPropertyName("subscriptions")]
+ public List? Subscriptions { get; set; }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Topic/TopicListCommandTests.cs b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Topic/TopicListCommandTests.cs
index 1a8505678c..47bd2ebaa0 100644
--- a/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Topic/TopicListCommandTests.cs
+++ b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Topic/TopicListCommandTests.cs
@@ -50,8 +50,8 @@ public async Task ExecuteAsync_NoParameters_ReturnsTopics()
new("topic2", "westus", "https://topic2.westus.eventgrid.azure.net/api/events", "Succeeded", "Enabled", "EventGridSchema")
};
- _eventGridService.GetTopicsAsync(Arg.Is(subscriptionId), Arg.Any(), Arg.Any())
- .Returns(expectedTopics);
+ _eventGridService.GetTopicsAsync(Arg.Is(subscriptionId), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(expectedTopics));
var args = _commandDefinition.Parse(["--subscription", subscriptionId]);
@@ -77,8 +77,8 @@ public async Task ExecuteAsync_ReturnsEmpty_WhenNoTopics()
// Arrange
var subscriptionId = "sub123";
- _eventGridService.GetTopicsAsync(Arg.Is(subscriptionId), Arg.Any(), Arg.Any())
- .Returns([]);
+ _eventGridService.GetTopicsAsync(Arg.Is(subscriptionId), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(new List()));
var args = _commandDefinition.Parse(["--subscription", subscriptionId]);
@@ -91,8 +91,8 @@ public async Task ExecuteAsync_ReturnsEmpty_WhenNoTopics()
var json = JsonSerializer.Serialize(response.Results);
var result = JsonSerializer.Deserialize(json, EventGridJsonContext.Default.TopicListCommandResult);
-
Assert.NotNull(result);
+ Assert.NotNull(result!.Topics);
Assert.Empty(result.Topics);
}
@@ -103,7 +103,7 @@ public async Task ExecuteAsync_HandlesException()
var expectedError = "Test error";
var subscriptionId = "sub123";
- _eventGridService.GetTopicsAsync(Arg.Is(subscriptionId), null, Arg.Any())
+ _eventGridService.GetTopicsAsync(Arg.Is(subscriptionId), null, Arg.Any(), Arg.Any())
.ThrowsAsync(new Exception(expectedError));
var args = _commandDefinition.Parse(["--subscription", subscriptionId]);
@@ -133,8 +133,8 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS
new("topic1", "eastus", "https://topic1.eastus.eventgrid.azure.net/api/events", "Succeeded", "Enabled", "EventGridSchema"),
new("topic2", "westus", "https://topic2.westus.eventgrid.azure.net/api/events", "Succeeded", "Enabled", "EventGridSchema")
};
- _eventGridService.GetTopicsAsync(Arg.Any(), Arg.Any(), Arg.Any())
- .Returns(expectedTopics);
+ _eventGridService.GetTopicsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(Task.FromResult(expectedTopics));
}
var parseResult = _commandDefinition.Parse(args.Split(' ', StringSplitOptions.RemoveEmptyEntries));
diff --git a/tools/Azure.Mcp.Tools.Storage/tests/Azure.Mcp.Tools.Storage.UnitTests/Account/AccountGetCommandTests.cs b/tools/Azure.Mcp.Tools.Storage/tests/Azure.Mcp.Tools.Storage.UnitTests/Account/AccountGetCommandTests.cs
index 0c7d43de19..070defce44 100644
--- a/tools/Azure.Mcp.Tools.Storage/tests/Azure.Mcp.Tools.Storage.UnitTests/Account/AccountGetCommandTests.cs
+++ b/tools/Azure.Mcp.Tools.Storage/tests/Azure.Mcp.Tools.Storage.UnitTests/Account/AccountGetCommandTests.cs
@@ -3,6 +3,8 @@
using System.CommandLine;
using System.Text.Json;
+using System.Text.Json.Serialization;
+using Azure.Mcp.Core.Helpers;
using Azure.Mcp.Core.Models.Command;
using Azure.Mcp.Core.Options;
using Azure.Mcp.Tools.Storage.Commands.Account;
@@ -144,14 +146,14 @@ public void Constructor_InitializesCommandCorrectly()
public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed)
{
// Arrange
- var originalSubscriptionEnv = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID");
+ var originalSubscriptionEnv = EnvironmentHelpers.GetAzureSubscriptionId();
try
{
// Clear environment variable for failing test cases to ensure proper validation
if (!shouldSucceed && !args.Contains("--subscription"))
{
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", null);
+ EnvironmentHelpers.SetAzureSubscriptionId(null);
}
if (shouldSucceed)
@@ -185,7 +187,7 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS
finally
{
// Restore original environment variable
- Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", originalSubscriptionEnv);
+ EnvironmentHelpers.SetAzureSubscriptionId(originalSubscriptionEnv);
}
}