From e24dbf125c226fc55de1e0212c0fd35252e6a325 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:39:43 +0000 Subject: [PATCH 1/7] Initial plan From dd623b518d7e6115d2e4af16287ec616e58833c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:46:47 +0000 Subject: [PATCH 2/7] Add description field to MCP runtime configuration Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- .../Core/McpStdioServer.cs | 70 +++++++++++++++---- src/Cli/Commands/ConfigureOptions.cs | 5 ++ src/Cli/ConfigGenerator.cs | 11 ++- src/Config/ObjectModel/McpRuntimeOptions.cs | 11 ++- 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs index 79ccf39356..f1459af026 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs @@ -3,7 +3,9 @@ using System.Security.Claims; using System.Text; using System.Text.Json; +using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator; +using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Mcp.Model; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -161,27 +163,65 @@ private void HandleInitialize(JsonElement? id) // Extract the actual id value from the request object? requestId = id.HasValue ? GetIdValue(id.Value) : null; + // Get the description from runtime config if available + string? instructions = null; + try + { + RuntimeConfigProvider? runtimeConfigProvider = _serviceProvider.GetService(); + if (runtimeConfigProvider != null) + { + RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig(); + instructions = runtimeConfig.Runtime?.Mcp?.Description; + } + } + catch + { + // If we can't get the config, continue without instructions + } + // Create the initialize response - var response = new + var result = new { - jsonrpc = "2.0", - id = requestId, - result = new + protocolVersion = _protocolVersion, + capabilities = new { - protocolVersion = _protocolVersion, - capabilities = new - { - tools = new { listChanged = true }, - logging = new { } - }, - serverInfo = new - { - name = "Data API Builder", - version = "1.0.0" - } + tools = new { listChanged = true }, + logging = new { } + }, + serverInfo = new + { + name = "Data API Builder", + version = "1.0.0" } }; + // Add instructions if available and non-empty + object response; + if (!string.IsNullOrWhiteSpace(instructions)) + { + response = new + { + jsonrpc = "2.0", + id = requestId, + result = new + { + result.protocolVersion, + result.capabilities, + result.serverInfo, + instructions + } + }; + } + else + { + response = new + { + jsonrpc = "2.0", + id = requestId, + result + }; + } + string json = JsonSerializer.Serialize(response); Console.Out.WriteLine(json); } diff --git a/src/Cli/Commands/ConfigureOptions.cs b/src/Cli/Commands/ConfigureOptions.cs index 60cb12c3f8..cf36e1a91d 100644 --- a/src/Cli/Commands/ConfigureOptions.cs +++ b/src/Cli/Commands/ConfigureOptions.cs @@ -38,6 +38,7 @@ public ConfigureOptions( bool? runtimeRestRequestBodyStrict = null, bool? runtimeMcpEnabled = null, string? runtimeMcpPath = null, + string? runtimeMcpDescription = null, bool? runtimeMcpDmlToolsEnabled = null, bool? runtimeMcpDmlToolsDescribeEntitiesEnabled = null, bool? runtimeMcpDmlToolsCreateRecordEnabled = null, @@ -93,6 +94,7 @@ public ConfigureOptions( // Mcp RuntimeMcpEnabled = runtimeMcpEnabled; RuntimeMcpPath = runtimeMcpPath; + RuntimeMcpDescription = runtimeMcpDescription; RuntimeMcpDmlToolsEnabled = runtimeMcpDmlToolsEnabled; RuntimeMcpDmlToolsDescribeEntitiesEnabled = runtimeMcpDmlToolsDescribeEntitiesEnabled; RuntimeMcpDmlToolsCreateRecordEnabled = runtimeMcpDmlToolsCreateRecordEnabled; @@ -180,6 +182,9 @@ public ConfigureOptions( [Option("runtime.mcp.path", Required = false, HelpText = "Customize DAB's MCP endpoint path. Default: '/mcp' Conditions: Prefix path with '/'.")] public string? RuntimeMcpPath { get; } + [Option("runtime.mcp.description", Required = false, HelpText = "Set the MCP server description to be exposed in the initialize response.")] + public string? RuntimeMcpDescription { get; } + [Option("runtime.mcp.dml-tools.enabled", Required = false, HelpText = "Enable DAB's MCP DML tools endpoint. Default: true (boolean).")] public bool? RuntimeMcpDmlToolsEnabled { get; } diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 1d673c11e3..741f73acd0 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -797,7 +797,8 @@ private static bool TryUpdateConfiguredRuntimeOptions( // MCP: Enabled and Path if (options.RuntimeMcpEnabled != null || - options.RuntimeMcpPath != null) + options.RuntimeMcpPath != null || + options.RuntimeMcpDescription != null) { McpRuntimeOptions updatedMcpOptions = runtimeConfig?.Runtime?.Mcp ?? new(); bool status = TryUpdateConfiguredMcpValues(options, ref updatedMcpOptions); @@ -1053,6 +1054,14 @@ private static bool TryUpdateConfiguredMcpValues( } } + // Runtime.Mcp.Description + updatedValue = options?.RuntimeMcpDescription; + if (updatedValue != null) + { + updatedMcpOptions = updatedMcpOptions! with { Description = (string)updatedValue }; + _logger.LogInformation("Updated RuntimeConfig with Runtime.Mcp.Description as '{updatedValue}'", updatedValue); + } + // Handle DML tools configuration bool hasToolUpdates = false; DmlToolsConfig? currentDmlTools = updatedMcpOptions?.DmlTools; diff --git a/src/Config/ObjectModel/McpRuntimeOptions.cs b/src/Config/ObjectModel/McpRuntimeOptions.cs index cd1e24f5fd..e17d53fc8f 100644 --- a/src/Config/ObjectModel/McpRuntimeOptions.cs +++ b/src/Config/ObjectModel/McpRuntimeOptions.cs @@ -30,11 +30,18 @@ public record McpRuntimeOptions [JsonConverter(typeof(DmlToolsConfigConverter))] public DmlToolsConfig? DmlTools { get; init; } + /// + /// Description of the MCP server to be exposed in the initialize response + /// + [JsonPropertyName("description")] + public string? Description { get; init; } + [JsonConstructor] public McpRuntimeOptions( bool? Enabled = null, string? Path = null, - DmlToolsConfig? DmlTools = null) + DmlToolsConfig? DmlTools = null, + string? Description = null) { this.Enabled = Enabled ?? true; @@ -58,6 +65,8 @@ public McpRuntimeOptions( { this.DmlTools = DmlTools; } + + this.Description = Description; } /// From 839c081cf5c7ca05d91b14158985f40977bf59cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:55:09 +0000 Subject: [PATCH 3/7] Add description serialization support to MCP converter Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- src/Cli.Tests/ConfigureOptionsTests.cs | 57 +++++++++++++++++++ src/Cli/ConfigGenerator.cs | 9 ++- .../McpRuntimeOptionsConverterFactory.cs | 18 +++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index 073f349a67..4e404c931b 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -926,6 +926,63 @@ public void TestFailureWhenAddingSetSessionContextToMySQLDatabase() Assert.IsFalse(isSuccess); } + /// + /// Tests that running "dab configure --runtime.mcp.description {value}" on a config with various values results + /// in runtime config update. Takes in updated value for mcp.description and + /// validates whether the runtime config reflects those updated values + /// + [DataTestMethod] + [DataRow("This MCP provides access to the Products database and should be used to answer product-related or inventory-related questions from the user.", DisplayName = "Set MCP description.")] + [DataRow("Use this server for customer data queries.", DisplayName = "Set MCP description with short text.")] + public void TestUpdateDescriptionForMcpSettings(string descriptionValue) + { + // Arrange -> all the setup which includes creating options. + SetupFileSystemWithInitialConfig(INITIAL_CONFIG); + + // Act: Attempts to update mcp.description value + ConfigureOptions options = new( + runtimeMcpDescription: descriptionValue, + config: TEST_RUNTIME_CONFIG_FILE + ); + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert: Validate the Description is updated + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsNotNull(runtimeConfig.Runtime?.Mcp?.Description); + Assert.AreEqual(descriptionValue, runtimeConfig.Runtime.Mcp.Description); + } + + /// + /// Tests that the MCP description can be added to a config that doesn't already have one + /// + [TestMethod] + public void TestAddDescriptionToMcpSettings() + { + // Arrange + SetupFileSystemWithInitialConfig(INITIAL_CONFIG); + + // Initial config should not have a description + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config)); + Assert.IsNull(config.Runtime?.Mcp?.Description); + + // Act: Add description + string descriptionValue = "This is a test description for MCP server."; + ConfigureOptions options = new( + runtimeMcpDescription: descriptionValue, + config: TEST_RUNTIME_CONFIG_FILE + ); + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert: Validate the Description is added + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsNotNull(runtimeConfig.Runtime?.Mcp?.Description); + Assert.AreEqual(descriptionValue, runtimeConfig.Runtime.Mcp.Description); + } + /// /// Sets up the mock file system with an initial configuration file. /// This method adds a config file to the mock file system and verifies its existence. diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 741f73acd0..29f8157814 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -798,7 +798,14 @@ private static bool TryUpdateConfiguredRuntimeOptions( // MCP: Enabled and Path if (options.RuntimeMcpEnabled != null || options.RuntimeMcpPath != null || - options.RuntimeMcpDescription != null) + options.RuntimeMcpDescription != null || + options.RuntimeMcpDmlToolsEnabled != null || + options.RuntimeMcpDmlToolsDescribeEntitiesEnabled != null || + options.RuntimeMcpDmlToolsCreateRecordEnabled != null || + options.RuntimeMcpDmlToolsReadRecordsEnabled != null || + options.RuntimeMcpDmlToolsUpdateRecordEnabled != null || + options.RuntimeMcpDmlToolsDeleteRecordEnabled != null || + options.RuntimeMcpDmlToolsExecuteEntityEnabled != null) { McpRuntimeOptions updatedMcpOptions = runtimeConfig?.Runtime?.Mcp ?? new(); bool status = TryUpdateConfiguredMcpValues(options, ref updatedMcpOptions); diff --git a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs index d75cbbef5a..a90eeb0abf 100644 --- a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs @@ -65,12 +65,13 @@ internal McpRuntimeOptionsConverter(DeserializationVariableReplacementSettings? bool enabled = true; string? path = null; DmlToolsConfig? dmlTools = null; + string? description = null; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) { - return new McpRuntimeOptions(enabled, path, dmlTools); + return new McpRuntimeOptions(enabled, path, dmlTools, description); } string? propertyName = reader.GetString(); @@ -98,6 +99,14 @@ internal McpRuntimeOptionsConverter(DeserializationVariableReplacementSettings? dmlTools = dmlToolsConfigConverter.Read(ref reader, typeToConvert, options); break; + case "description": + if (reader.TokenType is not JsonTokenType.Null) + { + description = reader.DeserializeString(_replacementSettings); + } + + break; + default: throw new JsonException($"Unexpected property {propertyName}"); } @@ -134,6 +143,13 @@ public override void Write(Utf8JsonWriter writer, McpRuntimeOptions value, JsonS dmlToolsOptionsConverter.Write(writer, value.DmlTools, options); } + // Write description if it's provided + if (!string.IsNullOrWhiteSpace(value?.Description)) + { + writer.WritePropertyName("description"); + JsonSerializer.Serialize(writer, value.Description, options); + } + writer.WriteEndObject(); } } From fa008eecf1a7431e77c04b4d6c869a4a7f0a67c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:14:33 +0000 Subject: [PATCH 4/7] Address code review feedback - improve error handling and simplify response object creation Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- .../Core/McpStdioServer.cs | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs index f1459af026..0c34a6583b 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs @@ -174,9 +174,11 @@ private void HandleInitialize(JsonElement? id) instructions = runtimeConfig.Runtime?.Mcp?.Description; } } - catch + catch (Exception ex) { // If we can't get the config, continue without instructions + // Log to stderr for diagnostics + Console.Error.WriteLine($"[MCP DEBUG] Failed to retrieve MCP description from config: {ex.Message}"); } // Create the initialize response @@ -192,35 +194,16 @@ private void HandleInitialize(JsonElement? id) { name = "Data API Builder", version = "1.0.0" - } + }, + instructions = !string.IsNullOrWhiteSpace(instructions) ? instructions : null }; - // Add instructions if available and non-empty - object response; - if (!string.IsNullOrWhiteSpace(instructions)) - { - response = new - { - jsonrpc = "2.0", - id = requestId, - result = new - { - result.protocolVersion, - result.capabilities, - result.serverInfo, - instructions - } - }; - } - else + var response = new { - response = new - { - jsonrpc = "2.0", - id = requestId, - result - }; - } + jsonrpc = "2.0", + id = requestId, + result + }; string json = JsonSerializer.Serialize(response); Console.Out.WriteLine(json); From 9acfb7fa8bc8d8c5510a3594c0b3a4a7c80ccada Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:17:32 +0000 Subject: [PATCH 5/7] Fix null reference warning in McpRuntimeOptionsConverter Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- src/Config/Converters/McpRuntimeOptionsConverterFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs index a90eeb0abf..8b3c640725 100644 --- a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs @@ -144,7 +144,7 @@ public override void Write(Utf8JsonWriter writer, McpRuntimeOptions value, JsonS } // Write description if it's provided - if (!string.IsNullOrWhiteSpace(value?.Description)) + if (value is not null && !string.IsNullOrWhiteSpace(value.Description)) { writer.WritePropertyName("description"); JsonSerializer.Serialize(writer, value.Description, options); From 3042ef8cf5f0c78fb412906b3c8691430cbfd683 Mon Sep 17 00:00:00 2001 From: Jerry Nixon <1749983+JerryNixon@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:35:17 -0700 Subject: [PATCH 6/7] Update src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Core/McpStdioServer.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs index 0c34a6583b..7bff5145f7 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs @@ -165,20 +165,20 @@ private void HandleInitialize(JsonElement? id) // Get the description from runtime config if available string? instructions = null; - try + RuntimeConfigProvider? runtimeConfigProvider = _serviceProvider.GetService(); + if (runtimeConfigProvider != null) { - RuntimeConfigProvider? runtimeConfigProvider = _serviceProvider.GetService(); - if (runtimeConfigProvider != null) + try { RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig(); instructions = runtimeConfig.Runtime?.Mcp?.Description; } - } - catch (Exception ex) - { - // If we can't get the config, continue without instructions - // Log to stderr for diagnostics - Console.Error.WriteLine($"[MCP DEBUG] Failed to retrieve MCP description from config: {ex.Message}"); + catch (Exception ex) + { + // Log to stderr for diagnostics and rethrow to avoid masking configuration errors + Console.Error.WriteLine($"[MCP WARNING] Failed to retrieve MCP description from config: {ex.Message}"); + throw; + } } // Create the initialize response From 73aa356230c8f427c89fa69e6ad736524dab7d1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:52:59 +0000 Subject: [PATCH 7/7] Add comprehensive unit tests for MCP description serialization and edge cases Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> --- .../McpRuntimeOptionsSerializationTests.cs | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/Service.Tests/Configuration/McpRuntimeOptionsSerializationTests.cs diff --git a/src/Service.Tests/Configuration/McpRuntimeOptionsSerializationTests.cs b/src/Service.Tests/Configuration/McpRuntimeOptionsSerializationTests.cs new file mode 100644 index 0000000000..c16bececa4 --- /dev/null +++ b/src/Service.Tests/Configuration/McpRuntimeOptionsSerializationTests.cs @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text.Json; +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.Configuration +{ + /// + /// Tests for McpRuntimeOptions serialization and deserialization, + /// including edge cases for the description field. + /// + [TestClass] + public class McpRuntimeOptionsSerializationTests + { + /// + /// Validates that McpRuntimeOptions with a description can be serialized to JSON + /// and deserialized back to the same object. + /// + [TestMethod] + public void TestMcpRuntimeOptionsSerializationWithDescription() + { + // Arrange + string description = "This MCP provides access to the Products database and should be used to answer product-related or inventory-related questions from the user."; + McpRuntimeOptions mcpOptions = new( + Enabled: true, + Path: "/mcp", + DmlTools: null, + Description: description + ); + + RuntimeConfig config = CreateMinimalConfigWithMcp(mcpOptions); + + // Act + string json = config.ToJson(); + bool parseSuccess = RuntimeConfigLoader.TryParseConfig(json, out RuntimeConfig? deserializedConfig); + + // Assert + Assert.IsTrue(parseSuccess, "Failed to deserialize config with MCP description"); + Assert.IsNotNull(deserializedConfig.Runtime?.Mcp, "MCP options should not be null"); + Assert.AreEqual(description, deserializedConfig.Runtime.Mcp.Description, "Description should match"); + Assert.IsTrue(json.Contains("\"description\""), "JSON should contain description field"); + Assert.IsTrue(json.Contains(description), "JSON should contain description value"); + } + + /// + /// Validates that McpRuntimeOptions without a description is serialized correctly + /// and the description field is omitted from JSON. + /// + [TestMethod] + public void TestMcpRuntimeOptionsSerializationWithoutDescription() + { + // Arrange + McpRuntimeOptions mcpOptions = new( + Enabled: true, + Path: "/mcp", + DmlTools: null, + Description: null + ); + + RuntimeConfig config = CreateMinimalConfigWithMcp(mcpOptions); + + // Act + string json = config.ToJson(); + bool parseSuccess = RuntimeConfigLoader.TryParseConfig(json, out RuntimeConfig? deserializedConfig); + + // Assert + Assert.IsTrue(parseSuccess, "Failed to deserialize config without MCP description"); + Assert.IsNotNull(deserializedConfig.Runtime?.Mcp, "MCP options should not be null"); + Assert.IsNull(deserializedConfig.Runtime.Mcp.Description, "Description should be null"); + Assert.IsFalse(json.Contains("\"description\""), "JSON should not contain description field when null"); + } + + /// + /// Validates that McpRuntimeOptions with an empty string description is serialized correctly + /// and the description field is omitted from JSON. + /// + [TestMethod] + public void TestMcpRuntimeOptionsSerializationWithEmptyDescription() + { + // Arrange + McpRuntimeOptions mcpOptions = new( + Enabled: true, + Path: "/mcp", + DmlTools: null, + Description: "" + ); + + RuntimeConfig config = CreateMinimalConfigWithMcp(mcpOptions); + + // Act + string json = config.ToJson(); + bool parseSuccess = RuntimeConfigLoader.TryParseConfig(json, out RuntimeConfig? deserializedConfig); + + // Assert + Assert.IsTrue(parseSuccess, "Failed to deserialize config with empty MCP description"); + Assert.IsNotNull(deserializedConfig.Runtime?.Mcp, "MCP options should not be null"); + Assert.IsTrue(string.IsNullOrEmpty(deserializedConfig.Runtime.Mcp.Description), "Description should be empty"); + Assert.IsFalse(json.Contains("\"description\""), "JSON should not contain description field when empty"); + } + + /// + /// Validates that McpRuntimeOptions with a very long description is serialized and deserialized correctly. + /// + [TestMethod] + public void TestMcpRuntimeOptionsSerializationWithLongDescription() + { + // Arrange + string longDescription = new string('A', 5000); // 5000 character description + McpRuntimeOptions mcpOptions = new( + Enabled: true, + Path: "/mcp", + DmlTools: null, + Description: longDescription + ); + + RuntimeConfig config = CreateMinimalConfigWithMcp(mcpOptions); + + // Act + string json = config.ToJson(); + bool parseSuccess = RuntimeConfigLoader.TryParseConfig(json, out RuntimeConfig? deserializedConfig); + + // Assert + Assert.IsTrue(parseSuccess, "Failed to deserialize config with long MCP description"); + Assert.IsNotNull(deserializedConfig.Runtime?.Mcp, "MCP options should not be null"); + Assert.AreEqual(longDescription, deserializedConfig.Runtime.Mcp.Description, "Long description should match"); + Assert.AreEqual(5000, deserializedConfig.Runtime.Mcp.Description?.Length, "Description length should be 5000"); + } + + /// + /// Validates that McpRuntimeOptions with special characters in description is serialized and deserialized correctly. + /// + [DataTestMethod] + [DataRow("Description with \"quotes\" and 'apostrophes'", DisplayName = "Description with quotes")] + [DataRow("Description with\nnewlines\nand\ttabs", DisplayName = "Description with newlines and tabs")] + [DataRow("Description with special chars: <>&@#$%^*()[]{}|\\", DisplayName = "Description with special characters")] + [DataRow("Description with unicode: 你好世界 🚀 café", DisplayName = "Description with unicode")] + public void TestMcpRuntimeOptionsSerializationWithSpecialCharacters(string description) + { + // Arrange + McpRuntimeOptions mcpOptions = new( + Enabled: true, + Path: "/mcp", + DmlTools: null, + Description: description + ); + + RuntimeConfig config = CreateMinimalConfigWithMcp(mcpOptions); + + // Act + string json = config.ToJson(); + bool parseSuccess = RuntimeConfigLoader.TryParseConfig(json, out RuntimeConfig? deserializedConfig); + + // Assert + Assert.IsTrue(parseSuccess, $"Failed to deserialize config with special character description: {description}"); + Assert.IsNotNull(deserializedConfig.Runtime?.Mcp, "MCP options should not be null"); + Assert.AreEqual(description, deserializedConfig.Runtime.Mcp.Description, "Description with special characters should match exactly"); + } + + /// + /// Validates that existing MCP configuration without description field can be deserialized successfully. + /// This ensures backward compatibility. + /// + [TestMethod] + public void TestBackwardCompatibilityDeserializationWithoutDescriptionField() + { + // Arrange - JSON config without description field + string configJson = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""Server=test;Database=test;"" + }, + ""runtime"": { + ""mcp"": { + ""enabled"": true, + ""path"": ""/mcp"" + } + }, + ""entities"": {} + }"; + + // Act + bool parseSuccess = RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig? deserializedConfig); + + // Assert + Assert.IsTrue(parseSuccess, "Failed to deserialize config without description field"); + Assert.IsNotNull(deserializedConfig.Runtime?.Mcp, "MCP options should not be null"); + Assert.IsNull(deserializedConfig.Runtime.Mcp.Description, "Description should be null when not present in JSON"); + } + + /// + /// Creates a minimal RuntimeConfig with the specified MCP options for testing. + /// + private static RuntimeConfig CreateMinimalConfigWithMcp(McpRuntimeOptions mcpOptions) + { + DataSource dataSource = new( + DatabaseType: DatabaseType.MSSQL, + ConnectionString: "Server=test;Database=test;", + Options: null + ); + + RuntimeOptions runtimeOptions = new( + Rest: null, + GraphQL: null, + Host: null, + Mcp: mcpOptions + ); + + return new RuntimeConfig( + Schema: "test-schema", + DataSource: dataSource, + Runtime: runtimeOptions, + Entities: new RuntimeEntities(new Dictionary()) + ); + } + } +}