diff --git a/servers/Azure.Mcp.Server/changelog-entries/jairmyree-fix-healthmodels-entity-get.yaml b/servers/Azure.Mcp.Server/changelog-entries/jairmyree-fix-healthmodels-entity-get.yaml new file mode 100644 index 0000000000..a8da71caad --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/jairmyree-fix-healthmodels-entity-get.yaml @@ -0,0 +1,4 @@ +changes: + - section: "Bugs Fixed" + description: | + Fixed `monitor_healthmodels_entity_get` returning HTTP 400 (Bad Request) when querying entity health. The tool now uses a supported `Microsoft.CloudHealth` control-plane API version (`2025-05-01-preview`), and the data-plane request URL is built correctly by joining the dataplane endpoint with a path separator and URL-encoding the entity name. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Services/MonitorHealthModelService.cs b/tools/Azure.Mcp.Tools.Monitor/src/Services/MonitorHealthModelService.cs index 28ca9db20f..1cbdeaf24d 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Services/MonitorHealthModelService.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Services/MonitorHealthModelService.cs @@ -14,7 +14,7 @@ namespace Azure.Mcp.Tools.Monitor.Services; public class MonitorHealthModelService(ITenantService tenantService, IHttpClientFactory httpClientFactory) : BaseAzureService(tenantService), IMonitorHealthModelService { - private const string ApiVersion = "2023-10-01-preview"; + private const string ApiVersion = "2025-05-01-preview"; private readonly IHttpClientFactory _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService)); @@ -45,7 +45,7 @@ public async Task GetEntityHealth( ValidateRequiredParameters((nameof(entity), entity), (nameof(healthModelName), healthModelName), (nameof(resourceGroupName), resourceGroupName), (nameof(subscription), subscription)); string dataplaneEndpoint = await GetDataplaneEndpointAsync(subscription, resourceGroupName, healthModelName, cancellationToken); - string entityHealthUrl = $"{dataplaneEndpoint}api/entities/{entity}/history"; + string entityHealthUrl = $"{dataplaneEndpoint.TrimEnd('/')}/api/entities/{Uri.EscapeDataString(entity)}/history"; string healthResponseString = await GetDataplaneResponseAsync(entityHealthUrl, cancellationToken); return JsonNode.Parse(healthResponseString) ?? throw new Exception("Failed to parse health response to JSON."); 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 f3d363ce6e..34ade78463 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 @@ -35,6 +35,7 @@ public sealed class MonitorCommandTests : RecordedCommandTestsBase private string? _storageAccountName; private string? _appInsightsName; private string? _bingWebTestName; + private string? _healthModelName; public MonitorCommandTests(ITestOutputHelper output, TestProxyFixture fixture, LiveServerFixture liveServerFixture) : base(output, fixture, liveServerFixture) @@ -91,6 +92,7 @@ public override async ValueTask InitializeAsync() _storageAccountName = $"{Settings.ResourceBaseName}mon"; _appInsightsName = $"{Settings.ResourceBaseName}-ai"; _bingWebTestName = $"{Settings.ResourceBaseName}-bing-test"; + _healthModelName = $"{Settings.ResourceBaseName}-health"; if (TestMode == TestMode.Playback) { @@ -519,6 +521,42 @@ public override async ValueTask DisposeAsync() // } // } + #region HealthModels Integration Tests + + [Fact] + public async Task Should_Get_Entity_Health() + { + var result = await CallToolAsync( + "monitor_healthmodels_entity_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "health-model", _healthModelName }, + { "entity", "root" } + }); + + Assert.NotNull(result); + } + + [Theory] + [InlineData("--invalid-param")] + [InlineData("--subscription invalidSub")] + [InlineData("--subscription sub --resource-group rg")] // Missing required entity/health-model + public async Task Should_Return400_WithInvalidHealthModelInput(string args) + { + var result = await CallToolAsync( + "monitor_healthmodels_entity_get", + new() + { + { "args", args } + }); + + Assert.NotEqual(200, result?.GetProperty("status").GetInt32() ?? 500); + } + + #endregion + #region WebTests Integration Tests [Fact]