Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@
*.verified.txt text eol=lf working-tree-encoding=UTF-8
*.verified.xml text eol=lf working-tree-encoding=UTF-8
*.verified.json text eol=lf working-tree-encoding=UTF-8

# This can't be enabled as there are tests that rely on the CRLF endings in files'
#*.cs text eol=lf
37 changes: 31 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ We use `dotnet format` to enforce code conventions. It is run automatically in C

#### Enforcing code style with git hooks

You can copy paste the following commands to install a git pre-commit hook. This will cause a commit to fail if you forgot to run `dotnet format`. If you have run on save enabled in your editor this is not necessary.
You can copy paste the following commands to install a git pre-commit hook (creates a pre-commit file in your .git folder, which isn't shown in vs code). This will cause a commit to fail if you forgot to run `dotnet format`. If you have run on save enabled in your editor this is not necessary.

```bash
cat > .git/hooks/pre-commit << __EOF__
Expand All @@ -112,17 +112,42 @@ if [ "\$(get_files)" = '' ]; then
fi

get_files |
xargs dotnet format src/Azure.DataApiBuilder.Service.sln \\
--check \\
--fix-whitespace --fix-style warn --fix-analyzers warn \\
xargs dotnet format src/Azure.DataApiBuilder.sln \\
--verify-no-changes
--include \\
|| {
get_files |
xargs dotnet format src/Azure.DataApiBuilder.Service.sln \\
--fix-whitespace --fix-style warn --fix-analyzers warn \\
xargs dotnet format src/Azure.DataApiBuilder.sln \\
--include
exit 1
}
__EOF__
chmod +x .git/hooks/pre-commit
```

The file should look like this

``` bash
#!/bin/bash
set -euo pipefail

get_files() {
git diff --cached --name-only --diff-filter=ACMR | \
grep '\.cs$'
}

if [ "$(get_files)" = '' ]; then
exit 0
fi

get_files |
xargs dotnet format src/Azure.DataApiBuilder.sln \
--verify-no-changes \
--include \
|| {
get_files |
xargs dotnet format src/Azure.DataApiBuilder.sln \
--include
exit 1
}
```
24 changes: 16 additions & 8 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable health check endpoint",
"$ref": "#/$defs/boolean-or-string",
"description": "Enable health check endpoint for something",
"default": true,
"additionalProperties": false
},
Expand Down Expand Up @@ -186,7 +186,7 @@
"type": "string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling REST requests for all entities."
},
"request-body-strict": {
Expand All @@ -210,7 +210,7 @@
"type": "string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling GraphQL requests for all entities."
},
"depth-limit": {
Expand Down Expand Up @@ -438,7 +438,7 @@
"description": "Application Insights connection string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling Application Insights telemetry.",
"default": true
}
Expand Down Expand Up @@ -481,7 +481,7 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling Azure Log Analytics.",
"default": false
},
Expand Down Expand Up @@ -618,7 +618,7 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Enable health check endpoint globally",
"default": true,
"additionalProperties": false
Expand Down Expand Up @@ -1179,7 +1179,15 @@
"type": "string"
}
},
"required": ["singular"]
"required": [ "singular" ]
}
]
},
"boolean-or-string": {
"oneOf":[
{
"type": [ "boolean", "string" ],
"pattern": "^(?:true|false|1|0|@env\\('.*'\\)|@akv\\('.*'\\))$"
}
]
},
Expand Down
26 changes: 24 additions & 2 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 @@ -158,7 +160,26 @@ public async Task RunAsync(CancellationToken cancellationToken)
/// </remarks>
private void HandleInitialize(JsonElement? id)
{
var result = new
// 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
object result = new
{
protocolVersion = _protocolVersion,
capabilities = new
Expand All @@ -170,7 +191,8 @@ private void HandleInitialize(JsonElement? id)
{
name = McpProtocolDefaults.MCP_SERVER_NAME,
version = McpProtocolDefaults.MCP_SERVER_VERSION
}
},
instructions = !string.IsNullOrWhiteSpace(instructions) ? instructions : null
};

WriteResult(id, result);
Expand Down
28 changes: 28 additions & 0 deletions src/Cli.Tests/ConfigureOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,34 @@ 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 TestConfigureDescriptionForMcpSettings(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>
/// 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 @@ -797,7 +797,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 @@ -1053,6 +1061,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
60 changes: 60 additions & 0 deletions src/Config/Converters/BooleanJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.Converters;

/// <summary>
/// JSON converter for boolean values that also supports string representations such as
/// "true", "false", "1", and "0". Any environment variable replacement is handled by
/// other converters (for example, the string converter) before the value is parsed here.
/// </summary>
public class BoolJsonConverter : JsonConverter<bool>
{

public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.Null)
{

throw new JsonException("Unexpected null JSON token. Expected a boolean literal or a valid @expression.");
}

if (reader.TokenType == JsonTokenType.String)
{

string? tempBoolean = JsonSerializer.Deserialize<string>(ref reader, options);

bool result = tempBoolean?.ToLower() switch
{
//numeric values have to be checked here as they may come from string replacement
"true" or "1" => true,
"false" or "0" => false,
_ => throw new JsonException($"Invalid boolean value: {tempBoolean}. Specify either true or 1 for true, false or 0 for false"),
};

return result;
}
else if (reader.TokenType == JsonTokenType.Number)
{
bool result = reader.GetInt32() switch
{
1 => true,
0 => false,
_ => throw new JsonException($"Invalid value for boolean attribute. Specify either 1 or 0."),
};
return result;
}
else
{
return reader.GetBoolean();
}
}

public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
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
Loading