Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,11 +18,17 @@ internal static class McpServerConfiguration
/// <summary>
/// Configures the MCP server with tool capabilities
/// </summary>
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
53 changes: 38 additions & 15 deletions src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot, Even though HandleInitialize of the McpStdioServer class has been modified to include the description. We need similar changes to the Mcp Http Server that provides the tool registry in McpServerConfiguration.

Also, please dont update PR description. Retain the original PR description that described this complete change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated MCP HTTP server configuration in McpServerConfiguration.cs:

  • Changed server name to "SQL MCP Server" to match stdio server
  • Modified ConfigureMcpServer to accept RuntimeConfig parameter
  • Added comment noting that instructions support will be added when ModelContextProtocol.AspNetCore library supports it

The HTTP server now receives the runtime config but can't set instructions yet as the library doesn't expose this property on McpServerOptions. The description is ready to be wired when library support is available.

Original PR description has been retained as requested. Commit: b162ad4

// Get the description from runtime config if available
string? instructions = null;
RuntimeConfigProvider? runtimeConfigProvider = _serviceProvider.GetService<RuntimeConfigProvider>();
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);
Expand Down
57 changes: 57 additions & 0 deletions src/Cli.Tests/ConfigureOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,63 @@ public void TestFailureWhenAddingSetSessionContextToMySQLDatabase()
Assert.IsFalse(isSuccess);
}

/// <summary>
/// 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
/// </summary>
[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);
}

/// <summary>
/// Tests that the MCP description can be added to a config that doesn't already have one
/// </summary>
[TestMethod]
public void TestAddDescriptionToMcpSettings()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestAddDescriptionToMcpSettings is exactly same as TestUpdateDescriptionForMcpSettings, can be removed. The test TestUpdateDescriptionForMcpSettings can be renamed to TestConfigureDescriptionForMcpSettings.

{
// 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);
}

/// <summary>
/// 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.
Expand Down
5 changes: 5 additions & 0 deletions src/Cli/Commands/ConfigureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -93,6 +94,7 @@ public ConfigureOptions(
// Mcp
RuntimeMcpEnabled = runtimeMcpEnabled;
RuntimeMcpPath = runtimeMcpPath;
RuntimeMcpDescription = runtimeMcpDescription;
RuntimeMcpDmlToolsEnabled = runtimeMcpDmlToolsEnabled;
RuntimeMcpDmlToolsDescribeEntitiesEnabled = runtimeMcpDmlToolsDescribeEntitiesEnabled;
RuntimeMcpDmlToolsCreateRecordEnabled = runtimeMcpDmlToolsCreateRecordEnabled;
Expand Down Expand Up @@ -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; }

Expand Down
18 changes: 17 additions & 1 deletion src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 17 additions & 1 deletion src/Config/Converters/McpRuntimeOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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}");
}
Expand Down Expand Up @@ -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();
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/Config/ObjectModel/McpRuntimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@ public record McpRuntimeOptions
[JsonConverter(typeof(DmlToolsConfigConverter))]
public DmlToolsConfig? DmlTools { get; init; }

/// <summary>
/// Description of the MCP server to be exposed in the initialize response
/// </summary>
[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;

Expand All @@ -58,6 +65,8 @@ public McpRuntimeOptions(
{
this.DmlTools = DmlTools;
}

this.Description = Description;
}

/// <summary>
Expand Down
Loading