diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
index 86cccd2aaf..046563b93e 100644
--- a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
+++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Text.Json;
+using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Mcp.Model;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol;
@@ -17,11 +18,17 @@ 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, RuntimeConfig runtimeConfig)
{
services.AddMcpServer(options =>
{
- options.ServerInfo = new() { Name = "Data API builder MCP Server", Version = "1.0.0" };
+ options.ServerInfo = new() { Name = "SQL MCP Server", Version = "1.0.0" };
+
+ // Note: The ModelContextProtocol.AspNetCore library does not currently support
+ // setting instructions on the server options. The description from
+ // runtimeConfig.Runtime.Mcp.Description is available for future use when
+ // the library adds support for instructions in the initialize response.
+
options.Capabilities = new()
{
Tools = new()
diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs
index 01f6015786..d2bbd3729d 100644
--- a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs
+++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs
@@ -38,8 +38,8 @@ public static IServiceCollection AddDabMcpServer(this IServiceCollection service
// Auto-discover and register all MCP tools
RegisterAllMcpTools(services);
- // Configure MCP server
- services.ConfigureMcpServer();
+ // Configure MCP server with runtime config
+ services.ConfigureMcpServer(runtimeConfig);
return services;
}
diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
index 79ccf39356..fa78064b7e 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,25 +163,46 @@ 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;
+ RuntimeConfigProvider? runtimeConfigProvider = _serviceProvider.GetService();
+ if (runtimeConfigProvider != null)
+ {
+ try
+ {
+ RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig();
+ instructions = runtimeConfig.Runtime?.Mcp?.Description;
+ }
+ 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
- var response = new
+ object result = new
+ {
+ protocolVersion = _protocolVersion,
+ capabilities = new
+ {
+ tools = new { listChanged = true },
+ logging = new { }
+ },
+ serverInfo = new
+ {
+ name = "SQL MCP Server",
+ version = "1.0.0"
+ },
+ instructions = !string.IsNullOrWhiteSpace(instructions) ? instructions : null
+ };
+
+ object response = new
{
jsonrpc = "2.0",
id = requestId,
- result = new
- {
- protocolVersion = _protocolVersion,
- capabilities = new
- {
- tools = new { listChanged = true },
- logging = new { }
- },
- serverInfo = new
- {
- name = "Data API Builder",
- version = "1.0.0"
- }
- }
+ result
};
string json = JsonSerializer.Serialize(response);
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/Commands/ConfigureOptions.cs b/src/Cli/Commands/ConfigureOptions.cs
index 11dca2a4eb..c3e0352249 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 648edc1950..77ccf88fe1 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -810,7 +810,15 @@ private static bool TryUpdateConfiguredRuntimeOptions(
// MCP: Enabled and Path
if (options.RuntimeMcpEnabled != null ||
- options.RuntimeMcpPath != null)
+ options.RuntimeMcpPath != 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);
@@ -1066,6 +1074,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/Converters/McpRuntimeOptionsConverterFactory.cs b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs
index d75cbbef5a..8b3c640725 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 (value is not null && !string.IsNullOrWhiteSpace(value.Description))
+ {
+ writer.WritePropertyName("description");
+ JsonSerializer.Serialize(writer, value.Description, options);
+ }
+
writer.WriteEndObject();
}
}
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;
}
///
diff --git a/src/Service.Tests/Configuration/McpRuntimeOptionsSerializationTests.cs b/src/Service.Tests/Configuration/McpRuntimeOptionsSerializationTests.cs
new file mode 100644
index 0000000000..6ed3ef1b71
--- /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.IsTrue(json.Contains("\"description\""), "JSON should contain description field");
+ Assert.IsTrue(json.Contains(description), "JSON should contain description value");
+ Assert.AreEqual(description, deserializedConfig.Runtime.Mcp.Description, "Description should match");
+ }
+
+ ///
+ /// 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())
+ );
+ }
+ }
+}