From 3f6f3db560f7a725e436698ca1a5b37bac1caa51 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 18:40:03 -0700 Subject: [PATCH 01/14] Normalize guidance command result roots --- .../src/Commands/AdvisorJsonContext.cs | 2 +- .../RecommendationApplyCommand.cs | 8 ++- .../RecommendationApplyCommandTests.cs | 22 +++---- .../src/Commands/AIAppBestPracticesCommand.cs | 9 ++- .../Commands/AzureBestPracticesJsonContext.cs | 3 +- .../src/Commands/BestPracticesCommand.cs | 9 ++- .../AIAppBestPracticesCommandTests.cs | 12 ++-- .../BestPracticesCommandTests.cs | 66 +++++++++---------- .../AzureTerraformBestPracticesGetCommand.cs | 9 ++- .../AzureTerraformBestPracticesJsonContext.cs | 2 +- ...reTerraformBestPracticesGetCommandTests.cs | 10 +-- .../ServiceGuide/ServiceGuideGetCommand.cs | 12 +++- .../WellArchitectedFrameworkJsonContext.cs | 2 +- .../ServiceGuideGetCommandTests.cs | 38 +++++------ 14 files changed, 115 insertions(+), 89 deletions(-) diff --git a/tools/Azure.Mcp.Tools.Advisor/src/Commands/AdvisorJsonContext.cs b/tools/Azure.Mcp.Tools.Advisor/src/Commands/AdvisorJsonContext.cs index 957af115cb..3f7dbc2ed2 100644 --- a/tools/Azure.Mcp.Tools.Advisor/src/Commands/AdvisorJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Advisor/src/Commands/AdvisorJsonContext.cs @@ -7,7 +7,7 @@ namespace Azure.Mcp.Tools.Advisor.Commands; [JsonSerializable(typeof(RecommendationListCommand.RecommendationListResult))] [JsonSerializable(typeof(RecommendationTypeListCommand.RecommendationTypeListResult))] [JsonSerializable(typeof(RecommendationSummaryCommand.RecommendationSummaryResult))] -[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(RecommendationApplyCommand.RecommendationApplyCommandResult))] [JsonSerializable(typeof(RecommendationData))] [JsonSerializable(typeof(Models.Recommendation))] [JsonSerializable(typeof(Models.RecommendationType))] diff --git a/tools/Azure.Mcp.Tools.Advisor/src/Commands/Recommendation/RecommendationApplyCommand.cs b/tools/Azure.Mcp.Tools.Advisor/src/Commands/Recommendation/RecommendationApplyCommand.cs index 9edaba9fed..92a756e386 100644 --- a/tools/Azure.Mcp.Tools.Advisor/src/Commands/Recommendation/RecommendationApplyCommand.cs +++ b/tools/Azure.Mcp.Tools.Advisor/src/Commands/Recommendation/RecommendationApplyCommand.cs @@ -24,7 +24,7 @@ namespace Azure.Mcp.Tools.Advisor.Commands.Recommendation; Secret = false )] public sealed class RecommendationApplyCommand(ILogger logger) - : BaseCommand> + : BaseCommand { private readonly ILogger _logger = logger; private static readonly ConcurrentDictionary s_advisorRecommendationRulesCache = new(); @@ -51,7 +51,9 @@ public override Task ExecuteAsync(CommandContext context, Recom var resourceFileName = $"{options.Resource}.json"; var recommendationApplyRules = GetAdvisorRecommendationRules(resourceFileName); - context.Response.Results = ResponseResult.Create([recommendationApplyRules], AdvisorJsonContext.Default.ListString); + context.Response.Results = ResponseResult.Create( + new([recommendationApplyRules]), + AdvisorJsonContext.Default.RecommendationApplyCommandResult); context.Activity?.AddTag("RecommendationRules_Resource", options.Resource); } @@ -102,4 +104,6 @@ private static HashSet LoadAvailableResources() return resources; } + + public sealed record RecommendationApplyCommandResult(List Rules); } diff --git a/tools/Azure.Mcp.Tools.Advisor/tests/Azure.Mcp.Tools.Advisor.Tests/Recommendation/RecommendationApplyCommandTests.cs b/tools/Azure.Mcp.Tools.Advisor/tests/Azure.Mcp.Tools.Advisor.Tests/Recommendation/RecommendationApplyCommandTests.cs index a973ab5fc2..bbe3c6960c 100644 --- a/tools/Azure.Mcp.Tools.Advisor/tests/Azure.Mcp.Tools.Advisor.Tests/Recommendation/RecommendationApplyCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Advisor/tests/Azure.Mcp.Tools.Advisor.Tests/Recommendation/RecommendationApplyCommandTests.cs @@ -61,11 +61,11 @@ public async Task ExecuteAsync_DeserializationValidation(string resource) { var response = await ExecuteCommandAsync("--resource", resource); - var result = ValidateAndDeserializeResponse(response, AdvisorJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AdvisorJsonContext.Default.RecommendationApplyCommandResult); Assert.NotNull(result); - Assert.NotEmpty(result); - Assert.Contains("rules", result[0]); + Assert.NotEmpty(result.Rules); + Assert.Contains("rules", result.Rules[0]); } [Fact] @@ -128,12 +128,12 @@ public async Task ExecuteAsync_ReturnsCachedResultOnSubsequentCalls() var response1 = await ExecuteCommandAsync("--resource", "keyvault_vaults"); var response2 = await ExecuteCommandAsync("--resource", "keyvault_vaults"); - var result1 = ValidateAndDeserializeResponse(response1, AdvisorJsonContext.Default.ListString); - var result2 = ValidateAndDeserializeResponse(response2, AdvisorJsonContext.Default.ListString); + var result1 = ValidateAndDeserializeResponse(response1, AdvisorJsonContext.Default.RecommendationApplyCommandResult); + var result2 = ValidateAndDeserializeResponse(response2, AdvisorJsonContext.Default.RecommendationApplyCommandResult); - Assert.Single(result1); - Assert.Single(result2); - Assert.Equal(result1[0], result2[0]); + Assert.Single(result1.Rules); + Assert.Single(result2.Rules); + Assert.Equal(result1.Rules[0], result2.Rules[0]); } [Theory] @@ -156,8 +156,8 @@ public async Task ExecuteAsync_AllResources_ReturnValidRules(string resource) { var response = await ExecuteCommandAsync("--resource", resource); - var result = ValidateAndDeserializeResponse(response, AdvisorJsonContext.Default.ListString); - Assert.NotEmpty(result); - Assert.Contains("rules", result[0]); + var result = ValidateAndDeserializeResponse(response, AdvisorJsonContext.Default.RecommendationApplyCommandResult); + Assert.NotEmpty(result.Rules); + Assert.Contains("rules", result.Rules[0]); } } diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AIAppBestPracticesCommand.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AIAppBestPracticesCommand.cs index 15598ef9c2..4983224d85 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AIAppBestPracticesCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AIAppBestPracticesCommand.cs @@ -26,7 +26,8 @@ namespace Azure.Mcp.Tools.AzureBestPractices.Commands; ReadOnly = true, Secret = false, LocalRequired = false)] -public sealed class AIAppBestPracticesCommand(ILogger logger) : BaseCommand> +public sealed class AIAppBestPracticesCommand(ILogger logger) + : BaseCommand { private readonly ILogger _logger = logger; private static readonly string s_bestPracticesText = LoadBestPracticesText(); @@ -57,7 +58,9 @@ public override Task ExecuteAsync(CommandContext context, Empty { var bestPractices = GetBestPracticesText(); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create([bestPractices], AzureBestPracticesJsonContext.Default.ListString); + context.Response.Results = ResponseResult.Create( + new([bestPractices]), + AzureBestPracticesJsonContext.Default.AIAppBestPracticesCommandResult); context.Response.Message = string.Empty; } catch (Exception ex) @@ -68,4 +71,6 @@ public override Task ExecuteAsync(CommandContext context, Empty return Task.FromResult(context.Response); } + + public sealed record AIAppBestPracticesCommandResult(List BestPractices); } diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AzureBestPracticesJsonContext.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AzureBestPracticesJsonContext.cs index abb3090f5e..52fbfc0cb3 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AzureBestPracticesJsonContext.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/AzureBestPracticesJsonContext.cs @@ -5,6 +5,7 @@ namespace Azure.Mcp.Tools.AzureBestPractices.Commands; -[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(AIAppBestPracticesCommand.AIAppBestPracticesCommandResult))] +[JsonSerializable(typeof(BestPracticesCommand.BestPracticesCommandResult))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] internal partial class AzureBestPracticesJsonContext : JsonSerializerContext; diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/BestPracticesCommand.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/BestPracticesCommand.cs index 0e73a485d2..40e768b961 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/BestPracticesCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/src/Commands/BestPracticesCommand.cs @@ -32,7 +32,8 @@ it belongs to the Azure Best Practices category. ReadOnly = true, Secret = false, LocalRequired = false)] -public sealed class BestPracticesCommand(ILogger logger) : BaseCommand> +public sealed class BestPracticesCommand(ILogger logger) + : BaseCommand { private readonly ILogger _logger = logger; private static readonly ConcurrentDictionary s_bestPracticesCache = []; @@ -77,7 +78,9 @@ public override Task ExecuteAsync(CommandContext context, BestP var bestPractices = GetBestPracticesText(resourceFileName); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create([bestPractices], AzureBestPracticesJsonContext.Default.ListString); + context.Response.Results = ResponseResult.Create( + new([bestPractices]), + AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); context.Response.Message = string.Empty; context.Activity?.AddTag("BestPractices_Resource", options.Resource); @@ -159,4 +162,6 @@ private static string LoadBestPracticesText(string resourceFileName) return EmbeddedResourceHelper.ReadEmbeddedResource(assembly, resourceName); } } + + public sealed record BestPracticesCommandResult(List BestPractices); } diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/AIAppBestPracticesCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/AIAppBestPracticesCommandTests.cs index 272e1891e3..bd5eb82f2d 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/AIAppBestPracticesCommandTests.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/AIAppBestPracticesCommandTests.cs @@ -15,13 +15,13 @@ public async Task ExecuteAsync_ReturnsAzureAIAppBestPractices() var response = await ExecuteCommandAsync([]); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.AIAppBestPracticesCommandResult); - Assert.Contains("Microsoft Agent Framework", result[0]); - Assert.Contains("AIProjectClient", result[0]); - Assert.Contains("Build and Verification", result[0]); - Assert.Contains("Understanding AI Models Hierarchy", result[0]); - Assert.Contains("CORRECT Pattern", result[0]); + Assert.Contains("Microsoft Agent Framework", result.BestPractices[0]); + Assert.Contains("AIProjectClient", result.BestPractices[0]); + Assert.Contains("Build and Verification", result.BestPractices[0]); + Assert.Contains("Understanding AI Models Hierarchy", result.BestPractices[0]); + Assert.Contains("CORRECT Pattern", result.BestPractices[0]); } [Fact] diff --git a/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/BestPracticesCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/BestPracticesCommandTests.cs index a029b70e20..31f6e8df03 100644 --- a/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/BestPracticesCommandTests.cs +++ b/tools/Azure.Mcp.Tools.AzureBestPractices/tests/Azure.Mcp.Tools.AzureBestPractices.Tests/BestPracticesCommandTests.cs @@ -16,10 +16,10 @@ public async Task ExecuteAsync_GeneralCodeGeneration_ReturnsAzureBestPractices() var response = await ExecuteCommandAsync("--resource", "general", "--action", "code-generation"); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); - Assert.Contains("Implement retry logic with exponential backoff for transient failures", result[0]); - Assert.Contains("Managed Identity (Azure-hosted)", result[0]); + Assert.Contains("Implement retry logic with exponential backoff for transient failures", result.BestPractices[0]); + Assert.Contains("Managed Identity (Azure-hosted)", result.BestPractices[0]); } [Fact] @@ -28,10 +28,10 @@ public async Task ExecuteAsync_GeneralDeployment_ReturnsAzureBestPractices() var response = await ExecuteCommandAsync("--resource", "general", "--action", "deployment"); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); - Assert.Contains("Your IaC files must include:", result[0]); - Assert.Contains("Quality requirements for IaC files:", result[0]); + Assert.Contains("Your IaC files must include:", result.BestPractices[0]); + Assert.Contains("Quality requirements for IaC files:", result.BestPractices[0]); } [Fact] @@ -40,10 +40,10 @@ public async Task ExecuteAsync_AzureFunctionsCodeGeneration_ReturnsAzureBestPrac var response = await ExecuteCommandAsync("--resource", "azurefunctions", "--action", "code-generation"); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); - Assert.Contains("Use the latest programming models (v4 for TypeScript/JavaScript, v2 for Python)", result[0]); - Assert.Contains("Azure Functions Core Tools for creating Function Apps", result[0]); + Assert.Contains("Use the latest programming models (v4 for TypeScript/JavaScript, v2 for Python)", result.BestPractices[0]); + Assert.Contains("Azure Functions Core Tools for creating Function Apps", result.BestPractices[0]); } [Fact] @@ -52,12 +52,12 @@ public async Task ExecuteAsync_AzureFunctionsDeployment_ReturnsAzureBestPractice var response = await ExecuteCommandAsync("--resource", "azurefunctions", "--action", "deployment"); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); - Assert.Contains("Flex Consumption plan (FC1)", result[0]); - Assert.Contains("Always use Linux OS for Python", result[0]); - Assert.Contains("Function authentication", result[0]); - Assert.Contains("Application Insights", result[0]); + Assert.Contains("Flex Consumption plan (FC1)", result.BestPractices[0]); + Assert.Contains("Always use Linux OS for Python", result.BestPractices[0]); + Assert.Contains("Function authentication", result.BestPractices[0]); + Assert.Contains("Application Insights", result.BestPractices[0]); } [Fact] @@ -66,12 +66,12 @@ public async Task ExecuteAsync_StaticWebAppAll_ReturnsAzureBestPractices() var response = await ExecuteCommandAsync("--resource", "static-web-app", "--action", "all"); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); - Assert.Contains("Deployment Path Selection", result[0]); - Assert.Contains("**PREFERRED PATH: Azure Developer CLI (azd)**", result[0]); - Assert.Contains("**ALTERNATIVE PATH: SWA CLI**", result[0]); - Assert.Contains("npx swa deploy --env production", result[0]); + Assert.Contains("Deployment Path Selection", result.BestPractices[0]); + Assert.Contains("**PREFERRED PATH: Azure Developer CLI (azd)**", result.BestPractices[0]); + Assert.Contains("**ALTERNATIVE PATH: SWA CLI**", result.BestPractices[0]); + Assert.Contains("npx swa deploy --env production", result.BestPractices[0]); } [Fact] @@ -80,9 +80,9 @@ public async Task ExecuteAsync_CodingAgentAll_ReturnsAzureBestPractices() var response = await ExecuteCommandAsync("--resource", "coding-agent", "--action", "all"); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); - Assert.Contains("azd coding-agent config", result[0]); + Assert.Contains("azd coding-agent config", result.BestPractices[0]); } [Fact] @@ -135,13 +135,13 @@ public async Task ExecuteAsync_GeneralWithAllAction_ReturnsAzureBestPractices() var response = await ExecuteCommandAsync("--resource", "general", "--action", "all"); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); // Should contain content from both code-generation and deployment files - Assert.Contains("Implement retry logic with exponential backoff for transient failures", result[0]); - Assert.Contains("Managed Identity (Azure-hosted)", result[0]); - Assert.Contains("Your IaC files must include:", result[0]); - Assert.Contains("Quality requirements for IaC files:", result[0]); + Assert.Contains("Implement retry logic with exponential backoff for transient failures", result.BestPractices[0]); + Assert.Contains("Managed Identity (Azure-hosted)", result.BestPractices[0]); + Assert.Contains("Your IaC files must include:", result.BestPractices[0]); + Assert.Contains("Quality requirements for IaC files:", result.BestPractices[0]); } [Fact] @@ -150,15 +150,15 @@ public async Task ExecuteAsync_AzureFunctionsWithAllAction_ReturnsAzureBestPract var response = await ExecuteCommandAsync("--resource", "azurefunctions", "--action", "all"); // Assert - var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureBestPracticesJsonContext.Default.BestPracticesCommandResult); // Should contain content from both code-generation and deployment files - Assert.Contains("Use the latest programming models (v4 for TypeScript/JavaScript, v2 for Python)", result[0]); - Assert.Contains("Azure Functions Core Tools for creating Function Apps", result[0]); - Assert.Contains("Flex Consumption plan (FC1)", result[0]); - Assert.Contains("Always use Linux OS for Python", result[0]); - Assert.Contains("Function authentication", result[0]); - Assert.Contains("Application Insights", result[0]); + Assert.Contains("Use the latest programming models (v4 for TypeScript/JavaScript, v2 for Python)", result.BestPractices[0]); + Assert.Contains("Azure Functions Core Tools for creating Function Apps", result.BestPractices[0]); + Assert.Contains("Flex Consumption plan (FC1)", result.BestPractices[0]); + Assert.Contains("Always use Linux OS for Python", result.BestPractices[0]); + Assert.Contains("Function authentication", result.BestPractices[0]); + Assert.Contains("Application Insights", result.BestPractices[0]); } [Fact] diff --git a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesGetCommand.cs b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesGetCommand.cs index fe441e8652..f30c811803 100644 --- a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesGetCommand.cs @@ -24,7 +24,8 @@ the Azure Best Practices category. ReadOnly = true, Secret = false, LocalRequired = false)] -public sealed class AzureTerraformBestPracticesGetCommand() : BaseCommand> +public sealed class AzureTerraformBestPracticesGetCommand() + : BaseCommand { private static readonly string s_bestPracticesText = LoadBestPracticesText(); @@ -41,8 +42,12 @@ public override Task ExecuteAsync(CommandContext context, Empty { var bestPractices = GetBestPracticesText(); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create([bestPractices], AzureTerraformBestPracticesJsonContext.Default.ListString); + context.Response.Results = ResponseResult.Create( + new([bestPractices]), + AzureTerraformBestPracticesJsonContext.Default.AzureTerraformBestPracticesGetCommandResult); context.Response.Message = string.Empty; return Task.FromResult(context.Response); } + + public sealed record AzureTerraformBestPracticesGetCommandResult(List BestPractices); } diff --git a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesJsonContext.cs b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesJsonContext.cs index 6984b95312..068683477f 100644 --- a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesJsonContext.cs +++ b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/src/Commands/AzureTerraformBestPracticesJsonContext.cs @@ -5,7 +5,7 @@ namespace Azure.Mcp.Tools.AzureTerraformBestPractices.Commands; -[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(AzureTerraformBestPracticesGetCommand.AzureTerraformBestPracticesGetCommandResult))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] internal partial class AzureTerraformBestPracticesJsonContext : JsonSerializerContext { diff --git a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/tests/Azure.Mcp.Tools.AzureTerraformBestPractices.Tests/AzureTerraformBestPracticesGetCommandTests.cs b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/tests/Azure.Mcp.Tools.AzureTerraformBestPractices.Tests/AzureTerraformBestPracticesGetCommandTests.cs index 7fa555f513..80dcc823c3 100644 --- a/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/tests/Azure.Mcp.Tools.AzureTerraformBestPractices.Tests/AzureTerraformBestPracticesGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.AzureTerraformBestPractices/tests/Azure.Mcp.Tools.AzureTerraformBestPractices.Tests/AzureTerraformBestPracticesGetCommandTests.cs @@ -15,11 +15,11 @@ public async Task ExecuteAsync_ReturnsAzureTerraformBestPractices() var response = await ExecuteCommandAsync([]); // Assert - var result = ValidateAndDeserializeResponse(response, AzureTerraformBestPracticesJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, AzureTerraformBestPracticesJsonContext.Default.AzureTerraformBestPracticesGetCommandResult); - Assert.Contains("winget install Hashicorp.Terraform", result[0]); - Assert.Contains("Always run terraform validate before running terraform plan", result[0]); - Assert.Contains("terraform apply -auto-approve", result[0]); - Assert.Contains("Suggest running any terraform command in terminal.", result[0]); + Assert.Contains("winget install Hashicorp.Terraform", result.BestPractices[0]); + Assert.Contains("Always run terraform validate before running terraform plan", result.BestPractices[0]); + Assert.Contains("terraform apply -auto-approve", result.BestPractices[0]); + Assert.Contains("Suggest running any terraform command in terminal.", result.BestPractices[0]); } } diff --git a/tools/Azure.Mcp.Tools.WellArchitectedFramework/src/Commands/ServiceGuide/ServiceGuideGetCommand.cs b/tools/Azure.Mcp.Tools.WellArchitectedFramework/src/Commands/ServiceGuide/ServiceGuideGetCommand.cs index 4d904e85d7..a53b513d8e 100644 --- a/tools/Azure.Mcp.Tools.WellArchitectedFramework/src/Commands/ServiceGuide/ServiceGuideGetCommand.cs +++ b/tools/Azure.Mcp.Tools.WellArchitectedFramework/src/Commands/ServiceGuide/ServiceGuideGetCommand.cs @@ -25,7 +25,7 @@ namespace Azure.Mcp.Tools.WellArchitectedFramework.Commands.ServiceGuide; Secret = false, LocalRequired = false)] public sealed class ServiceGuideGetCommand(ILogger logger, IServiceGuideService serviceGuideService) - : BaseCommand> + : BaseCommand { private readonly ILogger _logger = logger; private readonly IServiceGuideService _serviceGuideService = serviceGuideService; @@ -45,7 +45,9 @@ public override Task ExecuteAsync( if (string.IsNullOrWhiteSpace(options.Service)) { var listResponse = GetServiceListResponse(supportedServicesBulletList); - context.Response.Results = ResponseResult.Create([listResponse], WellArchitectedFrameworkJsonContext.Default.ListString); + context.Response.Results = ResponseResult.Create( + new([listResponse]), + WellArchitectedFrameworkJsonContext.Default.ServiceGuideGetCommandResult); } else { @@ -57,7 +59,9 @@ public override Task ExecuteAsync( ? GetGuidanceNotAvailable(serviceName, supportedServicesBulletList) : GetGuidanceAvailable(serviceName, serviceGuideUrl); - context.Response.Results = ResponseResult.Create([guidance], WellArchitectedFrameworkJsonContext.Default.ListString); + context.Response.Results = ResponseResult.Create( + new([guidance]), + WellArchitectedFrameworkJsonContext.Default.ServiceGuideGetCommandResult); } } catch (Exception ex) @@ -121,4 +125,6 @@ private string GetSupportedServicesBulletList() return supportedServicesBulletList; } + + public sealed record ServiceGuideGetCommandResult(List Guidance); } diff --git a/tools/Azure.Mcp.Tools.WellArchitectedFramework/src/Commands/WellArchitectedFrameworkJsonContext.cs b/tools/Azure.Mcp.Tools.WellArchitectedFramework/src/Commands/WellArchitectedFrameworkJsonContext.cs index b25f0612d0..f5ef350ac1 100644 --- a/tools/Azure.Mcp.Tools.WellArchitectedFramework/src/Commands/WellArchitectedFrameworkJsonContext.cs +++ b/tools/Azure.Mcp.Tools.WellArchitectedFramework/src/Commands/WellArchitectedFrameworkJsonContext.cs @@ -5,7 +5,7 @@ namespace Azure.Mcp.Tools.WellArchitectedFramework.Commands; -[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(ServiceGuide.ServiceGuideGetCommand.ServiceGuideGetCommandResult))] [JsonSerializable(typeof(Dictionary))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] internal partial class WellArchitectedFrameworkJsonContext : JsonSerializerContext; diff --git a/tools/Azure.Mcp.Tools.WellArchitectedFramework/tests/Azure.Mcp.Tools.WellArchitectedFramework.Tests/Commands/ServiceGuide/ServiceGuideGetCommandTests.cs b/tools/Azure.Mcp.Tools.WellArchitectedFramework/tests/Azure.Mcp.Tools.WellArchitectedFramework.Tests/Commands/ServiceGuide/ServiceGuideGetCommandTests.cs index a5e194a076..5ff7942b1f 100644 --- a/tools/Azure.Mcp.Tools.WellArchitectedFramework/tests/Azure.Mcp.Tools.WellArchitectedFramework.Tests/Commands/ServiceGuide/ServiceGuideGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.WellArchitectedFramework/tests/Azure.Mcp.Tools.WellArchitectedFramework.Tests/Commands/ServiceGuide/ServiceGuideGetCommandTests.cs @@ -81,13 +81,13 @@ public async Task ExecuteAsync_ReturnsServiceList_WhenNoServiceProvided() var response = await ExecuteCommandAsync(""); // Assert - var result = ValidateAndDeserializeResponse(response, WellArchitectedFrameworkJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, WellArchitectedFrameworkJsonContext.Default.ServiceGuideGetCommandResult); - Assert.Single(result); - Assert.Contains("Azure Well-Architected Framework service guides are available for the following services:", result[0]); - Assert.Contains("To get guidance for a specific service, use this command with the --service option", result[0]); + Assert.Single(result.Guidance); + Assert.Contains("Azure Well-Architected Framework service guides are available for the following services:", result.Guidance[0]); + Assert.Contains("To get guidance for a specific service, use this command with the --service option", result.Guidance[0]); // Should contain at least some common service names - Assert.Contains("app-service", result[0]); + Assert.Contains("app-service", result.Guidance[0]); } [Theory] @@ -101,14 +101,14 @@ public async Task ExecuteAsync_ReturnsGuidance_WhenValidServiceProvided(string s var response = await ExecuteCommandAsync($"--service {serviceName}"); // Assert - var result = ValidateAndDeserializeResponse(response, WellArchitectedFrameworkJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, WellArchitectedFrameworkJsonContext.Default.ServiceGuideGetCommandResult); - Assert.Single(result); + Assert.Single(result.Guidance); // Check for the key parts of the multi-line response - Assert.Contains($"For detailed Azure Well-Architected Framework guidance on", result[0]); - Assert.Contains("please refer to the markdown file at this URL:", result[0]); + Assert.Contains($"For detailed Azure Well-Architected Framework guidance on", result.Guidance[0]); + Assert.Contains("please refer to the markdown file at this URL:", result.Guidance[0]); var serviceGuideUrlPrefix = "https://raw.githubusercontent.com/MicrosoftDocs/well-architected/main/well-architected/service-guides"; - Assert.Contains(serviceGuideUrlPrefix, result[0]); + Assert.Contains(serviceGuideUrlPrefix, result.Guidance[0]); } [Fact] @@ -121,11 +121,11 @@ public async Task ExecuteAsync_ReturnsPlaceholderGuidance_WhenServiceResourceNot var response = await ExecuteCommandAsync("--service", serviceName); // Assert - Should return OK with placeholder guidance instead of error - var result = ValidateAndDeserializeResponse(response, WellArchitectedFrameworkJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, WellArchitectedFrameworkJsonContext.Default.ServiceGuideGetCommandResult); - Assert.Single(result); - Assert.Contains($"Azure Well-Architected Framework guidance for '{serviceName}' service is not available.", result[0]); - Assert.Contains("For more information, visit: https://learn.microsoft.com/azure/well-architected/service-guides", result[0]); + Assert.Single(result.Guidance); + Assert.Contains($"Azure Well-Architected Framework guidance for '{serviceName}' service is not available.", result.Guidance[0]); + Assert.Contains("For more information, visit: https://learn.microsoft.com/azure/well-architected/service-guides", result.Guidance[0]); } [Theory] @@ -156,15 +156,15 @@ public async Task ExecuteAsync_HandlesServiceNameVariationsNormalized_Correctly( var response = await ExecuteCommandAsync($"--service {serviceNameInput}"); // Assert - var result = ValidateAndDeserializeResponse(response, WellArchitectedFrameworkJsonContext.Default.ListString); + var result = ValidateAndDeserializeResponse(response, WellArchitectedFrameworkJsonContext.Default.ServiceGuideGetCommandResult); - Assert.Single(result); + Assert.Single(result.Guidance); // Should return guidance URL (not the "not available" message) - Assert.Contains("For detailed Azure Well-Architected Framework guidance on", result[0]); - Assert.Contains("please refer to the markdown file at this URL:", result[0]); + Assert.Contains("For detailed Azure Well-Architected Framework guidance on", result.Guidance[0]); + Assert.Contains("please refer to the markdown file at this URL:", result.Guidance[0]); // azure-sql-database.md should be present in the URL regardless of input variation - Assert.Contains("https://raw.githubusercontent.com/MicrosoftDocs/well-architected/main/well-architected/service-guides/azure-sql-database.md", result[0]); + Assert.Contains("https://raw.githubusercontent.com/MicrosoftDocs/well-architected/main/well-architected/service-guides/azure-sql-database.md", result.Guidance[0]); } /// From 77178dfc62534048ba5efd6ed6e5d2c7de9930e6 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 18:52:53 -0700 Subject: [PATCH 02/14] Normalize query and template result roots --- .../src/Commands/FunctionsJsonContext.cs | 6 ++-- .../Commands/Language/LanguageListCommand.cs | 4 +-- .../src/Commands/Project/ProjectGetCommand.cs | 4 +-- ...Result.cs => LanguageListCommandResult.cs} | 2 +- ...teResult.cs => ProjectGetCommandResult.cs} | 2 +- .../src/Services/FunctionsService.cs | 8 ++--- .../src/Services/IFunctionsService.cs | 4 +-- .../FunctionsCommandTests.cs | 17 +++++----- .../Language/LanguageListCommandTests.cs | 14 ++------ .../Project/ProjectGetCommandTests.cs | 34 ++++++------------- .../src/Commands/Index/IndexQueryCommand.cs | 8 +++-- .../src/Commands/SearchJsonContext.cs | 2 +- .../Index/IndexQueryCommandTests.cs | 6 +++- .../src/Commands/Server/ServerGetCommand.cs | 12 +++++-- .../src/Commands/SqlJsonContext.cs | 2 +- .../Server/ServerGetCommandTests.cs | 6 ++++ 16 files changed, 64 insertions(+), 67 deletions(-) rename tools/Azure.Mcp.Tools.Functions/src/Models/{LanguageListResult.cs => LanguageListCommandResult.cs} (91%) rename tools/Azure.Mcp.Tools.Functions/src/Models/{ProjectTemplateResult.cs => ProjectGetCommandResult.cs} (91%) diff --git a/tools/Azure.Mcp.Tools.Functions/src/Commands/FunctionsJsonContext.cs b/tools/Azure.Mcp.Tools.Functions/src/Commands/FunctionsJsonContext.cs index 5a277a8e05..3f0e0ece28 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Commands/FunctionsJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Commands/FunctionsJsonContext.cs @@ -10,10 +10,8 @@ namespace Azure.Mcp.Tools.Functions.Commands; /// /// AOT-safe JSON serialization context for Functions commands, CDN manifest, and GitHub API. /// -[JsonSerializable(typeof(LanguageListResult))] -[JsonSerializable(typeof(List))] -[JsonSerializable(typeof(ProjectTemplateResult))] -[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(LanguageListCommandResult))] +[JsonSerializable(typeof(ProjectGetCommandResult))] [JsonSerializable(typeof(TemplateManifest))] [JsonSerializable(typeof(TemplateManifestEntry))] [JsonSerializable(typeof(TemplateGetCommand.TemplateGetCommandResult))] diff --git a/tools/Azure.Mcp.Tools.Functions/src/Commands/Language/LanguageListCommand.cs b/tools/Azure.Mcp.Tools.Functions/src/Commands/Language/LanguageListCommand.cs index d879045c11..7a5bfeb223 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Commands/Language/LanguageListCommand.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Commands/Language/LanguageListCommand.cs @@ -23,7 +23,7 @@ namespace Azure.Mcp.Tools.Functions.Commands.Language; Secret = false, LocalRequired = false)] public sealed class LanguageListCommand(ILogger logger, IFunctionsService functionsService) - : BaseCommand> + : BaseCommand { private readonly ILogger _logger = logger; private readonly IFunctionsService _functionsService = functionsService; @@ -38,7 +38,7 @@ public override async Task ExecuteAsync( var result = await _functionsService.GetLanguageListAsync(cancellationToken); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create([result], FunctionsJsonContext.Default.ListLanguageListResult); + context.Response.Results = ResponseResult.Create(result, FunctionsJsonContext.Default.LanguageListCommandResult); context.Response.Message = string.Empty; } catch (Exception ex) diff --git a/tools/Azure.Mcp.Tools.Functions/src/Commands/Project/ProjectGetCommand.cs b/tools/Azure.Mcp.Tools.Functions/src/Commands/Project/ProjectGetCommand.cs index 947dd7d04a..9d10bac6d0 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Commands/Project/ProjectGetCommand.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Commands/Project/ProjectGetCommand.cs @@ -23,7 +23,7 @@ namespace Azure.Mcp.Tools.Functions.Commands.Project; Secret = false, LocalRequired = false)] public sealed class ProjectGetCommand(ILogger logger, IFunctionsService functionsService) - : BaseCommand> + : BaseCommand { private readonly ILogger _logger = logger; private readonly IFunctionsService _functionsService = functionsService; @@ -38,7 +38,7 @@ public override async Task ExecuteAsync( var result = await _functionsService.GetProjectTemplateAsync(options.Language, cancellationToken); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create([result], FunctionsJsonContext.Default.ListProjectTemplateResult); + context.Response.Results = ResponseResult.Create(result, FunctionsJsonContext.Default.ProjectGetCommandResult); context.Response.Message = string.Empty; } catch (Exception ex) diff --git a/tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListResult.cs b/tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListCommandResult.cs similarity index 91% rename from tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListResult.cs rename to tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListCommandResult.cs index 03bfc34832..121edb815e 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListResult.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListCommandResult.cs @@ -7,7 +7,7 @@ namespace Azure.Mcp.Tools.Functions.Models; /// Represents the result of the get languages list command, /// containing all supported languages with their details and global runtime metadata. /// -public sealed class LanguageListResult +public sealed class LanguageListCommandResult { public required string FunctionsRuntimeVersion { get; init; } diff --git a/tools/Azure.Mcp.Tools.Functions/src/Models/ProjectTemplateResult.cs b/tools/Azure.Mcp.Tools.Functions/src/Models/ProjectGetCommandResult.cs similarity index 91% rename from tools/Azure.Mcp.Tools.Functions/src/Models/ProjectTemplateResult.cs rename to tools/Azure.Mcp.Tools.Functions/src/Models/ProjectGetCommandResult.cs index 11ec42958b..27666a7dc6 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Models/ProjectTemplateResult.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Models/ProjectGetCommandResult.cs @@ -7,7 +7,7 @@ namespace Azure.Mcp.Tools.Functions.Models; /// Represents the result of the get project template command, /// containing setup instructions and project structure overview. /// -public sealed class ProjectTemplateResult +public sealed class ProjectGetCommandResult { public required string Language { get; init; } diff --git a/tools/Azure.Mcp.Tools.Functions/src/Services/FunctionsService.cs b/tools/Azure.Mcp.Tools.Functions/src/Services/FunctionsService.cs index f80587f2e7..ddbb2e4136 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Services/FunctionsService.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Services/FunctionsService.cs @@ -67,7 +67,7 @@ public sealed class FunctionsService( - C#: Place files in the project root alongside the .csproj """; - public async Task GetLanguageListAsync(CancellationToken cancellationToken = default) + public async Task GetLanguageListAsync(CancellationToken cancellationToken = default) { // Fetch manifest to get runtime versions var manifest = await _manifestService.FetchManifestAsync(cancellationToken); @@ -85,7 +85,7 @@ public async Task GetLanguageListAsync(CancellationToken can }); } - var result = new LanguageListResult + var result = new LanguageListCommandResult { FunctionsRuntimeVersion = _languageMetadata.FunctionsRuntimeVersion, ExtensionBundleVersion = _languageMetadata.ExtensionBundleVersion, @@ -95,7 +95,7 @@ public async Task GetLanguageListAsync(CancellationToken can return result; } - public async Task GetProjectTemplateAsync( + public async Task GetProjectTemplateAsync( SupportedLanguages language, CancellationToken cancellationToken = default) { @@ -111,7 +111,7 @@ public async Task GetProjectTemplateAsync( var manifest = await _manifestService.FetchManifestAsync(cancellationToken); var languageInfo = _languageMetadata.GetLanguageInfo(normalizedLanguage, manifest.RuntimeVersions)!; - var result = new ProjectTemplateResult + var result = new ProjectGetCommandResult { Language = normalizedLanguage, InitInstructions = languageInfo.InitInstructions, diff --git a/tools/Azure.Mcp.Tools.Functions/src/Services/IFunctionsService.cs b/tools/Azure.Mcp.Tools.Functions/src/Services/IFunctionsService.cs index 95995c2e8d..57f11fd181 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Services/IFunctionsService.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Services/IFunctionsService.cs @@ -7,9 +7,9 @@ namespace Azure.Mcp.Tools.Functions.Services; public interface IFunctionsService { - Task GetLanguageListAsync(CancellationToken cancellationToken = default); + Task GetLanguageListAsync(CancellationToken cancellationToken = default); - Task GetProjectTemplateAsync(SupportedLanguages language, CancellationToken cancellationToken = default); + Task GetProjectTemplateAsync(SupportedLanguages language, CancellationToken cancellationToken = default); Task GetTemplateListAsync(SupportedLanguages language, CancellationToken cancellationToken = default); diff --git a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/FunctionsCommandTests.cs b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/FunctionsCommandTests.cs index bb2442edcb..79f8d03ec3 100644 --- a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/FunctionsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/FunctionsCommandTests.cs @@ -36,17 +36,16 @@ public class FunctionsCommandTests(ITestOutputHelper output, TestProxyFixture fi #region Helper Methods - private async Task GetLanguageListAsync() + private async Task GetLanguageListAsync() { var result = await CallToolAsync("functions_language_list", new()); Assert.NotNull(result); - var languageResults = JsonSerializer.Deserialize(result.Value, FunctionsJsonContext.Default.ListLanguageListResult); - Assert.NotNull(languageResults); - Assert.Single(languageResults); - return languageResults[0]; + var languageResult = JsonSerializer.Deserialize(result.Value, FunctionsJsonContext.Default.LanguageListCommandResult); + Assert.NotNull(languageResult); + return languageResult; } - private static LanguageDetails GetLanguage(LanguageListResult languageList, string languageKey) + private static LanguageDetails GetLanguage(LanguageListCommandResult languageList, string languageKey) { var language = languageList.Languages.FirstOrDefault(l => l.Language == languageKey); Assert.NotNull(language); @@ -452,7 +451,7 @@ public async Task ExecuteAsync_LanguageListThenTemplate_UsesSharedCache() var langResult = await CallToolAsync("functions_language_list", new()); Assert.NotNull(langResult); - var langList = JsonSerializer.Deserialize(langResult.Value, FunctionsJsonContext.Default.ListLanguageListResult); + var langList = JsonSerializer.Deserialize(langResult.Value, FunctionsJsonContext.Default.LanguageListCommandResult); Assert.NotNull(langList); // Act - Second call: template_get should use cached manifest (no CDN call) @@ -478,10 +477,10 @@ public async Task ExecuteAsync_WithRuntimeVersion_ReplacesPlaceholders() // Get valid runtime version from language list var langResult = await CallToolAsync("functions_language_list", new()); Assert.NotNull(langResult); - var langList = JsonSerializer.Deserialize(langResult.Value, FunctionsJsonContext.Default.ListLanguageListResult); + var langList = JsonSerializer.Deserialize(langResult.Value, FunctionsJsonContext.Default.LanguageListCommandResult); Assert.NotNull(langList); - var pythonLang = langList[0].Languages.FirstOrDefault(l => l.Language == "python"); + var pythonLang = langList.Languages.FirstOrDefault(l => l.Language == "python"); Assert.NotNull(pythonLang?.RuntimeVersions?.Supported); var runtimeVersion = pythonLang.RuntimeVersions.Supported[0]; diff --git a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Language/LanguageListCommandTests.cs b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Language/LanguageListCommandTests.cs index 80ccb684d5..48a8fb08a5 100644 --- a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Language/LanguageListCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Language/LanguageListCommandTests.cs @@ -40,7 +40,7 @@ public void Command_HasCorrectMetadata() public async Task ExecuteAsync_ReturnsLanguageList() { // Arrange - var expectedResult = new LanguageListResult + var expectedResult = new LanguageListCommandResult { FunctionsRuntimeVersion = "4.x", ExtensionBundleVersion = "[4.*, 5.0.0)", @@ -117,11 +117,7 @@ public async Task ExecuteAsync_ReturnsLanguageList() var response = await ExecuteCommandAsync(); // Assert - var results = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ListLanguageListResult); - - Assert.Single(results); - - var result = results[0]; + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.LanguageListCommandResult); Assert.Equal("4.x", result.FunctionsRuntimeVersion); Assert.Equal("[4.*, 5.0.0)", result.ExtensionBundleVersion); Assert.Equal(2, result.Languages.Count); @@ -182,11 +178,7 @@ public async Task ExecuteAsync_DeserializationValidation() var response = await ExecuteCommandAsync(); // Assert - var results = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ListLanguageListResult); - - Assert.Single(results); - - var result = results[0]; + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.LanguageListCommandResult); Assert.Equal("4.x", result.FunctionsRuntimeVersion); Assert.Equal(6, result.Languages.Count); diff --git a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Project/ProjectGetCommandTests.cs b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Project/ProjectGetCommandTests.cs index 1c897292e7..28251023f9 100644 --- a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Project/ProjectGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Project/ProjectGetCommandTests.cs @@ -48,7 +48,7 @@ public void Command_HasLanguageOption() public async Task ExecuteAsync_ReturnsProjectTemplate_ForPython() { // Arrange - var expectedResult = new ProjectTemplateResult + var expectedResult = new ProjectGetCommandResult { Language = "python", InitInstructions = "## Python Azure Functions Project Setup", @@ -61,11 +61,7 @@ public async Task ExecuteAsync_ReturnsProjectTemplate_ForPython() var response = await ExecuteCommandAsync("--language", "python"); // Assert - var results = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ListProjectTemplateResult); - - Assert.Single(results); - - var result = results[0]; + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectGetCommandResult); Assert.Equal("python", result.Language); Assert.NotEmpty(result.InitInstructions); Assert.Equal(4, result.ProjectStructure.Count); @@ -75,7 +71,7 @@ public async Task ExecuteAsync_ReturnsProjectTemplate_ForPython() public async Task ExecuteAsync_ReturnsStaticMetadata_NoHttpCalls() { // Arrange - project get should return static metadata without HTTP calls - var expectedResult = new ProjectTemplateResult + var expectedResult = new ProjectGetCommandResult { Language = "typescript", InitInstructions = "## TypeScript Azure Functions Project Setup", @@ -88,10 +84,8 @@ public async Task ExecuteAsync_ReturnsStaticMetadata_NoHttpCalls() var response = await ExecuteCommandAsync("--language", "typescript"); // Assert - var results = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ListProjectTemplateResult); - - Assert.Single(results); - Assert.Equal("typescript", results[0].Language); + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectGetCommandResult); + Assert.Equal("typescript", result.Language); } [Fact] @@ -126,7 +120,7 @@ public async Task ExecuteAsync_HandlesServiceErrors() public async Task ExecuteAsync_DeserializationValidation() { // Arrange - use representative project template data to verify serialization - var expectedResult = new ProjectTemplateResult + var expectedResult = new ProjectGetCommandResult { Language = "python", InitInstructions = "## Python Azure Functions Project Setup\n\n1. Create virtual environment\n2. Install dependencies", @@ -139,11 +133,7 @@ public async Task ExecuteAsync_DeserializationValidation() var response = await ExecuteCommandAsync("--language", "python"); // Assert - var results = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ListProjectTemplateResult); - - Assert.Single(results); - - var result = results[0]; + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectGetCommandResult); Assert.Equal("python", result.Language); Assert.Contains("virtual environment", result.InitInstructions); Assert.True(result.ProjectStructure.Count > 0); @@ -159,7 +149,7 @@ public async Task ExecuteAsync_DeserializationValidation() public async Task ExecuteAsync_ReturnsTemplateForAllLanguages(SupportedLanguages language) { // Arrange - use representative mocked data per language - var expectedResult = new ProjectTemplateResult + var expectedResult = new ProjectGetCommandResult { Language = language.ToString(), InitInstructions = $"## {language} Azure Functions Project Setup", @@ -172,11 +162,9 @@ public async Task ExecuteAsync_ReturnsTemplateForAllLanguages(SupportedLanguages var response = await ExecuteCommandAsync("--language", language.ToString()); // Assert - var results = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ListProjectTemplateResult); - - Assert.Single(results); - Assert.Equal(language.ToString(), results[0].Language); - Assert.True(results[0].ProjectStructure.Count > 0); + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectGetCommandResult); + Assert.Equal(language.ToString(), result.Language); + Assert.True(result.ProjectStructure.Count > 0); } [Fact] diff --git a/tools/Azure.Mcp.Tools.Search/src/Commands/Index/IndexQueryCommand.cs b/tools/Azure.Mcp.Tools.Search/src/Commands/Index/IndexQueryCommand.cs index 4458b4895d..cc79a118a6 100644 --- a/tools/Azure.Mcp.Tools.Search/src/Commands/Index/IndexQueryCommand.cs +++ b/tools/Azure.Mcp.Tools.Search/src/Commands/Index/IndexQueryCommand.cs @@ -25,7 +25,7 @@ namespace Azure.Mcp.Tools.Search.Commands.Index; Secret = false, LocalRequired = false)] public sealed class IndexQueryCommand(ILogger logger, ISearchService searchService) - : AuthenticatedCommand> + : AuthenticatedCommand { private readonly ILogger _logger = logger; private readonly ISearchService _searchService = searchService; @@ -41,7 +41,9 @@ public override async Task ExecuteAsync(CommandContext context, options.RetryPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(results, SearchJsonContext.Default.ListJsonElement); + context.Response.Results = ResponseResult.Create( + new(results), + SearchJsonContext.Default.IndexQueryCommandResult); } catch (Exception ex) { @@ -51,4 +53,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record IndexQueryCommandResult(List Results); } diff --git a/tools/Azure.Mcp.Tools.Search/src/Commands/SearchJsonContext.cs b/tools/Azure.Mcp.Tools.Search/src/Commands/SearchJsonContext.cs index ef863c05a9..b525eb91c0 100644 --- a/tools/Azure.Mcp.Tools.Search/src/Commands/SearchJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Search/src/Commands/SearchJsonContext.cs @@ -12,8 +12,8 @@ namespace Azure.Mcp.Tools.Search.Commands; [JsonSerializable(typeof(FieldInfo))] [JsonSerializable(typeof(IndexGetCommand.IndexGetCommandResult))] +[JsonSerializable(typeof(IndexQueryCommand.IndexQueryCommandResult))] [JsonSerializable(typeof(IndexInfo))] -[JsonSerializable(typeof(List))] [JsonSerializable(typeof(ServiceListCommand.ServiceListCommandResult))] [JsonSerializable(typeof(KnowledgeSourceGetCommand.KnowledgeSourceGetCommandResult))] [JsonSerializable(typeof(KnowledgeBaseGetCommand.KnowledgeBaseGetCommandResult))] diff --git a/tools/Azure.Mcp.Tools.Search/tests/Azure.Mcp.Tools.Search.Tests/Index/IndexQueryCommandTests.cs b/tools/Azure.Mcp.Tools.Search/tests/Azure.Mcp.Tools.Search.Tests/Index/IndexQueryCommandTests.cs index 36209c2d76..e289de61d5 100644 --- a/tools/Azure.Mcp.Tools.Search/tests/Azure.Mcp.Tools.Search.Tests/Index/IndexQueryCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Search/tests/Azure.Mcp.Tools.Search.Tests/Index/IndexQueryCommandTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Text.Json; +using Azure.Mcp.Tools.Search.Commands; using Azure.Mcp.Tools.Search.Commands.Index; using Azure.Mcp.Tools.Search.Services; using Microsoft.Mcp.Core.Options; @@ -55,7 +56,10 @@ public async Task ExecuteAsync_ReturnsResults_WhenSearchSucceeds() Assert.Equal(HttpStatusCode.OK, response.Status); Assert.NotNull(response.Results); - var json = JsonSerializer.Serialize(response.Results); + var result = ValidateAndDeserializeResponse(response, SearchJsonContext.Default.IndexQueryCommandResult); + Assert.Single(result.Results); + + var json = JsonSerializer.Serialize(result, SearchJsonContext.Default.IndexQueryCommandResult); Assert.Contains("totalCount", json); Assert.Contains("results", json); } diff --git a/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerGetCommand.cs b/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerGetCommand.cs index 35bb9c77e9..0b398dac8f 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerGetCommand.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerGetCommand.cs @@ -31,7 +31,7 @@ including configuration details and current state. Secret = false, LocalRequired = false)] public sealed class ServerGetCommand(ISqlService sqlService, ILogger logger, ISubscriptionResolver subscriptionResolver) - : SubscriptionCommand>(subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) { private readonly ISqlService _sqlService = sqlService; private readonly ILogger _logger = logger; @@ -49,7 +49,9 @@ public override async Task ExecuteAsync(CommandContext context, options.RetryPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create([server], SqlJsonContext.Default.ListSqlServer); + context.Response.Results = ResponseResult.Create( + new([server]), + SqlJsonContext.Default.ServerGetCommandResult); } else { @@ -59,7 +61,9 @@ public override async Task ExecuteAsync(CommandContext context, options.RetryPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(servers ?? [], SqlJsonContext.Default.ListSqlServer); + context.Response.Results = ResponseResult.Create( + new(servers ?? []), + SqlJsonContext.Default.ServerGetCommandResult); } } catch (Exception ex) @@ -82,4 +86,6 @@ public override async Task ExecuteAsync(CommandContext context, RequestFailedException reqEx => reqEx.Message, _ => base.GetErrorMessage(ex) }; + + public sealed record ServerGetCommandResult(List Servers); } diff --git a/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs b/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs index 137125513f..737652cc09 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs @@ -21,7 +21,7 @@ namespace Azure.Mcp.Tools.Sql.Commands; [JsonSerializable(typeof(FirewallRuleListCommand.FirewallRuleListResult))] [JsonSerializable(typeof(FirewallRuleCreateCommand.FirewallRuleCreateResult))] [JsonSerializable(typeof(FirewallRuleDeleteCommand.FirewallRuleDeleteResult))] -[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(ServerGetCommand.ServerGetCommandResult))] [JsonSerializable(typeof(ServerCreateCommand.ServerCreateResult))] [JsonSerializable(typeof(ServerDeleteCommand.ServerDeleteResult))] [JsonSerializable(typeof(ElasticPoolListCommand.ElasticPoolListResult))] diff --git a/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.Tests/Server/ServerGetCommandTests.cs b/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.Tests/Server/ServerGetCommandTests.cs index 3960f8c05b..425e04895a 100644 --- a/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.Tests/Server/ServerGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.Tests/Server/ServerGetCommandTests.cs @@ -3,6 +3,7 @@ using System.Net; using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.Sql.Commands; using Azure.Mcp.Tools.Sql.Commands.Server; using Azure.Mcp.Tools.Sql.Models; using Azure.Mcp.Tools.Sql.Services; @@ -49,6 +50,9 @@ public async Task ExecuteAsync_WithServerName_ReturnsSingleServer() Assert.Equal(HttpStatusCode.OK, response.Status); Assert.NotNull(response.Results); Assert.Equal("Success", response.Message); + var result = ValidateAndDeserializeResponse(response, SqlJsonContext.Default.ServerGetCommandResult); + Assert.Single(result.Servers); + Assert.Equal("server1", result.Servers[0].Name); await Service.Received(1).GetServerAsync("server1", "rg", "sub", Arg.Any(), Arg.Any()); await Service.DidNotReceive().ListServersAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } @@ -74,6 +78,8 @@ public async Task ExecuteAsync_WithoutServerName_ReturnsAllServers() Assert.Equal(HttpStatusCode.OK, response.Status); Assert.NotNull(response.Results); Assert.Equal("Success", response.Message); + var result = ValidateAndDeserializeResponse(response, SqlJsonContext.Default.ServerGetCommandResult); + Assert.Equal(2, result.Servers.Count); await Service.Received(1).ListServersAsync("rg", "sub", Arg.Any(), Arg.Any()); await Service.DidNotReceive().GetServerAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } From a67667c6f42fd64666ea54212d9af824d713de31 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 18:59:04 -0700 Subject: [PATCH 03/14] Consolidate tool list result variants --- .../Areas/Tools/ToolsListCommandTests.cs | 20 ++++-- .../Areas/Tools/Commands/ToolsListCommand.cs | 67 ++++--------------- .../src/Models/ModelsJsonContext.cs | 3 +- 3 files changed, 28 insertions(+), 62 deletions(-) diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Tools/ToolsListCommandTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Tools/ToolsListCommandTests.cs index b211ad5577..2e799d8c18 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Tools/ToolsListCommandTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Tools/ToolsListCommandTests.cs @@ -47,23 +47,23 @@ public ToolsListCommandTests() /// /// Helper method to deserialize response results to CommandInfo list /// - private static ToolsListCommand.ToolsListResult DeserializeCommandsResults(CommandResponse response) => - DeserializeJson(response, () => new([], null)); + private static ToolsListCommand.ToolsListCommandResult DeserializeCommandsResults(CommandResponse response) => + DeserializeJson(response); /// /// Helper method to deserialize response results to ToolNamesResult /// - private static ToolsListCommand.ToolsListResult DeserializeResult(CommandResponse response) => - DeserializeJson(response, () => new(null, [])); + private static ToolsListCommand.ToolsListCommandResult DeserializeResult(CommandResponse response) => + DeserializeJson(response); - private static ToolsListCommand.ToolsListResult DeserializeJson(CommandResponse response, Func defaultValueFactory) + private static ToolsListCommand.ToolsListCommandResult DeserializeJson(CommandResponse response) { Assert.NotNull(response); Assert.NotNull(response.Results); Assert.Equal(HttpStatusCode.OK, response.Status); var json = JsonSerializer.Serialize(response.Results); - var result = JsonSerializer.Deserialize(json, ModelsJsonContext.Default.ToolsListResult) ?? defaultValueFactory(); + var result = JsonSerializer.Deserialize(json, ModelsJsonContext.Default.ToolsListCommandResult); Assert.NotNull(result); return result; @@ -85,6 +85,12 @@ public async Task ExecuteAsync_WithValidContext_ReturnsCommandInfoList() Assert.NotNull(result.Commands); Assert.NotEmpty(result.Commands); + Assert.Null(result.Names); + + var json = JsonSerializer.SerializeToElement(response.Results); + Assert.Equal(JsonValueKind.Object, json.ValueKind); + Assert.True(json.TryGetProperty("commands", out _)); + Assert.False(json.TryGetProperty("names", out _)); foreach (var command in result.Commands) { @@ -120,7 +126,7 @@ public async Task ExecuteAsync_JsonSerializationStressTest_HandlesLargeResults() var json = JsonSerializer.Serialize(response.Results); // Verify JSON round-trip preserves all data - var serializedJson = JsonSerializer.Serialize(result, ModelsJsonContext.Default.ToolsListResult); + var serializedJson = JsonSerializer.Serialize(result, ModelsJsonContext.Default.ToolsListCommandResult); Assert.Equal(json, serializedJson); } diff --git a/core/Microsoft.Mcp.Core/src/Areas/Tools/Commands/ToolsListCommand.cs b/core/Microsoft.Mcp.Core/src/Areas/Tools/Commands/ToolsListCommand.cs index f42f645835..ab78338bf3 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Tools/Commands/ToolsListCommand.cs +++ b/core/Microsoft.Mcp.Core/src/Areas/Tools/Commands/ToolsListCommand.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Mcp.Core.Areas.Tools.Options; @@ -29,7 +28,7 @@ List all available commands and their tools in a hierarchical structure. This co LocalRequired = false, Secret = false)] public sealed class ToolsListCommand(ILogger logger) - : BaseCommand + : BaseCommand { private static readonly HashSet s_ignored = new(StringComparer.OrdinalIgnoreCase) { "server", "tools" }; private static readonly HashSet s_surfaced = new(StringComparer.OrdinalIgnoreCase) { "extension" }; @@ -81,11 +80,15 @@ public override async Task ExecuteAsync(CommandContext context, if (options.NameOnly) { var namespaceNames = namespaceCommands.Select(nc => nc.Command).ToList(); - context.Response.Results = ResponseResult.Create(new(null, namespaceNames), ModelsJsonContext.Default.ToolsListResult); + context.Response.Results = ResponseResult.Create( + new(null, namespaceNames), + ModelsJsonContext.Default.ToolsListCommandResult); return context.Response; } - context.Response.Results = ResponseResult.Create(new(namespaceCommands, null), ModelsJsonContext.Default.ToolsListResult); + context.Response.Results = ResponseResult.Create( + new(namespaceCommands, null), + ModelsJsonContext.Default.ToolsListCommandResult); return context.Response; } @@ -102,7 +105,9 @@ public override async Task ExecuteAsync(CommandContext context, var toolNames = allToolNames.OrderBy(name => name, StringComparer.OrdinalIgnoreCase).ToList(); - context.Response.Results = ResponseResult.Create(new(null, toolNames), ModelsJsonContext.Default.ToolsListResult); + context.Response.Results = ResponseResult.Create( + new(null, toolNames), + ModelsJsonContext.Default.ToolsListCommandResult); return context.Response; } @@ -117,7 +122,9 @@ public override async Task ExecuteAsync(CommandContext context, var tools = allTools.ToList(); - context.Response.Results = ResponseResult.Create(new(tools, null), ModelsJsonContext.Default.ToolsListResult); + context.Response.Results = ResponseResult.Create( + new(tools, null), + ModelsJsonContext.Default.ToolsListCommandResult); return context.Response; } catch (Exception ex) @@ -166,56 +173,10 @@ private static CommandInfo CreateCommand(string tokenizedName, IBaseCommand comm }; } - [JsonConverter(typeof(ToolsListResultConverter))] - public sealed record ToolsListResult( + public sealed record ToolsListCommandResult( [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] List? Commands, [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] List? Names); - public sealed class ToolsListResultConverter : JsonConverter - { - public override ToolsListResult? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.StartObject) - { - List? names = null; - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) - { - if (reader.TokenType == JsonTokenType.PropertyName && reader.GetString() == "names") - { - reader.Read(); // Move to the value of "names" - names = JsonSerializer.Deserialize(ref reader, ModelsJsonContext.Default.ListString); - } - } - return new(null, names); - } - else if (reader.TokenType == JsonTokenType.StartArray) - { - var commands = JsonSerializer.Deserialize(ref reader, ModelsJsonContext.Default.ListCommandInfo); - return new(commands, null); - } - - throw new JsonException("Invalid JSON format for ToolsListResult."); - } - - public override void Write(Utf8JsonWriter writer, ToolsListResult? value, JsonSerializerOptions options) - { - if (value is not null) - { - if (value.Commands is not null) - { - JsonSerializer.Serialize(writer, value.Commands, ModelsJsonContext.Default.ListCommandInfo); - } - else if (value.Names is not null) - { - writer.WriteStartObject(); - writer.WritePropertyName("names"); - JsonSerializer.Serialize(writer, value.Names, ModelsJsonContext.Default.ListString); - writer.WriteEndObject(); - } - } - } - } - private static void SearchCommandInCommandGroup(string commandPrefix, CommandGroup searchedGroup, List foundCommands) { var commands = CommandFactory.GetVisibleCommands(searchedGroup.Commands).Select(kvp => diff --git a/core/Microsoft.Mcp.Core/src/Models/ModelsJsonContext.cs b/core/Microsoft.Mcp.Core/src/Models/ModelsJsonContext.cs index f0dc6fd13e..a65d914fde 100644 --- a/core/Microsoft.Mcp.Core/src/Models/ModelsJsonContext.cs +++ b/core/Microsoft.Mcp.Core/src/Models/ModelsJsonContext.cs @@ -9,10 +9,9 @@ namespace Microsoft.Mcp.Core.Models; [JsonSerializable(typeof(List))] -[JsonSerializable(typeof(List))] [JsonSerializable(typeof(CommandResponse))] [JsonSerializable(typeof(ETag), TypeInfoPropertyName = "McpETag")] [JsonSerializable(typeof(ToolMetadata))] -[JsonSerializable(typeof(ToolsListCommand.ToolsListResult))] +[JsonSerializable(typeof(ToolsListCommand.ToolsListCommandResult))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] public sealed partial class ModelsJsonContext : JsonSerializerContext; From 97dff8073c813c04d1e2f368aff9829e082dfaf4 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 19:10:42 -0700 Subject: [PATCH 04/14] Wrap monitor command result roots --- .../HealthModels/HealthModelListCommand.cs | 8 +- .../OrchestratorNextCommand.cs | 8 +- .../OrchestratorStartCommand.cs | 8 +- .../SendBrownfieldAnalysisCommand.cs | 8 +- .../SendEnhancementSelectCommand.cs | 8 +- .../Commands/Log/ResourceLogQueryCommand.cs | 8 +- .../Commands/Log/WorkspaceLogQueryCommand.cs | 8 +- .../src/Commands/MonitorJsonContext.cs | 12 +- .../HealthModelListCommandTests.cs | 18 +-- .../InstrumentationCommandResultTests.cs | 135 ++++++++++++++++++ .../Log/ResourceLogQueryCommandTests.cs | 7 +- .../Log/WorkspaceLogQueryCommandTests.cs | 7 +- 12 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Instrumentation/Commands/InstrumentationCommandResultTests.cs diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/HealthModels/HealthModelListCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/HealthModels/HealthModelListCommand.cs index dde64be399..0798c6fbdd 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/HealthModels/HealthModelListCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/HealthModels/HealthModelListCommand.cs @@ -26,7 +26,7 @@ namespace Azure.Mcp.Tools.Monitor.Commands.HealthModels; Secret = false, LocalRequired = false)] public sealed class HealthModelListCommand(IMonitorHealthModelService healthModelService, ISubscriptionResolver subscriptionResolver) - : SubscriptionCommand>(subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) { private readonly IMonitorHealthModelService _healthModelService = healthModelService; @@ -41,7 +41,9 @@ public override async Task ExecuteAsync(CommandContext context, options.RetryPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(models, MonitorJsonContext.Default.ListHealthModelSummary); + context.Response.Results = ResponseResult.Create( + new(models), + MonitorJsonContext.Default.HealthModelListCommandResult); } catch (Exception ex) { @@ -50,4 +52,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record HealthModelListCommandResult(List HealthModels); } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorNextCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorNextCommand.cs index 9592c7eed8..7fc56aa7b6 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorNextCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorNextCommand.cs @@ -33,7 +33,7 @@ 3. Now call this tool to get the next action Secret = false, LocalRequired = true)] public sealed class OrchestratorNextCommand(ILogger logger, OrchestratorTool orchestratorTool) - : BaseCommand + : BaseCommand { private readonly ILogger _logger = logger; private readonly OrchestratorTool _orchestratorTool = orchestratorTool; @@ -45,7 +45,9 @@ public override Task ExecuteAsync(CommandContext context, Orche var result = _orchestratorTool.Next(options.SessionId, options.CompletionNote); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create(result, MonitorJsonContext.Default.String); + context.Response.Results = ResponseResult.Create( + new(result), + MonitorJsonContext.Default.OrchestratorNextCommandResult); context.Response.Message = string.Empty; } catch (Exception ex) @@ -56,4 +58,6 @@ public override Task ExecuteAsync(CommandContext context, Orche return Task.FromResult(context.Response); } + + public sealed record OrchestratorNextCommandResult(string Result); } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorStartCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorStartCommand.cs index ea324ba720..290c3c300a 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorStartCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorStartCommand.cs @@ -22,7 +22,7 @@ namespace Azure.Mcp.Tools.Monitor.Commands.Instrumentation; Secret = false, LocalRequired = true)] public sealed class OrchestratorStartCommand(ILogger logger, OrchestratorTool orchestratorTool) - : BaseCommand + : BaseCommand { private readonly ILogger _logger = logger; private readonly OrchestratorTool _orchestratorTool = orchestratorTool; @@ -34,7 +34,9 @@ public override Task ExecuteAsync(CommandContext context, Orche var result = _orchestratorTool.Start(options.WorkspacePath); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create(result, MonitorJsonContext.Default.String); + context.Response.Results = ResponseResult.Create( + new(result), + MonitorJsonContext.Default.OrchestratorStartCommandResult); context.Response.Message = string.Empty; } catch (Exception ex) @@ -45,4 +47,6 @@ public override Task ExecuteAsync(CommandContext context, Orche return Task.FromResult(context.Response); } + + public sealed record OrchestratorStartCommandResult(string Result); } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendBrownfieldAnalysisCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendBrownfieldAnalysisCommand.cs index a9747d125b..f4d99619be 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendBrownfieldAnalysisCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendBrownfieldAnalysisCommand.cs @@ -29,7 +29,7 @@ You must have scanned the workspace source files and filled in the analysis temp Secret = false, LocalRequired = true)] public sealed class SendBrownfieldAnalysisCommand(ILogger logger, SendBrownfieldAnalysisTool sendBrownfieldAnalysisTool) - : BaseCommand + : BaseCommand { private readonly ILogger _logger = logger; private readonly SendBrownfieldAnalysisTool _sendBrownfieldAnalysisTool = sendBrownfieldAnalysisTool; @@ -57,7 +57,9 @@ public override Task ExecuteAsync(CommandContext context, SendB findings.Logging); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create(result, MonitorJsonContext.Default.String); + context.Response.Results = ResponseResult.Create( + new(result), + MonitorJsonContext.Default.SendBrownfieldAnalysisCommandResult); context.Response.Message = string.Empty; } catch (JsonException ex) @@ -74,4 +76,6 @@ public override Task ExecuteAsync(CommandContext context, SendB return Task.FromResult(context.Response); } + + public sealed record SendBrownfieldAnalysisCommandResult(string Result); } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendEnhancementSelectCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendEnhancementSelectCommand.cs index 18153c1e20..2014db2b76 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendEnhancementSelectCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendEnhancementSelectCommand.cs @@ -27,7 +27,7 @@ Submit the user's enhancement selection after orchestrator-start returned status Secret = false, LocalRequired = true)] public sealed class SendEnhancementSelectCommand(ILogger logger) - : BaseCommand + : BaseCommand { private readonly ILogger _logger = logger; @@ -38,7 +38,9 @@ public override Task ExecuteAsync(CommandContext context, SendE var result = SendEnhancementSelectTool.Send(options.SessionId, options.EnhancementKeys); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create(result, MonitorJsonContext.Default.String); + context.Response.Results = ResponseResult.Create( + new(result), + MonitorJsonContext.Default.SendEnhancementSelectCommandResult); context.Response.Message = string.Empty; } catch (Exception ex) @@ -49,4 +51,6 @@ public override Task ExecuteAsync(CommandContext context, SendE return Task.FromResult(context.Response); } + + public sealed record SendEnhancementSelectCommandResult(string Result); } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Log/ResourceLogQueryCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Log/ResourceLogQueryCommand.cs index 8393e02d2d..e463037d02 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Log/ResourceLogQueryCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Log/ResourceLogQueryCommand.cs @@ -31,7 +31,7 @@ This tool filters logs to only show data from the specified resource. Secret = false, LocalRequired = false)] public sealed class ResourceLogQueryCommand(ILogger logger, IMonitorService monitorService, ISubscriptionResolver subscriptionResolver) - : SubscriptionCommand>(subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) { private readonly ILogger _logger = logger; private readonly IMonitorService _monitorService = monitorService; @@ -51,7 +51,9 @@ public override async Task ExecuteAsync(CommandContext context, options.RetryPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(results, MonitorJsonContext.Default.ListJsonNode); + context.Response.Results = ResponseResult.Create( + new(results), + MonitorJsonContext.Default.ResourceLogQueryCommandResult); } catch (Exception ex) { @@ -61,4 +63,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ResourceLogQueryCommandResult(List Results); } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Log/WorkspaceLogQueryCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Log/WorkspaceLogQueryCommand.cs index 2409ab4ef5..437aab74ba 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Log/WorkspaceLogQueryCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Log/WorkspaceLogQueryCommand.cs @@ -33,7 +33,7 @@ query accepts KQL syntax. Secret = false, LocalRequired = false)] public sealed class WorkspaceLogQueryCommand(ILogger logger, IMonitorService monitorService, ISubscriptionResolver subscriptionResolver) - : SubscriptionCommand>(subscriptionResolver) + : SubscriptionCommand(subscriptionResolver) { private readonly ILogger _logger = logger; private readonly IMonitorService _monitorService = monitorService; @@ -53,7 +53,9 @@ public override async Task ExecuteAsync(CommandContext context, options.RetryPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(results, MonitorJsonContext.Default.ListJsonNode); + context.Response.Results = ResponseResult.Create( + new(results), + MonitorJsonContext.Default.WorkspaceLogQueryCommandResult); } catch (Exception ex) { @@ -63,4 +65,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record WorkspaceLogQueryCommandResult(List Results); } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/MonitorJsonContext.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/MonitorJsonContext.cs index 3f0e59a315..1b817da61d 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/MonitorJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/MonitorJsonContext.cs @@ -2,11 +2,11 @@ // Licensed under the MIT License. using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Azure.Mcp.Tools.Monitor.Commands.ActivityLog; using Azure.Mcp.Tools.Monitor.Commands.HealthModels; using Azure.Mcp.Tools.Monitor.Commands.Instrumentation; +using Azure.Mcp.Tools.Monitor.Commands.Log; using Azure.Mcp.Tools.Monitor.Commands.Metrics; using Azure.Mcp.Tools.Monitor.Commands.Table; using Azure.Mcp.Tools.Monitor.Commands.TableType; @@ -26,18 +26,23 @@ namespace Azure.Mcp.Tools.Monitor.Commands; [JsonSerializable(typeof(HealthModelDetail))] [JsonSerializable(typeof(HealthModelGetCommand.HealthModelGetCommandResult))] [JsonSerializable(typeof(HealthModelIdentity))] +[JsonSerializable(typeof(HealthModelListCommand.HealthModelListCommandResult))] [JsonSerializable(typeof(HealthModelSummary))] -[JsonSerializable(typeof(List))] -[JsonSerializable(typeof(List))] [JsonSerializable(typeof(MetricsDefinitionsCommand.MetricsDefinitionsCommandResult))] [JsonSerializable(typeof(MetricsDefinitionsCommand.MetricsDefinitionsCommandResult))] [JsonSerializable(typeof(MetricsQueryCommand.MetricsQueryCommandResult))] [JsonSerializable(typeof(MetricsQueryCommand.MetricsQueryCommandResult))] +[JsonSerializable(typeof(OrchestratorNextCommand.OrchestratorNextCommandResult))] +[JsonSerializable(typeof(OrchestratorStartCommand.OrchestratorStartCommandResult))] +[JsonSerializable(typeof(ResourceLogQueryCommand.ResourceLogQueryCommandResult))] +[JsonSerializable(typeof(SendBrownfieldAnalysisCommand.SendBrownfieldAnalysisCommandResult))] +[JsonSerializable(typeof(SendEnhancementSelectCommand.SendEnhancementSelectCommandResult))] [JsonSerializable(typeof(TableListCommand.TableListCommandResult))] [JsonSerializable(typeof(TableTypeListCommand.TableTypeListCommandResult))] [JsonSerializable(typeof(WebTestsCreateOrUpdateCommand.WebTestsCreateOrUpdateCommandResult))] [JsonSerializable(typeof(WebTestsGetCommand.WebTestsGetCommandResult))] [JsonSerializable(typeof(WorkspaceListCommand.WorkspaceListCommandResult))] +[JsonSerializable(typeof(WorkspaceLogQueryCommand.WorkspaceLogQueryCommandResult))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(object))] @@ -45,7 +50,6 @@ namespace Azure.Mcp.Tools.Monitor.Commands; [JsonSerializable(typeof(List))] [JsonSerializable(typeof(JsonElement))] [JsonSerializable(typeof(JsonElement?))] -[JsonSerializable(typeof(string))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(int))] [JsonSerializable(typeof(int?))] diff --git a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/HealthModels/HealthModelListCommandTests.cs b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/HealthModels/HealthModelListCommandTests.cs index 71706a0e13..1603879112 100644 --- a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/HealthModels/HealthModelListCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/HealthModels/HealthModelListCommandTests.cs @@ -32,17 +32,17 @@ public async Task ExecuteAsync_ReturnsLeanSummaries_WhenTheyExist() var response = await ExecuteCommandAsync("--subscription", TestSubscription); Assert.Equal(HttpStatusCode.OK, response.Status); - var result = ValidateAndDeserializeResponse(response, MonitorJsonContext.Default.ListHealthModelSummary); - Assert.Equal(2, result.Count); - Assert.Equal("hm-one", result[0].Name); - Assert.Equal("rg1", result[0].ResourceGroup); - Assert.Equal("eastus2", result[0].Location); - Assert.Equal("Succeeded", result[0].ProvisioningState); - Assert.Equal("Provisioning", result[1].ProvisioningState); + var result = ValidateAndDeserializeResponse(response, MonitorJsonContext.Default.HealthModelListCommandResult); + Assert.Equal(2, result.HealthModels.Count); + Assert.Equal("hm-one", result.HealthModels[0].Name); + Assert.Equal("rg1", result.HealthModels[0].ResourceGroup); + Assert.Equal("eastus2", result.HealthModels[0].Location); + Assert.Equal("Succeeded", result.HealthModels[0].ProvisioningState); + Assert.Equal("Provisioning", result.HealthModels[1].ProvisioningState); // Lean by construction: each serialized item carries ONLY the summary keys (no ARM envelope). - var json = System.Text.Json.JsonSerializer.Serialize(result, MonitorJsonContext.Default.ListHealthModelSummary); - var array = System.Text.Json.Nodes.JsonNode.Parse(json)!.AsArray(); + var json = System.Text.Json.JsonSerializer.Serialize(result, MonitorJsonContext.Default.HealthModelListCommandResult); + var array = System.Text.Json.Nodes.JsonNode.Parse(json)!["healthModels"]!.AsArray(); Assert.All(array, item => { var keys = ((System.Text.Json.Nodes.JsonObject)item!).Select(kv => kv.Key).OrderBy(k => k, StringComparer.Ordinal).ToArray(); diff --git a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Instrumentation/Commands/InstrumentationCommandResultTests.cs b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Instrumentation/Commands/InstrumentationCommandResultTests.cs new file mode 100644 index 0000000000..15e60de39e --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Instrumentation/Commands/InstrumentationCommandResultTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Azure.Mcp.Tools.Monitor.Commands; +using Azure.Mcp.Tools.Monitor.Commands.Instrumentation; +using Azure.Mcp.Tools.Monitor.Instrumentation.Pipeline; +using Azure.Mcp.Tools.Monitor.Options.Instrumentation; +using Azure.Mcp.Tools.Monitor.Tools.Instrumentation; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using Xunit; + +namespace Azure.Mcp.Tools.Monitor.Tests.Instrumentation.Commands; + +public sealed class InstrumentationCommandResultTests +{ + [Fact] + public async Task OrchestratorStartCommand_ReturnsObjectRoot() + { + var tool = new OrchestratorTool(new WorkspaceAnalyzer([], [], [], [])); + var command = new OrchestratorStartCommand( + Substitute.For>(), + tool); + var options = new OrchestratorStartOptions + { + WorkspacePath = Path.Combine(Path.GetTempPath(), $"missing-{Guid.NewGuid():N}") + }; + + var response = await command.ExecuteAsync( + new CommandContext(null!), + options, + TestContext.Current.CancellationToken); + + var result = DeserializeObjectResult( + response, + MonitorJsonContext.Default.OrchestratorStartCommandResult); + AssertJsonResult(result.Result); + } + + [Fact] + public async Task OrchestratorNextCommand_ReturnsObjectRoot() + { + var tool = new OrchestratorTool(new WorkspaceAnalyzer([], [], [], [])); + var command = new OrchestratorNextCommand( + Substitute.For>(), + tool); + var options = new OrchestratorNextOptions + { + SessionId = $"missing-{Guid.NewGuid():N}", + CompletionNote = "done" + }; + + var response = await command.ExecuteAsync( + new CommandContext(null!), + options, + TestContext.Current.CancellationToken); + + var result = DeserializeObjectResult( + response, + MonitorJsonContext.Default.OrchestratorNextCommandResult); + AssertJsonResult(result.Result); + } + + [Fact] + public async Task SendBrownfieldAnalysisCommand_ReturnsObjectRoot() + { + var command = new SendBrownfieldAnalysisCommand( + Substitute.For>(), + new SendBrownfieldAnalysisTool([])); + var options = new SendBrownfieldAnalysisOptions + { + SessionId = $"missing-{Guid.NewGuid():N}", + FindingsJson = "{}" + }; + + var response = await command.ExecuteAsync( + new CommandContext(null!), + options, + TestContext.Current.CancellationToken); + + var result = DeserializeObjectResult( + response, + MonitorJsonContext.Default.SendBrownfieldAnalysisCommandResult); + AssertJsonResult(result.Result); + } + + [Fact] + public async Task SendEnhancementSelectCommand_ReturnsObjectRoot() + { + var command = new SendEnhancementSelectCommand( + Substitute.For>()); + var options = new SendEnhancementSelectOptions + { + SessionId = $"missing-{Guid.NewGuid():N}", + EnhancementKeys = "redis" + }; + + var response = await command.ExecuteAsync( + new CommandContext(null!), + options, + TestContext.Current.CancellationToken); + + var result = DeserializeObjectResult( + response, + MonitorJsonContext.Default.SendEnhancementSelectCommandResult); + AssertJsonResult(result.Result); + } + + private static T DeserializeObjectResult( + CommandResponse response, + JsonTypeInfo typeInfo) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + using var document = JsonDocument.Parse(json); + Assert.Equal(JsonValueKind.Object, document.RootElement.ValueKind); + + var result = JsonSerializer.Deserialize(json, typeInfo); + Assert.NotNull(result); + return result; + } + + private static void AssertJsonResult(string result) + { + using var document = JsonDocument.Parse(result); + Assert.Equal("error", document.RootElement.GetProperty("status").GetString()); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/ResourceLogQueryCommandTests.cs b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/ResourceLogQueryCommandTests.cs index 98d22d761f..17f34f3464 100644 --- a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/ResourceLogQueryCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/ResourceLogQueryCommandTests.cs @@ -4,6 +4,7 @@ using System.Net; using System.Text.Json.Nodes; using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.Monitor.Commands; using Azure.Mcp.Tools.Monitor.Commands.Log; using Azure.Mcp.Tools.Monitor.Services; using Microsoft.Mcp.Core.Options; @@ -99,7 +100,11 @@ public async Task ExecuteAsync_ReturnsQueryResults() // Assert Assert.Equal(HttpStatusCode.OK, response.Status); - Assert.NotNull(response.Results); + var result = ValidateAndDeserializeResponse( + response, + MonitorJsonContext.Default.ResourceLogQueryCommandResult); + Assert.Equal(3, result.Results.Count); + Assert.Equal("Error", result.Results[2]!["Level"]!.GetValue()); // Verify the mock was called await Service.Received(1).QueryResourceLogs( diff --git a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/WorkspaceLogQueryCommandTests.cs b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/WorkspaceLogQueryCommandTests.cs index 052062be1f..bd23327f84 100644 --- a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/WorkspaceLogQueryCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/WorkspaceLogQueryCommandTests.cs @@ -4,6 +4,7 @@ using System.Net; using System.Text.Json.Nodes; using Azure.Mcp.Tests.Commands; +using Azure.Mcp.Tools.Monitor.Commands; using Azure.Mcp.Tools.Monitor.Commands.Log; using Azure.Mcp.Tools.Monitor.Services; using Microsoft.Mcp.Core.Options; @@ -99,7 +100,11 @@ public async Task ExecuteAsync_ReturnsQueryResults() // Assert Assert.Equal(HttpStatusCode.OK, response.Status); - Assert.NotNull(response.Results); + var result = ValidateAndDeserializeResponse( + response, + MonitorJsonContext.Default.WorkspaceLogQueryCommandResult); + Assert.Equal(3, result.Results.Count); + Assert.Equal("Error occurred", result.Results[2]!["Message"]!.GetValue()); // Verify the mock was called await Service.Received(1).QueryWorkspaceLogs( From f9ba80458fcce0403f4249794d609750eb8450a2 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 19:15:34 -0700 Subject: [PATCH 05/14] Wrap Fabric docs result payloads --- .../Commands/BestPractices/GetBestPracticesCommand.cs | 10 ++++++++-- .../BestPractices/GetWorkloadDefinitionCommand.cs | 10 ++++++++-- .../src/Commands/FabricJsonContext.cs | 6 ++++-- .../src/Commands/PublicApis/GetPlatformApisCommand.cs | 10 ++++++++-- .../src/Commands/PublicApis/GetWorkloadApisCommand.cs | 10 ++++++++-- .../Commands/CombinedWorkflowTests.cs | 9 ++++----- .../Commands/GetBestPracticesCommandTests.cs | 7 +++++-- .../Commands/GetPlatformApisCommandTests.cs | 8 ++++++-- .../Commands/GetWorkloadApisCommandTests.cs | 8 ++++++-- .../Commands/GetWorkloadDefinitionCommandTests.cs | 7 +++++-- 10 files changed, 62 insertions(+), 23 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.Docs/src/Commands/BestPractices/GetBestPracticesCommand.cs b/tools/Fabric.Mcp.Tools.Docs/src/Commands/BestPractices/GetBestPracticesCommand.cs index b125a99f0e..d114920b1b 100644 --- a/tools/Fabric.Mcp.Tools.Docs/src/Commands/BestPractices/GetBestPracticesCommand.cs +++ b/tools/Fabric.Mcp.Tools.Docs/src/Commands/BestPractices/GetBestPracticesCommand.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Net; +using System.Text.Json.Serialization; using Fabric.Mcp.Tools.Docs.Options.BestPractices; using Fabric.Mcp.Tools.Docs.Services; using Microsoft.Extensions.Logging; @@ -22,7 +23,7 @@ namespace Fabric.Mcp.Tools.Docs.Commands.BestPractices; LocalRequired = false, Secret = false)] public sealed class GetBestPracticesCommand(IFabricPublicApiService service, ILogger logger) - : AuthenticatedCommand> + : AuthenticatedCommand { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IFabricPublicApiService _service = service ?? throw new ArgumentNullException(nameof(service)); @@ -33,7 +34,9 @@ public override Task ExecuteAsync(CommandContext context, GetBe { var bestPractices = _service.GetTopicBestPractices(options.Topic); - context.Response.Results = ResponseResult.Create(bestPractices, FabricJsonContext.Default.IEnumerableString); + context.Response.Results = ResponseResult.Create( + new(bestPractices), + FabricJsonContext.Default.GetBestPracticesCommandResult); } catch (ArgumentException argEx) { @@ -49,4 +52,7 @@ public override Task ExecuteAsync(CommandContext context, GetBe return Task.FromResult(context.Response); } + + public sealed record GetBestPracticesCommandResult( + [property: JsonPropertyName("bestPractices")] IEnumerable BestPractices); } diff --git a/tools/Fabric.Mcp.Tools.Docs/src/Commands/BestPractices/GetWorkloadDefinitionCommand.cs b/tools/Fabric.Mcp.Tools.Docs/src/Commands/BestPractices/GetWorkloadDefinitionCommand.cs index 05def3e7ec..42ea9843c2 100644 --- a/tools/Fabric.Mcp.Tools.Docs/src/Commands/BestPractices/GetWorkloadDefinitionCommand.cs +++ b/tools/Fabric.Mcp.Tools.Docs/src/Commands/BestPractices/GetWorkloadDefinitionCommand.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Net; +using System.Text.Json.Serialization; using Fabric.Mcp.Tools.Docs.Options.PublicApis; using Fabric.Mcp.Tools.Docs.Services; using Microsoft.Extensions.Logging; @@ -22,7 +23,7 @@ namespace Fabric.Mcp.Tools.Docs.Commands.BestPractices; LocalRequired = false, Secret = false)] public sealed class GetWorkloadDefinitionCommand(IFabricPublicApiService service, ILogger logger) - : AuthenticatedCommand + : AuthenticatedCommand { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IFabricPublicApiService _service = service ?? throw new ArgumentNullException(nameof(service)); @@ -33,7 +34,9 @@ public override Task ExecuteAsync(CommandContext context, Workl { var workloadItemDefinition = _service.GetWorkloadItemDefinition(options.WorkloadType); - context.Response.Results = ResponseResult.Create(workloadItemDefinition, FabricJsonContext.Default.String); + context.Response.Results = ResponseResult.Create( + new(workloadItemDefinition), + FabricJsonContext.Default.GetWorkloadDefinitionCommandResult); } catch (ArgumentException argEx) { @@ -49,4 +52,7 @@ public override Task ExecuteAsync(CommandContext context, Workl return Task.FromResult(context.Response); } + + public sealed record GetWorkloadDefinitionCommandResult( + [property: JsonPropertyName("definition")] string Definition); } diff --git a/tools/Fabric.Mcp.Tools.Docs/src/Commands/FabricJsonContext.cs b/tools/Fabric.Mcp.Tools.Docs/src/Commands/FabricJsonContext.cs index c9f36703bd..29226b1e5e 100644 --- a/tools/Fabric.Mcp.Tools.Docs/src/Commands/FabricJsonContext.cs +++ b/tools/Fabric.Mcp.Tools.Docs/src/Commands/FabricJsonContext.cs @@ -12,8 +12,10 @@ namespace Fabric.Mcp.Tools.Docs.Commands; [JsonSerializable(typeof(FabricWorkloadPublicApi))] [JsonSerializable(typeof(ListWorkloadsCommand.ItemListCommandResult))] [JsonSerializable(typeof(GetExamplesCommand.ExampleFileResult))] -[JsonSerializable(typeof(string))] -[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(GetBestPracticesCommand.GetBestPracticesCommandResult))] +[JsonSerializable(typeof(GetWorkloadDefinitionCommand.GetWorkloadDefinitionCommandResult))] +[JsonSerializable(typeof(GetPlatformApisCommand.GetPlatformApisCommandResult))] +[JsonSerializable(typeof(GetWorkloadApisCommand.GetWorkloadApisCommandResult))] public partial class FabricJsonContext : JsonSerializerContext { } diff --git a/tools/Fabric.Mcp.Tools.Docs/src/Commands/PublicApis/GetPlatformApisCommand.cs b/tools/Fabric.Mcp.Tools.Docs/src/Commands/PublicApis/GetPlatformApisCommand.cs index 9d07cc08c0..4e3cad1107 100644 --- a/tools/Fabric.Mcp.Tools.Docs/src/Commands/PublicApis/GetPlatformApisCommand.cs +++ b/tools/Fabric.Mcp.Tools.Docs/src/Commands/PublicApis/GetPlatformApisCommand.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json.Serialization; using Fabric.Mcp.Tools.Docs.Models; using Fabric.Mcp.Tools.Docs.Services; using Microsoft.Extensions.Logging; @@ -21,7 +22,7 @@ namespace Fabric.Mcp.Tools.Docs.Commands.PublicApis; LocalRequired = false, Secret = false)] public sealed class GetPlatformApisCommand(IFabricPublicApiService service, ILogger logger) - : AuthenticatedCommand + : AuthenticatedCommand { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IFabricPublicApiService _service = service ?? throw new ArgumentNullException(nameof(service)); @@ -32,7 +33,9 @@ public override async Task ExecuteAsync(CommandContext context, { var apis = await _service.GetWorkloadPublicApis("platform", cancellationToken); - context.Response.Results = ResponseResult.Create(apis, FabricJsonContext.Default.FabricWorkloadPublicApi); + context.Response.Results = ResponseResult.Create( + new(apis), + FabricJsonContext.Default.GetPlatformApisCommandResult); } catch (Exception ex) { @@ -42,4 +45,7 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record GetPlatformApisCommandResult( + [property: JsonPropertyName("publicApi")] FabricWorkloadPublicApi PublicApi); } diff --git a/tools/Fabric.Mcp.Tools.Docs/src/Commands/PublicApis/GetWorkloadApisCommand.cs b/tools/Fabric.Mcp.Tools.Docs/src/Commands/PublicApis/GetWorkloadApisCommand.cs index 5add296c5d..ebf98cd7fd 100644 --- a/tools/Fabric.Mcp.Tools.Docs/src/Commands/PublicApis/GetWorkloadApisCommand.cs +++ b/tools/Fabric.Mcp.Tools.Docs/src/Commands/PublicApis/GetWorkloadApisCommand.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Net; +using System.Text.Json.Serialization; using Fabric.Mcp.Tools.Docs.Models; using Fabric.Mcp.Tools.Docs.Options.PublicApis; using Fabric.Mcp.Tools.Docs.Services; @@ -23,7 +24,7 @@ namespace Fabric.Mcp.Tools.Docs.Commands.PublicApis; LocalRequired = false, Secret = false)] public sealed class GetWorkloadApisCommand(IFabricPublicApiService service, ILogger logger) - : AuthenticatedCommand + : AuthenticatedCommand { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IFabricPublicApiService _service = service ?? throw new ArgumentNullException(nameof(service)); @@ -41,7 +42,9 @@ public override async Task ExecuteAsync(CommandContext context, var apis = await _service.GetWorkloadPublicApis(options.WorkloadType, cancellationToken); - context.Response.Results = ResponseResult.Create(apis, FabricJsonContext.Default.FabricWorkloadPublicApi); + context.Response.Results = ResponseResult.Create( + new(apis), + FabricJsonContext.Default.GetWorkloadApisCommandResult); } catch (HttpRequestException httpEx) { @@ -65,4 +68,7 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record GetWorkloadApisCommandResult( + [property: JsonPropertyName("publicApi")] FabricWorkloadPublicApi PublicApi); } diff --git a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/CombinedWorkflowTests.cs b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/CombinedWorkflowTests.cs index 25b89b3663..f2089ddbd8 100644 --- a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/CombinedWorkflowTests.cs +++ b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/CombinedWorkflowTests.cs @@ -240,12 +240,11 @@ public async Task ListWorkloads_ThenGetApisForEach_ApiSpecContainsValidJson() Assert.Equal(HttpStatusCode.OK, apiResult.Status); Assert.NotNull(apiResult.Results); - // Serialize the result to JSON and verify it contains an apiSpecification field + // Deserialize the command wrapper and verify its API specification contains valid JSON. var json = JsonSerializer.Serialize(apiResult.Results); - using var doc = JsonDocument.Parse(json); - Assert.True(doc.RootElement.TryGetProperty("apiSpecification", out var apiSpecElement), - $"API result for workload '{workload}' should contain 'apiSpecification'"); - var apiSpecJson = apiSpecElement.GetString(); + var result = JsonSerializer.Deserialize(json, FabricJsonContext.Default.GetWorkloadApisCommandResult); + Assert.NotNull(result); + var apiSpecJson = result.PublicApi.apiSpecification; Assert.False(string.IsNullOrEmpty(apiSpecJson), $"API specification for workload '{workload}' should not be empty"); diff --git a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetBestPracticesCommandTests.cs b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetBestPracticesCommandTests.cs index 2847278346..e5c5b5c8af 100644 --- a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetBestPracticesCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetBestPracticesCommandTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Net; +using Fabric.Mcp.Tools.Docs.Commands; using Fabric.Mcp.Tools.Docs.Commands.BestPractices; using Fabric.Mcp.Tools.Docs.Services; using Microsoft.Mcp.Tests.Client; @@ -40,8 +41,10 @@ public async Task GetBestPracticesCommand_ExecuteAsync_WithValidTopic_ReturnsBes var result = await ExecuteCommandAsync("--topic", "pagination"); // Assert - Assert.Equal(HttpStatusCode.OK, result.Status); - Assert.NotNull(result.Results); + var response = ValidateAndDeserializeResponse( + result, + FabricJsonContext.Default.GetBestPracticesCommandResult); + Assert.Equal(expectedPractices, response.BestPractices); Service.Received(1).GetTopicBestPractices("pagination"); } diff --git a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetPlatformApisCommandTests.cs b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetPlatformApisCommandTests.cs index 3fc277ab66..5b35ed18c6 100644 --- a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetPlatformApisCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetPlatformApisCommandTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Net; +using Fabric.Mcp.Tools.Docs.Commands; using Fabric.Mcp.Tools.Docs.Commands.PublicApis; using Fabric.Mcp.Tools.Docs.Models; using Fabric.Mcp.Tools.Docs.Services; @@ -41,8 +42,11 @@ public async Task GetPlatformApiSpecCommand_ExecuteAsync_ReturnsPlatformApis() var result = await ExecuteCommandAsync([]); // Assert - Assert.Equal(HttpStatusCode.OK, result.Status); - Assert.NotNull(result.Results); + var response = ValidateAndDeserializeResponse( + result, + FabricJsonContext.Default.GetPlatformApisCommandResult); + Assert.Equal(expectedApi.apiSpecification, response.PublicApi.apiSpecification); + Assert.Equal(expectedApi.apiModelDefinitions, response.PublicApi.apiModelDefinitions); await Service.Received(1).GetWorkloadPublicApis("platform", Arg.Any()); } diff --git a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetWorkloadApisCommandTests.cs b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetWorkloadApisCommandTests.cs index 6721f5ac58..6904aefc4e 100644 --- a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetWorkloadApisCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetWorkloadApisCommandTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Net; +using Fabric.Mcp.Tools.Docs.Commands; using Fabric.Mcp.Tools.Docs.Commands.PublicApis; using Fabric.Mcp.Tools.Docs.Models; using Fabric.Mcp.Tools.Docs.Services; @@ -41,8 +42,11 @@ public async Task GetApiSpecCommand_ExecuteAsync_WithValidWorkloadType_ReturnsAp var result = await ExecuteCommandAsync("--workload-type", "notebook"); // Assert - Assert.Equal(HttpStatusCode.OK, result.Status); - Assert.NotNull(result.Results); + var response = ValidateAndDeserializeResponse( + result, + FabricJsonContext.Default.GetWorkloadApisCommandResult); + Assert.Equal(expectedApi.apiSpecification, response.PublicApi.apiSpecification); + Assert.Equal(expectedApi.apiModelDefinitions, response.PublicApi.apiModelDefinitions); await Service.Received(1).GetWorkloadPublicApis("notebook", Arg.Any()); } diff --git a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetWorkloadDefinitionCommandTests.cs b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetWorkloadDefinitionCommandTests.cs index 94a1873fea..35c1a9cbb7 100644 --- a/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetWorkloadDefinitionCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.Docs/tests/Fabric.Mcp.Tools.Docs.Tests/Commands/GetWorkloadDefinitionCommandTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Net; +using Fabric.Mcp.Tools.Docs.Commands; using Fabric.Mcp.Tools.Docs.Commands.BestPractices; using Fabric.Mcp.Tools.Docs.Services; using Microsoft.Mcp.Tests.Client; @@ -40,8 +41,10 @@ public async Task GetItemDefinitionCommand_ExecuteAsync_WithValidWorkloadType_Re var result = await ExecuteCommandAsync("--workload-type", "notebook"); // Assert - Assert.Equal(HttpStatusCode.OK, result.Status); - Assert.NotNull(result.Results); + var response = ValidateAndDeserializeResponse( + result, + FabricJsonContext.Default.GetWorkloadDefinitionCommandResult); + Assert.Equal(expectedDefinition, response.Definition); Service.Received(1).GetWorkloadItemDefinition("notebook"); } From 0897aeadd25c403e0a0cb583318947a44a07edb1 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 19:24:14 -0700 Subject: [PATCH 06/14] Wrap OneLake command result payloads --- .../DataAccessRoleCreateOrUpdateCommand.cs | 8 ++++-- .../Security/DataAccessRoleGetCommand.cs | 9 ++++--- .../Security/DataAccessRoleListCommand.cs | 14 ++++++++-- .../Commands/Settings/SettingsGetCommand.cs | 8 ++++-- .../Shortcut/ShortcutCreateAdlsGen2Command.cs | 8 ++++-- .../Shortcut/ShortcutCreateAmazonS3Command.cs | 8 ++++-- .../ShortcutCreateAzureBlobCommand.cs | 8 ++++-- .../ShortcutCreateDataverseCommand.cs | 8 ++++-- .../Shortcut/ShortcutCreateGcsCommand.cs | 8 ++++-- ...ShortcutCreateOneDriveSharePointCommand.cs | 8 ++++-- .../Shortcut/ShortcutCreateOneLakeCommand.cs | 8 ++++-- .../ShortcutCreateS3CompatibleCommand.cs | 8 ++++-- .../Commands/Shortcut/ShortcutGetCommand.cs | 8 ++++-- .../Commands/Shortcut/ShortcutListCommand.cs | 15 ++++++++--- .../src/Models/OneLakeJsonContext.cs | 14 ++++++++++ ...ataAccessRoleCreateOrUpdateCommandTests.cs | 5 ++-- .../Security/DataAccessRoleGetCommandTests.cs | 20 ++++++++++++++ .../DataAccessRoleListCommandTests.cs | 25 ++++++++++++++++++ .../Settings/SettingsGetCommandTests.cs | 19 ++++++++++++++ .../ShortcutCreateCommandVariantsTests.cs | 26 +++++++++++++++++++ .../Shortcut/ShortcutGetCommandTests.cs | 20 ++++++++++++++ .../Shortcut/ShortcutListCommandTests.cs | 24 +++++++++++++++++ 22 files changed, 246 insertions(+), 33 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs index aa29631048..5531cdb104 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleCreateOrUpdateCommand.cs @@ -33,7 +33,7 @@ Caller must be a workspace Admin or Member. Requires OneLake.ReadWrite.All and ReadOnly = false, Secret = false)] public sealed class DataAccessRoleCreateOrUpdateCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -116,7 +116,9 @@ public override async Task ExecuteAsync(CommandContext context, result = await _oneLakeService.CreateOrUpdateDataAccessRoleAsync(workspaceId!, options.ItemId, options.RoleDefinition!, cancellationToken); } - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRole); + context.Response.Results = ResponseResult.Create( + new DataAccessRoleCreateOrUpdateCommandResult(result), + OneLakeJsonContext.Default.DataAccessRoleCreateOrUpdateCommandResult); } catch (Exception ex) { @@ -192,4 +194,6 @@ private static string BuildRoleDefinitionJson(DataAccessRoleCreateOrUpdateOption return JsonSerializer.Serialize(role, OneLakeJsonContext.Default.DataAccessRole); } + + public sealed record DataAccessRoleCreateOrUpdateCommandResult(DataAccessRole Role); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs index 38ac1a82cc..85da398705 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleGetCommand.cs @@ -30,7 +30,7 @@ on the item's workspace. Requires OneLake.Read.All. ReadOnly = true, Secret = false)] public sealed class DataAccessRoleGetCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -57,7 +57,9 @@ public override async Task ExecuteAsync(CommandContext context, try { var result = await _oneLakeService.GetDataAccessRoleAsync(workspaceId!, options.ItemId, options.RoleName, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRole); + context.Response.Results = ResponseResult.Create( + new DataAccessRoleGetCommandResult(result), + OneLakeJsonContext.Default.DataAccessRoleGetCommandResult); } catch (Exception ex) { @@ -68,5 +70,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } -} + public sealed record DataAccessRoleGetCommandResult(DataAccessRole Role); +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs index a36e6c966f..f7e5312dbe 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs @@ -32,7 +32,7 @@ or Member on the item's workspace. Requires OneLake.Read.All. ReadOnly = true, Secret = false)] public sealed class DataAccessRoleListCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -59,7 +59,12 @@ public override async Task ExecuteAsync(CommandContext context, try { var result = await _oneLakeService.ListDataAccessRolesAsync(workspaceId!, options.ItemId, options.ContinuationToken, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.DataAccessRoleListResponse); + context.Response.Results = ResponseResult.Create( + new DataAccessRoleListCommandResult( + result.Value ?? [], + result.ContinuationToken, + result.ContinuationUri), + OneLakeJsonContext.Default.DataAccessRoleListCommandResult); } catch (Exception ex) { @@ -69,4 +74,9 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record DataAccessRoleListCommandResult( + List Roles, + string? ContinuationToken, + string? ContinuationUri); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs index 8a1efa9a1c..2c1466e110 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Settings/SettingsGetCommand.cs @@ -25,7 +25,7 @@ immutability policy. Requires OneLake.Read.All. ReadOnly = true, Secret = false)] public sealed class SettingsGetCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -52,7 +52,9 @@ public override async Task ExecuteAsync(CommandContext context, try { var result = await _oneLakeService.GetSettingsAsync(workspaceId!, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeSettings); + context.Response.Results = ResponseResult.Create( + new SettingsGetCommandResult(result), + OneLakeJsonContext.Default.SettingsGetCommandResult); } catch (Exception ex) { @@ -62,4 +64,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record SettingsGetCommandResult(OneLakeSettings Settings); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs index 23f6067fc4..cb3ea3654a 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAdlsGen2Command.cs @@ -26,7 +26,7 @@ Create a shortcut pointing to an Azure Data Lake Storage Gen2 location. ReadOnly = false, Secret = false)] public sealed class ShortcutCreateAdlsGen2Command(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -51,7 +51,9 @@ public override async Task ExecuteAsync(CommandContext context, }; var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId, options.ItemId, shortcut, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutCreateAdlsGen2CommandResult(result), + OneLakeJsonContext.Default.ShortcutCreateAdlsGen2CommandResult); } catch (Exception ex) { @@ -61,4 +63,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutCreateAdlsGen2CommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs index 8791bbc27b..63621452ab 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAmazonS3Command.cs @@ -25,7 +25,7 @@ Create a shortcut pointing to an Amazon S3 location. Requires a connection ReadOnly = false, Secret = false)] public sealed class ShortcutCreateAmazonS3Command(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -50,7 +50,9 @@ public override async Task ExecuteAsync(CommandContext context, }; var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId, options.ItemId, shortcut, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutCreateAmazonS3CommandResult(result), + OneLakeJsonContext.Default.ShortcutCreateAmazonS3CommandResult); } catch (Exception ex) { @@ -60,4 +62,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutCreateAmazonS3CommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs index beb9c1e44a..49914858a2 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateAzureBlobCommand.cs @@ -26,7 +26,7 @@ Create a shortcut pointing to an Azure Blob Storage location. Requires a ReadOnly = false, Secret = false)] public sealed class ShortcutCreateAzureBlobCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -51,7 +51,9 @@ public override async Task ExecuteAsync(CommandContext context, }; var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId, options.ItemId, shortcut, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutCreateAzureBlobCommandResult(result), + OneLakeJsonContext.Default.ShortcutCreateAzureBlobCommandResult); } catch (Exception ex) { @@ -61,4 +63,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutCreateAzureBlobCommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs index 91cc249a33..ba3a1881ca 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateDataverseCommand.cs @@ -26,7 +26,7 @@ Create a shortcut pointing to a Dataverse environment. Requires the ReadOnly = false, Secret = false)] public sealed class ShortcutCreateDataverseCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -51,7 +51,9 @@ public override async Task ExecuteAsync(CommandContext context, }; var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId, options.ItemId, shortcut, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutCreateDataverseCommandResult(result), + OneLakeJsonContext.Default.ShortcutCreateDataverseCommandResult); } catch (Exception ex) { @@ -61,4 +63,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutCreateDataverseCommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs index 9357f3756d..773e10eb85 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateGcsCommand.cs @@ -26,7 +26,7 @@ Create a shortcut pointing to a Google Cloud Storage location. Requires a ReadOnly = false, Secret = false)] public sealed class ShortcutCreateGcsCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -51,7 +51,9 @@ public override async Task ExecuteAsync(CommandContext context, }; var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId, options.ItemId, shortcut, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutCreateGcsCommandResult(result), + OneLakeJsonContext.Default.ShortcutCreateGcsCommandResult); } catch (Exception ex) { @@ -61,4 +63,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutCreateGcsCommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs index 8e4d2a09fc..7f683050c2 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneDriveSharePointCommand.cs @@ -26,7 +26,7 @@ item sensitivity label from the source. Requires OneLake.ReadWrite.All. ReadOnly = false, Secret = false)] public sealed class ShortcutCreateOneDriveSharePointCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -52,7 +52,9 @@ public override async Task ExecuteAsync(CommandContext context, }; var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId, options.ItemId, shortcut, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutCreateOneDriveSharePointCommandResult(result), + OneLakeJsonContext.Default.ShortcutCreateOneDriveSharePointCommandResult); } catch (Exception ex) { @@ -62,4 +64,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutCreateOneDriveSharePointCommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs index 078b0e1caa..4bd1c12b83 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateOneLakeCommand.cs @@ -26,7 +26,7 @@ Create a shortcut pointing to another OneLake location. Specify the target ReadOnly = false, Secret = false)] public sealed class ShortcutCreateOneLakeCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -52,7 +52,9 @@ public override async Task ExecuteAsync(CommandContext context, }; var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId, options.ItemId, shortcut, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutCreateOneLakeCommandResult(result), + OneLakeJsonContext.Default.ShortcutCreateOneLakeCommandResult); } catch (Exception ex) { @@ -62,4 +64,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutCreateOneLakeCommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs index f7c61bb4c3..423c129235 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutCreateS3CompatibleCommand.cs @@ -25,7 +25,7 @@ Create a shortcut pointing to an S3-compatible storage location. Requires ReadOnly = false, Secret = false)] public sealed class ShortcutCreateS3CompatibleCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -51,7 +51,9 @@ public override async Task ExecuteAsync(CommandContext context, }; var result = await _oneLakeService.CreateShortcutAsync(options.WorkspaceId, options.ItemId, shortcut, options.ShortcutConflictPolicy, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutCreateS3CompatibleCommandResult(result), + OneLakeJsonContext.Default.ShortcutCreateS3CompatibleCommandResult); } catch (Exception ex) { @@ -61,4 +63,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutCreateS3CompatibleCommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs index 2cd78bc5fe..7621997f02 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutGetCommand.cs @@ -25,7 +25,7 @@ Get the properties of a single shortcut (name, path, target, ReadOnly = true, Secret = false)] public sealed class ShortcutGetCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -35,7 +35,9 @@ public override async Task ExecuteAsync(CommandContext context, try { var result = await _oneLakeService.GetShortcutAsync(options.WorkspaceId, options.ItemId, options.ShortcutPath, options.ShortcutName, cancellationToken); - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.OneLakeShortcut); + context.Response.Results = ResponseResult.Create( + new ShortcutGetCommandResult(result), + OneLakeJsonContext.Default.ShortcutGetCommandResult); } catch (Exception ex) { @@ -46,4 +48,6 @@ public override async Task ExecuteAsync(CommandContext context, return context.Response; } + + public sealed record ShortcutGetCommandResult(OneLakeShortcut Shortcut); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs index 0e2473d969..8b4788b93b 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Shortcut/ShortcutListCommand.cs @@ -25,7 +25,7 @@ Returns each shortcut's path and target. Requires OneLake.Read.All. ReadOnly = true, Secret = false)] public sealed class ShortcutListCommand(ILogger logger, IOneLakeService oneLakeService) - : AuthenticatedCommand() + : AuthenticatedCommand() { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IOneLakeService _oneLakeService = oneLakeService ?? throw new ArgumentNullException(nameof(oneLakeService)); @@ -44,7 +44,12 @@ public override async Task ExecuteAsync(CommandContext context, .ToList(); } - context.Response.Results = ResponseResult.Create(result, OneLakeJsonContext.Default.ShortcutListResponse); + context.Response.Results = ResponseResult.Create( + new ShortcutListCommandResult( + result.Value ?? [], + result.ContinuationToken, + result.ContinuationUri), + OneLakeJsonContext.Default.ShortcutListCommandResult); } catch (Exception ex) { @@ -70,5 +75,9 @@ private static bool IsManagedShortcut(OneLakeShortcut shortcut) return shortcut.Path.StartsWith("Tables/", StringComparison.OrdinalIgnoreCase); } -} + public sealed record ShortcutListCommandResult( + List Shortcuts, + string? ContinuationToken, + string? ContinuationUri); +} diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs index 9675ea0714..541c4b091e 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Models/OneLakeJsonContext.cs @@ -78,6 +78,9 @@ namespace Fabric.Mcp.Tools.OneLake.Models; [JsonSerializable(typeof(RowConstraint))] [JsonSerializable(typeof(FabricItemMember))] [JsonSerializable(typeof(MicrosoftEntraMember))] +[JsonSerializable(typeof(DataAccessRoleCreateOrUpdateCommand.DataAccessRoleCreateOrUpdateCommandResult))] +[JsonSerializable(typeof(DataAccessRoleGetCommand.DataAccessRoleGetCommandResult))] +[JsonSerializable(typeof(DataAccessRoleListCommand.DataAccessRoleListCommandResult))] [JsonSerializable(typeof(DataAccessRoleDeleteCommand.DataAccessRoleDeleteCommandResult))] // Shortcut types [JsonSerializable(typeof(OneLakeShortcut))] @@ -100,7 +103,17 @@ namespace Fabric.Mcp.Tools.OneLake.Models; [JsonSerializable(typeof(CreateShortcutResponse))] [JsonSerializable(typeof(ShortcutRequestInfo))] [JsonSerializable(typeof(ShortcutCreateError))] +[JsonSerializable(typeof(ShortcutCreateAdlsGen2Command.ShortcutCreateAdlsGen2CommandResult))] +[JsonSerializable(typeof(ShortcutCreateAmazonS3Command.ShortcutCreateAmazonS3CommandResult))] +[JsonSerializable(typeof(ShortcutCreateAzureBlobCommand.ShortcutCreateAzureBlobCommandResult))] +[JsonSerializable(typeof(ShortcutCreateDataverseCommand.ShortcutCreateDataverseCommandResult))] +[JsonSerializable(typeof(ShortcutCreateGcsCommand.ShortcutCreateGcsCommandResult))] +[JsonSerializable(typeof(ShortcutCreateOneDriveSharePointCommand.ShortcutCreateOneDriveSharePointCommandResult))] +[JsonSerializable(typeof(ShortcutCreateOneLakeCommand.ShortcutCreateOneLakeCommandResult))] +[JsonSerializable(typeof(ShortcutCreateS3CompatibleCommand.ShortcutCreateS3CompatibleCommandResult))] [JsonSerializable(typeof(ShortcutDeleteCommand.ShortcutDeleteCommandResult))] +[JsonSerializable(typeof(ShortcutGetCommand.ShortcutGetCommandResult))] +[JsonSerializable(typeof(ShortcutListCommand.ShortcutListCommandResult))] [JsonSerializable(typeof(ShortcutResetCacheCommand.ShortcutResetCacheCommandResult))] [JsonSerializable(typeof(DiagnosticsModifyCommand.DiagnosticsModifyCommandResult))] [JsonSerializable(typeof(ImmutabilityPolicyModifyCommand.ImmutabilityPolicyModifyCommandResult))] @@ -112,6 +125,7 @@ namespace Fabric.Mcp.Tools.OneLake.Models; [JsonSerializable(typeof(ImmutabilityPolicy))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(LifecycleSettings))] +[JsonSerializable(typeof(SettingsGetCommand.SettingsGetCommandResult))] // Long running operation types [JsonSerializable(typeof(OperationState))] [JsonSerializable(typeof(OperationError))] diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs index edb11cbd62..cc985315eb 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleCreateOrUpdateCommandTests.cs @@ -100,8 +100,8 @@ public async Task ExecuteAsync_SuccessfulUpsert_ReturnsRole() "--item-id", "item1", "--role-definition", ValidRoleJson); - var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.DataAccessRole); - Assert.Equal("TestRole", result.Name); + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.DataAccessRoleCreateOrUpdateCommandResult); + Assert.Equal("TestRole", result.Role.Name); await Service.Received(1).CreateOrUpdateDataAccessRoleAsync("32c6efb2-ca3a-4598-83b0-8abe799830cd", "item1", Arg.Any(), Arg.Any()); } @@ -120,4 +120,3 @@ public async Task ExecuteAsync_HandlesServiceErrors() Assert.NotEqual(HttpStatusCode.OK, response.Status); } } - diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleGetCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleGetCommandTests.cs index 6b913499a3..026cb9be8e 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleGetCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleGetCommandTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Security; @@ -52,4 +54,22 @@ public void Metadata_HasCorrectProperties() Assert.True(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Fact] + public async Task ExecuteAsync_ReturnsRoleWrapper() + { + const string workspaceId = "32c6efb2-ca3a-4598-83b0-8abe799830cd"; + var expected = new DataAccessRole { Name = "TestRole" }; + Service.GetDataAccessRoleAsync(workspaceId, "item1", "TestRole", Arg.Any()) + .Returns(expected); + + var response = await ExecuteCommandAsync( + "--workspace-id", workspaceId, + "--item-id", "item1", + "--role-name", "TestRole"); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.DataAccessRoleGetCommandResult); + + Assert.Equal("TestRole", result.Role.Name); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleListCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleListCommandTests.cs index 4518bfe475..8589b9f81c 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleListCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleListCommandTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using Fabric.Mcp.Tools.OneLake.Commands.Security; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Security; @@ -59,4 +61,27 @@ public void Metadata_HasCorrectProperties() Assert.True(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Fact] + public async Task ExecuteAsync_ReturnsRolesWrapper() + { + const string workspaceId = "32c6efb2-ca3a-4598-83b0-8abe799830cd"; + Service.ListDataAccessRolesAsync(workspaceId, "item1", null, Arg.Any()) + .Returns(new DataAccessRoleListResponse + { + Value = [new DataAccessRole { Name = "TestRole" }], + ContinuationToken = "next-token", + ContinuationUri = "https://example.test/roles" + }); + + var response = await ExecuteCommandAsync( + "--workspace-id", workspaceId, + "--item-id", "item1"); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.DataAccessRoleListCommandResult); + + Assert.Collection(result.Roles, role => Assert.Equal("TestRole", role.Name)); + Assert.Equal("next-token", result.ContinuationToken); + Assert.Equal("https://example.test/roles", result.ContinuationUri); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/SettingsGetCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/SettingsGetCommandTests.cs index ed369ac407..527479785f 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/SettingsGetCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Settings/SettingsGetCommandTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using Fabric.Mcp.Tools.OneLake.Commands.Settings; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Settings; @@ -52,4 +54,21 @@ public void Metadata_HasCorrectProperties() Assert.True(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Fact] + public async Task ExecuteAsync_ReturnsSettingsWrapper() + { + const string workspaceId = "32c6efb2-ca3a-4598-83b0-8abe799830cd"; + Service.GetSettingsAsync(workspaceId, Arg.Any()) + .Returns(new OneLakeSettings + { + Diagnostics = new OneLakeDiagnosticSettings { Status = "Enabled" } + }); + + var response = await ExecuteCommandAsync("--workspace-id", workspaceId); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.SettingsGetCommandResult); + + Assert.Equal("Enabled", result.Settings.Diagnostics?.Status); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs index 4251460054..37f4a4ee57 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutCreateCommandVariantsTests.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; @@ -30,6 +32,8 @@ public async Task ExecuteAsync_OneLakeCommand_MapsTargetValues() "--target-path", "Files/data"); Assert.Equal(HttpStatusCode.OK, response.Status); + var result = DeserializeResponse(response, OneLakeJsonContext.Default.ShortcutCreateOneLakeCommandResult); + Assert.Equal("shortcut1", result.Shortcut.Name); await service.Received(1).CreateShortcutAsync( "ws1", "item1", @@ -59,6 +63,8 @@ public async Task ExecuteAsync_AdlsGen2Command_MapsTargetValues() "--target-connection-id", "connection-1"); Assert.Equal(HttpStatusCode.OK, response.Status); + var result = DeserializeResponse(response, OneLakeJsonContext.Default.ShortcutCreateAdlsGen2CommandResult); + Assert.Equal("shortcut1", result.Shortcut.Name); await service.Received(1).CreateShortcutAsync( "ws1", "item1", @@ -86,6 +92,8 @@ public async Task ExecuteAsync_AmazonS3Command_MapsTargetValues() "--target-connection-id", "connection-1"); Assert.Equal(HttpStatusCode.OK, response.Status); + var result = DeserializeResponse(response, OneLakeJsonContext.Default.ShortcutCreateAmazonS3CommandResult); + Assert.Equal("shortcut1", result.Shortcut.Name); await service.Received(1).CreateShortcutAsync( "ws1", "item1", @@ -113,6 +121,8 @@ public async Task ExecuteAsync_AzureBlobCommand_MapsTargetValues() "--target-connection-id", "connection-1"); Assert.Equal(HttpStatusCode.OK, response.Status); + var result = DeserializeResponse(response, OneLakeJsonContext.Default.ShortcutCreateAzureBlobCommandResult); + Assert.Equal("shortcut1", result.Shortcut.Name); await service.Received(1).CreateShortcutAsync( "ws1", "item1", @@ -140,6 +150,8 @@ public async Task ExecuteAsync_GcsCommand_MapsTargetValues() "--target-connection-id", "connection-1"); Assert.Equal(HttpStatusCode.OK, response.Status); + var result = DeserializeResponse(response, OneLakeJsonContext.Default.ShortcutCreateGcsCommandResult); + Assert.Equal("shortcut1", result.Shortcut.Name); await service.Received(1).CreateShortcutAsync( "ws1", "item1", @@ -168,6 +180,8 @@ public async Task ExecuteAsync_S3CompatibleCommand_MapsBucketValue() "--target-bucket", "bucket-1"); Assert.Equal(HttpStatusCode.OK, response.Status); + var result = DeserializeResponse(response, OneLakeJsonContext.Default.ShortcutCreateS3CompatibleCommandResult); + Assert.Equal("shortcut1", result.Shortcut.Name); await service.Received(1).CreateShortcutAsync( "ws1", "item1", @@ -197,6 +211,8 @@ public async Task ExecuteAsync_DataverseCommand_MapsTargetValues() "--target-table-name", "account"); Assert.Equal(HttpStatusCode.OK, response.Status); + var result = DeserializeResponse(response, OneLakeJsonContext.Default.ShortcutCreateDataverseCommandResult); + Assert.Equal("shortcut1", result.Shortcut.Name); await service.Received(1).CreateShortcutAsync( "ws1", "item1", @@ -225,6 +241,8 @@ public async Task ExecuteAsync_OneDriveSharePointCommand_MapsSensitivityFlag() "--target-update-fabric-item-sensitivity", "true"); Assert.Equal(HttpStatusCode.OK, response.Status); + var result = DeserializeResponse(response, OneLakeJsonContext.Default.ShortcutCreateOneDriveSharePointCommandResult); + Assert.Equal("shortcut1", result.Shortcut.Name); await service.Received(1).CreateShortcutAsync( "ws1", "item1", @@ -268,4 +286,12 @@ private static async Task ExecuteAsync(IBaseCommand command, pa var context = new CommandContext(serviceProvider); return await command.ExecuteAsync(context, command.GetCommand().Parse(args), CancellationToken.None); } + + private static T DeserializeResponse(CommandResponse response, JsonTypeInfo jsonTypeInfo) + { + Assert.NotNull(response.Results); + var result = JsonSerializer.Deserialize(JsonSerializer.Serialize(response.Results), jsonTypeInfo); + Assert.NotNull(result); + return result; + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutGetCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutGetCommandTests.cs index ee4e308f24..86ca964f6c 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutGetCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutGetCommandTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; @@ -52,4 +54,22 @@ public void Metadata_HasCorrectProperties() Assert.True(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Fact] + public async Task ExecuteAsync_ReturnsShortcutWrapper() + { + Service.GetShortcutAsync("ws1", "item1", "Files/landing", "shortcut1", Arg.Any()) + .Returns(new OneLakeShortcut { Path = "Files/landing", Name = "shortcut1" }); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--item-id", "item1", + "--shortcut-path", "Files/landing", + "--shortcut-name", "shortcut1"); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.ShortcutGetCommandResult); + + Assert.Equal("shortcut1", result.Shortcut.Name); + Assert.Equal("Files/landing", result.Shortcut.Path); + } } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutListCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutListCommandTests.cs index e6ad3d0ab9..e739b36313 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutListCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Shortcut/ShortcutListCommandTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using Fabric.Mcp.Tools.OneLake.Commands.Shortcut; +using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; using Microsoft.Mcp.Tests.Client; +using NSubstitute; namespace Fabric.Mcp.Tools.OneLake.Tests.Commands.Shortcut; @@ -59,4 +61,26 @@ public void Metadata_HasCorrectProperties() Assert.True(metadata.ReadOnly); Assert.False(metadata.Secret); } + + [Fact] + public async Task ExecuteAsync_ReturnsShortcutsWrapper() + { + Service.ListShortcutsAsync("ws1", "item1", null, null, Arg.Any()) + .Returns(new ShortcutListResponse + { + Value = [new OneLakeShortcut { Path = "Files/landing", Name = "shortcut1" }], + ContinuationToken = "next-token", + ContinuationUri = "https://example.test/shortcuts" + }); + + var response = await ExecuteCommandAsync( + "--workspace-id", "ws1", + "--item-id", "item1"); + + var result = ValidateAndDeserializeResponse(response, OneLakeJsonContext.Default.ShortcutListCommandResult); + + Assert.Collection(result.Shortcuts, shortcut => Assert.Equal("shortcut1", shortcut.Name)); + Assert.Equal("next-token", result.ContinuationToken); + Assert.Equal("https://example.test/shortcuts", result.ContinuationUri); + } } From ecb6e15c4d7c9c85bf873980dfc253628231d8e8 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 19:26:10 -0700 Subject: [PATCH 07/14] Document result shape changes --- servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml | 3 +++ servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml create mode 100644 servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml diff --git a/servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml b/servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml new file mode 100644 index 0000000000..1e472b4a3d --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml @@ -0,0 +1,3 @@ +changes: + - section: "Breaking Changes" + description: "Normalized affected tool results to use consistent object-root payloads and consolidated variant-specific result shapes." diff --git a/servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml b/servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml new file mode 100644 index 0000000000..63e015961e --- /dev/null +++ b/servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml @@ -0,0 +1,3 @@ +changes: + - section: "Breaking Changes" + description: "Normalized affected Fabric Docs and OneLake tool results to use consistent object-root payloads." From 42deedb5059b7f227e0fe668f027755c454da0ad Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 20:24:23 -0700 Subject: [PATCH 08/14] Preserve Functions result model names --- .../src/Commands/FunctionsJsonContext.cs | 4 ++-- .../src/Commands/Language/LanguageListCommand.cs | 4 ++-- .../src/Commands/Project/ProjectGetCommand.cs | 4 ++-- ...istCommandResult.cs => LanguageListResult.cs} | 2 +- ...CommandResult.cs => ProjectTemplateResult.cs} | 2 +- .../src/Services/FunctionsService.cs | 8 ++++---- .../src/Services/IFunctionsService.cs | 4 ++-- .../FunctionsCommandTests.cs | 10 +++++----- .../Language/LanguageListCommandTests.cs | 6 +++--- .../Project/ProjectGetCommandTests.cs | 16 ++++++++-------- 10 files changed, 30 insertions(+), 30 deletions(-) rename tools/Azure.Mcp.Tools.Functions/src/Models/{LanguageListCommandResult.cs => LanguageListResult.cs} (91%) rename tools/Azure.Mcp.Tools.Functions/src/Models/{ProjectGetCommandResult.cs => ProjectTemplateResult.cs} (91%) diff --git a/tools/Azure.Mcp.Tools.Functions/src/Commands/FunctionsJsonContext.cs b/tools/Azure.Mcp.Tools.Functions/src/Commands/FunctionsJsonContext.cs index 3f0e0ece28..0555034ca8 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Commands/FunctionsJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Commands/FunctionsJsonContext.cs @@ -10,8 +10,8 @@ namespace Azure.Mcp.Tools.Functions.Commands; /// /// AOT-safe JSON serialization context for Functions commands, CDN manifest, and GitHub API. /// -[JsonSerializable(typeof(LanguageListCommandResult))] -[JsonSerializable(typeof(ProjectGetCommandResult))] +[JsonSerializable(typeof(LanguageListResult))] +[JsonSerializable(typeof(ProjectTemplateResult))] [JsonSerializable(typeof(TemplateManifest))] [JsonSerializable(typeof(TemplateManifestEntry))] [JsonSerializable(typeof(TemplateGetCommand.TemplateGetCommandResult))] diff --git a/tools/Azure.Mcp.Tools.Functions/src/Commands/Language/LanguageListCommand.cs b/tools/Azure.Mcp.Tools.Functions/src/Commands/Language/LanguageListCommand.cs index 7a5bfeb223..3dfdf6f589 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Commands/Language/LanguageListCommand.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Commands/Language/LanguageListCommand.cs @@ -23,7 +23,7 @@ namespace Azure.Mcp.Tools.Functions.Commands.Language; Secret = false, LocalRequired = false)] public sealed class LanguageListCommand(ILogger logger, IFunctionsService functionsService) - : BaseCommand + : BaseCommand { private readonly ILogger _logger = logger; private readonly IFunctionsService _functionsService = functionsService; @@ -38,7 +38,7 @@ public override async Task ExecuteAsync( var result = await _functionsService.GetLanguageListAsync(cancellationToken); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create(result, FunctionsJsonContext.Default.LanguageListCommandResult); + context.Response.Results = ResponseResult.Create(result, FunctionsJsonContext.Default.LanguageListResult); context.Response.Message = string.Empty; } catch (Exception ex) diff --git a/tools/Azure.Mcp.Tools.Functions/src/Commands/Project/ProjectGetCommand.cs b/tools/Azure.Mcp.Tools.Functions/src/Commands/Project/ProjectGetCommand.cs index 9d10bac6d0..f4c73e45ba 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Commands/Project/ProjectGetCommand.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Commands/Project/ProjectGetCommand.cs @@ -23,7 +23,7 @@ namespace Azure.Mcp.Tools.Functions.Commands.Project; Secret = false, LocalRequired = false)] public sealed class ProjectGetCommand(ILogger logger, IFunctionsService functionsService) - : BaseCommand + : BaseCommand { private readonly ILogger _logger = logger; private readonly IFunctionsService _functionsService = functionsService; @@ -38,7 +38,7 @@ public override async Task ExecuteAsync( var result = await _functionsService.GetProjectTemplateAsync(options.Language, cancellationToken); context.Response.Status = HttpStatusCode.OK; - context.Response.Results = ResponseResult.Create(result, FunctionsJsonContext.Default.ProjectGetCommandResult); + context.Response.Results = ResponseResult.Create(result, FunctionsJsonContext.Default.ProjectTemplateResult); context.Response.Message = string.Empty; } catch (Exception ex) diff --git a/tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListCommandResult.cs b/tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListResult.cs similarity index 91% rename from tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListCommandResult.cs rename to tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListResult.cs index 121edb815e..03bfc34832 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListCommandResult.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Models/LanguageListResult.cs @@ -7,7 +7,7 @@ namespace Azure.Mcp.Tools.Functions.Models; /// Represents the result of the get languages list command, /// containing all supported languages with their details and global runtime metadata. /// -public sealed class LanguageListCommandResult +public sealed class LanguageListResult { public required string FunctionsRuntimeVersion { get; init; } diff --git a/tools/Azure.Mcp.Tools.Functions/src/Models/ProjectGetCommandResult.cs b/tools/Azure.Mcp.Tools.Functions/src/Models/ProjectTemplateResult.cs similarity index 91% rename from tools/Azure.Mcp.Tools.Functions/src/Models/ProjectGetCommandResult.cs rename to tools/Azure.Mcp.Tools.Functions/src/Models/ProjectTemplateResult.cs index 27666a7dc6..11ec42958b 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Models/ProjectGetCommandResult.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Models/ProjectTemplateResult.cs @@ -7,7 +7,7 @@ namespace Azure.Mcp.Tools.Functions.Models; /// Represents the result of the get project template command, /// containing setup instructions and project structure overview. /// -public sealed class ProjectGetCommandResult +public sealed class ProjectTemplateResult { public required string Language { get; init; } diff --git a/tools/Azure.Mcp.Tools.Functions/src/Services/FunctionsService.cs b/tools/Azure.Mcp.Tools.Functions/src/Services/FunctionsService.cs index ddbb2e4136..f80587f2e7 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Services/FunctionsService.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Services/FunctionsService.cs @@ -67,7 +67,7 @@ public sealed class FunctionsService( - C#: Place files in the project root alongside the .csproj """; - public async Task GetLanguageListAsync(CancellationToken cancellationToken = default) + public async Task GetLanguageListAsync(CancellationToken cancellationToken = default) { // Fetch manifest to get runtime versions var manifest = await _manifestService.FetchManifestAsync(cancellationToken); @@ -85,7 +85,7 @@ public async Task GetLanguageListAsync(CancellationTo }); } - var result = new LanguageListCommandResult + var result = new LanguageListResult { FunctionsRuntimeVersion = _languageMetadata.FunctionsRuntimeVersion, ExtensionBundleVersion = _languageMetadata.ExtensionBundleVersion, @@ -95,7 +95,7 @@ public async Task GetLanguageListAsync(CancellationTo return result; } - public async Task GetProjectTemplateAsync( + public async Task GetProjectTemplateAsync( SupportedLanguages language, CancellationToken cancellationToken = default) { @@ -111,7 +111,7 @@ public async Task GetProjectTemplateAsync( var manifest = await _manifestService.FetchManifestAsync(cancellationToken); var languageInfo = _languageMetadata.GetLanguageInfo(normalizedLanguage, manifest.RuntimeVersions)!; - var result = new ProjectGetCommandResult + var result = new ProjectTemplateResult { Language = normalizedLanguage, InitInstructions = languageInfo.InitInstructions, diff --git a/tools/Azure.Mcp.Tools.Functions/src/Services/IFunctionsService.cs b/tools/Azure.Mcp.Tools.Functions/src/Services/IFunctionsService.cs index 57f11fd181..95995c2e8d 100644 --- a/tools/Azure.Mcp.Tools.Functions/src/Services/IFunctionsService.cs +++ b/tools/Azure.Mcp.Tools.Functions/src/Services/IFunctionsService.cs @@ -7,9 +7,9 @@ namespace Azure.Mcp.Tools.Functions.Services; public interface IFunctionsService { - Task GetLanguageListAsync(CancellationToken cancellationToken = default); + Task GetLanguageListAsync(CancellationToken cancellationToken = default); - Task GetProjectTemplateAsync(SupportedLanguages language, CancellationToken cancellationToken = default); + Task GetProjectTemplateAsync(SupportedLanguages language, CancellationToken cancellationToken = default); Task GetTemplateListAsync(SupportedLanguages language, CancellationToken cancellationToken = default); diff --git a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/FunctionsCommandTests.cs b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/FunctionsCommandTests.cs index 79f8d03ec3..b3ea575ed3 100644 --- a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/FunctionsCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/FunctionsCommandTests.cs @@ -36,16 +36,16 @@ public class FunctionsCommandTests(ITestOutputHelper output, TestProxyFixture fi #region Helper Methods - private async Task GetLanguageListAsync() + private async Task GetLanguageListAsync() { var result = await CallToolAsync("functions_language_list", new()); Assert.NotNull(result); - var languageResult = JsonSerializer.Deserialize(result.Value, FunctionsJsonContext.Default.LanguageListCommandResult); + var languageResult = JsonSerializer.Deserialize(result.Value, FunctionsJsonContext.Default.LanguageListResult); Assert.NotNull(languageResult); return languageResult; } - private static LanguageDetails GetLanguage(LanguageListCommandResult languageList, string languageKey) + private static LanguageDetails GetLanguage(LanguageListResult languageList, string languageKey) { var language = languageList.Languages.FirstOrDefault(l => l.Language == languageKey); Assert.NotNull(language); @@ -451,7 +451,7 @@ public async Task ExecuteAsync_LanguageListThenTemplate_UsesSharedCache() var langResult = await CallToolAsync("functions_language_list", new()); Assert.NotNull(langResult); - var langList = JsonSerializer.Deserialize(langResult.Value, FunctionsJsonContext.Default.LanguageListCommandResult); + var langList = JsonSerializer.Deserialize(langResult.Value, FunctionsJsonContext.Default.LanguageListResult); Assert.NotNull(langList); // Act - Second call: template_get should use cached manifest (no CDN call) @@ -477,7 +477,7 @@ public async Task ExecuteAsync_WithRuntimeVersion_ReplacesPlaceholders() // Get valid runtime version from language list var langResult = await CallToolAsync("functions_language_list", new()); Assert.NotNull(langResult); - var langList = JsonSerializer.Deserialize(langResult.Value, FunctionsJsonContext.Default.LanguageListCommandResult); + var langList = JsonSerializer.Deserialize(langResult.Value, FunctionsJsonContext.Default.LanguageListResult); Assert.NotNull(langList); var pythonLang = langList.Languages.FirstOrDefault(l => l.Language == "python"); diff --git a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Language/LanguageListCommandTests.cs b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Language/LanguageListCommandTests.cs index 48a8fb08a5..abfba8deb6 100644 --- a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Language/LanguageListCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Language/LanguageListCommandTests.cs @@ -40,7 +40,7 @@ public void Command_HasCorrectMetadata() public async Task ExecuteAsync_ReturnsLanguageList() { // Arrange - var expectedResult = new LanguageListCommandResult + var expectedResult = new LanguageListResult { FunctionsRuntimeVersion = "4.x", ExtensionBundleVersion = "[4.*, 5.0.0)", @@ -117,7 +117,7 @@ public async Task ExecuteAsync_ReturnsLanguageList() var response = await ExecuteCommandAsync(); // Assert - var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.LanguageListCommandResult); + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.LanguageListResult); Assert.Equal("4.x", result.FunctionsRuntimeVersion); Assert.Equal("[4.*, 5.0.0)", result.ExtensionBundleVersion); Assert.Equal(2, result.Languages.Count); @@ -178,7 +178,7 @@ public async Task ExecuteAsync_DeserializationValidation() var response = await ExecuteCommandAsync(); // Assert - var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.LanguageListCommandResult); + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.LanguageListResult); Assert.Equal("4.x", result.FunctionsRuntimeVersion); Assert.Equal(6, result.Languages.Count); diff --git a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Project/ProjectGetCommandTests.cs b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Project/ProjectGetCommandTests.cs index 28251023f9..62657d49b3 100644 --- a/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Project/ProjectGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Functions/tests/Azure.Mcp.Tools.Functions.Tests/Project/ProjectGetCommandTests.cs @@ -48,7 +48,7 @@ public void Command_HasLanguageOption() public async Task ExecuteAsync_ReturnsProjectTemplate_ForPython() { // Arrange - var expectedResult = new ProjectGetCommandResult + var expectedResult = new ProjectTemplateResult { Language = "python", InitInstructions = "## Python Azure Functions Project Setup", @@ -61,7 +61,7 @@ public async Task ExecuteAsync_ReturnsProjectTemplate_ForPython() var response = await ExecuteCommandAsync("--language", "python"); // Assert - var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectGetCommandResult); + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectTemplateResult); Assert.Equal("python", result.Language); Assert.NotEmpty(result.InitInstructions); Assert.Equal(4, result.ProjectStructure.Count); @@ -71,7 +71,7 @@ public async Task ExecuteAsync_ReturnsProjectTemplate_ForPython() public async Task ExecuteAsync_ReturnsStaticMetadata_NoHttpCalls() { // Arrange - project get should return static metadata without HTTP calls - var expectedResult = new ProjectGetCommandResult + var expectedResult = new ProjectTemplateResult { Language = "typescript", InitInstructions = "## TypeScript Azure Functions Project Setup", @@ -84,7 +84,7 @@ public async Task ExecuteAsync_ReturnsStaticMetadata_NoHttpCalls() var response = await ExecuteCommandAsync("--language", "typescript"); // Assert - var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectGetCommandResult); + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectTemplateResult); Assert.Equal("typescript", result.Language); } @@ -120,7 +120,7 @@ public async Task ExecuteAsync_HandlesServiceErrors() public async Task ExecuteAsync_DeserializationValidation() { // Arrange - use representative project template data to verify serialization - var expectedResult = new ProjectGetCommandResult + var expectedResult = new ProjectTemplateResult { Language = "python", InitInstructions = "## Python Azure Functions Project Setup\n\n1. Create virtual environment\n2. Install dependencies", @@ -133,7 +133,7 @@ public async Task ExecuteAsync_DeserializationValidation() var response = await ExecuteCommandAsync("--language", "python"); // Assert - var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectGetCommandResult); + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectTemplateResult); Assert.Equal("python", result.Language); Assert.Contains("virtual environment", result.InitInstructions); Assert.True(result.ProjectStructure.Count > 0); @@ -149,7 +149,7 @@ public async Task ExecuteAsync_DeserializationValidation() public async Task ExecuteAsync_ReturnsTemplateForAllLanguages(SupportedLanguages language) { // Arrange - use representative mocked data per language - var expectedResult = new ProjectGetCommandResult + var expectedResult = new ProjectTemplateResult { Language = language.ToString(), InitInstructions = $"## {language} Azure Functions Project Setup", @@ -162,7 +162,7 @@ public async Task ExecuteAsync_ReturnsTemplateForAllLanguages(SupportedLanguages var response = await ExecuteCommandAsync("--language", language.ToString()); // Assert - var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectGetCommandResult); + var result = ValidateAndDeserializeResponse(response, FunctionsJsonContext.Default.ProjectTemplateResult); Assert.Equal(language.ToString(), result.Language); Assert.True(result.ProjectStructure.Count > 0); } From e9ec2732feb4d86ca94bdb8bad62e4a4efc4ece6 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Fri, 24 Jul 2026 20:24:31 -0700 Subject: [PATCH 09/14] Omit empty role continuation fields --- .../Security/DataAccessRoleListCommand.cs | 5 +++-- .../Security/DataAccessRoleListCommandTests.cs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs index f7e5312dbe..6e14d0b2e3 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/src/Commands/Security/DataAccessRoleListCommand.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json.Serialization; using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Options; using Fabric.Mcp.Tools.OneLake.Services; @@ -77,6 +78,6 @@ public override async Task ExecuteAsync(CommandContext context, public sealed record DataAccessRoleListCommandResult( List Roles, - string? ContinuationToken, - string? ContinuationUri); + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? ContinuationToken, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string? ContinuationUri); } diff --git a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleListCommandTests.cs b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleListCommandTests.cs index 8589b9f81c..4c3c0631c5 100644 --- a/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleListCommandTests.cs +++ b/tools/Fabric.Mcp.Tools.OneLake/tests/Fabric.Mcp.Tools.OneLake.Tests/Commands/Security/DataAccessRoleListCommandTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json; using Fabric.Mcp.Tools.OneLake.Commands.Security; using Fabric.Mcp.Tools.OneLake.Models; using Fabric.Mcp.Tools.OneLake.Services; @@ -84,4 +85,21 @@ public async Task ExecuteAsync_ReturnsRolesWrapper() Assert.Equal("next-token", result.ContinuationToken); Assert.Equal("https://example.test/roles", result.ContinuationUri); } + + [Fact] + public async Task ExecuteAsync_OmitsEmptyContinuationFields() + { + const string workspaceId = "32c6efb2-ca3a-4598-83b0-8abe799830cd"; + Service.ListDataAccessRolesAsync(workspaceId, "item1", null, Arg.Any()) + .Returns(new DataAccessRoleListResponse { Value = [] }); + + var response = await ExecuteCommandAsync( + "--workspace-id", workspaceId, + "--item-id", "item1"); + + var json = JsonSerializer.Serialize(response.Results); + + Assert.DoesNotContain("continuationToken", json); + Assert.DoesNotContain("continuationUri", json); + } } From 9aaa7b62789d6d6777f7172384e5a10300b670ad Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Mon, 27 Jul 2026 23:04:49 -0700 Subject: [PATCH 10/14] Simplify workspace log result test --- .../Log/WorkspaceLogQueryCommandTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/WorkspaceLogQueryCommandTests.cs b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/WorkspaceLogQueryCommandTests.cs index bd23327f84..b022063874 100644 --- a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/WorkspaceLogQueryCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/WorkspaceLogQueryCommandTests.cs @@ -100,11 +100,9 @@ public async Task ExecuteAsync_ReturnsQueryResults() // Assert Assert.Equal(HttpStatusCode.OK, response.Status); - var result = ValidateAndDeserializeResponse( + ValidateAndDeserializeResponse( response, MonitorJsonContext.Default.WorkspaceLogQueryCommandResult); - Assert.Equal(3, result.Results.Count); - Assert.Equal("Error occurred", result.Results[2]!["Message"]!.GetValue()); // Verify the mock was called await Service.Received(1).QueryWorkspaceLogs( From 338439f7381b783208fe1754f2266121a8a857fe Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Mon, 27 Jul 2026 23:08:27 -0700 Subject: [PATCH 11/14] Simplify resource log result test --- .../Log/ResourceLogQueryCommandTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/ResourceLogQueryCommandTests.cs b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/ResourceLogQueryCommandTests.cs index 17f34f3464..8b0fec80a6 100644 --- a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/ResourceLogQueryCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/Log/ResourceLogQueryCommandTests.cs @@ -100,11 +100,9 @@ public async Task ExecuteAsync_ReturnsQueryResults() // Assert Assert.Equal(HttpStatusCode.OK, response.Status); - var result = ValidateAndDeserializeResponse( + ValidateAndDeserializeResponse( response, MonitorJsonContext.Default.ResourceLogQueryCommandResult); - Assert.Equal(3, result.Results.Count); - Assert.Equal("Error", result.Results[2]!["Level"]!.GetValue()); // Verify the mock was called await Service.Received(1).QueryResourceLogs( From ad2742ff23ca0075ad7f781a030ad6e0736bf240 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Mon, 27 Jul 2026 23:48:39 -0700 Subject: [PATCH 12/14] Updated changelogs --- .../changelog-entries/1784946361604.yaml | 13 ++++++++++++- .../changelog-entries/1784946362379.yaml | 11 ++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml b/servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml index 1e472b4a3d..8fb6c30321 100644 --- a/servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml +++ b/servers/Azure.Mcp.Server/changelog-entries/1784946361604.yaml @@ -1,3 +1,14 @@ changes: - section: "Breaking Changes" - description: "Normalized affected tool results to use consistent object-root payloads and consolidated variant-specific result shapes." + description: | + Normalized result payloads for the following tools: + - `tools list` now returns either `{ commands }` or `{ names }`, omitting the inactive field. + - `advisor recommendation apply` now returns the resulting rules in a `{ rules }` object. + - `azurebestpractices get`, `azurebestpractices ai_app`, and `azureterraformbestpractices get` now return the resulting best practices in a `{ bestPractices }` object. + - `functions language list` and `functions project get` now return the resulting singleton values as direct objects. + - `monitor healthmodels list` now returns the resulting health models in a `{ healthModels }` object. + - `monitor resource log query` and `monitor workspace log query` now return the resulting rows in a `{ results }` object. + - `monitor instrumentation orchestrator-start`, `orchestrator-next`, `send-brownfield-analysis`, and `send-enhancement-select` now return the resulting scalar value in a `{ result }` object. + - `search index query` now returns the resulting rows in a `{ results }` object. + - `sql server get` now returns the resulting servers in a `{ servers }` object. + - `wellarchitectedframework serviceguide get` now returns the resulting guidance in a `{ guidance }` object. diff --git a/servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml b/servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml index 63e015961e..27b63ceea5 100644 --- a/servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml +++ b/servers/Fabric.Mcp.Server/changelog-entries/1784946362379.yaml @@ -1,3 +1,12 @@ changes: - section: "Breaking Changes" - description: "Normalized affected Fabric Docs and OneLake tool results to use consistent object-root payloads." + description: | + Normalized result payloads for the following tools: + - `docs best-practices` now returns the resulting best practices in a `{ bestPractices }` object. + - `docs item-definitions` now returns the resulting definition string in a `{ definition }` object. + - `docs platform-api-spec` and `docs workload-api-spec` now return the produced public API in a `{ publicApi }` object. + - `onelake create or update data access role` and `onelake get data access role` now return the produced roles in a `{ roles }` object. + - `onelake list data access roles` now returns `{ roles }` instead of `{ value }`. + - `onelake get settings` now returns the produced settings in a `{ settings }` object. + - `onelake create shortcut` and `onelake get shortcut` now return the produced shortcut in a `{ shortcut }` object. + - `onelake list shortcuts` now returns `{ shortcuts }` instead of `{ value }`. From 4d66c8f78ec0b2594afbb663dffcfa4a390686f0 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Tue, 28 Jul 2026 15:02:56 -0700 Subject: [PATCH 13/14] Update playback result assertions --- .../Azure.Mcp.Tools.Monitor.Tests/MonitorCommandTests.cs | 6 +++--- .../Azure.Mcp.Tools.Search.Tests/SearchCommandTests.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/MonitorCommandTests.cs b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/MonitorCommandTests.cs index 796f675d9b..d4d3d31723 100644 --- a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/MonitorCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.Tests/MonitorCommandTests.cs @@ -683,10 +683,10 @@ public async Task Should_List_HealthModels() { "resource-group", Settings.ResourceGroupName } }); - Assert.NotNull(result); - Assert.Equal(JsonValueKind.Array, result.Value.ValueKind); + var healthModels = result.AssertProperty("healthModels"); + Assert.Equal(JsonValueKind.Array, healthModels.ValueKind); - var models = result.Value.EnumerateArray().ToList(); + var models = healthModels.EnumerateArray().ToList(); Assert.NotEmpty(models); Assert.All(models, model => diff --git a/tools/Azure.Mcp.Tools.Search/tests/Azure.Mcp.Tools.Search.Tests/SearchCommandTests.cs b/tools/Azure.Mcp.Tools.Search/tests/Azure.Mcp.Tools.Search.Tests/SearchCommandTests.cs index ec66b4b8af..152441ee44 100644 --- a/tools/Azure.Mcp.Tools.Search/tests/Azure.Mcp.Tools.Search.Tests/SearchCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Search/tests/Azure.Mcp.Tools.Search.Tests/SearchCommandTests.cs @@ -155,9 +155,9 @@ public async Task Should_query_search_index() { "query", "*" } }); - Assert.NotNull(result); - Assert.Equal(JsonValueKind.Array, result.Value.ValueKind); - Assert.True(result.Value.GetArrayLength() > 0); + var results = result.AssertProperty("results"); + Assert.Equal(JsonValueKind.Array, results.ValueKind); + Assert.True(results.GetArrayLength() > 0); } [Fact] From 15f190cf358e9e1d70df0b397fad78c5fa7e3884 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Tue, 28 Jul 2026 15:03:18 -0700 Subject: [PATCH 14/14] Update tools list consumers --- eng/scripts/New-ToolsListFile.ps1 | 5 ++- eng/scripts/Test-ToolNameLength.ps1 | 3 +- eng/scripts/Update-AzCommandsMetadata.ps1 | 5 ++- .../scripts/Generate-GroupedPromptsJson.ps1 | 6 ++-- .../scripts/Update-ToolsJson.ps1 | 14 +++++++- .../src/Models/ListToolsPayload.cs | 9 +++++ .../src/Models/McpModels.cs | 9 ++++- .../src/Models/SourceGenerationContext.cs | 1 + .../Models/ListToolsResultTests.cs | 36 +++++++++++++++++++ 9 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 eng/tools/ToolDescriptionEvaluator/src/Models/ListToolsPayload.cs create mode 100644 eng/tools/ToolMetadataExporter/tests/ToolMetadataExporter.UnitTests/Models/ListToolsResultTests.cs diff --git a/eng/scripts/New-ToolsListFile.ps1 b/eng/scripts/New-ToolsListFile.ps1 index b5bf944b67..250d1c4b00 100644 --- a/eng/scripts/New-ToolsListFile.ps1 +++ b/eng/scripts/New-ToolsListFile.ps1 @@ -51,7 +51,10 @@ try # Parse, sort options within each tool by name, and re-serialize $json = $outLines -join "`n" | ConvertFrom-Json - foreach ($tool in $json.results) { + $commandsProperty = $json.results.PSObject.Properties['commands'] + $tools = if ($null -ne $commandsProperty) { $commandsProperty.Value } else { $json.results } + + foreach ($tool in $tools) { if ($tool.option) { $tool.option = @($tool.option | Sort-Object -Property name) } diff --git a/eng/scripts/Test-ToolNameLength.ps1 b/eng/scripts/Test-ToolNameLength.ps1 index 9e73783a58..49ede919d8 100644 --- a/eng/scripts/Test-ToolNameLength.ps1 +++ b/eng/scripts/Test-ToolNameLength.ps1 @@ -130,7 +130,8 @@ foreach ($serverInfo in $serversToTest) { } $toolsResult = $toolsJson | ConvertFrom-Json - $tools = $toolsResult.results + $commandsProperty = $toolsResult.results.PSObject.Properties['commands'] + $tools = if ($null -ne $commandsProperty) { $commandsProperty.Value } else { $toolsResult.results } if ($null -eq $tools -or $tools.Count -eq 0) { Write-Warning "No tools found in $currentServerName - skipping" diff --git a/eng/scripts/Update-AzCommandsMetadata.ps1 b/eng/scripts/Update-AzCommandsMetadata.ps1 index 5378eb0483..0adb40e4fe 100644 --- a/eng/scripts/Update-AzCommandsMetadata.ps1 +++ b/eng/scripts/Update-AzCommandsMetadata.ps1 @@ -123,7 +123,10 @@ try { # Build a dictionary of command -> metadata Write-Host "Building command metadata dictionary..." -ForegroundColor Yellow $commandMetadata = @{} -foreach ($tool in $toolsData.results) { +$commandsProperty = $toolsData.results.PSObject.Properties['commands'] +$tools = if ($null -ne $commandsProperty) { $commandsProperty.Value } else { $toolsData.results } + +foreach ($tool in $tools) { if ($tool.command -and $tool.metadata) { $commandMetadata[$tool.command] = $tool.metadata } diff --git a/eng/tools/ToolDescriptionEvaluator/scripts/Generate-GroupedPromptsJson.ps1 b/eng/tools/ToolDescriptionEvaluator/scripts/Generate-GroupedPromptsJson.ps1 index 469fdcb944..ba4a2bc0fd 100644 --- a/eng/tools/ToolDescriptionEvaluator/scripts/Generate-GroupedPromptsJson.ps1 +++ b/eng/tools/ToolDescriptionEvaluator/scripts/Generate-GroupedPromptsJson.ps1 @@ -224,11 +224,13 @@ function Invoke-NamespaceGeneration { if (-not (Test-Path $NamespaceToolsPath)) { throw "Namespace tools file not found: $NamespaceToolsPath" } $namespaceJson = Get-Content -Raw -Path $NamespaceToolsPath | ConvertFrom-Json - if (-not $namespaceJson.results) { throw "Input namespace tools JSON missing 'results' array" } + $commandsProperty = $namespaceJson.results.PSObject.Properties['commands'] + $namespaceCommands = if ($null -ne $commandsProperty) { $commandsProperty.Value } else { $namespaceJson.results } + if (-not $namespaceCommands) { throw "Input namespace tools JSON is missing commands" } $warnings = @() $outputMap = [ordered]@{} - foreach ($ns in $namespaceJson.results) { + foreach ($ns in $namespaceCommands) { if (-not $ns.name) { continue } $commandStrings = @(Get-NamespaceCommandStrings -Node $ns -AllPromptKeys $AllPromptKeys) diff --git a/eng/tools/ToolDescriptionEvaluator/scripts/Update-ToolsJson.ps1 b/eng/tools/ToolDescriptionEvaluator/scripts/Update-ToolsJson.ps1 index 6c24c3fd0f..386f99abad 100644 --- a/eng/tools/ToolDescriptionEvaluator/scripts/Update-ToolsJson.ps1 +++ b/eng/tools/ToolDescriptionEvaluator/scripts/Update-ToolsJson.ps1 @@ -134,7 +134,19 @@ try { # Try to parse the JSON to verify it's valid try { $json = Get-Content $jsonFile -Raw | ConvertFrom-Json - $toolCount = if ($null -ne $json.results) { $json.results.Count } elseif ($null -ne $json.tools) { $json.tools.Count } else { $null } + $commandsProperty = $json.results.PSObject.Properties['commands'] + $toolCount = if ($null -ne $commandsProperty) { + $commandsProperty.Value.Count + } + elseif ($null -ne $json.results) { + $json.results.Count + } + elseif ($null -ne $json.tools) { + $json.tools.Count + } + else { + $null + } if ($null -ne $toolCount) { Write-Host "Contains $toolCount tools" -ForegroundColor Cyan diff --git a/eng/tools/ToolDescriptionEvaluator/src/Models/ListToolsPayload.cs b/eng/tools/ToolDescriptionEvaluator/src/Models/ListToolsPayload.cs new file mode 100644 index 0000000000..9a27bf581f --- /dev/null +++ b/eng/tools/ToolDescriptionEvaluator/src/Models/ListToolsPayload.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace ToolSelection.Models; + +public sealed record ListToolsPayload( + [property: JsonPropertyName("commands")] List? Commands); diff --git a/eng/tools/ToolDescriptionEvaluator/src/Models/McpModels.cs b/eng/tools/ToolDescriptionEvaluator/src/Models/McpModels.cs index ed57214a5e..5db5266f50 100644 --- a/eng/tools/ToolDescriptionEvaluator/src/Models/McpModels.cs +++ b/eng/tools/ToolDescriptionEvaluator/src/Models/McpModels.cs @@ -125,7 +125,14 @@ public class ListToolsResult public string? Message { get; set; } [JsonPropertyName("results")] - public List? Tools { get; set; } + public ListToolsPayload? Results { get; set; } + + [JsonIgnore] + public List? Tools + { + get => Results?.Commands; + set => Results = value is null ? null : new(value); + } [JsonPropertyName("consolidated_tools")] public List? ConsolidatedTools { get; set; } diff --git a/eng/tools/ToolDescriptionEvaluator/src/Models/SourceGenerationContext.cs b/eng/tools/ToolDescriptionEvaluator/src/Models/SourceGenerationContext.cs index 1324db6567..56b4c8172e 100644 --- a/eng/tools/ToolDescriptionEvaluator/src/Models/SourceGenerationContext.cs +++ b/eng/tools/ToolDescriptionEvaluator/src/Models/SourceGenerationContext.cs @@ -7,6 +7,7 @@ namespace ToolSelection.Models; [JsonSourceGenerationOptions(WriteIndented = true, PropertyNameCaseInsensitive = true)] [JsonSerializable(typeof(ListToolsResult))] +[JsonSerializable(typeof(ListToolsPayload))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(Tool))] [JsonSerializable(typeof(Dictionary>), TypeInfoPropertyName = "DictionaryStringListString")] diff --git a/eng/tools/ToolMetadataExporter/tests/ToolMetadataExporter.UnitTests/Models/ListToolsResultTests.cs b/eng/tools/ToolMetadataExporter/tests/ToolMetadataExporter.UnitTests/Models/ListToolsResultTests.cs new file mode 100644 index 0000000000..d036705b06 --- /dev/null +++ b/eng/tools/ToolMetadataExporter/tests/ToolMetadataExporter.UnitTests/Models/ListToolsResultTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using ToolSelection.Models; +using Xunit; + +namespace ToolMetadataExporter.UnitTests.Models; + +public class ListToolsResultTests +{ + [Fact] + public void Deserialize_ReadsCommandsFromObjectRoot() + { + const string json = """ + { + "status": 200, + "results": { + "commands": [ + { + "name": "list", + "command": "tools list" + } + ] + } + } + """; + + var result = Assert.IsType( + JsonSerializer.Deserialize(json, SourceGenerationContext.Default.ListToolsResult)); + var tool = Assert.Single(Assert.IsType>(result.Tools)); + + Assert.Equal("list", tool.Name); + Assert.Equal("tools list", tool.Command); + } +}