diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/CommandFactoryHelpers.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/CommandFactoryHelpers.cs index 5e331c3560..f4b8be07bc 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/CommandFactoryHelpers.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/CommandFactoryHelpers.cs @@ -6,21 +6,35 @@ using Azure.Mcp.Core.Areas.Subscription; using Azure.Mcp.Core.Commands; using Azure.Mcp.Core.Configuration; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Services.Azure.ResourceGroup; +using Azure.Mcp.Core.Services.Azure.Subscription; +using Azure.Mcp.Core.Services.Azure.Tenant; +using Azure.Mcp.Core.Services.Caching; using Azure.Mcp.Core.Services.Telemetry; using Azure.Mcp.Tools.Acr; +using Azure.Mcp.Tools.Advisor; using Azure.Mcp.Tools.Aks; using Azure.Mcp.Tools.AppConfig; using Azure.Mcp.Tools.AppLens; +using Azure.Mcp.Tools.ApplicationInsights; +using Azure.Mcp.Tools.AppService; using Azure.Mcp.Tools.Authorization; using Azure.Mcp.Tools.AzureBestPractices; using Azure.Mcp.Tools.AzureIsv; +using Azure.Mcp.Tools.AzureMigrate; using Azure.Mcp.Tools.AzureTerraformBestPractices; using Azure.Mcp.Tools.BicepSchema; using Azure.Mcp.Tools.CloudArchitect; +using Azure.Mcp.Tools.Communication; +using Azure.Mcp.Tools.Compute; +using Azure.Mcp.Tools.ConfidentialLedger; using Azure.Mcp.Tools.Cosmos; using Azure.Mcp.Tools.Deploy; using Azure.Mcp.Tools.EventGrid; +using Azure.Mcp.Tools.EventHubs; using Azure.Mcp.Tools.Extension; +using Azure.Mcp.Tools.FileShares; using Azure.Mcp.Tools.Foundry; using Azure.Mcp.Tools.FunctionApp; using Azure.Mcp.Tools.Grafana; @@ -31,20 +45,27 @@ using Azure.Mcp.Tools.Marketplace; using Azure.Mcp.Tools.Monitor; using Azure.Mcp.Tools.MySql; +using Azure.Mcp.Tools.Policy; using Azure.Mcp.Tools.Postgres; +using Azure.Mcp.Tools.Pricing; using Azure.Mcp.Tools.Quota; using Azure.Mcp.Tools.Redis; using Azure.Mcp.Tools.ResourceHealth; using Azure.Mcp.Tools.Search; using Azure.Mcp.Tools.ServiceBus; +using Azure.Mcp.Tools.SignalR; +using Azure.Mcp.Tools.Speech; using Azure.Mcp.Tools.Sql; using Azure.Mcp.Tools.Storage; +using Azure.Mcp.Tools.StorageSync; using Azure.Mcp.Tools.VirtualDesktop; using Azure.Mcp.Tools.Workbooks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Mcp.Core.Areas; using ModelContextProtocol.Protocol; +using NSubstitute; namespace Azure.Mcp.Core.UnitTests.Areas.Server; @@ -59,20 +80,29 @@ public static ICommandFactory CreateCommandFactory(IServiceProvider? serviceProv // Tool areas new AcrSetup(), + new AdvisorSetup(), new AksSetup(), new AppConfigSetup(), new AppLensSetup(), + new ApplicationInsightsSetup(), + new AppServiceSetup(), new AuthorizationSetup(), new AzureBestPracticesSetup(), new AzureIsvSetup(), new ManagedLustreSetup(), + new AzureMigrateSetup(), new AzureTerraformBestPracticesSetup(), new BicepSchemaSetup(), new CloudArchitectSetup(), + new CommunicationSetup(), + new ComputeSetup(), + new ConfidentialLedgerSetup(), new CosmosSetup(), new DeploySetup(), new EventGridSetup(), + new EventHubsSetup(), new ExtensionSetup(), + new FileSharesSetup(), new FoundrySetup(), new FunctionAppSetup(), new GrafanaSetup(), @@ -82,14 +112,19 @@ public static ICommandFactory CreateCommandFactory(IServiceProvider? serviceProv new MarketplaceSetup(), new MonitorSetup(), new MySqlSetup(), + new PolicySetup(), new PostgresSetup(), + new PricingSetup(), new QuotaSetup(), new RedisSetup(), new ResourceHealthSetup(), new SearchSetup(), new ServiceBusSetup(), + new SignalRSetup(), + new SpeechSetup(), new SqlSetup(), new StorageSetup(), + new StorageSyncSetup(), new VirtualDesktopSetup(), new WorkbooksSetup(), ]; @@ -123,20 +158,29 @@ public static IServiceCollection SetupCommonServices() // Tool areas new AcrSetup(), + new AdvisorSetup(), new AksSetup(), new AppConfigSetup(), new AppLensSetup(), + new ApplicationInsightsSetup(), + new AppServiceSetup(), new AuthorizationSetup(), new AzureBestPracticesSetup(), new AzureIsvSetup(), new ManagedLustreSetup(), + new AzureMigrateSetup(), new AzureTerraformBestPracticesSetup(), new BicepSchemaSetup(), new CloudArchitectSetup(), + new CommunicationSetup(), + new ComputeSetup(), + new ConfidentialLedgerSetup(), new CosmosSetup(), new DeploySetup(), new EventGridSetup(), + new EventHubsSetup(), new ExtensionSetup(), + new FileSharesSetup(), new FoundrySetup(), new FunctionAppSetup(), new GrafanaSetup(), @@ -146,22 +190,34 @@ public static IServiceCollection SetupCommonServices() new MarketplaceSetup(), new MonitorSetup(), new MySqlSetup(), + new PolicySetup(), new PostgresSetup(), + new PricingSetup(), new QuotaSetup(), new RedisSetup(), new ResourceHealthSetup(), new SearchSetup(), new ServiceBusSetup(), + new SignalRSetup(), + new SpeechSetup(), new SqlSetup(), new StorageSetup(), + new StorageSyncSetup(), new VirtualDesktopSetup(), new WorkbooksSetup(), ]; var builder = new ServiceCollection() .AddLogging() + .AddMemoryCache() + .AddHttpClientServices(configureDefaults: true) + .AddSingleUserCliCacheService() .AddSingleton(); + builder.AddSingleton(_ => Substitute.For()); + builder.AddSingleton(_ => Substitute.For()); + builder.AddSingleton(_ => Substitute.For()); + foreach (var area in areaSetups) { area.ConfigureServices(builder); diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategyTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategyTests.cs index c59b23267e..558f1fd06b 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategyTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.UnitTests/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategyTests.cs @@ -6,6 +6,7 @@ using Azure.Mcp.Core.Commands; using Azure.Mcp.Core.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Mcp.Core.Commands; using Xunit; namespace Azure.Mcp.Core.UnitTests.Areas.Server.Commands.Discovery; @@ -65,6 +66,46 @@ public void CreateConsolidatedCommandFactory_WithDefaultOptions_ReturnsCommandFa Assert.True(factory.AllCommands.Count > 10); } + [Fact] + public void CreateConsolidatedCommandFactory_MapsAllRegisteredCommands() + { + // Arrange + var sourceFactory = CommandFactoryHelpers.CreateCommandFactory(); + var strategy = CreateStrategy(commandFactory: sourceFactory); + + // Act - in DEBUG, CreateConsolidatedCommandFactory throws if any commands are unmapped + // or if metadata mismatches are detected + var consolidatedFactory = strategy.CreateConsolidatedCommandFactory(); + + // Assert - consolidated factory should map all non-ignored source commands + var ignoredGroups = new HashSet( + ConsolidatedToolDiscoveryStrategy.IgnoredCommandGroups, + StringComparer.OrdinalIgnoreCase); + + var expectedCount = sourceFactory.AllCommands.Count(kvp => + { + var area = sourceFactory.GetServiceArea(kvp.Key); + return area == null || !ignoredGroups.Contains(area); + }); + + Assert.Equal(expectedCount, consolidatedFactory.AllCommands.Count); + } + + [Fact] + public void CreateConsolidatedCommandFactory_WithAllAreas_HasSubstantialCommandCount() + { + // Arrange + var strategy = CreateStrategy(); + + // Act + var factory = strategy.CreateConsolidatedCommandFactory(); + + // Assert - with all tool areas registered, expect a substantial number of commands + Assert.True(factory.AllCommands.Count > 200, + $"Expected more than 200 consolidated commands but found {factory.AllCommands.Count}. " + + "Ensure CommandFactoryHelpers registers all tool areas matching production Program.cs."); + } + [Fact] public void CreateConsolidatedCommandFactory_WithNamespaceFilter_FiltersCommands() { @@ -116,4 +157,50 @@ public void CreateConsolidatedCommandFactory_HandlesEmptyNamespaceFilter() var allCommands = factory.AllCommands; Assert.True(allCommands.Count > 0); } + + [Fact] + public void AreMetadataEqual_BothNull_ReturnsTrue() + { + Assert.True(ConsolidatedToolDiscoveryStrategy.AreMetadataEqual(null, null)); + } + + [Fact] + public void AreMetadataEqual_OneNull_ReturnsFalse() + { + var metadata = new ToolMetadata { Destructive = false, ReadOnly = true }; + Assert.False(ConsolidatedToolDiscoveryStrategy.AreMetadataEqual(metadata, null)); + Assert.False(ConsolidatedToolDiscoveryStrategy.AreMetadataEqual(null, metadata)); + } + + [Fact] + public void AreMetadataEqual_MatchingValues_ReturnsTrue() + { + var metadata1 = new ToolMetadata + { + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false + }; + var metadata2 = new ToolMetadata + { + Destructive = false, + Idempotent = true, + OpenWorld = false, + ReadOnly = true, + Secret = false, + LocalRequired = false + }; + Assert.True(ConsolidatedToolDiscoveryStrategy.AreMetadataEqual(metadata1, metadata2)); + } + + [Fact] + public void AreMetadataEqual_DifferentValues_ReturnsFalse() + { + var metadata1 = new ToolMetadata { Destructive = false, ReadOnly = true }; + var metadata2 = new ToolMetadata { Destructive = true, ReadOnly = true }; + Assert.False(ConsolidatedToolDiscoveryStrategy.AreMetadataEqual(metadata1, metadata2)); + } } diff --git a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategy.cs b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategy.cs index 1fb93d7265..9673fcab95 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategy.cs +++ b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategy.cs @@ -78,7 +78,7 @@ public ICommandFactory CreateConsolidatedCommandFactory() #if DEBUG // In debug mode, validate that all tools in MappedToolList found a match when conditions are met - if (_options.Value.ReadOnly == false && (_options.Value.Namespace == null || _options.Value.Namespace.Length == 0)) + if ((_options.Value.ReadOnly ?? false) == false && (_options.Value.Namespace == null || _options.Value.Namespace.Length == 0)) { if (consolidatedTool.MappedToolList != null) { @@ -205,7 +205,7 @@ private Dictionary FilterCommands(IReadOnlyDictionary _options.Value.ReadOnly == false || kvp.Value.Metadata.ReadOnly == true) + .Where(kvp => (_options.Value.ReadOnly ?? false) == false || kvp.Value.Metadata.ReadOnly == true) .Where(kvp => { // Filter by namespace if specified