diff --git a/servers/Azure.Mcp.Server/changelog-entries/1780624204134.yaml b/servers/Azure.Mcp.Server/changelog-entries/1780624204134.yaml new file mode 100644 index 0000000000..0dca6f6ffe --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/1780624204134.yaml @@ -0,0 +1,3 @@ +changes: + - section: "Bugs Fixed" + description: "Fixed an issue where the Cosmos DB QueryItems operation silently parsed non-success (e.g. throttled or auth-failure) responses as data. The response is now validated before parsing so failures surface as errors." diff --git a/tools/Azure.Mcp.Tools.Cosmos/src/Services/CosmosService.cs b/tools/Azure.Mcp.Tools.Cosmos/src/Services/CosmosService.cs index 98fca9d2ad..42527e796a 100644 --- a/tools/Azure.Mcp.Tools.Cosmos/src/Services/CosmosService.cs +++ b/tools/Azure.Mcp.Tools.Cosmos/src/Services/CosmosService.cs @@ -288,6 +288,8 @@ public async Task> QueryItems( while (queryIterator.HasMoreResults) { using ResponseMessage response = await queryIterator.ReadNextAsync(cancellationToken); + response.EnsureSuccessStatusCode(); + using var document = JsonDocument.Parse(response.Content); items.Add(document.RootElement.Clone()); } diff --git a/tools/Azure.Mcp.Tools.Cosmos/tests/Azure.Mcp.Tools.Cosmos.Tests/CosmosServiceTests.cs b/tools/Azure.Mcp.Tools.Cosmos/tests/Azure.Mcp.Tools.Cosmos.Tests/CosmosServiceTests.cs index 32c60b5893..f8982f963e 100644 --- a/tools/Azure.Mcp.Tools.Cosmos/tests/Azure.Mcp.Tools.Cosmos.Tests/CosmosServiceTests.cs +++ b/tools/Azure.Mcp.Tools.Cosmos/tests/Azure.Mcp.Tools.Cosmos.Tests/CosmosServiceTests.cs @@ -195,6 +195,31 @@ await _cacheService.DidNotReceive().GetAsync( Arg.Any()); } + [Fact] + public async Task QueryItems_NonSuccessResponse_Throws() + { + // Arrange: route the CosmosClient's transport through the mocked IHttpClientFactory so it always returns 403. + var handler = new MockHttpHandler(HttpStatusCode.Forbidden); + _httpClientFactory.CreateClient(Arg.Any()).Returns(new HttpClient(handler)); + var clientOptions = new CosmosClientOptions { HttpClientFactory = () => _httpClientFactory.CreateClient() }; + + var credential = Substitute.For(); + var token = new AccessToken("fake-token", DateTimeOffset.UtcNow.AddHours(1)); + credential.GetTokenAsync(Arg.Any(), Arg.Any()) + .Returns(new ValueTask(token)); + credential.GetToken(Arg.Any(), Arg.Any()) + .Returns(token); + + using var cosmosClient = new CosmosClient("https://myaccount.documents.azure.com", credential, clientOptions); + + _cacheService.GetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(cosmosClient); + + // Act & Assert: a non-success response must throw rather than being returned as a data item + await Assert.ThrowsAnyAsync(() => + _service.QueryItems("myaccount", "mydb", "mycontainer", "SELECT * FROM c", "sub123", AuthMethod.Key, cancellationToken: TestContext.Current.CancellationToken)); + } + private sealed class MockHttpHandler(HttpStatusCode statusCode) : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)