From 5ee6e73d2c5b58fa2e73f8326caf4c5b7366fb75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:08:24 +0000 Subject: [PATCH 1/5] Initial plan From e21f4f02962629f803f2366dff637fd07cbb1488 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:25:13 +0000 Subject: [PATCH 2/5] Add MCP check to comprehensive health endpoint --- .../HealthCheck/HealthCheckConstants.cs | 1 + .../Configuration/HealthEndpointTests.cs | 87 +++++++++++++++++++ src/Service/HealthCheck/HealthCheckHelper.cs | 44 ++++++++++ src/Service/HealthCheck/HttpUtilities.cs | 59 +++++++++++++ src/Service/HealthCheck/Utilities.cs | 21 +++++ 5 files changed, 212 insertions(+) diff --git a/src/Config/HealthCheck/HealthCheckConstants.cs b/src/Config/HealthCheck/HealthCheckConstants.cs index b57526fb75..1d566b6714 100644 --- a/src/Config/HealthCheck/HealthCheckConstants.cs +++ b/src/Config/HealthCheck/HealthCheckConstants.cs @@ -12,6 +12,7 @@ public static class HealthCheckConstants public const string DATASOURCE = "data-source"; public const string REST = "rest"; public const string GRAPHQL = "graphql"; + public const string MCP = "mcp"; public const string EMBEDDING = "embedding"; public const int ERROR_RESPONSE_TIME_MS = -1; public const int DEFAULT_THRESHOLD_RESPONSE_TIME_MS = 1000; diff --git a/src/Service.Tests/Configuration/HealthEndpointTests.cs b/src/Service.Tests/Configuration/HealthEndpointTests.cs index a1a8bf4223..4d3942e50d 100644 --- a/src/Service.Tests/Configuration/HealthEndpointTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointTests.cs @@ -110,6 +110,7 @@ public async Task ComprehensiveHealthEndpoint_ValidateContents( ValidateConfigurationDetailsHealthCheckResponse(responseProperties, enableGlobalRest, enableGlobalGraphql, enableGlobalMcp); ValidateIfAttributePresentInResponse(responseProperties, enableDatasourceHealth, HealthCheckConstants.DATASOURCE); ValidateIfAttributePresentInResponse(responseProperties, enableEntityHealth, HealthCheckConstants.ENDPOINT); + ValidateIfAttributePresentInResponse(responseProperties, enableGlobalMcp, HealthCheckConstants.MCP); if (enableEntityHealth) { ValidateEntityRestAndGraphQLResponse(responseProperties, enableEntityRest, enableEntityGraphQL, enableGlobalRest, enableGlobalGraphql); @@ -223,6 +224,52 @@ public async Task TestFailureHealthCheckGraphQLResponseAsync() Assert.IsNotNull(errorMessageFromGraphQL); } + /// + /// Simulates the function call to HttpUtilities.ExecuteMcpQueryAsync. + /// while setting up mock HTTP client to simulate the response from the server to send OK code. + /// Validates the response to ensure no error message is received. + /// + [TestMethod] + public async Task TestHealthCheckMcpResponseAsync() + { + // Arrange + RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true, true); + HttpUtilities httpUtilities = SetupMcpTest(runtimeConfig); + + // Act + // Simulate an MCP initialize POST request to the endpoint. + // Response should be null as error message is not expected to be returned. + string errorMessageFromMcp = await httpUtilities.ExecuteMcpQueryAsync( + mcpUriSuffix: runtimeConfig.McpPath, + incomingRoleHeader: string.Empty, + incomingRoleToken: string.Empty); + + // Assert + Assert.IsNull(errorMessageFromMcp); + } + + /// + /// Simulates the function call to HttpUtilities.ExecuteMcpQueryAsync. + /// while setting up mock HTTP client to simulate the response from the server to send InternalServerError code. + /// Validates the response to ensure error message is received. + /// + [TestMethod] + public async Task TestFailureHealthCheckMcpResponseAsync() + { + // Arrange + RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true, true); + HttpUtilities httpUtilities = SetupMcpTest(runtimeConfig, HttpStatusCode.InternalServerError); + + // Act + string errorMessageFromMcp = await httpUtilities.ExecuteMcpQueryAsync( + mcpUriSuffix: runtimeConfig.McpPath, + incomingRoleHeader: string.Empty, + incomingRoleToken: string.Empty); + + // Assert + Assert.IsNotNull(errorMessageFromMcp); + } + /// /// Tests the serialization behavior of for the property." /// @@ -366,6 +413,46 @@ private static HttpUtilities SetupGraphQLTest(RuntimeConfig runtimeConfig, HttpS mockHttpClientFactory.Object); } + private static HttpUtilities SetupMcpTest(RuntimeConfig runtimeConfig, HttpStatusCode httpStatusCode = HttpStatusCode.OK) + { + // Arrange + // Create a mock entity map with a single entity for testing and load in RuntimeConfigProvider + MockFileSystem fileSystem = new(); + fileSystem.AddFile(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME, new MockFileData(runtimeConfig.ToJson())); + FileSystemRuntimeConfigLoader loader = new(fileSystem); + RuntimeConfigProvider provider = new(loader); + Mock metadataProviderFactory = new(); + + // Mock the handler to return the supplied status code for the MCP initialize POST request. + Mock mockHandler = new(); + mockHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri == new Uri($"{BASE_DAB_URL}{runtimeConfig.McpPath}")), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(httpStatusCode) + { + Content = new StringContent("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}") + }); + + Mock mockHttpClientFactory = new(); + mockHttpClientFactory.Setup(x => x.CreateClient("ContextConfiguredHealthCheckClient")) + .Returns(new HttpClient(mockHandler.Object) + { + BaseAddress = new Uri($"{BASE_DAB_URL}") + }); + + Mock> logger = new(); + + return new( + logger.Object, + metadataProviderFactory.Object, + provider, + mockHttpClientFactory.Object); + } + private static void ValidateEntityRestAndGraphQLResponse( Dictionary responseProperties, bool enableEntityRest, diff --git a/src/Service/HealthCheck/HealthCheckHelper.cs b/src/Service/HealthCheck/HealthCheckHelper.cs index 0fa5a0d539..4f506154eb 100644 --- a/src/Service/HealthCheck/HealthCheckHelper.cs +++ b/src/Service/HealthCheck/HealthCheckHelper.cs @@ -169,6 +169,7 @@ private async Task UpdateHealthCheckDetailsAsync(ComprehensiveHealthCheckReport comprehensiveHealthCheckReport.Checks = new List(); await UpdateDataSourceHealthCheckResultsAsync(comprehensiveHealthCheckReport, runtimeConfig); await UpdateEntityHealthCheckResultsAsync(comprehensiveHealthCheckReport, runtimeConfig, roleHeader, roleToken); + await UpdateMcpHealthCheckResultsAsync(comprehensiveHealthCheckReport, runtimeConfig, roleHeader, roleToken); await UpdateEmbeddingsHealthCheckResultsAsync(comprehensiveHealthCheckReport, runtimeConfig); } @@ -213,6 +214,49 @@ private async Task UpdateDataSourceHealthCheckResultsAsync(ComprehensiveHealthCh return (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); } + // Updates the MCP Health Check Result in the response. + // The check verifies that the MCP endpoint is reachable and responds within the threshold. + // It runs only when MCP is enabled in the runtime configuration. + private async Task UpdateMcpHealthCheckResultsAsync(ComprehensiveHealthCheckReport comprehensiveHealthCheckReport, RuntimeConfig runtimeConfig, string roleHeader, string roleToken) + { + if (comprehensiveHealthCheckReport.Checks is null || !runtimeConfig.IsMcpEnabled) + { + return; + } + + (int, string?) response = await ExecuteMcpQueryAsync(runtimeConfig.McpPath, roleHeader, roleToken); + bool isResponseTimeWithinThreshold = response.Item1 >= 0 && response.Item1 < HealthCheckConstants.DEFAULT_THRESHOLD_RESPONSE_TIME_MS; + + comprehensiveHealthCheckReport.Checks.Add(new HealthCheckResultEntry + { + Name = HealthCheckConstants.MCP, + ResponseTimeData = new ResponseTimeData + { + ResponseTimeMs = response.Item1, + ThresholdMs = HealthCheckConstants.DEFAULT_THRESHOLD_RESPONSE_TIME_MS + }, + Tags = [HealthCheckConstants.MCP], + Exception = response.Item2 ?? (!isResponseTimeWithinThreshold ? TIME_EXCEEDED_ERROR_MESSAGE : null), + Status = isResponseTimeWithinThreshold ? HealthStatus.Healthy : HealthStatus.Unhealthy + }); + } + + // Executes the MCP Query and keeps track of the response time and error message. + private async Task<(int, string?)> ExecuteMcpQueryAsync(string mcpUriSuffix, string roleHeader, string roleToken) + { + string? errorMessage = null; + if (!string.IsNullOrEmpty(mcpUriSuffix)) + { + Stopwatch stopwatch = new(); + stopwatch.Start(); + errorMessage = await _httpUtility.ExecuteMcpQueryAsync(mcpUriSuffix, roleHeader, roleToken); + stopwatch.Stop(); + return string.IsNullOrEmpty(errorMessage) ? ((int)stopwatch.ElapsedMilliseconds, errorMessage) : (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); + } + + return (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); + } + // Updates the Entity Health Check Results in the response. // Goes through the entities one by one and executes the rest and graphql checks (if enabled). // Stored procedures are excluded from health checks because they require parameters and are not guaranteed to be deterministic. diff --git a/src/Service/HealthCheck/HttpUtilities.cs b/src/Service/HealthCheck/HttpUtilities.cs index 2a8d7b9f3e..78f16f9410 100644 --- a/src/Service/HealthCheck/HttpUtilities.cs +++ b/src/Service/HealthCheck/HttpUtilities.cs @@ -136,6 +136,65 @@ public HttpUtilities( } } + // Executes the MCP query by sending an initialize JSON-RPC POST request to the MCP endpoint. + public async Task ExecuteMcpQueryAsync(string mcpUriSuffix, string incomingRoleHeader, string incomingRoleToken) + { + string? errorMessage = null; + try + { + if (string.IsNullOrEmpty(mcpUriSuffix)) + { + _logger.LogError("The MCP route is not available, hence HealthEndpoint is not available."); + return errorMessage; + } + + if (!Program.CheckSanityOfUrl($"{_httpClient.BaseAddress}{mcpUriSuffix.TrimStart('/')}")) + { + _logger.LogError("Blocked outbound request due to invalid or unsafe URI."); + return "Blocked outbound request due to invalid or unsafe URI."; + } + + string jsonPayload = Utilities.CreateHttpMcpQuery(); + HttpContent content = new StringContent(jsonPayload, Encoding.UTF8, Utilities.JSON_CONTENT_TYPE); + + HttpRequestMessage message = new(method: HttpMethod.Post, requestUri: mcpUriSuffix) + { + Content = content + }; + + // The MCP Streamable HTTP transport requires the client to accept both + // JSON and SSE responses. + message.Headers.Add("Accept", "application/json, text/event-stream"); + + if (!string.IsNullOrEmpty(incomingRoleToken)) + { + message.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, incomingRoleToken); + } + + if (!string.IsNullOrEmpty(incomingRoleHeader)) + { + message.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, incomingRoleHeader); + } + + HttpResponseMessage response = await _httpClient.SendAsync(message); + if (response.IsSuccessStatusCode) + { + _logger.LogTrace($"The MCP HealthEndpoint query executed successfully with code {response.StatusCode}."); + } + else + { + errorMessage = $"The MCP HealthEndpoint query failed with code: {response.StatusCode}."; + } + + return errorMessage; + } + catch (Exception ex) + { + _logger.LogError($"An exception occurred while executing the health check MCP query: {ex.Message}"); + return ex.Message; + } + } + // Executes the GraphQL query by sending a POST request to the API. // Internally calls the metadata provider to fetch the column names to create the graphql payload. public async Task ExecuteGraphQLQueryAsync(string graphqlUriSuffix, string entityName, Entity entity, string incomingRoleHeader, string incomingRoleToken) diff --git a/src/Service/HealthCheck/Utilities.cs b/src/Service/HealthCheck/Utilities.cs index 888ffbca91..dc9125577b 100644 --- a/src/Service/HealthCheck/Utilities.cs +++ b/src/Service/HealthCheck/Utilities.cs @@ -72,6 +72,27 @@ public static string CreateHttpRestQuery(string entityName, int first) return $"/{entityName}?$first={first}"; } + public static string CreateHttpMcpQuery() + { + // Create a minimal MCP request (initialize) as a valid JSON-RPC request. + // 'initialize' is used because other methods (e.g. 'tools/list') require an active + // session in the MCP Streamable HTTP transport. + var payload = new + { + jsonrpc = "2.0", + id = 1, + method = "initialize", + @params = new + { + protocolVersion = "2025-03-26", + capabilities = new { }, + clientInfo = new { name = "dab-health-check", version = "1.0.0" } + } + }; + + return JsonSerializer.Serialize(payload); + } + public static string NormalizeConnectionString(string connectionString, DatabaseType dbType, ILogger? logger = null) { try From e8495b06418203049b1e72c0dbaa592c3dcc3357 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Tue, 30 Jun 2026 11:17:08 +0530 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Service.Tests/Configuration/HealthEndpointTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/Configuration/HealthEndpointTests.cs b/src/Service.Tests/Configuration/HealthEndpointTests.cs index 4d3942e50d..9572d0baa1 100644 --- a/src/Service.Tests/Configuration/HealthEndpointTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointTests.cs @@ -430,7 +430,11 @@ private static HttpUtilities SetupMcpTest(RuntimeConfig runtimeConfig, HttpStatu "SendAsync", ItExpr.Is(req => req.Method == HttpMethod.Post && - req.RequestUri == new Uri($"{BASE_DAB_URL}{runtimeConfig.McpPath}")), + req.RequestUri == new Uri($"{BASE_DAB_URL}{runtimeConfig.McpPath}") && + req.Content != null && + req.Content.ReadAsStringAsync().Result.Contains("\"method\":\"initialize\"") && + req.Headers.TryGetValues("Accept", out IEnumerable? acceptValues) && + acceptValues.Any(v => v.Contains("application/json") && v.Contains("text/event-stream"))), ItExpr.IsAny()) .ReturnsAsync(new HttpResponseMessage(httpStatusCode) { From 751990d8ac95e1446dd23e4a726eefbc1cb5ec30 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Tue, 30 Jun 2026 11:17:18 +0530 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Service/HealthCheck/HealthCheckHelper.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Service/HealthCheck/HealthCheckHelper.cs b/src/Service/HealthCheck/HealthCheckHelper.cs index 4f506154eb..2acc5cb43c 100644 --- a/src/Service/HealthCheck/HealthCheckHelper.cs +++ b/src/Service/HealthCheck/HealthCheckHelper.cs @@ -244,17 +244,19 @@ private async Task UpdateMcpHealthCheckResultsAsync(ComprehensiveHealthCheckRepo // Executes the MCP Query and keeps track of the response time and error message. private async Task<(int, string?)> ExecuteMcpQueryAsync(string mcpUriSuffix, string roleHeader, string roleToken) { - string? errorMessage = null; - if (!string.IsNullOrEmpty(mcpUriSuffix)) + if (string.IsNullOrEmpty(mcpUriSuffix)) { - Stopwatch stopwatch = new(); - stopwatch.Start(); - errorMessage = await _httpUtility.ExecuteMcpQueryAsync(mcpUriSuffix, roleHeader, roleToken); - stopwatch.Stop(); - return string.IsNullOrEmpty(errorMessage) ? ((int)stopwatch.ElapsedMilliseconds, errorMessage) : (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); + return (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, "MCP is enabled but no MCP path is configured."); } - return (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); + Stopwatch stopwatch = new(); + stopwatch.Start(); + string? errorMessage = await _httpUtility.ExecuteMcpQueryAsync(mcpUriSuffix, roleHeader, roleToken); + stopwatch.Stop(); + + return string.IsNullOrEmpty(errorMessage) + ? ((int)stopwatch.ElapsedMilliseconds, errorMessage) + : (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); } // Updates the Entity Health Check Results in the response. From 417bc495bcc1a3313223ffcb21516697a23b37f2 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Tue, 30 Jun 2026 11:17:27 +0530 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Service/HealthCheck/HttpUtilities.cs | 29 +++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Service/HealthCheck/HttpUtilities.cs b/src/Service/HealthCheck/HttpUtilities.cs index 78f16f9410..1e2a15ae22 100644 --- a/src/Service/HealthCheck/HttpUtilities.cs +++ b/src/Service/HealthCheck/HttpUtilities.cs @@ -142,13 +142,30 @@ public HttpUtilities( string? errorMessage = null; try { - if (string.IsNullOrEmpty(mcpUriSuffix)) + if (string.IsNullOrWhiteSpace(mcpUriSuffix)) { - _logger.LogError("The MCP route is not available, hence HealthEndpoint is not available."); - return errorMessage; + const string msg = "MCP path is not configured."; + _logger.LogError(msg); + return msg; + } + + if (_httpClient.BaseAddress is null) + { + const string msg = "Health check HTTP client BaseAddress is not configured."; + _logger.LogError(msg); + return msg; } - if (!Program.CheckSanityOfUrl($"{_httpClient.BaseAddress}{mcpUriSuffix.TrimStart('/')}")) + // Ensure the configured MCP path cannot override the host (e.g. absolute URIs). + if (!Uri.TryCreate(mcpUriSuffix, UriKind.Relative, out _)) + { + _logger.LogError("Blocked outbound request due to invalid or unsafe URI."); + return "Blocked outbound request due to invalid or unsafe URI."; + } + + Uri requestUri = new(_httpClient.BaseAddress, mcpUriSuffix); + + if (!Program.CheckSanityOfUrl(requestUri.AbsoluteUri)) { _logger.LogError("Blocked outbound request due to invalid or unsafe URI."); return "Blocked outbound request due to invalid or unsafe URI."; @@ -157,10 +174,10 @@ public HttpUtilities( string jsonPayload = Utilities.CreateHttpMcpQuery(); HttpContent content = new StringContent(jsonPayload, Encoding.UTF8, Utilities.JSON_CONTENT_TYPE); - HttpRequestMessage message = new(method: HttpMethod.Post, requestUri: mcpUriSuffix) + HttpRequestMessage message = new(method: HttpMethod.Post, requestUri: requestUri) { Content = content - }; + } // The MCP Streamable HTTP transport requires the client to accept both // JSON and SSE responses.