From bffdefe5e1b0decb4edae920c0aff11a2ae9be00 Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Thu, 9 Apr 2026 13:24:08 -0700 Subject: [PATCH] Added changes to set McpServerOptions.Instructions in HTTPS/SSE mode. --- .../Core/McpServerConfiguration.cs | 3 +- .../Core/McpServiceCollectionExtensions.cs | 4 +- .../Configuration/ConfigurationTests.cs | 115 ++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs index 2b48c37a83..387ea7427f 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs @@ -19,7 +19,7 @@ internal static class McpServerConfiguration /// /// Configures the MCP server with tool capabilities. /// - internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services) + internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services, string? instructions) { services.AddMcpServer() .WithListToolsHandler((RequestContext request, CancellationToken ct) => @@ -93,6 +93,7 @@ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection se options.ServerInfo = new() { Name = McpProtocolDefaults.MCP_SERVER_NAME, Version = McpProtocolDefaults.MCP_SERVER_VERSION }; options.Capabilities ??= new(); options.Capabilities.Tools ??= new(); + options.ServerInstructions = !string.IsNullOrWhiteSpace(instructions) ? instructions : null; }); return services; diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs index bc87602da9..c88cae148d 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs @@ -41,8 +41,8 @@ public static IServiceCollection AddDabMcpServer(this IServiceCollection service // Register custom tools from configuration RegisterCustomTools(services, runtimeConfig); - // Configure MCP server - services.ConfigureMcpServer(); + // Configure MCP server and propagate runtime description to MCP initialize instructions. + services.ConfigureMcpServer(runtimeConfig.Runtime?.Mcp?.Description); return services; } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index a8925f3ad6..3deb8d5cb7 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -2804,6 +2804,44 @@ public async Task TestGlobalFlagToEnableRestGraphQLAndMcpForHostedAndNonHostedEn } } + [TestMethod] + [TestCategory(TestCategory.MSSQL)] + public async Task TestMcpInitializeIncludesInstructionsFromRuntimeDescription() + { + const string MCP_INSTRUCTIONS = "Use SQL tools to query the database."; + const string CUSTOM_CONFIG = "custom-config-mcp-instructions.json"; + + TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); + + GraphQLRuntimeOptions graphqlOptions = new(Enabled: false); + RestRuntimeOptions restRuntimeOptions = new(Enabled: false); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: true, Description: MCP_INSTRUCTIONS); + + SqlConnectionStringBuilder connectionStringBuilder = new(GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)) + { + TrustServerCertificate = true + }; + + DataSource dataSource = new(DatabaseType.MSSQL, + connectionStringBuilder.ConnectionString, Options: null); + + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions); + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; + + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + + JsonElement initializeResponse = await GetMcpInitializeResponse(client, configuration.Runtime.Mcp); + JsonElement result = initializeResponse.GetProperty("result"); + + Assert.AreEqual(MCP_INSTRUCTIONS, result.GetProperty("instructions").GetString(), "MCP initialize response should include instructions from runtime.mcp.description."); + } + /// /// For mutation operations, both the respective operation(create/update/delete) + read permissions are needed to receive a valid response. /// In this test, Anonymous role is configured with only create permission. @@ -6284,6 +6322,83 @@ public static async Task GetMcpResponse(HttpClient httpClient, M return responseCode; } + /// + /// Executes MCP initialize over HTTP and returns the parsed JSON response. + /// + public static async Task GetMcpInitializeResponse(HttpClient httpClient, McpRuntimeOptions mcp) + { + int retryCount = 0; + HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; + string responseBody = string.Empty; + + while (retryCount < RETRY_COUNT) + { + object payload = new + { + jsonrpc = "2.0", + id = 1, + method = "initialize", + @params = new + { + protocolVersion = "2025-03-26", + capabilities = new { }, + clientInfo = new { name = "dab-test", version = "1.0.0" } + } + }; + + HttpRequestMessage mcpRequest = new(HttpMethod.Post, mcp.Path) + { + Content = JsonContent.Create(payload) + }; + mcpRequest.Headers.Add("Accept", "application/json, text/event-stream"); + + HttpResponseMessage mcpResponse = await httpClient.SendAsync(mcpRequest); + responseCode = mcpResponse.StatusCode; + responseBody = await mcpResponse.Content.ReadAsStringAsync(); + + if (responseCode == HttpStatusCode.ServiceUnavailable || responseCode == HttpStatusCode.NotFound) + { + retryCount++; + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(RETRY_WAIT_SECONDS, retryCount))); + continue; + } + + break; + } + + Assert.AreEqual(HttpStatusCode.OK, responseCode, "MCP initialize should return HTTP 200."); + Assert.IsFalse(string.IsNullOrWhiteSpace(responseBody), "MCP initialize response body should not be empty."); + + // Depending on transport/content negotiation, initialize can return plain JSON + // or SSE-formatted text where JSON payload is carried in a data: line. + string payloadToParse = responseBody.TrimStart().StartsWith('{') + ? responseBody + : ExtractJsonFromSsePayload(responseBody); + + Assert.IsFalse(string.IsNullOrWhiteSpace(payloadToParse), "MCP initialize response did not contain a JSON payload."); + + using JsonDocument responseDocument = JsonDocument.Parse(payloadToParse); + return responseDocument.RootElement.Clone(); + } + + private static string ExtractJsonFromSsePayload(string ssePayload) + { + foreach (string line in ssePayload.Split('\n')) + { + string trimmed = line.Trim(); + if (trimmed.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + string data = trimmed.Substring("data:".Length).Trim(); + if (!string.IsNullOrWhiteSpace(data) && data.StartsWith('{')) + { + return data; + } + } + } + + return string.Empty; + } + /// /// Helper method to instantiate RuntimeConfig object needed for multiple create tests. ///