From af45e25bc3388778a0fcf6753ac4801acebdd82a Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 11:39:15 -0700 Subject: [PATCH 01/18] Resolved merge conflict- 1 --- .vscode/mcp.json | 48 ++++++ .../src/Commands/EventGridJsonContext.cs | 3 + .../Subscription/SubscriptionListCommand.cs | 82 +++++++++ .../src/EventGridSetup.cs | 7 + .../src/Models/EventGridSubscriptionInfo.cs | 18 ++ .../src/Options/EventGridOptionDefinitions.cs | 11 ++ .../Subscription/SubscriptionListOptions.cs | 9 + .../src/Services/EventGridService.cs | 28 ++++ .../src/Services/IEventGridService.cs | 6 + .../EventGridCommandTests.cs | 31 ++++ .../SubscriptionListCommandTests.cs | 155 ++++++++++++++++++ 11 files changed, 398 insertions(+) create mode 100644 .vscode/mcp.json create mode 100644 tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs create mode 100644 tools/Azure.Mcp.Tools.EventGrid/src/Models/EventGridSubscriptionInfo.cs create mode 100644 tools/Azure.Mcp.Tools.EventGrid/src/Options/Subscription/SubscriptionListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Subscription/SubscriptionListCommandTests.cs diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000000..6f0a9a2c45 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,48 @@ +{ + "servers": { + "Azure MCP Server - Local Development": { + "command": "dotnet", + "args": [ + "run", + "--project", + "servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj", + "--", + "server", + "start" + ], + "env": { + "AZURE_MCP_LOG_LEVEL": "Debug", + "AZURE_MCP_COLLECT_TELEMETRY": "false" + } + }, + "Azure MCP Server - Release Build": { + "command": "dotnet", + "args": [ + "servers/Azure.Mcp.Server/src/bin/Release/net9.0/azmcp.dll", + "server", + "start" + ], + "env": { + "AZURE_MCP_LOG_LEVEL": "Debug", + "AZURE_MCP_COLLECT_TELEMETRY": "false" + } + }, + "Azure Event Grid Tools Only": { + "command": "dotnet", + "args": [ + "run", + "--project", + "servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj", + "--", + "server", + "start", + "--namespace", + "eventgrid" + ], + "env": { + "AZURE_MCP_LOG_LEVEL": "Debug", + "AZURE_MCP_COLLECT_TELEMETRY": "false" + } + } + } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs index 0a0a96b08d..8dd17b565d 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs @@ -2,12 +2,15 @@ // 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(TopicListCommand.TopicListCommandResult))] +[JsonSerializable(typeof(SubscriptionListCommand.SubscriptionListCommandResult))] [JsonSerializable(typeof(EventGridTopicInfo))] +[JsonSerializable(typeof(EventGridSubscriptionInfo))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] 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..e6849d8cd4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Commands; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.EventGrid.Commands; +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; +using Microsoft.Extensions.Logging; + +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; + private readonly Option _topicNameOption = EventGridOptionDefinitions.TopicName; + + 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 topic-name or subscription-id. + """; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() { Destructive = false, ReadOnly = true }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + UseResourceGroup(); // Optional resource group filtering + command.Options.Add(_topicNameOption); + } + + protected override SubscriptionListOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.TopicName = parseResult.GetValue(_topicNameOption); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var eventGridService = context.GetService(); + var subscriptions = await eventGridService.GetSubscriptionsAsync( + options.Subscription!, + options.ResourceGroup, + options.TopicName, + options.RetryPolicy); + + context.Response.Results = subscriptions?.Count > 0 + ? ResponseResult.Create(new SubscriptionListCommandResult(subscriptions), EventGridJsonContext.Default.SubscriptionListCommandResult) + : null; + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error listing Event Grid subscriptions. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, TopicName: {TopicName}, Options: {@Options}", + options.Subscription, options.ResourceGroup, options.TopicName, options); + HandleException(context, ex); + } + + return context.Response; + } + + internal record SubscriptionListCommandResult(List Subscriptions); +} diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs b/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs index 76281e760e..23e1ccdd58 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs @@ -27,7 +27,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..56147794b2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Models/EventGridSubscriptionInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.EventGrid.Models; + +public record EventGridSubscriptionInfo( + string Name, + string Type, + string? EndpointType, + string? EndpointUrl, + string? ProvisioningState, + string? DeadLetterDestination, + string? Filter, + int? MaxDeliveryAttempts, + int? EventTimeToLiveInMinutes, + string? CreatedDateTime, + 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..c8d5efe26f 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs @@ -1,8 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.CommandLine; + namespace Azure.Mcp.Tools.EventGrid.Options; public static class EventGridOptionDefinitions { + public const string TopicNameParam = "topic-name"; + + public static readonly Option TopicName = new( + $"--{TopicNameParam}" + ) + { + Description = "The name of the Event Grid topic.", + Required = false + }; } 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..3332a81fdd --- /dev/null +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/Subscription/SubscriptionListOptions.cs @@ -0,0 +1,9 @@ +// 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; } +} diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs index ffbfccb602..10706a5d52 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs @@ -41,6 +41,34 @@ public async Task> GetTopicsAsync( return topics; } + public Task> GetSubscriptionsAsync( + string subscription, + string? resourceGroup = null, + string? topicName = null, + RetryPolicyOptions? retryPolicy = null) + { + // For now, return a placeholder implementation + // This will be enhanced once we determine the correct Azure SDK methods + var subscriptions = new List(); + + // Add a placeholder subscription for demonstration + subscriptions.Add(new EventGridSubscriptionInfo( + Name: "placeholder-subscription", + Type: "Microsoft.EventGrid/eventSubscriptions", + EndpointType: "WebHook", + EndpointUrl: "https://example.com/webhook", + ProvisioningState: "Succeeded", + DeadLetterDestination: null, + Filter: null, + MaxDeliveryAttempts: 30, + EventTimeToLiveInMinutes: 1440, + CreatedDateTime: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), + UpdatedDateTime: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + )); + + return Task.FromResult(subscriptions); + } + private static EventGridTopicInfo CreateTopicInfo(EventGridTopicData topicData) { return new EventGridTopicInfo( diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs index 3fe6b2bc63..e929057045 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs @@ -9,4 +9,10 @@ Task> GetTopicsAsync( string subscription, string? resourceGroup = null, RetryPolicyOptions? retryPolicy = null); + + Task> GetSubscriptionsAsync( + string subscription, + string? resourceGroup = null, + string? topicName = 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..afbd01916c --- /dev/null +++ b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Subscription/SubscriptionListCommandTests.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine; +using System.Text.Json; +using System.Text.Json.Serialization; +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 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 ILogger _logger; + private readonly SubscriptionListCommand _command; + private readonly CommandContext _context; + private readonly Command _commandDefinition; + + public SubscriptionListCommandTests() + { + _eventGridService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection(); + collection.AddSingleton(_eventGridService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [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()) + .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()) + .Returns(Task.FromResult(expectedSubscriptions)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--resource-group", resourceGroup, "--topic-name", 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()) + .Returns(Task.FromResult(new List())); + + var args = _commandDefinition.Parse(["--subscription", subscription]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.NotNull(response); + Assert.Null(response.Results); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + var expectedError = "Test error"; + var subscription = "sub123"; + + _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception(expectedError)); + + var args = _commandDefinition.Parse(["--subscription", subscription]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.NotNull(response); + Assert.Equal(500, response.Status); + Assert.StartsWith(expectedError, response.Message); + } + + private class SubscriptionListResult + { + [JsonPropertyName("subscriptions")] + public List? Subscriptions { get; set; } + } +} From f50081c9acaa69170db30df2ee99031b4e27c629 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 11:41:57 -0700 Subject: [PATCH 02/18] Resolved merge conflict- 2 --- Directory.Packages.props | 7 +- docs/azmcp-commands.md | 7 + docs/e2eTestPrompts.md | 6 + .../Subscription/SubscriptionListCommand.cs | 2 +- .../src/Options/EventGridOptionDefinitions.cs | 3 +- .../src/Services/EventGridService.cs | 156 +++++++++++++++--- 6 files changed, 154 insertions(+), 27 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 176a5cb12d..6ddfe76b15 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,7 +27,7 @@ - + @@ -62,8 +62,7 @@ - + @@ -95,4 +94,4 @@ - + \ No newline at end of file diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index ebc09224e9..87a9592393 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-name ] + [--location ] ``` ### Azure Function App Operations diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md index e0eb067e08..b27a879ec8 100644 --- a/docs/e2eTestPrompts.md +++ b/docs/e2eTestPrompts.md @@ -146,6 +146,12 @@ 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 subscriptions for topic | +| 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 | +| azmcp_eventgrid_subscription_list | List subscriptions for topic in subscription | ## Azure Function App diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index e6849d8cd4..1f1d49c0cd 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -24,7 +24,7 @@ public sealed class SubscriptionListCommand(ILogger log """ 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 topic-name or subscription-id. + details as JSON array. Requires topic-name or subscription. """; public override string Title => CommandTitle; diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs index c8d5efe26f..0454cc9954 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs @@ -13,7 +13,6 @@ public static class EventGridOptionDefinitions $"--{TopicNameParam}" ) { - Description = "The name of the Event Grid topic.", - Required = false + Description = "The name of the Event Grid topic." }; } diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs index 10706a5d52..836e2242db 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs @@ -1,7 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +<<<<<<< HEAD +======= +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Core.Services; +using Azure.Mcp.Tools.EventGrid.Models; +>>>>>>> 6f127f56 (AzureMcp Merge Conflicts resolved) using Azure.ResourceManager.EventGrid; +using Azure.ResourceManager.Resources; namespace Azure.Mcp.Tools.EventGrid.Services; @@ -41,34 +50,81 @@ public async Task> GetTopicsAsync( return topics; } - public Task> GetSubscriptionsAsync( + public async Task> GetSubscriptionsAsync( string subscription, string? resourceGroup = null, string? topicName = null, RetryPolicyOptions? retryPolicy = null) { - // For now, return a placeholder implementation - // This will be enhanced once we determine the correct Azure SDK methods var subscriptions = new List(); - - // Add a placeholder subscription for demonstration - subscriptions.Add(new EventGridSubscriptionInfo( - Name: "placeholder-subscription", - Type: "Microsoft.EventGrid/eventSubscriptions", - EndpointType: "WebHook", - EndpointUrl: "https://example.com/webhook", - ProvisioningState: "Succeeded", - DeadLetterDestination: null, - Filter: null, - MaxDeliveryAttempts: 30, - EventTimeToLiveInMinutes: 1440, - CreatedDateTime: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), - UpdatedDateTime: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") - )); - - return Task.FromResult(subscriptions); + + try + { + // Get all topics first, then get subscriptions for each + var topics = await GetTopicsAsync(subscription, resourceGroup, retryPolicy); + + // Filter to specific topic if requested + if (!string.IsNullOrEmpty(topicName)) + { + topics = topics.Where(t => t.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + // For each topic, use Azure CLI to get its event subscriptions + foreach (var topic in topics) + { + try + { + await GetSubscriptionsForTopicUsingCli(subscription, topic, subscriptions); + } + catch + { + // Continue with other topics if one fails + continue; + } + } + } + catch (Exception) + { + // Return partial results on error + } + + return subscriptions; } + private Task GetSubscriptionsForTopicUsingCli( + string subscription, + EventGridTopicInfo topic, + List subscriptions) + { + try + { + // For demonstration purposes, create a sample subscription for each topic + // In the full implementation, this would use the actual Azure API to get real subscriptions + subscriptions.Add(new EventGridSubscriptionInfo( + Name: $"{topic.Name}-demo-subscription", + Type: "Microsoft.EventGrid/eventSubscriptions", + EndpointType: "WebHook", + EndpointUrl: "https://example.com/webhook", + ProvisioningState: "Succeeded", + DeadLetterDestination: null, + Filter: null, + MaxDeliveryAttempts: 30, + EventTimeToLiveInMinutes: 1440, + CreatedDateTime: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), + UpdatedDateTime: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + )); + } + catch + { + // Skip this topic on error + } + + return Task.CompletedTask; + } + + // Remove the helper methods that were causing compilation issues + // In the full implementation, these would be replaced with working Azure SDK calls + private static EventGridTopicInfo CreateTopicInfo(EventGridTopicData topicData) { return new EventGridTopicInfo( @@ -79,4 +135,64 @@ private static EventGridTopicInfo CreateTopicInfo(EventGridTopicData topicData) PublicNetworkAccess: topicData.PublicNetworkAccess?.ToString(), InputSchema: topicData.InputSchema?.ToString()); } + + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "EventGrid destination types are well-known SDK types")] + private static EventGridSubscriptionInfo CreateSubscriptionInfo(EventGridSubscriptionData subscriptionData) + { + string? endpointType = null; + string? endpointUrl = null; + + // Extract endpoint information based on type + if (subscriptionData.Destination != null) + { + endpointType = subscriptionData.Destination.GetType().Name; + + // Try to extract endpoint URL from different destination types + var destinationType = subscriptionData.Destination.GetType(); + var endpointProperty = destinationType.GetProperty("EndpointUri") ?? + destinationType.GetProperty("EndpointUrl") ?? + destinationType.GetProperty("Endpoint"); + + if (endpointProperty != null) + { + var endpointValue = endpointProperty.GetValue(subscriptionData.Destination); + endpointUrl = endpointValue?.ToString(); + } + } + + // 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") + ); + } } From 0c651bb0ebab3068c388eeeef7e4f0e692148a5e Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 11:43:29 -0700 Subject: [PATCH 03/18] Resolved merge conflict- 3 --- Directory.Packages.props | 2 +- EventGridDebug/EventGridDebug.csproj | 16 ++ docs/e2eTestPrompts.md | 6 +- .../Subscription/SubscriptionListCommand.cs | 14 +- .../src/Commands/Topic/TopicListCommand.cs | 6 +- .../src/Options/EventGridOptionDefinitions.cs | 8 + .../Subscription/SubscriptionListOptions.cs | 1 + .../src/Services/EventGridService.cs | 256 +++++++++++++++--- .../src/Services/IEventGridService.cs | 1 + .../SubscriptionListCommandTests.cs | 51 +++- .../Topic/TopicListCommandTests.cs | 9 +- 11 files changed, 314 insertions(+), 56 deletions(-) create mode 100644 EventGridDebug/EventGridDebug.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index 6ddfe76b15..37953169b0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,7 +27,7 @@ - + diff --git a/EventGridDebug/EventGridDebug.csproj b/EventGridDebug/EventGridDebug.csproj new file mode 100644 index 0000000000..8416e908cc --- /dev/null +++ b/EventGridDebug/EventGridDebug.csproj @@ -0,0 +1,16 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md index b27a879ec8..cf4982970a 100644 --- a/docs/e2eTestPrompts.md +++ b/docs/e2eTestPrompts.md @@ -146,12 +146,12 @@ 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 subscriptions for topic | +| azmcp_eventgrid_subscription_list | Show me all Event grid subscriptions for topic | | 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 | -| azmcp_eventgrid_subscription_list | List subscriptions for topic in subscription | +| azmcp_eventgrid_subscription_list | Show Event Grid subscriptions in resource group in subscription | +| azmcp_eventgrid_subscription_list | List Event Grid subscriptions for topic in subscription | ## Azure Function App diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index 1f1d49c0cd..cd723f1a72 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -17,6 +17,7 @@ public sealed class SubscriptionListCommand(ILogger log private const string CommandTitle = "List Event Grid Subscriptions"; private readonly ILogger _logger = logger; private readonly Option _topicNameOption = EventGridOptionDefinitions.TopicName; + private readonly Option _locationOption = EventGridOptionDefinitions.Location; public override string Name => "list"; @@ -36,12 +37,14 @@ protected override void RegisterOptions(Command command) base.RegisterOptions(command); UseResourceGroup(); // Optional resource group filtering command.Options.Add(_topicNameOption); + command.Options.Add(_locationOption); } protected override SubscriptionListOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); options.TopicName = parseResult.GetValue(_topicNameOption); + options.Location = parseResult.GetValue(_locationOption); return options; } @@ -61,17 +64,18 @@ public override async Task ExecuteAsync(CommandContext context, options.Subscription!, options.ResourceGroup, options.TopicName, + options.Location, options.RetryPolicy); - context.Response.Results = subscriptions?.Count > 0 - ? ResponseResult.Create(new SubscriptionListCommandResult(subscriptions), EventGridJsonContext.Default.SubscriptionListCommandResult) - : null; + 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}, Options: {@Options}", - options.Subscription, options.ResourceGroup, options.TopicName, options); + "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); } 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 443876c359..55ecd9e2c2 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs @@ -64,9 +64,9 @@ public override async Task ExecuteAsync(CommandContext context, options.ResourceGroup, options.RetryPolicy); - context.Response.Results = topics?.Count > 0 - ? ResponseResult.Create(new TopicListCommandResult(topics), EventGridJsonContext.Default.TopicListCommandResult) - : null; + context.Response.Results = ResponseResult.Create( + new TopicListCommandResult(topics ?? []), + EventGridJsonContext.Default.TopicListCommandResult); } catch (Exception ex) { diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs index 0454cc9954..cd407242eb 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs @@ -8,6 +8,7 @@ namespace Azure.Mcp.Tools.EventGrid.Options; public static class EventGridOptionDefinitions { public const string TopicNameParam = "topic-name"; + public const string LocationParam = "location"; public static readonly Option TopicName = new( $"--{TopicNameParam}" @@ -15,4 +16,11 @@ public static class EventGridOptionDefinitions { 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 index 3332a81fdd..6041fef566 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Options/Subscription/SubscriptionListOptions.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/Subscription/SubscriptionListOptions.cs @@ -6,4 +6,5 @@ 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 836e2242db..ce1592161f 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs @@ -11,6 +11,7 @@ >>>>>>> 6f127f56 (AzureMcp Merge Conflicts resolved) using Azure.ResourceManager.EventGrid; using Azure.ResourceManager.Resources; +using Azure.ResourceManager; namespace Azure.Mcp.Tools.EventGrid.Services; @@ -54,76 +55,255 @@ public async Task> GetSubscriptionsAsync( string subscription, string? resourceGroup = null, string? topicName = null, + string? location = null, RetryPolicyOptions? retryPolicy = null) { var subscriptions = new List(); try { - // Get all topics first, then get subscriptions for each - var topics = await GetTopicsAsync(subscription, resourceGroup, retryPolicy); + var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy); - // Filter to specific topic if requested + // If specific topic is requested, get subscriptions for that topic only if (!string.IsNullOrEmpty(topicName)) { - topics = topics.Where(t => t.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase)).ToList(); + 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); + } - // For each topic, use Azure CLI to get its event subscriptions - foreach (var topic in topics) + 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) { - try + // Check if location filter applies + if (string.IsNullOrEmpty(location) || string.Equals(topic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) { - await GetSubscriptionsForTopicUsingCli(subscription, topic, subscriptions); + // Get event subscriptions for the specific topic using the correct ARM SDK pattern + await foreach (var subscription in topic.GetTopicEventSubscriptions().GetAllAsync()) + { + subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); + } } - catch + 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) + { + // Check if location filter applies + if (string.IsNullOrEmpty(location) || string.Equals(systemTopic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) { - // Continue with other topics if one fails - continue; + await foreach (var subscription in systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync()) + { + subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); + } } } } - catch (Exception) + catch (Exception ex) { - // Return partial results on error + // Log and re-throw to preserve error information + throw new InvalidOperationException($"Failed to get subscriptions for topic '{topicName}': {ex.Message}", ex); } - - return subscriptions; } - private Task GetSubscriptionsForTopicUsingCli( - string subscription, - EventGridTopicInfo topic, + private async Task GetSubscriptionsFromAllTopics( + SubscriptionResource subscriptionResource, + string? resourceGroup, + string? location, List subscriptions) { try { - // For demonstration purposes, create a sample subscription for each topic - // In the full implementation, this would use the actual Azure API to get real subscriptions - subscriptions.Add(new EventGridSubscriptionInfo( - Name: $"{topic.Name}-demo-subscription", - Type: "Microsoft.EventGrid/eventSubscriptions", - EndpointType: "WebHook", - EndpointUrl: "https://example.com/webhook", - ProvisioningState: "Succeeded", - DeadLetterDestination: null, - Filter: null, - MaxDeliveryAttempts: 30, - EventTimeToLiveInMinutes: 1440, - CreatedDateTime: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), - UpdatedDateTime: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") - )); + 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 + { + // Check if location filter applies + if (string.IsNullOrEmpty(location) || string.Equals(topic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) + { + // Get event subscriptions for each topic using the correct ARM SDK pattern + await foreach (var subscription in topic.GetTopicEventSubscriptions().GetAllAsync()) + { + subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); + } + } + } + catch + { + // Continue with other topics if one fails + continue; + } + } + + // Also check system topics in the resource group + await foreach (var systemTopic in resourceGroupResource.Value.GetSystemTopics().GetAllAsync()) + { + try + { + // Check if location filter applies + if (string.IsNullOrEmpty(location) || string.Equals(systemTopic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) + { + await foreach (var subscription in systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync()) + { + subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); + } + } + } + catch + { + // Continue with other system topics if one fails + continue; + } + } + } + else + { + // Get topics from all resource groups and their subscriptions + await foreach (var topic in subscriptionResource.GetEventGridTopicsAsync()) + { + try + { + // Check if location filter applies + if (string.IsNullOrEmpty(location) || string.Equals(topic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) + { + // Get event subscriptions for each topic using the correct ARM SDK pattern + await foreach (var subscription in topic.GetTopicEventSubscriptions().GetAllAsync()) + { + subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); + } + } + } + catch + { + // Continue with other topics if one fails + continue; + } + } + + // Also check system topics across all resource groups + await foreach (var systemTopic in subscriptionResource.GetSystemTopicsAsync()) + { + try + { + // Check if location filter applies + if (string.IsNullOrEmpty(location) || string.Equals(systemTopic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) + { + await foreach (var subscription in systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync()) + { + subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); + } + } + } + catch + { + // Continue with other system topics if one fails + 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; + } + } } - catch + else { - // Skip this topic on error + // Search in all resource groups + await foreach (var topic in subscriptionResource.GetEventGridTopicsAsync()) + { + if (topic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase)) + { + return topic; + } + } } - return Task.CompletedTask; + return null; } - // Remove the helper methods that were causing compilation issues - // In the full implementation, these would be replaced with working Azure SDK calls + 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) { diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs index e929057045..ccc51acee2 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs @@ -14,5 +14,6 @@ Task> GetSubscriptionsAsync( string subscription, string? resourceGroup = null, string? topicName = null, + string? location = null, RetryPolicyOptions? retryPolicy = null); } 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 index afbd01916c..33810a06f1 100644 --- 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 @@ -6,6 +6,7 @@ using System.Text.Json.Serialization; using Azure.Mcp.Core.Models.Command; using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.EventGrid.Commands; using Azure.Mcp.Tools.EventGrid.Commands.Subscription; using Azure.Mcp.Tools.EventGrid.Services; using Microsoft.Extensions.DependencyInjection; @@ -51,7 +52,7 @@ public async Task ExecuteAsync_NoParameters_ReturnsSubscriptions() 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()) + _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(expectedSubscriptions)); var args = _commandDefinition.Parse(["--subscription", subscription]); @@ -85,7 +86,7 @@ public async Task ExecuteAsync_WithTopicNameFilter_FiltersCorrectly() 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()) + _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Is(resourceGroup), Arg.Is(topicName), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(expectedSubscriptions)); var args = _commandDefinition.Parse(["--subscription", subscription, "--resource-group", resourceGroup, "--topic-name", topicName]); @@ -113,7 +114,7 @@ public async Task ExecuteAsync_ReturnsNull_WhenNoSubscriptions() // Arrange var subscription = "sub123"; - _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any()) + _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new List())); var args = _commandDefinition.Parse(["--subscription", subscription]); @@ -123,7 +124,47 @@ public async Task ExecuteAsync_ReturnsNull_WhenNoSubscriptions() // Assert Assert.NotNull(response); - Assert.Null(response.Results); + 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()) + .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] @@ -133,7 +174,7 @@ public async Task ExecuteAsync_HandlesException() var expectedError = "Test error"; var subscription = "sub123"; - _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any()) + _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new Exception(expectedError)); var args = _commandDefinition.Parse(["--subscription", subscription]); 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 fbe9357368..6d161fdbd5 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 @@ -87,7 +87,14 @@ public async Task ExecuteAsync_ReturnsNull_WhenNoTopics() // Assert Assert.NotNull(response); - Assert.Null(response.Results); + 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.Topics); + Assert.Empty(result.Topics); } [Fact] From 75c08eb709680a71a2174bc13462e73f54f82830 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 11:51:41 -0700 Subject: [PATCH 04/18] Resolved merge conflict- 4 --- EventGridDebug/EventGridDebug.csproj | 16 ---------- docs/e2eTestPrompts.md | 4 ++- servers/Azure.Mcp.Server/CHANGELOG.md | 1 + servers/Azure.Mcp.Server/README.md | 7 ++++- .../Subscription/SubscriptionListCommand.cs | 5 +--- .../src/Commands/Topic/TopicListCommand.cs | 2 +- .../src/Services/EventGridService.cs | 30 ++++++++----------- .../SubscriptionListCommandTests.cs | 3 +- .../Topic/TopicListCommandTests.cs | 2 +- 9 files changed, 26 insertions(+), 44 deletions(-) delete mode 100644 EventGridDebug/EventGridDebug.csproj diff --git a/EventGridDebug/EventGridDebug.csproj b/EventGridDebug/EventGridDebug.csproj deleted file mode 100644 index 8416e908cc..0000000000 --- a/EventGridDebug/EventGridDebug.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - - - - - - - diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md index cf4982970a..c52c725649 100644 --- a/docs/e2eTestPrompts.md +++ b/docs/e2eTestPrompts.md @@ -146,12 +146,14 @@ 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 | Show me all Event Grid subscriptions for topic | | 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 topic in subscription | +| azmcp_eventgrid_subscription_list | Show Event Grid subscriptions for topic in location | +| azmcp_eventgrid_subscription_list | List Event Grid subscriptions for subscription in location | ## Azure Function App diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 9f346ab4b1..2071d0fadf 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -67,6 +67,7 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Features Added +<<<<<<< HEAD - Added support for listing all Event Grid topics in a subscription via the command `azmcp_eventgrid_topic_list`. [[#43](https://github.com/microsoft/mcp/pull/43)] - Added support for retrieving knowledge index schema information in Azure AI Foundry projects via the command `azmcp_foundry_knowledge_index_schema`. [[#41](https://github.com/microsoft/mcp/pull/41)] - Added support for listing service health events in a subscription via the command `azmcp_resourcehealth_service-health-events_list`. [[#367](https://github.com/microsoft/mcp/pull/367)] diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index e1de2943ab..72b6928b1c 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -98,7 +98,10 @@ 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" +* "Show me all Event Grid subscriptions for topic 'my-topic'" +* "List Event Grid subscriptions for topic 'my-topic' in resource group 'my-resourcegroup'" +* "Show Event Grid subscriptions in location 'my-location'" ### ⚡ Azure Managed Lustre @@ -211,6 +214,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.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index cd723f1a72..e97e638dde 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -1,14 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Azure.Mcp.Core.Commands; using Azure.Mcp.Core.Models.Option; -using Azure.Mcp.Tools.EventGrid.Commands; 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; -using Microsoft.Extensions.Logging; namespace Azure.Mcp.Tools.EventGrid.Commands.Subscription; @@ -68,7 +65,7 @@ public override async Task ExecuteAsync(CommandContext context, options.RetryPolicy); context.Response.Results = ResponseResult.Create( - new SubscriptionListCommandResult(subscriptions ?? []), + new SubscriptionListCommandResult(subscriptions ?? []), EventGridJsonContext.Default.SubscriptionListCommandResult); } catch (Exception ex) 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 55ecd9e2c2..c8c108bcc3 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs @@ -65,7 +65,7 @@ public override async Task ExecuteAsync(CommandContext context, options.RetryPolicy); context.Response.Results = ResponseResult.Create( - new TopicListCommandResult(topics ?? []), + new TopicListCommandResult(topics ?? []), EventGridJsonContext.Default.TopicListCommandResult); } catch (Exception ex) diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs index ce1592161f..ac8db0fc58 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs @@ -1,17 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -<<<<<<< HEAD -======= using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Azure.Mcp.Core.Options; -using Azure.Mcp.Core.Services; -using Azure.Mcp.Tools.EventGrid.Models; ->>>>>>> 6f127f56 (AzureMcp Merge Conflicts resolved) +using Azure.ResourceManager; using Azure.ResourceManager.EventGrid; using Azure.ResourceManager.Resources; -using Azure.ResourceManager; namespace Azure.Mcp.Tools.EventGrid.Services; @@ -142,7 +136,7 @@ private async Task GetSubscriptionsFromAllTopics( { // 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()) { @@ -248,7 +242,7 @@ private async Task GetSubscriptionsFromAllTopics( { // 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)) @@ -281,7 +275,7 @@ private async Task GetSubscriptionsFromAllTopics( { // 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)) @@ -321,18 +315,18 @@ private static EventGridSubscriptionInfo CreateSubscriptionInfo(EventGridSubscri { string? endpointType = null; string? endpointUrl = null; - + // Extract endpoint information based on type if (subscriptionData.Destination != null) { endpointType = subscriptionData.Destination.GetType().Name; - + // Try to extract endpoint URL from different destination types var destinationType = subscriptionData.Destination.GetType(); - var endpointProperty = destinationType.GetProperty("EndpointUri") ?? + var endpointProperty = destinationType.GetProperty("EndpointUri") ?? destinationType.GetProperty("EndpointUrl") ?? destinationType.GetProperty("Endpoint"); - + if (endpointProperty != null) { var endpointValue = endpointProperty.GetValue(subscriptionData.Destination); @@ -345,16 +339,16 @@ private static EventGridSubscriptionInfo CreateSubscriptionInfo(EventGridSubscri 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}"); 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 index 33810a06f1..155d54930f 100644 --- 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 @@ -6,7 +6,6 @@ using System.Text.Json.Serialization; using Azure.Mcp.Core.Models.Command; using Azure.Mcp.Core.Options; -using Azure.Mcp.Tools.EventGrid.Commands; using Azure.Mcp.Tools.EventGrid.Commands.Subscription; using Azure.Mcp.Tools.EventGrid.Services; using Microsoft.Extensions.DependencyInjection; @@ -125,7 +124,7 @@ public async Task ExecuteAsync_ReturnsNull_WhenNoSubscriptions() // 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); 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 6d161fdbd5..894aeb5aff 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 @@ -88,7 +88,7 @@ public async Task ExecuteAsync_ReturnsNull_WhenNoTopics() // 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); From b13bd153160a76e079350c950899cc5313698e33 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Mon, 15 Sep 2025 23:21:43 -0700 Subject: [PATCH 05/18] Merge conflict 2 --- servers/Azure.Mcp.Server/CHANGELOG.md | 2 +- .../src/Services/EventGridService.cs | 28 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 2071d0fadf..33710a7ae5 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -67,7 +67,6 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Features Added -<<<<<<< HEAD - Added support for listing all Event Grid topics in a subscription via the command `azmcp_eventgrid_topic_list`. [[#43](https://github.com/microsoft/mcp/pull/43)] - Added support for retrieving knowledge index schema information in Azure AI Foundry projects via the command `azmcp_foundry_knowledge_index_schema`. [[#41](https://github.com/microsoft/mcp/pull/41)] - Added support for listing service health events in a subscription via the command `azmcp_resourcehealth_service-health-events_list`. [[#367](https://github.com/microsoft/mcp/pull/367)] @@ -79,6 +78,7 @@ The Azure MCP Server updates automatically by default whenever a new release com - `azmcp_storage_blob_container_create`: Removed the ability to configure `blob-container-public-access` (always `false` now). - `azmcp_storage_blob_upload`: Removed the ability to configure `overwrite` (always `false` now). + ### Bugs Fixed - Fixed telemetry bug where "ToolArea" was incorrectly populated in with "ToolName". [[#346](https://github.com/microsoft/mcp/pull/346)] diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs index ac8db0fc58..07f483f9c2 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs @@ -152,14 +152,14 @@ private async Task GetSubscriptionsFromAllTopics( } } } - catch + catch (Exception ex) { - // Continue with other topics if one fails + // Continue with other topics if one fails - individual topic access errors + // shouldn't block the entire operation since we're aggregating from multiple topics + System.Diagnostics.Debug.WriteLine($"Failed to get subscriptions for topic '{topic.Data.Name}': {ex.Message}"); continue; } - } - - // Also check system topics in the resource group + } // Also check system topics in the resource group await foreach (var systemTopic in resourceGroupResource.Value.GetSystemTopics().GetAllAsync()) { try @@ -173,9 +173,11 @@ private async Task GetSubscriptionsFromAllTopics( } } } - catch + catch (Exception ex) { - // Continue with other system topics if one fails + // 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 + System.Diagnostics.Debug.WriteLine($"Failed to get subscriptions for system topic '{systemTopic.Data.Name}': {ex.Message}"); continue; } } @@ -197,9 +199,11 @@ private async Task GetSubscriptionsFromAllTopics( } } } - catch + catch (Exception ex) { - // Continue with other topics if one fails + // Continue with other topics if one fails - individual topic access errors + // shouldn't block the entire operation since we're aggregating from multiple topics + System.Diagnostics.Debug.WriteLine($"Failed to get subscriptions for topic '{topic.Data.Name}': {ex.Message}"); continue; } } @@ -218,9 +222,11 @@ private async Task GetSubscriptionsFromAllTopics( } } } - catch + catch (Exception ex) { - // Continue with other system topics if one fails + // 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 + System.Diagnostics.Debug.WriteLine($"Failed to get subscriptions for system topic '{systemTopic.Data.Name}': {ex.Message}"); continue; } } From 6b09bebcbbf07ab4d79e7144ed6644bbd9c44538 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 11:52:27 -0700 Subject: [PATCH 06/18] Resolved merge conflict- 5 --- .../src/GlobalUsings.cs | 4 + .../src/Options/EventGridOptionDefinitions.cs | 2 +- .../src/Services/EventGridService.cs | 128 ++++++++---------- .../SubscriptionListCommandTests.cs | 2 +- 4 files changed, 64 insertions(+), 72 deletions(-) diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs index 487469cc4b..986a60b802 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs @@ -2,6 +2,10 @@ // Licensed under the MIT License. global using System.CommandLine; +<<<<<<< HEAD +======= +global using System.CommandLine.Parsing; +>>>>>>> 216a9350 (Addressed review comments) global using Azure.Mcp.Core.Commands; global using Azure.Mcp.Core.Models.Command; global using Azure.Mcp.Core.Options; diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs index cd407242eb..525831ae7f 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs @@ -7,7 +7,7 @@ namespace Azure.Mcp.Tools.EventGrid.Options; public static class EventGridOptionDefinitions { - public const string TopicNameParam = "topic-name"; + public const string TopicNameParam = "topic"; public const string LocationParam = "location"; public static readonly Option TopicName = new( diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs index 07f483f9c2..6b00c9b6b9 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs @@ -5,14 +5,16 @@ 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, @@ -91,15 +93,7 @@ private async Task GetSubscriptionsForSpecificTopic( var topic = await FindTopic(subscriptionResource, resourceGroup, topicName); if (topic != null) { - // Check if location filter applies - if (string.IsNullOrEmpty(location) || string.Equals(topic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) - { - // Get event subscriptions for the specific topic using the correct ARM SDK pattern - await foreach (var subscription in topic.GetTopicEventSubscriptions().GetAllAsync()) - { - subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); - } - } + await AddSubscriptionsFromTopic(topic.Data.Location.ToString(), location, subscriptions, topic.GetTopicEventSubscriptions().GetAllAsync()); return; // Found custom topic, no need to check system topics } @@ -107,14 +101,7 @@ private async Task GetSubscriptionsForSpecificTopic( var systemTopic = await FindSystemTopic(subscriptionResource, resourceGroup, topicName); if (systemTopic != null) { - // Check if location filter applies - if (string.IsNullOrEmpty(location) || string.Equals(systemTopic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) - { - await foreach (var subscription in systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync()) - { - subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); - } - } + await AddSubscriptionsFromSystemTopic(systemTopic.Data.Location.ToString(), location, subscriptions, systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync()); } } catch (Exception ex) @@ -142,21 +129,13 @@ private async Task GetSubscriptionsFromAllTopics( { try { - // Check if location filter applies - if (string.IsNullOrEmpty(location) || string.Equals(topic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) - { - // Get event subscriptions for each topic using the correct ARM SDK pattern - await foreach (var subscription in topic.GetTopicEventSubscriptions().GetAllAsync()) - { - subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); - } - } + 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 - System.Diagnostics.Debug.WriteLine($"Failed to get subscriptions for topic '{topic.Data.Name}': {ex.Message}"); + _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 @@ -164,20 +143,13 @@ private async Task GetSubscriptionsFromAllTopics( { try { - // Check if location filter applies - if (string.IsNullOrEmpty(location) || string.Equals(systemTopic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) - { - await foreach (var subscription in systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync()) - { - subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); - } - } + 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 - System.Diagnostics.Debug.WriteLine($"Failed to get subscriptions for system topic '{systemTopic.Data.Name}': {ex.Message}"); + _logger.LogWarning(ex, "Failed to get subscriptions for system topic '{SystemTopicName}'. Continuing with other topics.", systemTopic.Data.Name); continue; } } @@ -189,21 +161,13 @@ private async Task GetSubscriptionsFromAllTopics( { try { - // Check if location filter applies - if (string.IsNullOrEmpty(location) || string.Equals(topic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) - { - // Get event subscriptions for each topic using the correct ARM SDK pattern - await foreach (var subscription in topic.GetTopicEventSubscriptions().GetAllAsync()) - { - subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); - } - } + 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 - System.Diagnostics.Debug.WriteLine($"Failed to get subscriptions for topic '{topic.Data.Name}': {ex.Message}"); + _logger.LogWarning(ex, "Failed to get subscriptions for topic '{TopicName}'. Continuing with other topics.", topic.Data.Name); continue; } } @@ -213,20 +177,13 @@ private async Task GetSubscriptionsFromAllTopics( { try { - // Check if location filter applies - if (string.IsNullOrEmpty(location) || string.Equals(systemTopic.Data.Location.ToString(), location, StringComparison.OrdinalIgnoreCase)) - { - await foreach (var subscription in systemTopic.GetSystemTopicEventSubscriptions().GetAllAsync()) - { - subscriptions.Add(CreateSubscriptionInfo(subscription.Data)); - } - } + 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 - System.Diagnostics.Debug.WriteLine($"Failed to get subscriptions for system topic '{systemTopic.Data.Name}': {ex.Message}"); + _logger.LogWarning(ex, "Failed to get subscriptions for system topic '{SystemTopicName}'. Continuing with other topics.", systemTopic.Data.Name); continue; } } @@ -316,28 +273,29 @@ private static EventGridTopicInfo CreateTopicInfo(EventGridTopicData topicData) InputSchema: topicData.InputSchema?.ToString()); } - [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "EventGrid destination types are well-known SDK types")] private static EventGridSubscriptionInfo CreateSubscriptionInfo(EventGridSubscriptionData subscriptionData) { string? endpointType = null; string? endpointUrl = null; - // Extract endpoint information based on type + // Extract endpoint information based on destination type if (subscriptionData.Destination != null) { - endpointType = subscriptionData.Destination.GetType().Name; - - // Try to extract endpoint URL from different destination types - var destinationType = subscriptionData.Destination.GetType(); - var endpointProperty = destinationType.GetProperty("EndpointUri") ?? - destinationType.GetProperty("EndpointUrl") ?? - destinationType.GetProperty("Endpoint"); - - if (endpointProperty != null) + // Extract both endpoint type and URL using type-safe pattern matching + (endpointType, endpointUrl) = subscriptionData.Destination switch { - var endpointValue = endpointProperty.GetValue(subscriptionData.Destination); - endpointUrl = endpointValue?.ToString(); - } + 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 @@ -375,4 +333,34 @@ private static EventGridSubscriptionInfo CreateSubscriptionInfo(EventGridSubscri 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/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Subscription/SubscriptionListCommandTests.cs b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Subscription/SubscriptionListCommandTests.cs index 155d54930f..412c5a6f04 100644 --- 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 @@ -88,7 +88,7 @@ public async Task ExecuteAsync_WithTopicNameFilter_FiltersCorrectly() _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Is(resourceGroup), Arg.Is(topicName), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(expectedSubscriptions)); - var args = _commandDefinition.Parse(["--subscription", subscription, "--resource-group", resourceGroup, "--topic-name", topicName]); + var args = _commandDefinition.Parse(["--subscription", subscription, "--resource-group", resourceGroup, "--topic", topicName]); // Act var response = await _command.ExecuteAsync(_context, args); From 0798fb3e8ddc9dd18bf1fb1e86135a7115ceae41 Mon Sep 17 00:00:00 2001 From: Anannya Patra <40665106+anannya03@users.noreply.github.com> Date: Wed, 10 Sep 2025 07:20:29 -0700 Subject: [PATCH 07/18] Removed unnecessary line from CHANGELOG.md --- servers/Azure.Mcp.Server/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 33710a7ae5..9f346ab4b1 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -78,7 +78,6 @@ The Azure MCP Server updates automatically by default whenever a new release com - `azmcp_storage_blob_container_create`: Removed the ability to configure `blob-container-public-access` (always `false` now). - `azmcp_storage_blob_upload`: Removed the ability to configure `overwrite` (always `false` now). - ### Bugs Fixed - Fixed telemetry bug where "ToolArea" was incorrectly populated in with "ToolName". [[#346](https://github.com/microsoft/mcp/pull/346)] From 0e55a18ac03fc5e9495895eddc720db0e99f3fad Mon Sep 17 00:00:00 2001 From: anannya03 Date: Sun, 14 Sep 2025 18:27:02 -0700 Subject: [PATCH 08/18] Made topic or subscription mandatory --- docs/e2eTestPrompts.md | 1 - servers/Azure.Mcp.Server/README.md | 2 +- .../Subscription/SubscriptionListCommand.cs | 108 ++++++++++++++++-- .../SubscriptionListCommandTests.cs | 44 +++++++ 4 files changed, 142 insertions(+), 13 deletions(-) diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md index c52c725649..67d62bb5ba 100644 --- a/docs/e2eTestPrompts.md +++ b/docs/e2eTestPrompts.md @@ -152,7 +152,6 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | 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 topic in subscription | -| azmcp_eventgrid_subscription_list | Show Event Grid subscriptions for topic in location | | 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 72b6928b1c..00d4b0f469 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -101,7 +101,7 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some * "List all Event Grid topics in resource group 'my-resourcegroup' in my subscription" * "Show me all Event Grid subscriptions for topic 'my-topic'" * "List Event Grid subscriptions for topic 'my-topic' in resource group 'my-resourcegroup'" -* "Show Event Grid subscriptions in location 'my-location'" +* "List Event Grid Subscriptions in subscription 'my-subscription'" ### ⚡ Azure Managed Lustre diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index e97e638dde..0e495397e1 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Azure.Mcp.Core.Extensions; using Azure.Mcp.Core.Models.Option; using Azure.Mcp.Tools.EventGrid.Models; using Azure.Mcp.Tools.EventGrid.Options; @@ -22,7 +23,9 @@ public sealed class SubscriptionListCommand(ILogger log """ 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 topic-name or 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. Examples:\n --subscription \n --subscription --topic \n --topic """; public override string Title => CommandTitle; @@ -35,6 +38,22 @@ protected override void RegisterOptions(Command command) UseResourceGroup(); // Optional resource group filtering command.Options.Add(_topicNameOption); command.Options.Add(_locationOption); + + // Remove the default subscription validator from base class and add custom validation + command.Validators.Clear(); + + // Custom validation: Either --topic or --subscription (or env var) is required + command.Validators.Add(commandResult => + { + var hasSubscriptionOption = commandResult.HasOptionResult(_subscriptionOption); + var hasEnv = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID")); + var hasTopicOption = commandResult.HasOptionResult(_topicNameOption); + + if (!hasSubscriptionOption && !hasEnv && !hasTopicOption) + { + commandResult.AddError("Either --subscription or --topic is required."); + } + }); } protected override SubscriptionListOptions BindOptions(ParseResult parseResult) @@ -54,19 +73,86 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); + // Validation rules / behaviors: + // 1. Either --topic (bare topic name) or --subscription is mandatory. + // 2. --resource-group and --location are optional but cannot be provided alone (must accompany --topic or --subscription). + // 3. If only --topic (bare name) is provided (no --subscription) perform a cross-subscription search + // across all accessible subscriptions for a topic with that name and aggregate results. + // 4. If only --subscription is provided (no --topic) list all event subscriptions from every custom and system + // topic in that subscription (optionally filtered by --resource-group / --location). + + var hasSubscription = !string.IsNullOrWhiteSpace(options.Subscription); + var hasTopic = !string.IsNullOrWhiteSpace(options.TopicName); + var hasRg = !string.IsNullOrWhiteSpace(options.ResourceGroup); + var hasLocation = !string.IsNullOrWhiteSpace(options.Location); + + // Either topic or subscription is mandatory + if (!hasSubscription && !hasTopic) + { + context.Response.Status = 400; + context.Response.Message = "Either --subscription or --topic is required."; + return context.Response; + } + + // Location and resource-group can only be used with subscription or topic + if ((hasRg || hasLocation) && !hasSubscription && !hasTopic) + { + context.Response.Status = 400; + context.Response.Message = "Either --subscription or --topic is required when using --resource-group or --location."; + return context.Response; + } + + // Bare topic name without subscription triggers cross-subscription search + bool crossSubscriptionSearch = !hasSubscription && hasTopic; + try { var eventGridService = context.GetService(); - var subscriptions = await eventGridService.GetSubscriptionsAsync( - options.Subscription!, - options.ResourceGroup, - options.TopicName, - options.Location, - options.RetryPolicy); - - context.Response.Results = ResponseResult.Create( - new SubscriptionListCommandResult(subscriptions ?? []), - EventGridJsonContext.Default.SubscriptionListCommandResult); + + 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.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.RetryPolicy); + + context.Response.Results = ResponseResult.Create( + new SubscriptionListCommandResult(subscriptions ?? []), + EventGridJsonContext.Default.SubscriptionListCommandResult); + } } catch (Exception ex) { 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 index 412c5a6f04..eacc283299 100644 --- 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 @@ -187,6 +187,50 @@ public async Task ExecuteAsync_HandlesException() Assert.StartsWith(expectedError, response.Message); } + + [Fact] + public async Task ExecuteAsync_ErrorWhenOnlyLocationProvided() + { + var args = _commandDefinition.Parse(["--location", "eastus"]); + var response = await _command.ExecuteAsync(_context, args); + Assert.Equal(400, response.Status); + Assert.Contains("Either --subscription or --topic is required", response.Message); + } + + [Fact] + public async Task ExecuteAsync_BareTopicName_SearchesAllSubscriptions() + { + // Arrange: We only validate that the command executes successfully without a subscription parameter. + // Detailed cross-subscription enumeration is handled inside the command and service; here we simulate one hit. + var topicName = "myTopic"; + var subscription = "sub-search"; + var expected = new List + { + new("from-search", "Microsoft.EventGrid/eventSubscriptions", "WebHook", "https://example.com/hook", "Succeeded", null, null, 30, 1440, "2023-01-01T00:00:00Z", "2023-01-02T00:00:00Z") + }; + + // When GetSubscriptionsAsync is called with sub-search and topic name return list + _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Is(topicName), Arg.Any(), Arg.Any()) + .Returns(expected); + + var args = _commandDefinition.Parse(["--topic", topicName]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert basic success (can't fully assert aggregate without subscription service mock wiring) + Assert.NotEqual(400, response.Status); + } + + [Fact] + public async Task ExecuteAsync_ErrorWhenOnlyResourceGroupProvided() + { + var args = _commandDefinition.Parse(["--resource-group", "rg1"]); + var response = await _command.ExecuteAsync(_context, args); + Assert.Equal(400, response.Status); + Assert.Contains("Either --subscription or --topic is required", response.Message); + } + private class SubscriptionListResult { [JsonPropertyName("subscriptions")] From 821919dcd775456bf049a809eda935dd28f24fd5 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Sun, 14 Sep 2025 19:22:51 -0700 Subject: [PATCH 09/18] dotnet format --- .../Subscription/SubscriptionListCommand.cs | 22 +++++++++---------- .../SubscriptionListCommandTests.cs | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index 0e495397e1..8d71e1ab5e 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -41,14 +41,14 @@ protected override void RegisterOptions(Command command) // Remove the default subscription validator from base class and add custom validation command.Validators.Clear(); - + // Custom validation: Either --topic or --subscription (or env var) is required command.Validators.Add(commandResult => { var hasSubscriptionOption = commandResult.HasOptionResult(_subscriptionOption); var hasEnv = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID")); var hasTopicOption = commandResult.HasOptionResult(_topicNameOption); - + if (!hasSubscriptionOption && !hasEnv && !hasTopicOption) { commandResult.AddError("Either --subscription or --topic is required."); @@ -73,13 +73,13 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); - // Validation rules / behaviors: - // 1. Either --topic (bare topic name) or --subscription is mandatory. - // 2. --resource-group and --location are optional but cannot be provided alone (must accompany --topic or --subscription). - // 3. If only --topic (bare name) is provided (no --subscription) perform a cross-subscription search - // across all accessible subscriptions for a topic with that name and aggregate results. - // 4. If only --subscription is provided (no --topic) list all event subscriptions from every custom and system - // topic in that subscription (optionally filtered by --resource-group / --location). + // Validation rules / behaviors: + // 1. Either --topic (bare topic name) or --subscription is mandatory. + // 2. --resource-group and --location are optional but cannot be provided alone (must accompany --topic or --subscription). + // 3. If only --topic (bare name) is provided (no --subscription) perform a cross-subscription search + // across all accessible subscriptions for a topic with that name and aggregate results. + // 4. If only --subscription is provided (no --topic) list all event subscriptions from every custom and system + // topic in that subscription (optionally filtered by --resource-group / --location). var hasSubscription = !string.IsNullOrWhiteSpace(options.Subscription); var hasTopic = !string.IsNullOrWhiteSpace(options.TopicName); @@ -102,8 +102,8 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } - // Bare topic name without subscription triggers cross-subscription search - bool crossSubscriptionSearch = !hasSubscription && hasTopic; + // Bare topic name without subscription triggers cross-subscription search + bool crossSubscriptionSearch = !hasSubscription && hasTopic; try { 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 index eacc283299..7f222604d8 100644 --- 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 @@ -200,8 +200,8 @@ public async Task ExecuteAsync_ErrorWhenOnlyLocationProvided() [Fact] public async Task ExecuteAsync_BareTopicName_SearchesAllSubscriptions() { - // Arrange: We only validate that the command executes successfully without a subscription parameter. - // Detailed cross-subscription enumeration is handled inside the command and service; here we simulate one hit. + // Arrange: We only validate that the command executes successfully without a subscription parameter. + // Detailed cross-subscription enumeration is handled inside the command and service; here we simulate one hit. var topicName = "myTopic"; var subscription = "sub-search"; var expected = new List From 662c55d6c1e9790f7e4a3eb005fce1eac8d98dd2 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 11:53:09 -0700 Subject: [PATCH 10/18] Resolved merge conflict- 6 --- docs/azmcp-commands.md | 2 +- .../Commands/Subscription/SubscriptionListCommand.cs | 2 ++ .../src/Commands/Topic/TopicListCommand.cs | 1 + tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs | 4 ---- .../src/Services/EventGridService.cs | 6 ++++-- .../src/Services/IEventGridService.cs | 2 ++ .../Subscription/SubscriptionListCommandTests.cs | 12 ++++++------ .../Topic/TopicListCommandTests.cs | 10 +++++----- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index 87a9592393..550df944c0 100644 --- a/docs/azmcp-commands.md +++ b/docs/azmcp-commands.md @@ -483,7 +483,7 @@ azmcp eventgrid topic list --subscription \ # List all Event Grid subscriptions in a subscription, resource group, or topic azmcp eventgrid subscription list --subscription \ [--resource-group ] \ - [--topic-name ] + [--topic ] [--location ] ``` diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index 8d71e1ab5e..4aeaca5354 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -124,6 +124,7 @@ public override async Task ExecuteAsync(CommandContext context, options.ResourceGroup, options.TopicName, // bare name options.Location, + options.Tenant, options.RetryPolicy); if (found?.Count > 0) { @@ -147,6 +148,7 @@ public override async Task ExecuteAsync(CommandContext context, options.ResourceGroup, options.TopicName, options.Location, + options.Tenant, options.RetryPolicy); context.Response.Results = ResponseResult.Create( 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 c8c108bcc3..96b59ecdac 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs @@ -62,6 +62,7 @@ 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( diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs index 986a60b802..487469cc4b 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/GlobalUsings.cs @@ -2,10 +2,6 @@ // Licensed under the MIT License. global using System.CommandLine; -<<<<<<< HEAD -======= -global using System.CommandLine.Parsing; ->>>>>>> 216a9350 (Addressed review comments) global using Azure.Mcp.Core.Commands; global using Azure.Mcp.Core.Models.Command; global using Azure.Mcp.Core.Options; diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs index 6b00c9b6b9..9d83eebd89 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs @@ -19,9 +19,10 @@ public class EventGridService(ISubscriptionService subscriptionService, ITenantS 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)) @@ -52,13 +53,14 @@ public async Task> GetSubscriptionsAsync( 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, null, retryPolicy); + var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy); // If specific topic is requested, get subscriptions for that topic only if (!string.IsNullOrEmpty(topicName)) diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs index ccc51acee2..72bbdb6e7c 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs @@ -8,6 +8,7 @@ public interface IEventGridService Task> GetTopicsAsync( string subscription, string? resourceGroup = null, + string? tenant = null, RetryPolicyOptions? retryPolicy = null); Task> GetSubscriptionsAsync( @@ -15,5 +16,6 @@ Task> GetSubscriptionsAsync( 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.UnitTests/Subscription/SubscriptionListCommandTests.cs b/tools/Azure.Mcp.Tools.EventGrid/tests/Azure.Mcp.Tools.EventGrid.UnitTests/Subscription/SubscriptionListCommandTests.cs index 7f222604d8..a3fc4bb355 100644 --- 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 @@ -51,7 +51,7 @@ public async Task ExecuteAsync_NoParameters_ReturnsSubscriptions() 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()) + _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]); @@ -85,7 +85,7 @@ public async Task ExecuteAsync_WithTopicNameFilter_FiltersCorrectly() 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()) + _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]); @@ -113,7 +113,7 @@ public async Task ExecuteAsync_ReturnsNull_WhenNoSubscriptions() // Arrange var subscription = "sub123"; - _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _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]); @@ -144,7 +144,7 @@ public async Task ExecuteAsync_WithLocationFilter_FiltersCorrectly() 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()) + _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]); @@ -173,7 +173,7 @@ public async Task ExecuteAsync_HandlesException() var expectedError = "Test error"; var subscription = "sub123"; - _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new Exception(expectedError)); var args = _commandDefinition.Parse(["--subscription", subscription]); @@ -210,7 +210,7 @@ public async Task ExecuteAsync_BareTopicName_SearchesAllSubscriptions() }; // When GetSubscriptionsAsync is called with sub-search and topic name return list - _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Is(topicName), Arg.Any(), Arg.Any()) + _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Is(topicName), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(expected); var args = _commandDefinition.Parse(["--topic", topicName]); 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 894aeb5aff..7d8b8d61b4 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,7 +50,7 @@ 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()) + _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_ReturnsNull_WhenNoTopics() // Arrange var subscriptionId = "sub123"; - _eventGridService.GetTopicsAsync(Arg.Is(subscriptionId), Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new List())); + _eventGridService.GetTopicsAsync(Arg.Is(subscriptionId), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); var args = _commandDefinition.Parse(["--subscription", subscriptionId]); @@ -104,7 +104,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]); @@ -134,7 +134,7 @@ 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()) + _eventGridService.GetTopicsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(expectedTopics)); } From 44a04878a8ffb0a5fe4aae631de4bf1347c01e8b Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 11:53:43 -0700 Subject: [PATCH 11/18] Resolved merge conflict- 7 --- .../Subscription/SubscriptionCommand.cs | 20 ++++++++++++++---- .../src/Helpers/EnvironmentVariableHelpers.cs | 21 +++++++++++++++++++ .../Subscription/SubscriptionCommandTests.cs | 20 +++++++++--------- .../Registry/RegistryListCommandTests.cs | 3 ++- .../Subscription/SubscriptionListCommand.cs | 9 +++----- .../Account/AccountGetCommandTests.cs | 7 ++++--- 6 files changed, 56 insertions(+), 24 deletions(-) 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..68c92928e6 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 @@ -8,6 +8,7 @@ using Azure.Mcp.Tools.Storage.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Azure.Mcp.Core.Helpers; using NSubstitute; using Xunit; @@ -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/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 2ea6ce7fb5..2439f01ff5 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 @@ -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.Acr.Commands.Registry; @@ -50,7 +51,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/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index 4aeaca5354..98bce96a49 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -39,17 +39,14 @@ protected override void RegisterOptions(Command command) command.Options.Add(_topicNameOption); command.Options.Add(_locationOption); - // Remove the default subscription validator from base class and add custom validation + // Use the helper method from base class to avoid duplicating AZURE_SUBSCRIPTION_ID parsing logic command.Validators.Clear(); - - // Custom validation: Either --topic or --subscription (or env var) is required command.Validators.Add(commandResult => { - var hasSubscriptionOption = commandResult.HasOptionResult(_subscriptionOption); - var hasEnv = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID")); + var hasSubscription = HasSubscriptionAvailable(commandResult); var hasTopicOption = commandResult.HasOptionResult(_topicNameOption); - if (!hasSubscriptionOption && !hasEnv && !hasTopicOption) + if (!hasSubscription && !hasTopicOption) { commandResult.AddError("Either --subscription or --topic is required."); } 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 767685cea2..4d746fc08e 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,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.Storage.Commands.Account; @@ -138,14 +139,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) @@ -179,7 +180,7 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS finally { // Restore original environment variable - Environment.SetEnvironmentVariable("AZURE_SUBSCRIPTION_ID", originalSubscriptionEnv); + EnvironmentHelpers.SetAzureSubscriptionId(originalSubscriptionEnv); } } From 5d3682909af4329bb0ef1e39d7af722596ec57a2 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Mon, 15 Sep 2025 23:10:26 -0700 Subject: [PATCH 12/18] Dotnet format --- .../Areas/Subscription/SubscriptionCommandTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 68c92928e6..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,13 +2,13 @@ // 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; using Azure.Mcp.Tools.Storage.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Azure.Mcp.Core.Helpers; using NSubstitute; using Xunit; From dc1ca97ef0f1f3f3deff501ec6ac87540e2b65ed Mon Sep 17 00:00:00 2001 From: anannya03 Date: Mon, 15 Sep 2025 23:27:12 -0700 Subject: [PATCH 13/18] AccountGetCommandTest change --- .../Account/AccountGetCommandTests.cs | 1 + 1 file changed, 1 insertion(+) 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 4d746fc08e..cbbd45ed8b 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,7 @@ 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; From 6bdffa61c875f9dd4e8bc6ecf75d0a57f6c277b3 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Tue, 16 Sep 2025 17:09:22 -0700 Subject: [PATCH 14/18] Used validate override --- docs/e2eTestPrompts.md | 2 +- servers/Azure.Mcp.Server/README.md | 3 +- .../Subscription/SubscriptionListCommand.cs | 78 ++++++++++--------- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md index 67d62bb5ba..1558e8e182 100644 --- a/docs/e2eTestPrompts.md +++ b/docs/e2eTestPrompts.md @@ -147,11 +147,11 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | 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 topic 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 00d4b0f469..a5f2a75a48 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -99,9 +99,10 @@ 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-resourcegroup' in my subscription" -* "Show me all Event Grid subscriptions for topic 'my-topic'" * "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 diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index 98bce96a49..9ae760db41 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -3,6 +3,7 @@ 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; @@ -38,27 +39,54 @@ protected override void RegisterOptions(Command command) UseResourceGroup(); // Optional resource group filtering command.Options.Add(_topicNameOption); command.Options.Add(_locationOption); + } + + protected override SubscriptionListOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.TopicName = parseResult.GetValue(_topicNameOption); + options.Location = parseResult.GetValue(_locationOption); + return options; + } - // Use the helper method from base class to avoid duplicating AZURE_SUBSCRIPTION_ID parsing logic - command.Validators.Clear(); - command.Validators.Add(commandResult => + public override ValidationResult Validate(CommandResult commandResult, CommandResponse? commandResponse = null) + { + var result = base.Validate(commandResult, commandResponse); + + if (result.IsValid) { var hasSubscription = HasSubscriptionAvailable(commandResult); var hasTopicOption = commandResult.HasOptionResult(_topicNameOption); + var hasRg = commandResult.HasOptionResult(OptionDefinitions.Common.ResourceGroup); + var hasLocation = commandResult.HasOptionResult(_locationOption); + // Either topic or subscription is mandatory if (!hasSubscription && !hasTopicOption) { - commandResult.AddError("Either --subscription or --topic is required."); + 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."; - protected override SubscriptionListOptions BindOptions(ParseResult parseResult) - { - var options = base.BindOptions(parseResult); - options.TopicName = parseResult.GetValue(_topicNameOption); - options.Location = parseResult.GetValue(_locationOption); - return options; + if (commandResponse != null) + { + commandResponse.Status = 400; + commandResponse.Message = result.ErrorMessage; + } + } + } + + return result; } public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult) @@ -70,34 +98,8 @@ public override async Task ExecuteAsync(CommandContext context, var options = BindOptions(parseResult); - // Validation rules / behaviors: - // 1. Either --topic (bare topic name) or --subscription is mandatory. - // 2. --resource-group and --location are optional but cannot be provided alone (must accompany --topic or --subscription). - // 3. If only --topic (bare name) is provided (no --subscription) perform a cross-subscription search - // across all accessible subscriptions for a topic with that name and aggregate results. - // 4. If only --subscription is provided (no --topic) list all event subscriptions from every custom and system - // topic in that subscription (optionally filtered by --resource-group / --location). - var hasSubscription = !string.IsNullOrWhiteSpace(options.Subscription); var hasTopic = !string.IsNullOrWhiteSpace(options.TopicName); - var hasRg = !string.IsNullOrWhiteSpace(options.ResourceGroup); - var hasLocation = !string.IsNullOrWhiteSpace(options.Location); - - // Either topic or subscription is mandatory - if (!hasSubscription && !hasTopic) - { - context.Response.Status = 400; - context.Response.Message = "Either --subscription or --topic is required."; - return context.Response; - } - - // Location and resource-group can only be used with subscription or topic - if ((hasRg || hasLocation) && !hasSubscription && !hasTopic) - { - context.Response.Status = 400; - context.Response.Message = "Either --subscription or --topic is required when using --resource-group or --location."; - return context.Response; - } // Bare topic name without subscription triggers cross-subscription search bool crossSubscriptionSearch = !hasSubscription && hasTopic; From 6421a7c766eb72059ce672a4b315cfd06777c597 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Tue, 16 Sep 2025 17:35:12 -0700 Subject: [PATCH 15/18] Fixed the validation logic --- .../Subscription/SubscriptionListCommand.cs | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index 9ae760db41..c1594c870a 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -51,38 +51,36 @@ protected override SubscriptionListOptions BindOptions(ParseResult parseResult) public override ValidationResult Validate(CommandResult commandResult, CommandResponse? commandResponse = null) { - var result = base.Validate(commandResult, commandResponse); + // Skip the base validation that requires subscription and implement custom validation + var result = new ValidationResult { IsValid = true }; - if (result.IsValid) + var hasSubscription = HasSubscriptionAvailable(commandResult); + var hasTopicOption = commandResult.HasOptionResult(_topicNameOption); + var hasRg = commandResult.HasOptionResult(OptionDefinitions.Common.ResourceGroup); + var hasLocation = commandResult.HasOptionResult(_locationOption); + + // Either topic or subscription is mandatory + if (!hasSubscription && !hasTopicOption) { - var hasSubscription = HasSubscriptionAvailable(commandResult); - var hasTopicOption = commandResult.HasOptionResult(_topicNameOption); - var hasRg = commandResult.HasOptionResult(OptionDefinitions.Common.ResourceGroup); - var hasLocation = commandResult.HasOptionResult(_locationOption); + result.IsValid = false; + result.ErrorMessage = "Either --subscription or --topic is required."; - // Either topic or subscription is mandatory - if (!hasSubscription && !hasTopicOption) + if (commandResponse != null) { - result.IsValid = false; - result.ErrorMessage = "Either --subscription or --topic is required."; - - if (commandResponse != null) - { - commandResponse.Status = 400; - commandResponse.Message = result.ErrorMessage; - } + 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."; + } + // 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; - } + if (commandResponse != null) + { + commandResponse.Status = 400; + commandResponse.Message = result.ErrorMessage; } } From 75d73bf95047a72878a492a017606adc05068d36 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 13:49:34 -0700 Subject: [PATCH 16/18] Rebased it to match the new structure --- .../Subscription/SubscriptionListCommand.cs | 18 ++-- .../src/EventGridSetup.cs | 1 + .../src/Options/EventGridOptionDefinitions.cs | 2 - .../SubscriptionListCommandTests.cs | 102 ++++++++++-------- 4 files changed, 66 insertions(+), 57 deletions(-) diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index c1594c870a..5c792d5cf7 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -1,6 +1,7 @@ // 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; @@ -15,8 +16,6 @@ public sealed class SubscriptionListCommand(ILogger log { private const string CommandTitle = "List Event Grid Subscriptions"; private readonly ILogger _logger = logger; - private readonly Option _topicNameOption = EventGridOptionDefinitions.TopicName; - private readonly Option _locationOption = EventGridOptionDefinitions.Location; public override string Name => "list"; @@ -36,16 +35,17 @@ public sealed class SubscriptionListCommand(ILogger log protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - UseResourceGroup(); // Optional resource group filtering - command.Options.Add(_topicNameOption); - command.Options.Add(_locationOption); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsOptional()); + command.Options.Add(EventGridOptionDefinitions.TopicName.AsOptional()); + command.Options.Add(EventGridOptionDefinitions.Location.AsOptional()); } protected override SubscriptionListOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.TopicName = parseResult.GetValue(_topicNameOption); - options.Location = parseResult.GetValue(_locationOption); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.TopicName = parseResult.GetValueOrDefault(EventGridOptionDefinitions.TopicName.Name); + options.Location = parseResult.GetValueOrDefault(EventGridOptionDefinitions.Location.Name); return options; } @@ -55,9 +55,9 @@ public override ValidationResult Validate(CommandResult commandResult, CommandRe var result = new ValidationResult { IsValid = true }; var hasSubscription = HasSubscriptionAvailable(commandResult); - var hasTopicOption = commandResult.HasOptionResult(_topicNameOption); + var hasTopicOption = commandResult.HasOptionResult(EventGridOptionDefinitions.TopicName); var hasRg = commandResult.HasOptionResult(OptionDefinitions.Common.ResourceGroup); - var hasLocation = commandResult.HasOptionResult(_locationOption); + var hasLocation = commandResult.HasOptionResult(EventGridOptionDefinitions.Location); // Either topic or subscription is mandatory if (!hasSubscription && !hasTopicOption) diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs b/tools/Azure.Mcp.Tools.EventGrid/src/EventGridSetup.cs index 23e1ccdd58..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; diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs index 525831ae7f..c51bc62339 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Options/EventGridOptionDefinitions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.CommandLine; - namespace Azure.Mcp.Tools.EventGrid.Options; public static class EventGridOptionDefinitions 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 index a3fc4bb355..e3cfdae527 100644 --- 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 @@ -4,10 +4,14 @@ 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; @@ -21,6 +25,7 @@ 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; @@ -29,17 +34,27 @@ public class SubscriptionListCommandTests public SubscriptionListCommandTests() { _eventGridService = Substitute.For(); + _subscriptionService = Substitute.For(); _logger = Substitute.For>(); - var collection = new ServiceCollection(); - collection.AddSingleton(_eventGridService); - + 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() { @@ -167,69 +182,64 @@ public async Task ExecuteAsync_WithLocationFilter_FiltersCorrectly() } [Fact] - public async Task ExecuteAsync_HandlesException() + public async Task ExecuteAsync_HandlesServiceErrors() { // Arrange - var expectedError = "Test error"; - var subscription = "sub123"; + _eventGridService.GetSubscriptionsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromException>(new Exception("Test error"))); - _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .ThrowsAsync(new Exception(expectedError)); - - var args = _commandDefinition.Parse(["--subscription", subscription]); + var parseResult = _commandDefinition.Parse(["--subscription", "sub"]); // Act - var response = await _command.ExecuteAsync(_context, args); + var response = await _command.ExecuteAsync(_context, parseResult); // Assert - Assert.NotNull(response); Assert.Equal(500, response.Status); - Assert.StartsWith(expectedError, response.Message); + Assert.Contains("Test error", response.Message); + Assert.Contains("troubleshooting", response.Message); } - [Fact] - public async Task ExecuteAsync_ErrorWhenOnlyLocationProvided() - { - var args = _commandDefinition.Parse(["--location", "eastus"]); - var response = await _command.ExecuteAsync(_context, args); - Assert.Equal(400, response.Status); - Assert.Contains("Either --subscription or --topic is required", response.Message); - } - [Fact] - public async Task ExecuteAsync_BareTopicName_SearchesAllSubscriptions() + + [Theory] + [InlineData("--subscription sub", true)] + [InlineData("--subscription sub --topic my-topic", true)] + [InlineData("--subscription sub --resource-group rg", true)] + [InlineData("", false)] + [InlineData("--location eastus", false)] + [InlineData("--resource-group rg", false)] + [InlineData("--topic my-topic", false)] // Cross-subscription search needs special handling, test separately + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { - // Arrange: We only validate that the command executes successfully without a subscription parameter. - // Detailed cross-subscription enumeration is handled inside the command and service; here we simulate one hit. - var topicName = "myTopic"; - var subscription = "sub-search"; - var expected = new List + // Arrange + if (shouldSucceed) { - new("from-search", "Microsoft.EventGrid/eventSubscriptions", "WebHook", "https://example.com/hook", "Succeeded", null, null, 30, 1440, "2023-01-01T00:00:00Z", "2023-01-02T00:00:00Z") - }; + _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") + }); + } - // When GetSubscriptionsAsync is called with sub-search and topic name return list - _eventGridService.GetSubscriptionsAsync(Arg.Is(subscription), Arg.Any(), Arg.Is(topicName), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(expected); - - var args = _commandDefinition.Parse(["--topic", topicName]); + var parseResult = _commandDefinition.Parse(args); // Act - var response = await _command.ExecuteAsync(_context, args); + var response = await _command.ExecuteAsync(_context, parseResult); - // Assert basic success (can't fully assert aggregate without subscription service mock wiring) - Assert.NotEqual(400, response.Status); + // Assert + Assert.Equal(shouldSucceed ? 200 : 400, response.Status); + if (shouldSucceed) + { + Assert.NotNull(response.Results); + } + else + { + Assert.Contains("required", response.Message.ToLower()); + } } - [Fact] - public async Task ExecuteAsync_ErrorWhenOnlyResourceGroupProvided() - { - var args = _commandDefinition.Parse(["--resource-group", "rg1"]); - var response = await _command.ExecuteAsync(_context, args); - Assert.Equal(400, response.Status); - Assert.Contains("Either --subscription or --topic is required", response.Message); - } + private class SubscriptionListResult { From a0144b275f0f4fac305aa74bc9bb82bf78369ad1 Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 14:43:26 -0700 Subject: [PATCH 17/18] Fixed the changes after rebase --- .../src/Commands/EventGridJsonContext.cs | 6 ++--- .../Subscription/SubscriptionListCommand.cs | 10 +++++++- .../src/Commands/Topic/TopicListCommand.cs | 7 +++--- .../src/Models/EventGridSubscriptionInfo.cs | 24 ++++++++++--------- .../src/Services/IEventGridService.cs | 2 ++ .../Topic/TopicListCommandTests.cs | 5 ++-- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs index 8dd17b565d..301ad71ac6 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/EventGridJsonContext.cs @@ -7,11 +7,11 @@ namespace Azure.Mcp.Tools.EventGrid.Commands; -[JsonSerializable(typeof(TopicListCommand.TopicListCommandResult))] [JsonSerializable(typeof(SubscriptionListCommand.SubscriptionListCommandResult))] -[JsonSerializable(typeof(EventGridTopicInfo))] +[JsonSerializable(typeof(TopicListCommand.TopicListCommandResult))] [JsonSerializable(typeof(EventGridSubscriptionInfo))] -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(EventGridTopicInfo))] +[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 index 5c792d5cf7..054e468606 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -30,7 +30,15 @@ public sealed class SubscriptionListCommand(ILogger log public override string Title => CommandTitle; - public override ToolMetadata Metadata => new() { Destructive = false, ReadOnly = true }; + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = true, + OpenWorld = true, + ReadOnly = true, + LocalRequired = false, + Secret = false + }; protected override void RegisterOptions(Command command) { 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 ef84cf35c4..3cf25100c1 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; @@ -65,9 +66,9 @@ public override async Task ExecuteAsync(CommandContext context, options.Tenant, options.RetryPolicy); - context.Response.Results = topics?.Count > 0 - ? ResponseResult.Create(new TopicListCommandResult(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/Models/EventGridSubscriptionInfo.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Models/EventGridSubscriptionInfo.cs index 56147794b2..7881da1f45 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Models/EventGridSubscriptionInfo.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Models/EventGridSubscriptionInfo.cs @@ -1,18 +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( - string Name, - string Type, - string? EndpointType, - string? EndpointUrl, - string? ProvisioningState, - string? DeadLetterDestination, - string? Filter, - int? MaxDeliveryAttempts, - int? EventTimeToLiveInMinutes, - string? CreatedDateTime, - string? UpdatedDateTime + [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/Services/IEventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/IEventGridService.cs index 72bbdb6e7c..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 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 b83ae5568f..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 @@ -90,10 +90,9 @@ public async Task ExecuteAsync_ReturnsEmpty_WhenNoTopics() Assert.NotNull(response.Results); var json = JsonSerializer.Serialize(response.Results); - var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - var result = JsonSerializer.Deserialize(json, options); + var result = JsonSerializer.Deserialize(json, EventGridJsonContext.Default.TopicListCommandResult); Assert.NotNull(result); - Assert.NotNull(result.Topics); + Assert.NotNull(result!.Topics); Assert.Empty(result.Topics); } From bfffbacb66cc133adba43a0876e91ce437bbd18d Mon Sep 17 00:00:00 2001 From: anannya03 Date: Wed, 17 Sep 2025 15:08:00 -0700 Subject: [PATCH 18/18] Addressed review comments --- .vscode/mcp.json | 48 ------------------- .../Registry/RegistryListCommandTests.cs | 2 +- .../Subscription/SubscriptionListCommand.cs | 8 ++-- .../src/Commands/Topic/TopicListCommand.cs | 2 +- .../SubscriptionListCommandTests.cs | 6 ++- 5 files changed, 11 insertions(+), 55 deletions(-) delete mode 100644 .vscode/mcp.json diff --git a/.vscode/mcp.json b/.vscode/mcp.json deleted file mode 100644 index 6f0a9a2c45..0000000000 --- a/.vscode/mcp.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "servers": { - "Azure MCP Server - Local Development": { - "command": "dotnet", - "args": [ - "run", - "--project", - "servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj", - "--", - "server", - "start" - ], - "env": { - "AZURE_MCP_LOG_LEVEL": "Debug", - "AZURE_MCP_COLLECT_TELEMETRY": "false" - } - }, - "Azure MCP Server - Release Build": { - "command": "dotnet", - "args": [ - "servers/Azure.Mcp.Server/src/bin/Release/net9.0/azmcp.dll", - "server", - "start" - ], - "env": { - "AZURE_MCP_LOG_LEVEL": "Debug", - "AZURE_MCP_COLLECT_TELEMETRY": "false" - } - }, - "Azure Event Grid Tools Only": { - "command": "dotnet", - "args": [ - "run", - "--project", - "servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj", - "--", - "server", - "start", - "--namespace", - "eventgrid" - ], - "env": { - "AZURE_MCP_LOG_LEVEL": "Debug", - "AZURE_MCP_COLLECT_TELEMETRY": "false" - } - } - } -} \ No newline at end of file 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 8e13af2a53..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 @@ -2,8 +2,8 @@ // Licensed under the MIT License. using System.CommandLine; -using Azure.Mcp.Core.Helpers; 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; diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs index 054e468606..f348a6109c 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Subscription/SubscriptionListCommand.cs @@ -25,7 +25,7 @@ public sealed class SubscriptionListCommand(ILogger log 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. Examples:\n --subscription \n --subscription --topic \n --topic + may only be used alongside --subscription or --topic. """; public override string Title => CommandTitle; @@ -43,9 +43,9 @@ public sealed class SubscriptionListCommand(ILogger log protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsOptional()); - command.Options.Add(EventGridOptionDefinitions.TopicName.AsOptional()); - command.Options.Add(EventGridOptionDefinitions.Location.AsOptional()); + command.Options.Add(OptionDefinitions.Common.ResourceGroup); + command.Options.Add(EventGridOptionDefinitions.TopicName); + command.Options.Add(EventGridOptionDefinitions.Location); } protected override SubscriptionListOptions BindOptions(ParseResult parseResult) 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 3cf25100c1..5c31a26fa7 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Commands/Topic/TopicListCommand.cs @@ -38,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) 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 index e3cfdae527..240d575327 100644 --- 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 @@ -206,10 +206,10 @@ public async Task ExecuteAsync_HandlesServiceErrors() [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)] - [InlineData("--topic my-topic", false)] // Cross-subscription search needs special handling, test separately public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { // Arrange @@ -220,6 +220,10 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS { 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);