From 6243df21f729dc21c3edf76695994adb1dba8828 Mon Sep 17 00:00:00 2001 From: vaishali123t Date: Tue, 20 Oct 2026 18:30:11 +0530 Subject: [PATCH 1/3] =?UTF-8?q?Fixed=20nested=20value=20envelope=20in=20MC?= =?UTF-8?q?P=20stored=20procedure=20responses=20Unwrap=20DAB=E2=80=99s=20{?= =?UTF-8?q?=20"value":=20[...]=20}=20response=20in=20built-in=20and=20cust?= =?UTF-8?q?om=20MCP=20tools,=20and=20add=20regression=20tests=20for=20both?= =?UTF-8?q?=20paths.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BuiltInTools/ExecuteEntityTool.cs | 17 +------------ .../Core/DynamicCustomTool.cs | 17 +------------ .../Utils/McpResponseBuilder.cs | 24 +++++++++++++++++++ .../DynamicCustomToolMsSqlIntegrationTests.cs | 10 +++----- .../Mcp/DynamicCustomToolTests.cs | 17 +++++++++++++ .../ExecuteEntityToolMsSqlIntegrationTests.cs | 10 +++----- .../Mcp/ExecuteEntityToolTests.cs | 18 ++++++++++++++ 7 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs index 90b51d2837..5291108fc8 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs @@ -381,22 +381,7 @@ private static CallToolResult BuildExecuteSuccessResponse( // Handle different result types if (queryResult is OkObjectResult okResult && okResult.Value != null) { - // Extract the actual data from the action result - if (okResult.Value is JsonDocument jsonDoc) - { - JsonElement root = jsonDoc.RootElement; - responseData["value"] = root.ValueKind == JsonValueKind.Array ? root : JsonSerializer.SerializeToElement(new[] { root }); - } - else if (okResult.Value is JsonElement jsonElement) - { - responseData["value"] = jsonElement.ValueKind == JsonValueKind.Array ? jsonElement : JsonSerializer.SerializeToElement(new[] { jsonElement }); - } - else - { - // Serialize the value directly - JsonElement serialized = JsonSerializer.SerializeToElement(okResult.Value); - responseData["value"] = serialized; - } + responseData["value"] = McpResponseBuilder.NormalizeStoredProcedureResult(okResult.Value); } else if (queryResult is BadRequestObjectResult badRequest) { diff --git a/src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs b/src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs index 1d78e81680..868a47995d 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs @@ -580,22 +580,7 @@ private static CallToolResult BuildExecuteSuccessResponse( // Handle different result types if (queryResult is OkObjectResult okResult && okResult.Value != null) { - // Extract the actual data from the action result - if (okResult.Value is JsonDocument jsonDoc) - { - JsonElement root = jsonDoc.RootElement; - responseData["value"] = root.ValueKind == JsonValueKind.Array ? root : JsonSerializer.SerializeToElement(new[] { root }); - } - else if (okResult.Value is JsonElement jsonElement) - { - responseData["value"] = jsonElement.ValueKind == JsonValueKind.Array ? jsonElement : JsonSerializer.SerializeToElement(new[] { jsonElement }); - } - else - { - // Serialize the value directly - JsonElement serialized = JsonSerializer.SerializeToElement(okResult.Value); - responseData["value"] = serialized; - } + responseData["value"] = McpResponseBuilder.NormalizeStoredProcedureResult(okResult.Value); } else if (queryResult is BadRequestObjectResult badRequest) { diff --git a/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs b/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs index 401f270f42..34de1956b2 100644 --- a/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs +++ b/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs @@ -102,6 +102,30 @@ public static string ExtractResultJson(IActionResult? result) } } + /// + /// Normalizes a stored procedure result to an array, unwrapping the DAB response envelope when present. + /// + public static JsonElement NormalizeStoredProcedureResult(object value) + { + JsonElement result = value switch + { + JsonDocument document => document.RootElement, + JsonElement element => element, + _ => JsonSerializer.SerializeToElement(value) + }; + + if (result.ValueKind == JsonValueKind.Object && + result.TryGetProperty("value", out JsonElement valueArray) && + valueArray.ValueKind == JsonValueKind.Array) + { + return valueArray.Clone(); + } + + return result.ValueKind == JsonValueKind.Array + ? result.Clone() + : JsonSerializer.SerializeToElement(new[] { result }); + } + /// /// Extracts value from a JsonElement. /// diff --git a/src/Service.Tests/Mcp/DynamicCustomToolMsSqlIntegrationTests.cs b/src/Service.Tests/Mcp/DynamicCustomToolMsSqlIntegrationTests.cs index 6ea4bb7f45..edc387636e 100644 --- a/src/Service.Tests/Mcp/DynamicCustomToolMsSqlIntegrationTests.cs +++ b/src/Service.Tests/Mcp/DynamicCustomToolMsSqlIntegrationTests.cs @@ -82,13 +82,9 @@ public async Task DynamicCustomTool_GetBookById_ReturnsMatchingRecord() Assert.IsTrue(root.TryGetProperty("value", out JsonElement valueWrapper), "Response should contain 'value' property."); - JsonElement records = valueWrapper.ValueKind == JsonValueKind.Object - ? valueWrapper.GetProperty("value") - : valueWrapper; - - Assert.AreEqual(JsonValueKind.Array, records.ValueKind); - Assert.IsTrue(records.GetArrayLength() > 0, "Expected at least one book record."); - Assert.AreEqual(1, records[0].GetProperty("id").GetInt32()); + Assert.AreEqual(JsonValueKind.Array, valueWrapper.ValueKind); + Assert.IsTrue(valueWrapper.GetArrayLength() > 0, "Expected at least one book record."); + Assert.AreEqual(1, valueWrapper[0].GetProperty("id").GetInt32()); } /// diff --git a/src/Service.Tests/Mcp/DynamicCustomToolTests.cs b/src/Service.Tests/Mcp/DynamicCustomToolTests.cs index aad83ed358..ce0450fa9f 100644 --- a/src/Service.Tests/Mcp/DynamicCustomToolTests.cs +++ b/src/Service.Tests/Mcp/DynamicCustomToolTests.cs @@ -554,6 +554,23 @@ public async Task ExecuteAsync_JsonDocumentObjectResult_WrapsValueInArray() Assert.AreEqual(JsonValueKind.Array, response.RootElement.GetProperty("value").ValueKind); } + [TestMethod] + public async Task ExecuteAsync_EngineValueEnvelope_IsUnwrapped() + { + using JsonDocument queryDocument = JsonDocument.Parse("{\"value\":[{\"id\":1}]}"); + CallToolResult result = await ExecuteCustomToolAsync( + dbParameters: new(), + userParameters: null, + queryOutcome: new OkObjectResult(queryDocument)); + + AssertSuccess(result, "The DAB response envelope should be removed from the stored procedure result."); + using JsonDocument response = JsonDocument.Parse(GetFirstText(result)); + JsonElement value = response.RootElement.GetProperty("value"); + Assert.AreEqual(JsonValueKind.Array, value.ValueKind); + Assert.AreEqual(1, value.GetArrayLength()); + Assert.AreEqual(1, value[0].GetProperty("id").GetInt32()); + } + [TestMethod] public async Task ExecuteAsync_PocoResult_IsSerialized() { diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs index f4089d8e19..75ce3d5730 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolMsSqlIntegrationTests.cs @@ -76,13 +76,9 @@ public async Task ExecuteEntity_GetBookById_ReturnsMatchingRecord() Assert.IsTrue(root.TryGetProperty("value", out JsonElement valueWrapper), "Response should contain 'value' property."); - JsonElement records = valueWrapper.ValueKind == JsonValueKind.Object - ? valueWrapper.GetProperty("value") - : valueWrapper; - - Assert.AreEqual(JsonValueKind.Array, records.ValueKind); - Assert.IsTrue(records.GetArrayLength() > 0, "Expected at least one book record."); - Assert.AreEqual(1, records[0].GetProperty("id").GetInt32()); + Assert.AreEqual(JsonValueKind.Array, valueWrapper.ValueKind); + Assert.IsTrue(valueWrapper.GetArrayLength() > 0, "Expected at least one book record."); + Assert.AreEqual(1, valueWrapper[0].GetProperty("id").GetInt32()); } /// diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs index 843cbb4cd3..fd62480ef7 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs @@ -538,6 +538,24 @@ public async Task ExecuteEntity_ObjectJsonResult_IsWrappedInArray() Assert.AreEqual(1, value[0].GetProperty("id").GetInt32()); } + [TestMethod] + public async Task ExecuteEntity_EngineValueEnvelope_IsUnwrapped() + { + using JsonDocument document = JsonDocument.Parse("{\"value\":[{\"id\":1}]}"); + CallToolResult result = await ExecuteWithMockedEngineAsync( + TEST_ENTITY, + new(), + null, + queryOutcome: new OkObjectResult(document.RootElement.Clone())); + + AssertSuccess(result, "The DAB response envelope should be removed from the stored procedure result."); + using JsonDocument response = JsonDocument.Parse(GetFirstText(result)); + JsonElement value = response.RootElement.GetProperty("value"); + Assert.AreEqual(JsonValueKind.Array, value.ValueKind); + Assert.AreEqual(1, value.GetArrayLength()); + Assert.AreEqual(1, value[0].GetProperty("id").GetInt32()); + } + [TestMethod] public async Task ExecuteEntity_UnknownResult_ReturnsEmptyValue() { From d6453d9c99336151460b557d7a9b1965529fb8db Mon Sep 17 00:00:00 2001 From: vaishali123t Date: Tue, 20 Oct 2026 21:03:39 +0530 Subject: [PATCH 2/3] tests updated --- src/Service.Tests/Mcp/DynamicCustomToolTests.cs | 14 ++++++++------ src/Service.Tests/Mcp/ExecuteEntityToolTests.cs | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Service.Tests/Mcp/DynamicCustomToolTests.cs b/src/Service.Tests/Mcp/DynamicCustomToolTests.cs index ce0450fa9f..7672bef938 100644 --- a/src/Service.Tests/Mcp/DynamicCustomToolTests.cs +++ b/src/Service.Tests/Mcp/DynamicCustomToolTests.cs @@ -554,21 +554,23 @@ public async Task ExecuteAsync_JsonDocumentObjectResult_WrapsValueInArray() Assert.AreEqual(JsonValueKind.Array, response.RootElement.GetProperty("value").ValueKind); } - [TestMethod] - public async Task ExecuteAsync_EngineValueEnvelope_IsUnwrapped() + [DataTestMethod] + [DataRow("[]", DisplayName = "Empty result")] + [DataRow("[{\"id\":1},{\"id\":2}]", DisplayName = "Multiple rows")] + [DataRow("[{\"id\":1,\"value\":[\"first\",{\"nested\":\"second\"}]}]", DisplayName = "Array-valued value column")] + public async Task ExecuteAsync_EngineResponse_RemovesOnlyOuterEnvelope(string rowsJson) { - using JsonDocument queryDocument = JsonDocument.Parse("{\"value\":[{\"id\":1}]}"); + using JsonDocument rows = JsonDocument.Parse(rowsJson); CallToolResult result = await ExecuteCustomToolAsync( dbParameters: new(), userParameters: null, - queryOutcome: new OkObjectResult(queryDocument)); + queryOutcome: SqlResponseHelpers.OkResponse(rows.RootElement.Clone())); AssertSuccess(result, "The DAB response envelope should be removed from the stored procedure result."); using JsonDocument response = JsonDocument.Parse(GetFirstText(result)); JsonElement value = response.RootElement.GetProperty("value"); Assert.AreEqual(JsonValueKind.Array, value.ValueKind); - Assert.AreEqual(1, value.GetArrayLength()); - Assert.AreEqual(1, value[0].GetProperty("id").GetInt32()); + Assert.AreEqual(rows.RootElement.GetRawText(), value.GetRawText(), "Rows should be unchanged after removing only the engine envelope."); } [TestMethod] diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs index fd62480ef7..f8eadd55e8 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs @@ -538,22 +538,24 @@ public async Task ExecuteEntity_ObjectJsonResult_IsWrappedInArray() Assert.AreEqual(1, value[0].GetProperty("id").GetInt32()); } - [TestMethod] - public async Task ExecuteEntity_EngineValueEnvelope_IsUnwrapped() + [DataTestMethod] + [DataRow("[]", DisplayName = "Empty result")] + [DataRow("[{\"id\":1},{\"id\":2}]", DisplayName = "Multiple rows")] + [DataRow("[{\"id\":1,\"value\":[\"first\",{\"nested\":\"second\"}]}]", DisplayName = "Array-valued value column")] + public async Task ExecuteEntity_EngineResponse_RemovesOnlyOuterEnvelope(string rowsJson) { - using JsonDocument document = JsonDocument.Parse("{\"value\":[{\"id\":1}]}"); + using JsonDocument rows = JsonDocument.Parse(rowsJson); CallToolResult result = await ExecuteWithMockedEngineAsync( TEST_ENTITY, new(), null, - queryOutcome: new OkObjectResult(document.RootElement.Clone())); + queryOutcome: SqlResponseHelpers.OkResponse(rows.RootElement.Clone())); AssertSuccess(result, "The DAB response envelope should be removed from the stored procedure result."); using JsonDocument response = JsonDocument.Parse(GetFirstText(result)); JsonElement value = response.RootElement.GetProperty("value"); Assert.AreEqual(JsonValueKind.Array, value.ValueKind); - Assert.AreEqual(1, value.GetArrayLength()); - Assert.AreEqual(1, value[0].GetProperty("id").GetInt32()); + Assert.AreEqual(rows.RootElement.GetRawText(), value.GetRawText(), "Rows should be unchanged after removing only the engine envelope."); } [TestMethod] From a1fd393a7e654f89e33386086e3d5fa93294f020 Mon Sep 17 00:00:00 2001 From: vaishali123t Date: Tue, 20 Oct 2026 21:11:50 +0530 Subject: [PATCH 3/3] tests updated --- src/Service.Tests/Mcp/DynamicCustomToolTests.cs | 2 +- src/Service.Tests/Mcp/ExecuteEntityToolTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/Mcp/DynamicCustomToolTests.cs b/src/Service.Tests/Mcp/DynamicCustomToolTests.cs index 7672bef938..10e4e95df5 100644 --- a/src/Service.Tests/Mcp/DynamicCustomToolTests.cs +++ b/src/Service.Tests/Mcp/DynamicCustomToolTests.cs @@ -570,7 +570,7 @@ public async Task ExecuteAsync_EngineResponse_RemovesOnlyOuterEnvelope(string ro using JsonDocument response = JsonDocument.Parse(GetFirstText(result)); JsonElement value = response.RootElement.GetProperty("value"); Assert.AreEqual(JsonValueKind.Array, value.ValueKind); - Assert.AreEqual(rows.RootElement.GetRawText(), value.GetRawText(), "Rows should be unchanged after removing only the engine envelope."); + Assert.AreEqual(JsonSerializer.Serialize(rows.RootElement), JsonSerializer.Serialize(value), "Rows should be unchanged after removing only the engine envelope."); } [TestMethod] diff --git a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs index f8eadd55e8..e27b667b75 100644 --- a/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs +++ b/src/Service.Tests/Mcp/ExecuteEntityToolTests.cs @@ -555,7 +555,7 @@ public async Task ExecuteEntity_EngineResponse_RemovesOnlyOuterEnvelope(string r using JsonDocument response = JsonDocument.Parse(GetFirstText(result)); JsonElement value = response.RootElement.GetProperty("value"); Assert.AreEqual(JsonValueKind.Array, value.ValueKind); - Assert.AreEqual(rows.RootElement.GetRawText(), value.GetRawText(), "Rows should be unchanged after removing only the engine envelope."); + Assert.AreEqual(JsonSerializer.Serialize(rows.RootElement), JsonSerializer.Serialize(value), "Rows should be unchanged after removing only the engine envelope."); } [TestMethod]