diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 35f2e08270..b348ac4a4f 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -158,13 +158,13 @@ "type": "object", "properties": { "max-page-size": { - "type": ["integer", "null"], + "type": [ "integer", "null" ], "description": "Defines the maximum number of records that can be returned in a single page of results. If set to null, the default value is 100,000.", "default": 100000, "minimum": 1 }, "default-page-size": { - "type": ["integer", "null"], + "type": [ "integer", "null" ], "description": "Sets the default number of records returned in a single response. When this limit is reached, a continuation token is provided to retrieve the next page. If set to null, the default value is 100.", "default": 100, "minimum": 1 @@ -214,7 +214,7 @@ "description": "Allow enabling/disabling GraphQL requests for all entities." }, "depth-limit": { - "type": ["integer", "null"], + "type": [ "integer", "null" ], "description": "Maximum allowed depth of a GraphQL query.", "default": null }, @@ -239,13 +239,74 @@ } } }, + "mcp": { + "type": "object", + "description": "Global MCP endpoint configuration", + "additionalProperties": false, + "properties": { + "path": { + "default": "/mcp", + "type": "string" + }, + "enabled": { + "type": "boolean", + "description": "Allow enabling/disabling MCP requests for all entities.", + "default": true + }, + "dml-tools": { + "oneOf": [ + { + "type": "boolean", + "description": "Enable/disable all DML tools with default settings." + }, + { + "type": "object", + "description": "Individual DML tools configuration", + "additionalProperties": false, + "properties": { + "describe-entities": { + "type": "boolean", + "description": "Enable/disable the describe-entities tool.", + "default": false + }, + "create-record": { + "type": "boolean", + "description": "Enable/disable the create-record tool.", + "default": false + }, + "read-records": { + "type": "boolean", + "description": "Enable/disable the read-records tool.", + "default": false + }, + "update-record": { + "type": "boolean", + "description": "Enable/disable the update-record tool.", + "default": false + }, + "delete-record": { + "type": "boolean", + "description": "Enable/disable the delete-record tool.", + "default": false + }, + "execute-entity": { + "type": "boolean", + "description": "Enable/disable the execute-entity tool.", + "default": false + } + } + } + ] + } + } + }, "host": { "type": "object", "description": "Global hosting configuration", "additionalProperties": false, "properties": { "max-response-size-mb": { - "type": ["integer", "null"], + "type": [ "integer", "null" ], "description": "Specifies the maximum size, in megabytes, of the database response allowed in a single result. If set to null, the default value is 158 MB.", "default": 158, "minimum": 1, @@ -253,12 +314,12 @@ }, "mode": { "description": "Set if running in Development or Production mode", - "type": ["string", "null"], + "type": [ "string", "null" ], "default": "production", - "enum": ["production", "development"] + "enum": [ "production", "development" ] }, "cors": { - "type": ["object", "null"], + "type": [ "object", "null" ], "description": "Configure CORS", "additionalProperties": false, "properties": { @@ -278,7 +339,7 @@ } }, "authentication": { - "type": ["object", "null"], + "type": [ "object", "null" ], "additionalProperties": false, "properties": { "provider": { @@ -322,7 +383,7 @@ "type": "string" } }, - "required": ["audience", "issuer"] + "required": [ "audience", "issuer" ] } }, "allOf": [ @@ -338,9 +399,9 @@ ] } }, - "required": ["provider"] + "required": [ "provider" ] }, - "then": { "required": ["jwt"] }, + "then": { "required": [ "jwt" ] }, "else": { "properties": { "jwt": false } } } ] @@ -382,7 +443,7 @@ "default": true } }, - "required": ["connection-string"] + "required": [ "connection-string" ] }, "open-telemetry": { "type": "object", @@ -405,7 +466,7 @@ "type": "string", "description": "Open Telemetry protocol", "default": "grpc", - "enum": ["grpc", "httpprotobuf"] + "enum": [ "grpc", "httpprotobuf" ] }, "enabled": { "type": "boolean", @@ -413,7 +474,7 @@ "default": true } }, - "required": ["endpoint"] + "required": [ "endpoint" ] }, "azure-log-analytics": { "type": "object", diff --git a/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj new file mode 100644 index 0000000000..f675f8d8d1 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs new file mode 100644 index 0000000000..ed5425c515 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.DataApiBuilder.Mcp.Model; +using ModelContextProtocol.Protocol; +using static Azure.DataApiBuilder.Mcp.Model.McpEnums; + +namespace Azure.DataApiBuilder.Mcp.BuiltInTools +{ + public class CreateRecordTool : IMcpTool + { + public ToolType ToolType { get; } = ToolType.BuiltIn; + + public Tool GetToolMetadata() + { + return new Tool + { + Name = "create-record", + Description = "Creates a new record in the specified entity.", + InputSchema = JsonSerializer.Deserialize( + @"{ + ""type"": ""object"", + ""properties"": { + ""entity"": { + ""type"": ""string"", + ""description"": ""The name of the entity"" + }, + ""data"": { + ""type"": ""object"", + ""description"": ""The data for the new record"" + } + }, + ""required"": [""entity"", ""data""] + }" + ) + }; + } + + public Task ExecuteAsync( + JsonDocument? arguments, + IServiceProvider serviceProvider, + CancellationToken cancellationToken = default) + { + if (arguments == null) + { + return Task.FromResult(new CallToolResult + { + Content = [new TextContentBlock { Type = "text", Text = "Error: No arguments provided" }] + }); + } + + try + { + // Extract arguments + JsonElement root = arguments.RootElement; + + if (!root.TryGetProperty("entity", out JsonElement entityElement) || + !root.TryGetProperty("data", out JsonElement dataElement)) + { + return Task.FromResult(new CallToolResult + { + Content = [new TextContentBlock { Type = "text", Text = "Error: Missing required arguments 'entity' or 'data'" }] + }); + } + + string entityName = entityElement.GetString() ?? string.Empty; + + // TODO: Implement actual create logic using DAB's internal services + // For now, return a placeholder response + string result = $"Would create record in entity '{entityName}' with data: {dataElement.GetRawText()}"; + + return Task.FromResult(new CallToolResult + { + Content = [new TextContentBlock { Type = "text", Text = result }] + }); + } + catch (Exception ex) + { + return Task.FromResult(new CallToolResult + { + Content = [new TextContentBlock { Type = "text", Text = $"Error: {ex.Message}" }] + }); + } + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs new file mode 100644 index 0000000000..3e7ade6075 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Mcp.Model; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Protocol; +using static Azure.DataApiBuilder.Mcp.Model.McpEnums; + +namespace Azure.DataApiBuilder.Mcp.BuiltInTools +{ + public class DescribeEntitiesTool : IMcpTool + { + public ToolType ToolType { get; } = ToolType.BuiltIn; + + public Tool GetToolMetadata() + { + return new Tool + { + Name = "describe-entities", + Description = "Lists and describes all entities in the database." + }; + } + + public Task ExecuteAsync( + JsonDocument? arguments, + IServiceProvider serviceProvider, + CancellationToken cancellationToken = default) + { + try + { + // Get the runtime config provider + RuntimeConfigProvider? runtimeConfigProvider = serviceProvider.GetService(); + if (runtimeConfigProvider == null || !runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig)) + { + return Task.FromResult(new CallToolResult + { + Content = [new TextContentBlock { Type = "text", Text = "Error: Runtime configuration not available." }] + }); + } + + // Extract entity information from the runtime config + Dictionary entities = new(); + + if (runtimeConfig.Entities != null) + { + foreach (KeyValuePair entity in runtimeConfig.Entities) + { + entities[entity.Key] = new + { + source = entity.Value.Source, + permissions = entity.Value.Permissions?.Select(p => new + { + role = p.Role, + actions = p.Actions + }) + }; + } + } + + string entitiesJson = JsonSerializer.Serialize(entities, new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + return Task.FromResult(new CallToolResult + { + Content = [new TextContentBlock { Type = "application/json", Text = entitiesJson }] + }); + } + catch (Exception ex) + { + return Task.FromResult(new CallToolResult + { + Content = [new TextContentBlock { Type = "text", Text = $"Error: {ex.Message}" }] + }); + } + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000000..6401e17e22 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpEndpointRouteBuilderExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Configurations; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// Extension methods for mapping MCP endpoints to an . + /// + public static class McpEndpointRouteBuilderExtensions + { + /// + /// Maps the MCP endpoint to the specified if MCP is enabled in the runtime configuration. + /// + public static IEndpointRouteBuilder MapDabMcp( + this IEndpointRouteBuilder endpoints, + RuntimeConfigProvider runtimeConfigProvider, + [StringSyntax("Route")] string pattern = "") + { + if (!TryGetMcpOptions(runtimeConfigProvider, out McpRuntimeOptions? mcpOptions) || mcpOptions == null || !mcpOptions.Enabled) + { + return endpoints; + } + + string mcpPath = mcpOptions.Path ?? McpRuntimeOptions.DEFAULT_PATH; + + // Map the MCP endpoint + endpoints.MapMcp(mcpPath); + + return endpoints; + } + + /// + /// Gets MCP options from the runtime configuration + /// + /// Runtime config provider + /// MCP options + /// True if MCP options were found, false otherwise + private static bool TryGetMcpOptions(RuntimeConfigProvider runtimeConfigProvider, out McpRuntimeOptions? mcpOptions) + { + mcpOptions = null; + + if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig)) + { + return false; + } + + mcpOptions = runtimeConfig?.Runtime?.Mcp; + return mcpOptions != null; + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs new file mode 100644 index 0000000000..86cccd2aaf --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.DataApiBuilder.Mcp.Model; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol; +using ModelContextProtocol.Protocol; + +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// Configuration for MCP server capabilities and handlers + /// + internal static class McpServerConfiguration + { + /// + /// Configures the MCP server with tool capabilities + /// + internal static IServiceCollection ConfigureMcpServer(this IServiceCollection services) + { + services.AddMcpServer(options => + { + options.ServerInfo = new() { Name = "Data API builder MCP Server", Version = "1.0.0" }; + options.Capabilities = new() + { + Tools = new() + { + ListToolsHandler = (request, ct) => + { + McpToolRegistry? toolRegistry = request.Services?.GetRequiredService(); + if (toolRegistry == null) + { + throw new InvalidOperationException("Tool registry is not available."); + } + + List tools = toolRegistry.GetAllTools().ToList(); + + return ValueTask.FromResult(new ListToolsResult + { + Tools = tools + }); + }, + CallToolHandler = async (request, ct) => + { + McpToolRegistry? toolRegistry = request.Services?.GetRequiredService(); + if (toolRegistry == null) + { + throw new InvalidOperationException("Tool registry is not available."); + } + + string? toolName = request.Params?.Name; + if (string.IsNullOrEmpty(toolName)) + { + throw new McpException("Tool name is required."); + } + + if (!toolRegistry.TryGetTool(toolName, out IMcpTool? tool)) + { + throw new McpException($"Unknown tool: '{toolName}'"); + } + + JsonDocument? arguments = null; + if (request.Params?.Arguments != null) + { + // Convert IReadOnlyDictionary to JsonDocument + Dictionary jsonObject = new(); + foreach (KeyValuePair kvp in request.Params.Arguments) + { + jsonObject[kvp.Key] = kvp.Value; + } + + string json = JsonSerializer.Serialize(jsonObject); + arguments = JsonDocument.Parse(json); + } + + try + { + return await tool!.ExecuteAsync(arguments, request.Services!, ct); + } + finally + { + arguments?.Dispose(); + } + } + } + }; + }) + .WithHttpTransport(); + + return services; + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs new file mode 100644 index 0000000000..01f6015786 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServiceCollectionExtensions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Reflection; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Mcp.Model; +using Microsoft.Extensions.DependencyInjection; + +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// Extension methods for configuring MCP services in the DI container + /// + public static class McpServiceCollectionExtensions + { + /// + /// Adds MCP server and related services to the service collection + /// + public static IServiceCollection AddDabMcpServer(this IServiceCollection services, RuntimeConfigProvider runtimeConfigProvider) + { + if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig)) + { + // If config is not available, skip MCP setup + return services; + } + + // Only add MCP server if it's enabled in the configuration + if (!runtimeConfig.IsMcpEnabled) + { + return services; + } + + // Register core MCP services + services.AddSingleton(); + services.AddHostedService(); + + // Auto-discover and register all MCP tools + RegisterAllMcpTools(services); + + // Configure MCP server + services.ConfigureMcpServer(); + + return services; + } + + /// + /// Automatically discovers and registers all classes implementing IMcpTool + /// + private static void RegisterAllMcpTools(IServiceCollection services) + { + Assembly mcpAssembly = typeof(IMcpTool).Assembly; + + IEnumerable toolTypes = mcpAssembly.GetTypes() + .Where(t => t.IsClass && + !t.IsAbstract && + typeof(IMcpTool).IsAssignableFrom(t)); + + foreach (Type toolType in toolTypes) + { + services.AddSingleton(typeof(IMcpTool), toolType); + } + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistry.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistry.cs new file mode 100644 index 0000000000..9c9b96d72b --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistry.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.DataApiBuilder.Mcp.Model; +using ModelContextProtocol.Protocol; + +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// Registry for managing MCP tools + /// + public class McpToolRegistry + { + private readonly Dictionary _tools = new(); + + /// + /// Registers a tool in the registry + /// + public void RegisterTool(IMcpTool tool) + { + Tool metadata = tool.GetToolMetadata(); + _tools[metadata.Name] = tool; + } + + /// + /// Gets all registered tools + /// + public IEnumerable GetAllTools() + { + return _tools.Values.Select(t => t.GetToolMetadata()); + } + + /// + /// Tries to get a tool by name + /// + public bool TryGetTool(string toolName, out IMcpTool? tool) + { + return _tools.TryGetValue(toolName, out tool); + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistryInitializer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistryInitializer.cs new file mode 100644 index 0000000000..97d0dac7f3 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpToolRegistryInitializer.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.DataApiBuilder.Mcp.Model; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// Hosted service to initialize the MCP tool registry + /// + public class McpToolRegistryInitializer : IHostedService + { + private readonly IServiceProvider _serviceProvider; + private readonly McpToolRegistry _toolRegistry; + + public McpToolRegistryInitializer(IServiceProvider serviceProvider, McpToolRegistry toolRegistry) + { + _serviceProvider = serviceProvider; + _toolRegistry = toolRegistry; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + // Register all IMcpTool implementations + IEnumerable tools = _serviceProvider.GetServices(); + foreach (IMcpTool tool in tools) + { + _toolRegistry.RegisterTool(tool); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Model/Enums.cs b/src/Azure.DataApiBuilder.Mcp/Model/Enums.cs new file mode 100644 index 0000000000..84ca49e1b0 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Model/Enums.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.DataApiBuilder.Mcp.Model +{ + public class McpEnums + { + /// + /// Specifies the type of tool. + /// + /// This enumeration defines whether a tool is a built-in tool provided by the system or + /// a custom tool defined by the user. + public enum ToolType + { + BuiltIn, + Custom + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Model/IMcpTool.cs b/src/Azure.DataApiBuilder.Mcp/Model/IMcpTool.cs new file mode 100644 index 0000000000..bbee6a9304 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Model/IMcpTool.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using ModelContextProtocol.Protocol; +using static Azure.DataApiBuilder.Mcp.Model.McpEnums; + +namespace Azure.DataApiBuilder.Mcp.Model +{ + /// + /// Interface for MCP tool implementations + /// + public interface IMcpTool + { + /// + /// Gets the type of the tool. + /// + ToolType ToolType { get; } + + /// + /// Gets the tool metadata + /// + Tool GetToolMetadata(); + + /// + /// Executes the tool with the provided arguments + /// + /// The JSON arguments passed to the tool + /// The service provider for resolving dependencies + /// Cancellation token + /// The tool execution result + Task ExecuteAsync( + JsonDocument? arguments, + IServiceProvider serviceProvider, + CancellationToken cancellationToken = default); + } +} diff --git a/src/Azure.DataApiBuilder.sln b/src/Azure.DataApiBuilder.sln index e7f61fa3ed..aa3c8e2bad 100644 --- a/src/Azure.DataApiBuilder.sln +++ b/src/Azure.DataApiBuilder.sln @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataApiBuilder.Core", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataApiBuilder.Product", "Product\Azure.DataApiBuilder.Product.csproj", "{E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.DataApiBuilder.Mcp", "Azure.DataApiBuilder.Mcp\Azure.DataApiBuilder.Mcp.csproj", "{A287E849-A043-4F37-BC40-A87C4705F583}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,10 @@ Global {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Release|Any CPU.Build.0 = Release|Any CPU + {A287E849-A043-4F37-BC40-A87C4705F583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A287E849-A043-4F37-BC40-A87C4705F583}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A287E849-A043-4F37-BC40-A87C4705F583}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A287E849-A043-4F37-BC40-A87C4705F583}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Cli.Tests/ConfigGeneratorTests.cs b/src/Cli.Tests/ConfigGeneratorTests.cs index 6094189f93..58e006b75d 100644 --- a/src/Cli.Tests/ConfigGeneratorTests.cs +++ b/src/Cli.Tests/ConfigGeneratorTests.cs @@ -163,6 +163,10 @@ public void TestSpecialCharactersInConnectionString() ""path"": ""/An_"", ""allow-introspection"": true }, + ""mcp"": { + ""enabled"": true, + ""path"": ""/mcp"" + }, ""host"": { ""cors"": { ""origins"": [], diff --git a/src/Cli.Tests/ExporterTests.cs b/src/Cli.Tests/ExporterTests.cs index aecd6455a3..3735dc43a1 100644 --- a/src/Cli.Tests/ExporterTests.cs +++ b/src/Cli.Tests/ExporterTests.cs @@ -21,7 +21,7 @@ public void ExportGraphQLFromDabService_LogsWhenHttpsWorks() RuntimeConfig runtimeConfig = new( Schema: "schema", DataSource: new DataSource(DatabaseType.MSSQL, "", new()), - Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)), + Runtime: new(Rest: new(), GraphQL: new(), Mcp: new(), Host: new(null, null)), Entities: new(new Dictionary()) ); @@ -59,7 +59,7 @@ public void ExportGraphQLFromDabService_LogsFallbackToHttp_WhenHttpsFails() RuntimeConfig runtimeConfig = new( Schema: "schema", DataSource: new DataSource(DatabaseType.MSSQL, "", new()), - Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)), + Runtime: new(Rest: new(), GraphQL: new(), Mcp: new(), Host: new(null, null)), Entities: new(new Dictionary()) ); @@ -105,7 +105,7 @@ public void ExportGraphQLFromDabService_ThrowsException_WhenBothHttpsAndHttpFail RuntimeConfig runtimeConfig = new( Schema: "schema", DataSource: new DataSource(DatabaseType.MSSQL, "", new()), - Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)), + Runtime: new(Rest: new(), GraphQL: new(), Mcp: new(), Host: new(null, null)), Entities: new(new Dictionary()) ); diff --git a/src/Cli.Tests/ModuleInitializer.cs b/src/Cli.Tests/ModuleInitializer.cs index 2cfba899ea..e00dc00a89 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -47,6 +47,10 @@ public static void Init() VerifierSettings.IgnoreMember(options => options.IsGraphQLEnabled); // Ignore the entity IsGraphQLEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsGraphQLEnabled); + // Ignore the global IsMcpEnabled as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.IsMcpEnabled); + // Ignore the global RuntimeOptions.IsMcpEnabled as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(options => options.IsMcpEnabled); // Ignore the global IsHealthEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsHealthEnabled); // Ignore the global RuntimeOptions.IsHealthCheckEnabled as that's unimportant from a test standpoint. @@ -67,12 +71,18 @@ public static void Init() VerifierSettings.IgnoreMember(config => config.IsGraphQLEnabled); // Ignore the IsRestEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsRestEnabled); + // Ignore the IsMcpEnabled as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.IsMcpEnabled); + // Ignore the McpDmlTools as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.McpDmlTools); // Ignore the IsStaticWebAppsIdentityProvider as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsStaticWebAppsIdentityProvider); // Ignore the RestPath as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.RestPath); // Ignore the GraphQLPath as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.GraphQLPath); + // Ignore the McpPath as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.McpPath); // Ignore the AllowIntrospection as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.AllowIntrospection); // Ignore the EnableAggregation as that's unimportant from a test standpoint. @@ -101,6 +111,8 @@ public static void Init() VerifierSettings.IgnoreMember(options => options.UserProvidedDepthLimit); // Ignore EnableLegacyDateTimeScalar as that's not serialized in our config file. VerifierSettings.IgnoreMember(options => options.EnableLegacyDateTimeScalar); + // Ignore UserProvidedPath as that's not serialized in our config file. + VerifierSettings.IgnoreMember(options => options.UserProvidedPath); // Customise the path where we store snapshots, so they are easier to locate in a PR review. VerifyBase.DerivePathInfo( (sourceFile, projectDirectory, type, method) => new( diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index a76f72b9a0..226c4e2a20 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt index 95415c1685..c4eb43648c 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt index ee8dbf6199..a77ecc134b 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt index 0d0afda2bf..a19694b688 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt index cbb2df5fb8..081c5f8e55 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt @@ -17,6 +17,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt index 0c20e9fc25..5a6a50d38e 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index 27b20753d3..540a1b5a1d 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt index 2af3cbc907..b3f63dd336 100644 --- a/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt @@ -17,6 +17,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt index ca3b61588b..42e0ff5e2f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt index 93190d1d9d..0af93023dc 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt index 5c52bc12c1..9e77b24d74 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt index 7b0a4674eb..32f72a7a54 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt index dc60d762cc..24416a0d02 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt index 7a67eca701..6c674a4772 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt b/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt index 8c2ffbbcac..b6aac13236 100644 --- a/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt @@ -16,6 +16,10 @@ Path: /abc, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt index da7937d1d9..8841c0f326 100644 --- a/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt b/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt index ef8c7173d5..68e4d231fd 100644 --- a/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt b/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt index 72f66f82c9..3c281ad6aa 100644 --- a/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt b/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt index 7b0a4674eb..32f72a7a54 100644 --- a/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { AllowCredentials: false diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt index cbaaa45754..888466ab4a 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt index da7937d1d9..8841c0f326 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt index 62fc407842..d56e05c483 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt @@ -21,6 +21,10 @@ } } }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt index 3285438ab7..bc31484242 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt index cbaaa45754..888466ab4a 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt index 3285438ab7..bc31484242 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt index a43e68277c..48f5e7a7c9 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt index 9740a85a77..8fa9677f1d 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt @@ -17,6 +17,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt index be47d537b2..e3108801f5 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt @@ -21,6 +21,10 @@ } } }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt index 673c21dae4..59f6636fb2 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt index cbaaa45754..888466ab4a 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt @@ -16,6 +16,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt index 9740a85a77..8fa9677f1d 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt @@ -17,6 +17,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt index 9740a85a77..8fa9677f1d 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt @@ -17,6 +17,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt index a43e68277c..48f5e7a7c9 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt index 673c21dae4..59f6636fb2 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt index a43e68277c..48f5e7a7c9 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt index 3285438ab7..bc31484242 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt index 673c21dae4..59f6636fb2 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Cli.Tests/UpdateEntityTests.cs b/src/Cli.Tests/UpdateEntityTests.cs index a500858c60..663334c5e8 100644 --- a/src/Cli.Tests/UpdateEntityTests.cs +++ b/src/Cli.Tests/UpdateEntityTests.cs @@ -1004,7 +1004,7 @@ public void TestVerifyCanUpdateRelationshipInvalidOptions(string db, string card RuntimeConfig runtimeConfig = new( Schema: "schema", DataSource: new DataSource(EnumExtensions.Deserialize(db), "", new()), - Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)), + Runtime: new(Rest: new(), GraphQL: new(), Mcp: new(), Host: new(null, null)), Entities: new(new Dictionary()) ); @@ -1056,7 +1056,7 @@ public void EnsureFailure_AddRelationshipToEntityWithDisabledGraphQL() RuntimeConfig runtimeConfig = new( Schema: "schema", DataSource: new DataSource(DatabaseType.MSSQL, "", new()), - Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)), + Runtime: new(Rest: new(), GraphQL: new(), Mcp: new(), Host: new(null, null)), Entities: new(entityMap) ); diff --git a/src/Cli/Commands/ConfigureOptions.cs b/src/Cli/Commands/ConfigureOptions.cs index 4f61b2007b..60cb12c3f8 100644 --- a/src/Cli/Commands/ConfigureOptions.cs +++ b/src/Cli/Commands/ConfigureOptions.cs @@ -36,6 +36,15 @@ public ConfigureOptions( bool? runtimeRestEnabled = null, string? runtimeRestPath = null, bool? runtimeRestRequestBodyStrict = null, + bool? runtimeMcpEnabled = null, + string? runtimeMcpPath = null, + bool? runtimeMcpDmlToolsEnabled = null, + bool? runtimeMcpDmlToolsDescribeEntitiesEnabled = null, + bool? runtimeMcpDmlToolsCreateRecordEnabled = null, + bool? runtimeMcpDmlToolsReadRecordsEnabled = null, + bool? runtimeMcpDmlToolsUpdateRecordEnabled = null, + bool? runtimeMcpDmlToolsDeleteRecordEnabled = null, + bool? runtimeMcpDmlToolsExecuteEntityEnabled = null, bool? runtimeCacheEnabled = null, int? runtimeCacheTtl = null, HostMode? runtimeHostMode = null, @@ -81,6 +90,16 @@ public ConfigureOptions( RuntimeRestEnabled = runtimeRestEnabled; RuntimeRestPath = runtimeRestPath; RuntimeRestRequestBodyStrict = runtimeRestRequestBodyStrict; + // Mcp + RuntimeMcpEnabled = runtimeMcpEnabled; + RuntimeMcpPath = runtimeMcpPath; + RuntimeMcpDmlToolsEnabled = runtimeMcpDmlToolsEnabled; + RuntimeMcpDmlToolsDescribeEntitiesEnabled = runtimeMcpDmlToolsDescribeEntitiesEnabled; + RuntimeMcpDmlToolsCreateRecordEnabled = runtimeMcpDmlToolsCreateRecordEnabled; + RuntimeMcpDmlToolsReadRecordsEnabled = runtimeMcpDmlToolsReadRecordsEnabled; + RuntimeMcpDmlToolsUpdateRecordEnabled = runtimeMcpDmlToolsUpdateRecordEnabled; + RuntimeMcpDmlToolsDeleteRecordEnabled = runtimeMcpDmlToolsDeleteRecordEnabled; + RuntimeMcpDmlToolsExecuteEntityEnabled = runtimeMcpDmlToolsExecuteEntityEnabled; // Cache RuntimeCacheEnabled = runtimeCacheEnabled; RuntimeCacheTTL = runtimeCacheTtl; @@ -155,6 +174,33 @@ public ConfigureOptions( [Option("runtime.rest.request-body-strict", Required = false, HelpText = "Prohibit extraneous REST request body fields. Default: true (boolean).")] public bool? RuntimeRestRequestBodyStrict { get; } + [Option("runtime.mcp.enabled", Required = false, HelpText = "Enable DAB's MCP endpoint. Default: true (boolean).")] + public bool? RuntimeMcpEnabled { get; } + + [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.dml-tools.enabled", Required = false, HelpText = "Enable DAB's MCP DML tools endpoint. Default: true (boolean).")] + public bool? RuntimeMcpDmlToolsEnabled { get; } + + [Option("runtime.mcp.dml-tools.describe-entities.enabled", Required = false, HelpText = "Enable DAB's MCP describe entities tool. Default: true (boolean).")] + public bool? RuntimeMcpDmlToolsDescribeEntitiesEnabled { get; } + + [Option("runtime.mcp.dml-tools.create-record.enabled", Required = false, HelpText = "Enable DAB's MCP create record tool. Default: true (boolean).")] + public bool? RuntimeMcpDmlToolsCreateRecordEnabled { get; } + + [Option("runtime.mcp.dml-tools.read-records.enabled", Required = false, HelpText = "Enable DAB's MCP read record tool. Default: true (boolean).")] + public bool? RuntimeMcpDmlToolsReadRecordsEnabled { get; } + + [Option("runtime.mcp.dml-tools.update-record.enabled", Required = false, HelpText = "Enable DAB's MCP update record tool. Default: true (boolean).")] + public bool? RuntimeMcpDmlToolsUpdateRecordEnabled { get; } + + [Option("runtime.mcp.dml-tools.delete-record.enabled", Required = false, HelpText = "Enable DAB's MCP delete record tool. Default: true (boolean).")] + public bool? RuntimeMcpDmlToolsDeleteRecordEnabled { get; } + + [Option("runtime.mcp.dml-tools.execute-entity.enabled", Required = false, HelpText = "Enable DAB's MCP execute entity tool. Default: true (boolean).")] + public bool? RuntimeMcpDmlToolsExecuteEntityEnabled { get; } + [Option("runtime.cache.enabled", Required = false, HelpText = "Enable DAB's cache globally. (You must also enable each entity's cache separately.). Default: false (boolean).")] public bool? RuntimeCacheEnabled { get; } diff --git a/src/Cli/Commands/InitOptions.cs b/src/Cli/Commands/InitOptions.cs index 5d5608a200..91786d99ff 100644 --- a/src/Cli/Commands/InitOptions.cs +++ b/src/Cli/Commands/InitOptions.cs @@ -35,8 +35,11 @@ public InitOptions( bool restDisabled = false, string graphQLPath = GraphQLRuntimeOptions.DEFAULT_PATH, bool graphqlDisabled = false, + string mcpPath = McpRuntimeOptions.DEFAULT_PATH, + bool mcpDisabled = false, CliBool restEnabled = CliBool.None, CliBool graphqlEnabled = CliBool.None, + CliBool mcpEnabled = CliBool.None, CliBool restRequestBodyStrict = CliBool.None, CliBool multipleCreateOperationEnabled = CliBool.None, string? config = null) @@ -58,8 +61,11 @@ public InitOptions( RestDisabled = restDisabled; GraphQLPath = graphQLPath; GraphQLDisabled = graphqlDisabled; + McpPath = mcpPath; + McpDisabled = mcpDisabled; RestEnabled = restEnabled; GraphQLEnabled = graphqlEnabled; + McpEnabled = mcpEnabled; RestRequestBodyStrict = restRequestBodyStrict; MultipleCreateOperationEnabled = multipleCreateOperationEnabled; } @@ -112,12 +118,21 @@ public InitOptions( [Option("graphql.disabled", Default = false, Required = false, HelpText = "Disables GraphQL endpoint for all entities.")] public bool GraphQLDisabled { get; } + [Option("mcp.path", Default = McpRuntimeOptions.DEFAULT_PATH, Required = false, HelpText = "Specify the MCP endpoint's default prefix.")] + public string McpPath { get; } + + [Option("mcp.disabled", Default = false, Required = false, HelpText = "Disables MCP endpoint for all entities.")] + public bool McpDisabled { get; } + [Option("rest.enabled", Required = false, HelpText = "(Default: true) Enables REST endpoint for all entities. Supported values: true, false.")] public CliBool RestEnabled { get; } [Option("graphql.enabled", Required = false, HelpText = "(Default: true) Enables GraphQL endpoint for all entities. Supported values: true, false.")] public CliBool GraphQLEnabled { get; } + [Option("mcp.enabled", Required = false, HelpText = "(Default: true) Enables MCP endpoint for all entities. Supported values: true, false.")] + public CliBool McpEnabled { get; } + // Since the rest.request-body-strict option does not have a default value, it is required to specify a value for this option if it is // included in the init command. [Option("rest.request-body-strict", Required = false, HelpText = "(Default: true) Allow extraneous fields in the request body for REST.")] diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index c7027ff78c..886447b256 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -89,6 +89,7 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime DatabaseType dbType = options.DatabaseType; string? restPath = options.RestPath; string graphQLPath = options.GraphQLPath; + string mcpPath = options.McpPath; string? runtimeBaseRoute = options.RuntimeBaseRoute; Dictionary dbOptions = new(); @@ -108,9 +109,10 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime " We recommend that you use the --graphql.enabled option instead."); } - bool restEnabled, graphQLEnabled; + bool restEnabled, graphQLEnabled, mcpEnabled; if (!TryDetermineIfApiIsEnabled(options.RestDisabled, options.RestEnabled, ApiType.REST, out restEnabled) || - !TryDetermineIfApiIsEnabled(options.GraphQLDisabled, options.GraphQLEnabled, ApiType.GraphQL, out graphQLEnabled)) + !TryDetermineIfApiIsEnabled(options.GraphQLDisabled, options.GraphQLEnabled, ApiType.GraphQL, out graphQLEnabled) || + !TryDetermineIfMcpIsEnabled(options.McpEnabled, out mcpEnabled)) { return false; } @@ -262,6 +264,7 @@ public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntime Runtime: new( Rest: new(restEnabled, restPath ?? RestRuntimeOptions.DEFAULT_PATH, options.RestRequestBodyStrict is CliBool.False ? false : true), GraphQL: new(Enabled: graphQLEnabled, Path: graphQLPath, MultipleMutationOptions: multipleMutationOptions), + Mcp: new(mcpEnabled, mcpPath ?? McpRuntimeOptions.DEFAULT_PATH), Host: new( Cors: new(options.CorsOrigin?.ToArray() ?? Array.Empty()), Authentication: new( @@ -314,6 +317,17 @@ private static bool TryDetermineIfApiIsEnabled(bool apiDisabledOptionValue, CliB return true; } + /// + /// Helper method to determine if the mcp api is enabled or not based on the enabled/disabled options in the dab init command. + /// + /// True, if MCP is enabled + /// Out param isMcpEnabled + /// True if MCP is enabled + private static bool TryDetermineIfMcpIsEnabled(CliBool mcpEnabledOptionValue, out bool isMcpEnabled) + { + return TryDetermineIfApiIsEnabled(false, mcpEnabledOptionValue, ApiType.MCP, out isMcpEnabled); + } + /// /// Helper method to determine if the multiple create operation is enabled or not based on the inputs from dab init command. /// @@ -744,6 +758,23 @@ private static bool TryUpdateConfiguredRuntimeOptions( } } + // MCP: Enabled and Path + if (options.RuntimeMcpEnabled != null || + options.RuntimeMcpPath != null) + { + McpRuntimeOptions updatedMcpOptions = runtimeConfig?.Runtime?.Mcp ?? new(); + bool status = TryUpdateConfiguredMcpValues(options, ref updatedMcpOptions); + + if (status) + { + runtimeConfig = runtimeConfig! with { Runtime = runtimeConfig.Runtime! with { Mcp = updatedMcpOptions } }; + } + else + { + return false; + } + } + // Cache: Enabled and TTL if (options.RuntimeCacheEnabled != null || options.RuntimeCacheTTL != null) @@ -944,6 +975,142 @@ private static bool TryUpdateConfiguredGraphQLValues( } } + /// + /// Attempts to update the Config parameters in the Mcp runtime settings based on the provided value. + /// Validates that any user-provided values are valid and then returns true if the updated Mcp options + /// need to be overwritten on the existing config parameters + /// + /// options. + /// updatedMcpOptions + /// True if the value needs to be updated in the runtime config, else false + private static bool TryUpdateConfiguredMcpValues( + ConfigureOptions options, + ref McpRuntimeOptions updatedMcpOptions) + { + object? updatedValue; + + try + { + // Runtime.Mcp.Enabled + updatedValue = options?.RuntimeMcpEnabled; + if (updatedValue != null) + { + updatedMcpOptions = updatedMcpOptions! with { Enabled = (bool)updatedValue }; + _logger.LogInformation("Updated RuntimeConfig with Runtime.Mcp.Enabled as '{updatedValue}'", updatedValue); + } + + // Runtime.Mcp.Path + updatedValue = options?.RuntimeMcpPath; + if (updatedValue != null) + { + bool status = RuntimeConfigValidatorUtil.TryValidateUriComponent(uriComponent: (string)updatedValue, out string exceptionMessage); + if (status) + { + updatedMcpOptions = updatedMcpOptions! with { Path = (string)updatedValue }; + _logger.LogInformation("Updated RuntimeConfig with Runtime.Mcp.Path as '{updatedValue}'", updatedValue); + } + else + { + _logger.LogError("Failed to update Runtime.Mcp.Path as '{updatedValue}' due to exception message: {exceptionMessage}", updatedValue, exceptionMessage); + return false; + } + } + + // Handle DML tools configuration + bool hasToolUpdates = false; + DmlToolsConfig? currentDmlTools = updatedMcpOptions?.DmlTools; + + // If setting all tools at once + updatedValue = options?.RuntimeMcpDmlToolsEnabled; + if (updatedValue != null) + { + updatedMcpOptions = updatedMcpOptions! with { DmlTools = DmlToolsConfig.FromBoolean((bool)updatedValue) }; + _logger.LogInformation("Updated RuntimeConfig with Runtime.Mcp.Dml-Tools as '{updatedValue}'", updatedValue); + return true; // Return early since we're setting all tools at once + } + + // Handle individual tool updates + bool? describeEntities = currentDmlTools?.DescribeEntities; + bool? createRecord = currentDmlTools?.CreateRecord; + bool? readRecord = currentDmlTools?.ReadRecords; + bool? updateRecord = currentDmlTools?.UpdateRecord; + bool? deleteRecord = currentDmlTools?.DeleteRecord; + bool? executeEntity = currentDmlTools?.ExecuteEntity; + + updatedValue = options?.RuntimeMcpDmlToolsDescribeEntitiesEnabled; + if (updatedValue != null) + { + describeEntities = (bool)updatedValue; + hasToolUpdates = true; + _logger.LogInformation("Updated RuntimeConfig with runtime.mcp.dml-tools.describe-entities as '{updatedValue}'", updatedValue); + } + + updatedValue = options?.RuntimeMcpDmlToolsCreateRecordEnabled; + if (updatedValue != null) + { + createRecord = (bool)updatedValue; + hasToolUpdates = true; + _logger.LogInformation("Updated RuntimeConfig with runtime.mcp.dml-tools.create-record as '{updatedValue}'", updatedValue); + } + + updatedValue = options?.RuntimeMcpDmlToolsReadRecordsEnabled; + if (updatedValue != null) + { + readRecord = (bool)updatedValue; + hasToolUpdates = true; + _logger.LogInformation("Updated RuntimeConfig with runtime.mcp.dml-tools.read-records as '{updatedValue}'", updatedValue); + } + + updatedValue = options?.RuntimeMcpDmlToolsUpdateRecordEnabled; + if (updatedValue != null) + { + updateRecord = (bool)updatedValue; + hasToolUpdates = true; + _logger.LogInformation("Updated RuntimeConfig with runtime.mcp.dml-tools.update-record as '{updatedValue}'", updatedValue); + } + + updatedValue = options?.RuntimeMcpDmlToolsDeleteRecordEnabled; + if (updatedValue != null) + { + deleteRecord = (bool)updatedValue; + hasToolUpdates = true; + _logger.LogInformation("Updated RuntimeConfig with runtime.mcp.dml-tools.delete-record as '{updatedValue}'", updatedValue); + } + + updatedValue = options?.RuntimeMcpDmlToolsExecuteEntityEnabled; + if (updatedValue != null) + { + executeEntity = (bool)updatedValue; + hasToolUpdates = true; + _logger.LogInformation("Updated RuntimeConfig with runtime.mcp.dml-tools.execute-entity as '{updatedValue}'", updatedValue); + } + + if (hasToolUpdates) + { + updatedMcpOptions = updatedMcpOptions! with + { + DmlTools = new DmlToolsConfig + { + AllToolsEnabled = false, + DescribeEntities = describeEntities, + CreateRecord = createRecord, + ReadRecords = readRecord, + UpdateRecord = updateRecord, + DeleteRecord = deleteRecord, + ExecuteEntity = executeEntity + } + }; + } + + return true; + } + catch (Exception ex) + { + _logger.LogError("Failed to update RuntimeConfig.Mcp with exception message: {exceptionMessage}.", ex.Message); + return false; + } + } + /// /// Attempts to update the Config parameters in the Cache runtime settings based on the provided value. /// Validates user-provided parameters and then returns true if the updated Cache options @@ -2229,7 +2396,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( { if (options.AzureKeyVaultRetryPolicyMaxCount.Value < 1) { - _logger.LogError("Failed to update azure-key-vault.retry-policy.max-count. Value must be at least 1."); + _logger.LogError("Failed to update configuration with runtime.azure-key-vault.retry-policy.max-count. Value must be a positive integer greater than 0."); return false; } @@ -2244,7 +2411,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( { if (options.AzureKeyVaultRetryPolicyDelaySeconds.Value < 1) { - _logger.LogError("Failed to update azure-key-vault.retry-policy.delay-seconds. Value must be at least 1."); + _logger.LogError("Failed to update configuration with runtime.azure-key-vault.retry-policy.delay-seconds. Value must be a positive integer greater than 0."); return false; } @@ -2259,7 +2426,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( { if (options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value < 1) { - _logger.LogError("Failed to update azure-key-vault.retry-policy.max-delay-seconds. Value must be at least 1."); + _logger.LogError("Failed to update configuration with runtime.azure-key-vault.retry-policy.max-delay-seconds. Value must be a positive integer greater than 0."); return false; } @@ -2274,7 +2441,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( { if (options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value < 1) { - _logger.LogError("Failed to update azure-key-vault.retry-policy.network-timeout-seconds. Value must be at least 1."); + _logger.LogError("Failed to update configuration with runtime.azure-key-vault.retry-policy.network-timeout-seconds. Value must be a positive integer greater than 0."); return false; } diff --git a/src/Config/Converters/DmlToolsConfigConverter.cs b/src/Config/Converters/DmlToolsConfigConverter.cs new file mode 100644 index 0000000000..9acef0f9b2 --- /dev/null +++ b/src/Config/Converters/DmlToolsConfigConverter.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +/// +/// JSON converter for DmlToolsConfig that handles both boolean and object formats. +/// +internal class DmlToolsConfigConverter : JsonConverter +{ + /// + /// Reads DmlToolsConfig from JSON which can be either: + /// - A boolean: all tools are enabled/disabled + /// - An object: individual tool settings (unspecified tools default to true) + /// - Null/undefined: defaults to all tools enabled (true) + /// + public override DmlToolsConfig? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Handle null + if (reader.TokenType is JsonTokenType.Null) + { + // Return default config with all tools enabled + return DmlToolsConfig.Default; + } + + // Handle boolean format: "dml-tools": true/false + if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) + { + bool enabled = reader.GetBoolean(); + return DmlToolsConfig.FromBoolean(enabled); + } + + // Handle object format + if (reader.TokenType is JsonTokenType.StartObject) + { + // When using object format, unspecified tools default to true + bool? describeEntities = null; + bool? createRecord = null; + bool? readRecords = null; + bool? updateRecord = null; + bool? deleteRecord = null; + bool? executeEntity = null; + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType is JsonTokenType.PropertyName) + { + string? property = reader.GetString(); + reader.Read(); + + // Handle the property value + if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) + { + bool value = reader.GetBoolean(); + + switch (property?.ToLowerInvariant()) + { + case "describe-entities": + describeEntities = value; + break; + case "create-record": + createRecord = value; + break; + case "read-records": + readRecords = value; + break; + case "update-record": + updateRecord = value; + break; + case "delete-record": + deleteRecord = value; + break; + case "execute-entity": + executeEntity = value; + break; + default: + // Skip unknown properties + break; + } + } + else + { + // Error on non-boolean values for known properties + if (property?.ToLowerInvariant() is "describe-entities" or "create-record" + or "read-records" or "update-record" or "delete-record" or "execute-entity") + { + throw new JsonException($"Property '{property}' must be a boolean value."); + } + + // Skip unknown properties + reader.Skip(); + } + } + } + + // Create the config with specified values + // Unspecified values (null) will default to true in the DmlToolsConfig constructor + return new DmlToolsConfig( + allToolsEnabled: null, + describeEntities: describeEntities, + createRecord: createRecord, + readRecords: readRecords, + updateRecord: updateRecord, + deleteRecord: deleteRecord, + executeEntity: executeEntity); + } + + // For any other unexpected token type, return default (all enabled) + return DmlToolsConfig.Default; + } + + /// + /// Writes DmlToolsConfig to JSON. + /// - If all tools have the same value, writes as boolean + /// - Otherwise writes as object with only user-provided properties + /// + public override void Write(Utf8JsonWriter writer, DmlToolsConfig? value, JsonSerializerOptions options) + { + if (value is null) + { + return; + } + + // Check if any individual settings were provided by the user + bool hasIndividualSettings = value.UserProvidedDescribeEntities || + value.UserProvidedCreateRecord || + value.UserProvidedReadRecords || + value.UserProvidedUpdateRecord || + value.UserProvidedDeleteRecord || + value.UserProvidedExecuteEntity; + + // Only write the boolean value if it's provided by user + // This prevents writing "dml-tools": true when it's the default + if (!hasIndividualSettings && value.UserProvidedAllToolsEnabled) + { + writer.WritePropertyName("dml-tools"); + writer.WriteBooleanValue(value.AllToolsEnabled); + } + else + { + writer.WritePropertyName("dml-tools"); + + // Write as object with only user-provided properties + writer.WriteStartObject(); + + if (value.UserProvidedDescribeEntities && value.DescribeEntities.HasValue) + { + writer.WriteBoolean("describe-entities", value.DescribeEntities.Value); + } + + if (value.UserProvidedCreateRecord && value.CreateRecord.HasValue) + { + writer.WriteBoolean("create-record", value.CreateRecord.Value); + } + + if (value.UserProvidedReadRecords && value.ReadRecords.HasValue) + { + writer.WriteBoolean("read-records", value.ReadRecords.Value); + } + + if (value.UserProvidedUpdateRecord && value.UpdateRecord.HasValue) + { + writer.WriteBoolean("update-record", value.UpdateRecord.Value); + } + + if (value.UserProvidedDeleteRecord && value.DeleteRecord.HasValue) + { + writer.WriteBoolean("delete-record", value.DeleteRecord.Value); + } + + if (value.UserProvidedExecuteEntity && value.ExecuteEntity.HasValue) + { + writer.WriteBoolean("execute-entity", value.ExecuteEntity.Value); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs new file mode 100644 index 0000000000..db9acfa603 --- /dev/null +++ b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +/// +/// JSON converter factory for McpRuntimeOptions that handles both boolean and object formats. +/// +internal class McpRuntimeOptionsConverterFactory : JsonConverterFactory +{ + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAssignableTo(typeof(McpRuntimeOptions)); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return new McpRuntimeOptionsConverter(_replaceEnvVar); + } + + internal McpRuntimeOptionsConverterFactory(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + + private class McpRuntimeOptionsConverter : JsonConverter + { + // Determines whether to replace environment variable with its + // value or not while deserializing. + private bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + internal McpRuntimeOptionsConverter(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + + /// + /// Defines how DAB reads MCP options and defines which values are + /// used to instantiate McpRuntimeOptions. + /// + /// Thrown when improperly formatted MCP options are provided. + public override McpRuntimeOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False) + { + return new McpRuntimeOptions(Enabled: reader.GetBoolean()); + } + + if (reader.TokenType is JsonTokenType.StartObject) + { + DmlToolsConfigConverter dmlToolsConfigConverter = new(); + + bool enabled = true; + string? path = null; + DmlToolsConfig? dmlTools = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return new McpRuntimeOptions(enabled, path, dmlTools); + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "enabled": + if (reader.TokenType is not JsonTokenType.Null) + { + enabled = reader.GetBoolean(); + } + + break; + + case "path": + if (reader.TokenType is not JsonTokenType.Null) + { + path = reader.DeserializeString(_replaceEnvVar); + } + + break; + + case "dml-tools": + dmlTools = dmlToolsConfigConverter.Read(ref reader, typeToConvert, options); + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + } + + throw new JsonException("Failed to read the MCP Options"); + } + + /// + /// When writing the McpRuntimeOptions back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + public override void Write(Utf8JsonWriter writer, McpRuntimeOptions value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteBoolean("enabled", value.Enabled); + + if (value?.UserProvidedPath is true) + { + writer.WritePropertyName("path"); + JsonSerializer.Serialize(writer, value.Path, options); + } + + // Only write the boolean value if it's not the default (true) + // This prevents writing "dml-tools": true when it's the default + if (value?.DmlTools is not null) + { + DmlToolsConfigConverter dmlToolsOptionsConverter = options.GetConverter(typeof(DmlToolsConfig)) as DmlToolsConfigConverter ?? + throw new JsonException("Failed to get mcp.dml-tools options converter"); + + dmlToolsOptionsConverter.Write(writer, value.DmlTools, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Config/DataApiBuilderException.cs b/src/Config/DataApiBuilderException.cs index 18b0395541..b7696c4deb 100644 --- a/src/Config/DataApiBuilderException.cs +++ b/src/Config/DataApiBuilderException.cs @@ -105,6 +105,10 @@ public enum SubStatusCodes /// GlobalRestEndpointDisabled, /// + /// Global MCP endpoint disabled in runtime configuration. + /// + GlobalMcpEndpointDisabled, + /// /// DataSource not found for multiple db scenario. /// DataSourceNotFound, diff --git a/src/Config/ObjectModel/ApiType.cs b/src/Config/ObjectModel/ApiType.cs index 5583e67098..fb57fe2859 100644 --- a/src/Config/ObjectModel/ApiType.cs +++ b/src/Config/ObjectModel/ApiType.cs @@ -10,6 +10,7 @@ public enum ApiType { REST, GraphQL, + MCP, // This is required to indicate features common between all APIs. All } diff --git a/src/Config/ObjectModel/DmlToolsConfig.cs b/src/Config/ObjectModel/DmlToolsConfig.cs new file mode 100644 index 0000000000..c14f8e49ed --- /dev/null +++ b/src/Config/ObjectModel/DmlToolsConfig.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// DML Tools configuration that can be either a boolean or object with individual tool settings +/// +public record DmlToolsConfig +{ + /// + /// Default value for all tools when not specified + /// + public const bool DEFAULT_ENABLED = true; + + /// + /// Indicates if all tools are enabled/disabled uniformly + /// + public bool AllToolsEnabled { get; init; } + + /// + /// Whether describe-entities tool is enabled + /// + public bool? DescribeEntities { get; init; } + + /// + /// Whether create-record tool is enabled + /// + public bool? CreateRecord { get; init; } + + /// + /// Whether read-records tool is enabled + /// + public bool? ReadRecords { get; init; } + + /// + /// Whether update-record tool is enabled + /// + public bool? UpdateRecord { get; init; } + + /// + /// Whether delete-record tool is enabled + /// + public bool? DeleteRecord { get; init; } + + /// + /// Whether execute-entity tool is enabled + /// + public bool? ExecuteEntity { get; init; } + + [JsonConstructor] + public DmlToolsConfig( + bool? allToolsEnabled = null, + bool? describeEntities = null, + bool? createRecord = null, + bool? readRecords = null, + bool? updateRecord = null, + bool? deleteRecord = null, + bool? executeEntity = null) + { + if (allToolsEnabled is not null) + { + AllToolsEnabled = allToolsEnabled.Value; + UserProvidedAllToolsEnabled = true; + } + else + { + AllToolsEnabled = DEFAULT_ENABLED; + } + + if (describeEntities is not null) + { + DescribeEntities = describeEntities; + UserProvidedDescribeEntities = true; + } + + if (createRecord is not null) + { + CreateRecord = createRecord; + UserProvidedCreateRecord = true; + } + + if (readRecords is not null) + { + ReadRecords = readRecords; + UserProvidedReadRecords = true; + } + + if (updateRecord is not null) + { + UpdateRecord = updateRecord; + UserProvidedUpdateRecord = true; + } + + if (deleteRecord is not null) + { + DeleteRecord = deleteRecord; + UserProvidedDeleteRecord = true; + } + + if (executeEntity is not null) + { + ExecuteEntity = executeEntity; + UserProvidedExecuteEntity = true; + } + } + + /// + /// Creates a DmlToolsConfig with all tools set to the same state + /// + public static DmlToolsConfig FromBoolean(bool enabled) + { + return new DmlToolsConfig + { + AllToolsEnabled = enabled, + DescribeEntities = null, + CreateRecord = null, + ReadRecords = null, + UpdateRecord = null, + DeleteRecord = null, + ExecuteEntity = null + }; + } + + /// + /// Creates a default DmlToolsConfig with all tools enabled + /// + public static DmlToolsConfig Default => FromBoolean(DEFAULT_ENABLED); + + /// + /// Flag which informs CLI and JSON serializer whether to write all-tools-enabled + /// property/value to the runtime config file. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(AllToolsEnabled))] + public bool UserProvidedAllToolsEnabled { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write describe-entities + /// property/value to the runtime config file. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(DescribeEntities))] + public bool UserProvidedDescribeEntities { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write create-record + /// property/value to the runtime config file. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(CreateRecord))] + public bool UserProvidedCreateRecord { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write read-records + /// property/value to the runtime config file. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(ReadRecords))] + public bool UserProvidedReadRecords { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write update-record + /// property/value to the runtime config file. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(UpdateRecord))] + public bool UserProvidedUpdateRecord { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write delete-record + /// property/value to the runtime config file. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(DeleteRecord))] + public bool UserProvidedDeleteRecord { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write execute-entity + /// property/value to the runtime config file. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(ExecuteEntity))] + public bool UserProvidedExecuteEntity { get; init; } = false; +} diff --git a/src/Config/ObjectModel/McpRuntimeOptions.cs b/src/Config/ObjectModel/McpRuntimeOptions.cs new file mode 100644 index 0000000000..73d695ee4a --- /dev/null +++ b/src/Config/ObjectModel/McpRuntimeOptions.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.Converters; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +public record McpRuntimeOptions +{ + public const string DEFAULT_PATH = "/mcp"; + + /// + /// Whether MCP endpoints are enabled + /// + [JsonPropertyName("enabled")] + public bool Enabled { get; init; } = true; + + /// + /// The path where MCP endpoints will be exposed + /// + [JsonPropertyName("path")] + public string Path { get; init; } = DEFAULT_PATH; + + /// + /// Configuration for DML tools + /// + [JsonPropertyName("dml-tools")] + [JsonConverter(typeof(DmlToolsConfigConverter))] + public DmlToolsConfig? DmlTools { get; init; } + + [JsonConstructor] + public McpRuntimeOptions( + bool Enabled = true, + string? Path = null, + DmlToolsConfig? DmlTools = null) + { + this.Enabled = Enabled; + + if (Path is not null) + { + this.Path = Path; + UserProvidedPath = true; + } + else + { + this.Path = DEFAULT_PATH; + } + + this.DmlTools = DmlTools; + } + + /// + /// Flag which informs CLI and JSON serializer whether to write path + /// property and value to the runtime config file. + /// When user doesn't provide the path property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Enabled))] + public bool UserProvidedPath { get; init; } = false; +} diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 7cf8159952..a450e1265c 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -72,6 +72,15 @@ Runtime.Rest is null || Runtime.Rest.Enabled) && DataSource.DatabaseType != DatabaseType.CosmosDB_NoSQL; + /// + /// Retrieves the value of runtime.mcp.enabled property if present, default is true. + /// + [JsonIgnore] + public bool IsMcpEnabled => + Runtime is null || + Runtime.Mcp is null || + Runtime.Mcp.Enabled; + [JsonIgnore] public bool IsHealthEnabled => Runtime is null || @@ -127,6 +136,25 @@ public string GraphQLPath } } + /// + /// The path at which MCP API is available + /// + [JsonIgnore] + public string McpPath + { + get + { + if (Runtime is null || Runtime.Mcp is null || Runtime.Mcp.Path is null) + { + return McpRuntimeOptions.DEFAULT_PATH; + } + else + { + return Runtime.Mcp.Path; + } + } + } + /// /// Indicates whether introspection is allowed or not. /// @@ -707,4 +735,10 @@ public LogLevel GetConfiguredLogLevel(string loggerFilter = "") return LogLevel.Error; } + + /// + /// Gets the MCP DML tools configuration + /// + [JsonIgnore] + public DmlToolsConfig? McpDmlTools => Runtime?.Mcp?.DmlTools; } diff --git a/src/Config/ObjectModel/RuntimeOptions.cs b/src/Config/ObjectModel/RuntimeOptions.cs index 8e05df4b62..6f6c046651 100644 --- a/src/Config/ObjectModel/RuntimeOptions.cs +++ b/src/Config/ObjectModel/RuntimeOptions.cs @@ -10,6 +10,7 @@ public record RuntimeOptions { public RestRuntimeOptions? Rest { get; init; } public GraphQLRuntimeOptions? GraphQL { get; init; } + public McpRuntimeOptions? Mcp { get; init; } public HostOptions? Host { get; set; } public string? BaseRoute { get; init; } public TelemetryOptions? Telemetry { get; init; } @@ -21,6 +22,7 @@ public record RuntimeOptions public RuntimeOptions( RestRuntimeOptions? Rest, GraphQLRuntimeOptions? GraphQL, + McpRuntimeOptions? Mcp, HostOptions? Host, string? BaseRoute = null, TelemetryOptions? Telemetry = null, @@ -30,6 +32,7 @@ public RuntimeOptions( { this.Rest = Rest; this.GraphQL = GraphQL; + this.Mcp = Mcp; this.Host = Host; this.BaseRoute = BaseRoute; this.Telemetry = Telemetry; @@ -60,6 +63,12 @@ GraphQL is null || GraphQL?.Enabled is null || GraphQL?.Enabled is true; + [JsonIgnore] + public bool IsMcpEnabled => + Mcp is null || + Mcp?.Enabled is null || + Mcp?.Enabled is true; + [JsonIgnore] public bool IsHealthCheckEnabled => Health is null || diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 4a220af0ea..f78c32ebc1 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -246,6 +246,8 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new EntityHealthOptionsConvertorFactory()); options.Converters.Add(new RestRuntimeOptionsConverterFactory()); options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new McpRuntimeOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new DmlToolsConfigConverter()); options.Converters.Add(new EntitySourceConverterFactory(replaceEnvVar)); options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replaceEnvVar)); options.Converters.Add(new EntityRestOptionsConverterFactory(replaceEnvVar)); diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index 12a8f82aa4..fd8f811c9e 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -702,11 +702,11 @@ private void ValidateNameRequirements(string entityName) /// The config that will be validated. public void ValidateGlobalEndpointRouteConfig(RuntimeConfig runtimeConfig) { - // Both REST and GraphQL endpoints cannot be disabled at the same time. - if (!runtimeConfig.IsRestEnabled && !runtimeConfig.IsGraphQLEnabled) + // REST, GraphQL and MCP endpoints cannot be disabled at the same time. + if (!runtimeConfig.IsRestEnabled && !runtimeConfig.IsGraphQLEnabled && !runtimeConfig.IsMcpEnabled) { HandleOrRecordException(new DataApiBuilderException( - message: $"Both GraphQL and REST endpoints are disabled.", + message: $"GraphQL, REST, and MCP endpoints are disabled.", statusCode: HttpStatusCode.ServiceUnavailable, subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); } @@ -735,19 +735,30 @@ public void ValidateGlobalEndpointRouteConfig(RuntimeConfig runtimeConfig) ValidateRestURI(runtimeConfig); ValidateGraphQLURI(runtimeConfig); - // Do not check for conflicts if GraphQL or REST endpoints are disabled. - if (!runtimeConfig.IsRestEnabled || !runtimeConfig.IsGraphQLEnabled) + ValidateMcpUri(runtimeConfig); + // Do not check for conflicts if two of the endpoints are disabled between GraphQL, REST, and MCP. + if ((!runtimeConfig.IsRestEnabled && !runtimeConfig.IsGraphQLEnabled) || + (!runtimeConfig.IsRestEnabled && !runtimeConfig.IsMcpEnabled) || + (!runtimeConfig.IsGraphQLEnabled && !runtimeConfig.IsMcpEnabled)) { return; } if (string.Equals( - a: runtimeConfig.RestPath, - b: runtimeConfig.GraphQLPath, - comparisonType: StringComparison.OrdinalIgnoreCase)) + a: runtimeConfig.RestPath, + b: runtimeConfig.GraphQLPath, + comparisonType: StringComparison.OrdinalIgnoreCase) || + string.Equals( + a: runtimeConfig.RestPath, + b: runtimeConfig.McpPath, + comparisonType: StringComparison.OrdinalIgnoreCase) || + string.Equals( + a: runtimeConfig.McpPath, + b: runtimeConfig.GraphQLPath, + comparisonType: StringComparison.OrdinalIgnoreCase)) { HandleOrRecordException(new DataApiBuilderException( - message: $"Conflicting GraphQL and REST path configuration.", + message: $"Conflicting path configuration between GraphQL, REST, and MCP.", statusCode: HttpStatusCode.ServiceUnavailable, subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); } @@ -794,6 +805,41 @@ public void ValidateGraphQLURI(RuntimeConfig runtimeConfig) } } + /// + /// Method to validate that the MCP URI (MCP path prefix). + /// + /// + public void ValidateMcpUri(RuntimeConfig runtimeConfig) + { + // Skip validation if MCP is not configured + if (runtimeConfig.Runtime?.Mcp is null) + { + return; + } + + // Get the MCP path from the configuration + string? mcpPath = runtimeConfig.Runtime.Mcp.Path; + + // Validate that the path is not null or empty when MCP is configured + if (string.IsNullOrWhiteSpace(mcpPath)) + { + HandleOrRecordException(new DataApiBuilderException( + message: "MCP path cannot be null or empty when MCP is configured.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); + return; + } + + // Validate the MCP path using the same validation as REST and GraphQL + if (!RuntimeConfigValidatorUtil.TryValidateUriComponent(mcpPath, out string exceptionMsgSuffix)) + { + HandleOrRecordException(new DataApiBuilderException( + message: $"MCP path {exceptionMsgSuffix}", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); + } + } + private void ValidateAuthenticationOptions(RuntimeConfig runtimeConfig) { // Bypass validation of auth if there is no auth provided diff --git a/src/Core/Services/RestService.cs b/src/Core/Services/RestService.cs index 0cf9f8a374..6a2308dd83 100644 --- a/src/Core/Services/RestService.cs +++ b/src/Core/Services/RestService.cs @@ -391,6 +391,14 @@ public string GetRouteAfterPathBase(string route) // forward slash '/'. configuredRestPathBase = configuredRestPathBase.Substring(1); + if (route.Equals(_runtimeConfigProvider.GetConfig().McpPath.Substring(1))) + { + throw new DataApiBuilderException( + message: $"Route {route} was not found.", + statusCode: HttpStatusCode.NotFound, + subStatusCode: DataApiBuilderException.SubStatusCodes.GlobalMcpEndpointDisabled); + } + if (!route.StartsWith(configuredRestPathBase)) { throw new DataApiBuilderException( diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 9213684e6e..14f097915c 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -29,6 +29,8 @@ + + @@ -58,25 +60,25 @@ We use an older version of Newtonsoft.Json.Schema because newer versions depend on Newtonsoft.Json >=13.0.3 which is not (and can not be made) available in Microsoft Private Nuget Feeds --> - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs b/src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs index 12c7db4fce..07a8a565ec 100644 --- a/src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs +++ b/src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs @@ -20,6 +20,7 @@ internal static RuntimeConfig CreateTestConfigWithAuthNProvider(AuthenticationOp Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: hostOptions ), Entities: new(new Dictionary()) diff --git a/src/Service.Tests/Authorization/AuthorizationHelpers.cs b/src/Service.Tests/Authorization/AuthorizationHelpers.cs index 7c6948b484..85f05a1c3b 100644 --- a/src/Service.Tests/Authorization/AuthorizationHelpers.cs +++ b/src/Service.Tests/Authorization/AuthorizationHelpers.cs @@ -126,6 +126,7 @@ public static RuntimeConfig InitRuntimeConfig( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new( Cors: null, Authentication: new(authProvider, null) diff --git a/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs b/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs index 733ec15b24..39a77bffff 100644 --- a/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs +++ b/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs @@ -1441,6 +1441,7 @@ private static RuntimeConfig BuildTestRuntimeConfig(EntityPermission[] permissio Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap) diff --git a/src/Service.Tests/Caching/HealthEndpointCachingTests.cs b/src/Service.Tests/Caching/HealthEndpointCachingTests.cs index 2dbff7cbb2..94216a4409 100644 --- a/src/Service.Tests/Caching/HealthEndpointCachingTests.cs +++ b/src/Service.Tests/Caching/HealthEndpointCachingTests.cs @@ -156,6 +156,7 @@ private static void CreateCustomConfigFile(Dictionary entityMap, Health: new(enabled: true, cacheTtlSeconds: cacheTtlSeconds), Rest: new(Enabled: true), GraphQL: new(Enabled: true), + Mcp: new(Enabled: true), Host: hostOptions ), Entities: new(entityMap)); diff --git a/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs b/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs index 8b01d29961..963211ae40 100644 --- a/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs +++ b/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs @@ -194,6 +194,7 @@ private static RuntimeConfig CreateRuntimeConfigWithOptionalAuthN(Authentication Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: hostOptions ), Entities: new(new Dictionary()) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 2522806049..0be24fa886 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -1608,7 +1608,7 @@ public async Task TestSqlMetadataForInvalidConfigEntities() GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new(), new()); // creating an entity with invalid table name Entity entityWithInvalidSourceName = new( @@ -1679,7 +1679,7 @@ public async Task TestSqlMetadataValidationForEntitiesWithInvalidSource() GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new(), new()); // creating an entity with invalid table name Entity entityWithInvalidSource = new( @@ -2214,7 +2214,7 @@ public async Task TestPathRewriteMiddlewareForGraphQL( GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, new()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, new(), new()); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); @@ -2543,7 +2543,7 @@ public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvir DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, null); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); @@ -2618,6 +2618,7 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission() { GraphQLRuntimeOptions graphqlOptions = new(Enabled: true); RestRuntimeOptions restRuntimeOptions = new(Enabled: false); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: false); DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); @@ -2648,7 +2649,7 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission() Mappings: null); string entityName = "Stock"; - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, entityName); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions, entity, entityName); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); @@ -2919,6 +2920,7 @@ public async Task ValidateInheritanceOfReadPermissionFromAnonymous() { GraphQLRuntimeOptions graphqlOptions = new(Enabled: true); RestRuntimeOptions restRuntimeOptions = new(Enabled: false); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: false); DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); @@ -2949,7 +2951,7 @@ public async Task ValidateInheritanceOfReadPermissionFromAnonymous() Mappings: null); string entityName = "Stock"; - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, entityName); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions, entity, entityName); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); @@ -3060,6 +3062,7 @@ public async Task ValidateLocationHeaderFieldForPostRequests(EntitySourceType en GraphQLRuntimeOptions graphqlOptions = new(Enabled: false); RestRuntimeOptions restRuntimeOptions = new(Enabled: true); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: false); DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); @@ -3077,11 +3080,11 @@ public async Task ValidateLocationHeaderFieldForPostRequests(EntitySourceType en ); string entityName = "GetBooks"; - configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, entityName); + configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions, entity, entityName); } else { - configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); + configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions); } const string CUSTOM_CONFIG = "custom-config.json"; @@ -3158,6 +3161,7 @@ public async Task ValidateLocationHeaderWhenBaseRouteIsConfigured( { GraphQLRuntimeOptions graphqlOptions = new(Enabled: false); RestRuntimeOptions restRuntimeOptions = new(Enabled: true); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: false); DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); @@ -3175,11 +3179,11 @@ public async Task ValidateLocationHeaderWhenBaseRouteIsConfigured( ); string entityName = "GetBooks"; - configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, entityName); + configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions, entity, entityName); } else { - configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); + configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions); } const string CUSTOM_CONFIG = "custom-config.json"; @@ -3188,7 +3192,7 @@ public async Task ValidateLocationHeaderWhenBaseRouteIsConfigured( HostOptions staticWebAppsHostOptions = new(null, authenticationOptions); RuntimeOptions runtimeOptions = configuration.Runtime; - RuntimeOptions baseRouteEnabledRuntimeOptions = new(runtimeOptions?.Rest, runtimeOptions?.GraphQL, staticWebAppsHostOptions, "/data-api"); + RuntimeOptions baseRouteEnabledRuntimeOptions = new(runtimeOptions?.Rest, runtimeOptions?.GraphQL, runtimeOptions?.Mcp, staticWebAppsHostOptions, "/data-api"); RuntimeConfig baseRouteEnabledConfig = configuration with { Runtime = baseRouteEnabledRuntimeOptions }; File.WriteAllText(CUSTOM_CONFIG, baseRouteEnabledConfig.ToJson()); @@ -3347,7 +3351,7 @@ public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL() Mappings: null ); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new(), viewEntity, "books_view_all"); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new(), new(), viewEntity, "books_view_all"); const string CUSTOM_CONFIG = "custom-config.json"; @@ -3568,6 +3572,7 @@ public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, Easy RuntimeOptions runtimeOptions = new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, authenticationOptions, hostMode) ); RuntimeConfig configWithCustomHostMode = config with { Runtime = runtimeOptions }; @@ -3608,10 +3613,11 @@ public async Task TestSchemaIntrospectionQuery(bool enableIntrospection, bool ex { GraphQLRuntimeOptions graphqlOptions = new(AllowIntrospection: enableIntrospection); RestRuntimeOptions restRuntimeOptions = new(); + McpRuntimeOptions mcpRuntimeOptions = new(); DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); @@ -3660,6 +3666,7 @@ public void TestInvalidDatabaseColumnNameHandling( { GraphQLRuntimeOptions graphqlOptions = new(Enabled: globalGraphQLEnabled); RestRuntimeOptions restRuntimeOptions = new(Enabled: true); + McpRuntimeOptions mcpOptions = new(Enabled: true); DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); @@ -3683,7 +3690,7 @@ public void TestInvalidDatabaseColumnNameHandling( Mappings: mappings ); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, "graphqlNameCompat"); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpOptions, entity, "graphqlNameCompat"); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); @@ -3739,7 +3746,8 @@ public async Task OpenApi_InteractiveSwaggerUI( RuntimeConfig configuration = InitMinimalRuntimeConfig( dataSource: dataSource, graphqlOptions: new(), - restOptions: new(Path: customRestPath)); + restOptions: new(Path: customRestPath), + mcpOptions: new()); configuration = configuration with @@ -4057,6 +4065,7 @@ private static RuntimeConfig InitializeRuntimeWithLogLevel(Dictionary entityMap, ? new( Rest: new(Enabled: enableGlobalRest), GraphQL: new(Enabled: true), + Mcp: new(Enabled: true), Host: hostOptions, Pagination: paginationOptions) : new( Rest: new(Enabled: enableGlobalRest), GraphQL: new(Enabled: true), + Mcp: new(Enabled: true), Host: hostOptions); RuntimeConfig runtimeConfig = new( @@ -5312,6 +5324,8 @@ public static RuntimeConfig InitialzieRuntimeConfigForMultipleCreateTests(bool i RestRuntimeOptions restRuntimeOptions = new(Enabled: false); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: false); + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); EntityAction createAction = new( @@ -5370,7 +5384,7 @@ public static RuntimeConfig InitialzieRuntimeConfigForMultipleCreateTests(bool i RuntimeConfig runtimeConfig = new(Schema: "IntegrationTestMinimalSchema", DataSource: dataSource, - Runtime: new(restRuntimeOptions, graphqlOptions, Host: new(Cors: null, Authentication: authenticationOptions, Mode: HostMode.Development), Cache: null), + Runtime: new(restRuntimeOptions, graphqlOptions, mcpRuntimeOptions, Host: new(Cors: null, Authentication: authenticationOptions, Mode: HostMode.Development), Cache: null), Entities: new(entityMap)); return runtimeConfig; } @@ -5383,6 +5397,7 @@ public static RuntimeConfig InitMinimalRuntimeConfig( DataSource dataSource, GraphQLRuntimeOptions graphqlOptions, RestRuntimeOptions restOptions, + McpRuntimeOptions mcpOptions, Entity entity = null, string entityName = null, RuntimeCacheOptions cacheOptions = null @@ -5420,7 +5435,7 @@ public static RuntimeConfig InitMinimalRuntimeConfig( return new( Schema: "IntegrationTestMinimalSchema", DataSource: dataSource, - Runtime: new(restOptions, graphqlOptions, + Runtime: new(restOptions, graphqlOptions, mcpOptions, Host: new(Cors: null, Authentication: authenticationOptions, Mode: HostMode.Development), Cache: cacheOptions ), @@ -5496,6 +5511,7 @@ private static RuntimeConfig CreateBasicRuntimeConfigWithNoEntity( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) @@ -5533,6 +5549,7 @@ private static RuntimeConfig CreateBasicRuntimeConfigWithSingleEntityAndAuthOpti Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: authenticationOptions) ), Entities: new(entityMap) diff --git a/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs b/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs index a492f2c167..2a83697a3a 100644 --- a/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs @@ -127,6 +127,7 @@ private static void CreateCustomConfigFile(Dictionary entityMap, Health: new(enabled: true, roles: role != null ? new HashSet { role } : null), Rest: new(Enabled: true), GraphQL: new(Enabled: true), + Mcp: new(Enabled: true), Host: hostOptions ), Entities: new(entityMap)); diff --git a/src/Service.Tests/Configuration/HealthEndpointTests.cs b/src/Service.Tests/Configuration/HealthEndpointTests.cs index 4fd2e52bf4..70e14e0108 100644 --- a/src/Service.Tests/Configuration/HealthEndpointTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointTests.cs @@ -53,19 +53,36 @@ public void CleanupAfterEachTest() /// [TestMethod] [TestCategory(TestCategory.MSSQL)] - [DataRow(true, true, true, true, true, true, true, DisplayName = "Validate Health Report all enabled.")] - [DataRow(false, true, true, true, true, true, true, DisplayName = "Validate when Comprehensive Health Report is disabled")] - [DataRow(true, true, true, false, true, true, true, DisplayName = "Validate Health Report when data-source health is disabled")] - [DataRow(true, true, true, true, false, true, true, DisplayName = "Validate Health Report when entity health is disabled")] - [DataRow(true, false, true, true, true, true, true, DisplayName = "Validate Health Report when global rest health is disabled")] - [DataRow(true, true, true, true, true, false, true, DisplayName = "Validate Health Report when entity rest health is disabled")] - [DataRow(true, true, false, true, true, true, true, DisplayName = "Validate Health Report when global graphql health is disabled")] - [DataRow(true, true, true, true, true, true, false, DisplayName = "Validate Health Report when entity graphql health is disabled")] - public async Task ComprehensiveHealthEndpoint_ValidateContents(bool enableGlobalHealth, bool enableGlobalRest, bool enableGlobalGraphql, bool enableDatasourceHealth, bool enableEntityHealth, bool enableEntityRest, bool enableEntityGraphQL) + [DataRow(true, true, true, true, true, true, true, true, DisplayName = "Validate Health Report all enabled.")] + [DataRow(false, true, true, true, true, true, true, true, DisplayName = "Validate when Comprehensive Health Report is disabled")] + [DataRow(true, true, true, false, true, true, true, true, DisplayName = "Validate Health Report when global MCP health is disabled")] + [DataRow(true, true, true, true, false, true, true, true, DisplayName = "Validate Health Report when data-source health is disabled")] + [DataRow(true, true, true, true, true, false, true, true, DisplayName = "Validate Health Report when entity health is disabled")] + [DataRow(true, false, true, true, true, true, true, true, DisplayName = "Validate Health Report when global REST health is disabled")] + [DataRow(true, true, false, true, true, true, true, true, DisplayName = "Validate Health Report when global GraphQL health is disabled")] + [DataRow(true, true, true, true, true, true, false, true, DisplayName = "Validate Health Report when entity REST health is disabled")] + [DataRow(true, true, true, true, true, true, true, false, DisplayName = "Validate Health Report when entity GraphQL health is disabled")] + public async Task ComprehensiveHealthEndpoint_ValidateContents( + bool enableGlobalHealth, + bool enableGlobalRest, + bool enableGlobalGraphql, + bool enableGlobalMcp, + bool enableDatasourceHealth, + bool enableEntityHealth, + bool enableEntityRest, + bool enableEntityGraphQL) { - // Arrange - // Create a mock entity map with a single entity for testing - RuntimeConfig runtimeConfig = SetupCustomConfigFile(enableGlobalHealth, enableGlobalRest, enableGlobalGraphql, enableDatasourceHealth, enableEntityHealth, enableEntityRest, enableEntityGraphQL); + // The body remains exactly the same except passing enableGlobalMcp + RuntimeConfig runtimeConfig = SetupCustomConfigFile( + enableGlobalHealth, + enableGlobalRest, + enableGlobalGraphql, + enableGlobalMcp, + enableDatasourceHealth, + enableEntityHealth, + enableEntityRest, + enableEntityGraphQL); + WriteToCustomConfigFile(runtimeConfig); string[] args = new[] @@ -90,7 +107,7 @@ public async Task ComprehensiveHealthEndpoint_ValidateContents(bool enableGlobal Assert.AreEqual(expected: HttpStatusCode.OK, actual: response.StatusCode, message: "Received unexpected HTTP code from health check endpoint."); ValidateBasicDetailsHealthCheckResponse(responseProperties); - ValidateConfigurationDetailsHealthCheckResponse(responseProperties, enableGlobalRest, enableGlobalGraphql); + ValidateConfigurationDetailsHealthCheckResponse(responseProperties, enableGlobalRest, enableGlobalGraphql, enableGlobalMcp); ValidateIfAttributePresentInResponse(responseProperties, enableDatasourceHealth, HealthCheckConstants.DATASOURCE); ValidateIfAttributePresentInResponse(responseProperties, enableEntityHealth, HealthCheckConstants.ENDPOINT); if (enableEntityHealth) @@ -110,7 +127,7 @@ public async Task ComprehensiveHealthEndpoint_ValidateContents(bool enableGlobal public async Task TestHealthCheckRestResponseAsync() { // Arrange - RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true); + RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true, true); HttpUtilities httpUtilities = SetupRestTest(runtimeConfig); // Act @@ -139,7 +156,7 @@ public async Task TestHealthCheckRestResponseAsync() public async Task TestFailureHealthCheckRestResponseAsync() { // Arrange - RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true); + RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true, true); HttpUtilities httpUtilities = SetupGraphQLTest(runtimeConfig, HttpStatusCode.BadRequest); // Act @@ -167,7 +184,7 @@ public async Task TestFailureHealthCheckRestResponseAsync() public async Task TestHealthCheckGraphQLResponseAsync() { // Arrange - RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true); + RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true, true); HttpUtilities httpUtilities = SetupGraphQLTest(runtimeConfig); // Act @@ -191,7 +208,7 @@ public async Task TestHealthCheckGraphQLResponseAsync() public async Task TestFailureHealthCheckGraphQLResponseAsync() { // Arrange - RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true); + RuntimeConfig runtimeConfig = SetupCustomConfigFile(true, true, true, true, true, true, true, true); HttpUtilities httpUtilities = SetupGraphQLTest(runtimeConfig, HttpStatusCode.InternalServerError); // Act @@ -427,7 +444,7 @@ private static void ValidateConfigurationIsCorrectFlag(Dictionary responseProperties, bool enableGlobalRest, bool enableGlobalGraphQL) + private static void ValidateConfigurationDetailsHealthCheckResponse(Dictionary responseProperties, bool enableGlobalRest, bool enableGlobalGraphQL, bool enableGlobalMcp) { if (responseProperties.TryGetValue("configuration", out JsonElement configElement) && configElement.ValueKind == JsonValueKind.Object) { @@ -443,6 +460,8 @@ private static void ValidateConfigurationDetailsHealthCheckResponse(Dictionary @@ -520,7 +539,7 @@ private static RuntimeConfig SetupCustomConfigFile(bool enableGlobalHealth, bool /// /// Collection of entityName -> Entity object. /// flag to enable or disabled REST globally. - private static RuntimeConfig CreateRuntimeConfig(Dictionary entityMap, bool enableGlobalRest = true, bool enableGlobalGraphql = true, bool enableGlobalHealth = true, bool enableDatasourceHealth = true, HostMode hostMode = HostMode.Production) + private static RuntimeConfig CreateRuntimeConfig(Dictionary entityMap, bool enableGlobalRest = true, bool enableGlobalGraphql = true, bool enabledGlobalMcp = true, bool enableGlobalHealth = true, bool enableDatasourceHealth = true, HostMode hostMode = HostMode.Production) { DataSource dataSource = new( DatabaseType.MSSQL, @@ -536,6 +555,7 @@ private static RuntimeConfig CreateRuntimeConfig(Dictionary enti Health: new(enabled: enableGlobalHealth), Rest: new(Enabled: enableGlobalRest), GraphQL: new(Enabled: enableGlobalGraphql), + Mcp: new(Enabled: enabledGlobalMcp), Host: hostOptions ), Entities: new(entityMap)); diff --git a/src/Service.Tests/Configuration/HotReload/AuthorizationResolverHotReloadTests.cs b/src/Service.Tests/Configuration/HotReload/AuthorizationResolverHotReloadTests.cs index cc397e0ca0..b5fcb6162b 100644 --- a/src/Service.Tests/Configuration/HotReload/AuthorizationResolverHotReloadTests.cs +++ b/src/Service.Tests/Configuration/HotReload/AuthorizationResolverHotReloadTests.cs @@ -131,6 +131,7 @@ private static void CreateCustomConfigFile(string fileName, Dictionary dbOptions = new(); HyphenatedNamingPolicy namingPolicy = new(); @@ -548,7 +549,7 @@ type Planet @model(name:""Planet"") { Mappings: null); string entityName = "Planet"; - RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, entityName); + RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions, entity, entityName); const string CUSTOM_CONFIG = "custom-config.json"; const string CUSTOM_SCHEMA = "custom-schema.gql"; @@ -642,6 +643,7 @@ type Planet @model(name:""Planet"") { }"; GraphQLRuntimeOptions graphqlOptions = new(Enabled: true); RestRuntimeOptions restRuntimeOptions = new(Enabled: false); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: false); Dictionary dbOptions = new(); HyphenatedNamingPolicy namingPolicy = new(); @@ -677,7 +679,7 @@ type Planet @model(name:""Planet"") { Mappings: null); string entityName = "Planet"; - RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, entityName); + RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions, entity, entityName); const string CUSTOM_CONFIG = "custom-config.json"; const string CUSTOM_SCHEMA = "custom-schema.gql"; diff --git a/src/Service.Tests/CosmosTests/QueryTests.cs b/src/Service.Tests/CosmosTests/QueryTests.cs index 97cffa3c98..c40c95c75b 100644 --- a/src/Service.Tests/CosmosTests/QueryTests.cs +++ b/src/Service.Tests/CosmosTests/QueryTests.cs @@ -682,6 +682,7 @@ type Planet @model(name:""Planet"") { GraphQLRuntimeOptions graphqlOptions = new(Enabled: true); RestRuntimeOptions restRuntimeOptions = new(Enabled: false); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: false); Dictionary dbOptions = new(); HyphenatedNamingPolicy namingPolicy = new(); @@ -724,7 +725,7 @@ type Planet @model(name:""Planet"") { string entityName = "Planet"; // cache configuration - RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, entityName, new RuntimeCacheOptions() { Enabled = true, TtlSeconds = 5 }); + RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions, entity, entityName, new RuntimeCacheOptions() { Enabled = true, TtlSeconds = 5 }); const string CUSTOM_CONFIG = "custom-config.json"; const string CUSTOM_SCHEMA = "custom-schema.gql"; diff --git a/src/Service.Tests/CosmosTests/SchemaGeneratorFactoryTests.cs b/src/Service.Tests/CosmosTests/SchemaGeneratorFactoryTests.cs index 20f415e3dc..562a5174d2 100644 --- a/src/Service.Tests/CosmosTests/SchemaGeneratorFactoryTests.cs +++ b/src/Service.Tests/CosmosTests/SchemaGeneratorFactoryTests.cs @@ -78,7 +78,7 @@ public async Task ExportGraphQLFromCosmosDB_GeneratesSchemaSuccessfully(string g {"database", globalDatabase}, {"container", globalContainer} }), - Runtime: new(Rest: null, GraphQL: new(), Host: new(null, null)), + Runtime: new(Rest: null, GraphQL: new(), Mcp: new(), Host: new(null, null)), Entities: new(new Dictionary() { {"Container1", new Entity( diff --git a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs index 0ed64ca6ee..94665d7c18 100644 --- a/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/MultipleMutationBuilderTests.cs @@ -360,6 +360,7 @@ private static RuntimeConfigProvider GetRuntimeConfigProvider() { Runtime = new RuntimeOptions(Rest: runtimeConfig.Runtime.Rest, GraphQL: new GraphQLRuntimeOptions(MultipleMutationOptions: new MultipleMutationOptions(new MultipleCreateOptions(enabled: true))), + Mcp: runtimeConfig.Runtime.Mcp, Host: runtimeConfig.Runtime.Host, BaseRoute: runtimeConfig.Runtime.BaseRoute, Telemetry: runtimeConfig.Runtime.Telemetry, diff --git a/src/Service.Tests/ModuleInitializer.cs b/src/Service.Tests/ModuleInitializer.cs index b099508604..ba0407ecd5 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -51,6 +51,10 @@ public static void Init() VerifierSettings.IgnoreMember(options => options.IsGraphQLEnabled); // Ignore the entity IsGraphQLEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(entity => entity.IsGraphQLEnabled); + // Ignore the global IsMcpEnabled as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.IsMcpEnabled); + // Ignore the global RuntimeOptions.IsMcpEnabled as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(options => options.IsMcpEnabled); // Ignore the global IsHealthEnabled as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsHealthEnabled); // Ignore the global RuntimeOptions.IsHealthCheckEnabled as that's unimportant from a test standpoint. @@ -69,16 +73,16 @@ public static void Init() VerifierSettings.IgnoreMember(config => config.CosmosDataSourceUsed); // Ignore the IsRequestBodyStrict as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsRequestBodyStrict); - // Ignore the IsGraphQLEnabled as that's unimportant from a test standpoint. - VerifierSettings.IgnoreMember(config => config.IsGraphQLEnabled); - // Ignore the IsRestEnabled as that's unimportant from a test standpoint. - VerifierSettings.IgnoreMember(config => config.IsRestEnabled); + // Ignore the McpDmlTools as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.McpDmlTools); // Ignore the IsStaticWebAppsIdentityProvider as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsStaticWebAppsIdentityProvider); // Ignore the RestPath as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.RestPath); // Ignore the GraphQLPath as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.GraphQLPath); + // Ignore the McpPath as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.McpPath); // Ignore the AllowIntrospection as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.AllowIntrospection); // Ignore the EnableAggregation as that's unimportant from a test standpoint. @@ -105,6 +109,8 @@ public static void Init() VerifierSettings.IgnoreMember(options => options.UserProvidedDepthLimit); // Ignore EnableLegacyDateTimeScalar as that's not serialized in our config file. VerifierSettings.IgnoreMember(options => options.EnableLegacyDateTimeScalar); + // Ignore UserProvidedPath as that's not serialized in our config file. + VerifierSettings.IgnoreMember(options => options.UserProvidedPath); // Customise the path where we store snapshots, so they are easier to locate in a PR review. VerifyBase.DerivePathInfo( (sourceFile, projectDirectory, type, method) => new( diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt index 51d8543ed5..420977ed26 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt @@ -17,6 +17,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 51b733b94e..4283eb432e 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -21,6 +21,10 @@ } } }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt index 23f67259d4..f34141c964 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt index a534867fee..75490a804b 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt @@ -13,6 +13,10 @@ Path: /graphql, AllowIntrospection: true }, + Mcp: { + Enabled: true, + Path: /mcp + }, Host: { Cors: { Origins: [ diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/DwSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/DwSqlGraphQLQueryTests.cs index fa977e48d5..8cf55c247d 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/DwSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/DwSqlGraphQLQueryTests.cs @@ -1239,6 +1239,7 @@ public void TestEnableDwNto1JoinQueryFeatureFlagLoadedFromRuntime() { EnableDwNto1JoinQueryOptimization = true }), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) @@ -1261,6 +1262,7 @@ public void TestEnableDwNto1JoinQueryFeatureFlagDefaultValueLoaded() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) diff --git a/src/Service.Tests/SqlTests/SqlTestHelper.cs b/src/Service.Tests/SqlTests/SqlTestHelper.cs index 6193d843a0..e739f6cc8c 100644 --- a/src/Service.Tests/SqlTests/SqlTestHelper.cs +++ b/src/Service.Tests/SqlTests/SqlTestHelper.cs @@ -389,6 +389,7 @@ public static RuntimeConfig InitBasicRuntimeConfigWithNoEntity( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: authenticationOptions) ), Entities: new(new Dictionary()) diff --git a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs index 5a9d783376..16caf29b49 100644 --- a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs @@ -257,6 +257,7 @@ public void TestAddingRelationshipWithInvalidTargetEntity() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap) @@ -317,6 +318,7 @@ public void TestAddingRelationshipWithDisabledGraphQL() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap) @@ -373,6 +375,7 @@ string relationshipEntity Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap) @@ -461,6 +464,7 @@ public void TestRelationshipWithNoLinkingObjectAndEitherSourceOrTargetFieldIsNul Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap)); @@ -553,6 +557,7 @@ public void TestRelationshipWithoutSourceAndTargetFieldsMatching( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap)); @@ -626,6 +631,7 @@ public void TestRelationshipWithoutSourceAndTargetFieldsAsValidBackingColumns( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap)); @@ -755,6 +761,7 @@ public void TestRelationshipWithoutLinkingSourceAndTargetFieldsMatching( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap)); @@ -1011,6 +1018,7 @@ public void TestOperationValidityAndCasing(string operationName, bool exceptionE Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap)); @@ -1083,6 +1091,7 @@ public void ValidateGraphQLTypeNamesFromConfig(string entityNameFromConfig, bool Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap) @@ -1440,21 +1449,27 @@ public void ValidateValidEntityDefinitionsDoesNotGenerateDuplicateQueries(Databa /// /// GraphQL global path /// REST global path + /// MCP global path /// Exception expected [DataTestMethod] - [DataRow("/graphql", "/graphql", true)] - [DataRow("/api", "/api", true)] - [DataRow("/graphql", "/api", false)] - public void TestGlobalRouteValidation(string graphQLConfiguredPath, string restConfiguredPath, bool expectError) + [DataRow("/graphql", "/graphql", "/mcp", true, DisplayName = "GraphQL and REST conflict (same path).")] + [DataRow("/api", "/api", "/mcp", true, DisplayName = "REST and GraphQL conflict (same path).")] + [DataRow("/graphql", "/api", "/mcp", false, DisplayName = "GraphQL, REST, and MCP distinct.")] + // Extra case: conflict with MCP + [DataRow("/mcp", "/api", "/mcp", true, DisplayName = "MCP and GraphQL conflict (same path).")] + [DataRow("/graphql", "/mcp", "/mcp", true, DisplayName = "MCP and REST conflict (same path).")] + public void TestGlobalRouteValidation(string graphQLConfiguredPath, string restConfiguredPath, string mcpConfiguredPath, bool expectError) { GraphQLRuntimeOptions graphQL = new(Path: graphQLConfiguredPath); RestRuntimeOptions rest = new(Path: restConfiguredPath); + McpRuntimeOptions mcp = new(Path: mcpConfiguredPath); RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig( new(DatabaseType.MSSQL, "", Options: null), graphQL, - rest); - string expectedErrorMessage = "Conflicting GraphQL and REST path configuration."; + rest, + mcp); + string expectedErrorMessage = "Conflicting path configuration between GraphQL, REST, and MCP."; try { @@ -1671,11 +1686,16 @@ public void ValidateApiURIsAreWellFormed( { string graphQLPathPrefix = GraphQLRuntimeOptions.DEFAULT_PATH; string restPathPrefix = RestRuntimeOptions.DEFAULT_PATH; + string mcpPathPrefix = McpRuntimeOptions.DEFAULT_PATH; if (apiType is ApiType.REST) { restPathPrefix = apiPathPrefix; } + else if (apiType is ApiType.MCP) + { + mcpPathPrefix = apiPathPrefix; + } else { graphQLPathPrefix = apiPathPrefix; @@ -1683,11 +1703,13 @@ public void ValidateApiURIsAreWellFormed( GraphQLRuntimeOptions graphQL = new(Path: graphQLPathPrefix); RestRuntimeOptions rest = new(Path: restPathPrefix); + McpRuntimeOptions mcp = new(Enabled: false); RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig( new(DatabaseType.MSSQL, "", Options: null), graphQL, - rest); + rest, + mcp); RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator(); @@ -1710,25 +1732,33 @@ public void ValidateApiURIsAreWellFormed( /// /// Boolean flag to indicate if REST endpoints are enabled globally. /// Boolean flag to indicate if GraphQL endpoints are enabled globally. + /// Boolean flag to indicate if MCP endpoints are enabled globally. /// Boolean flag to indicate if exception is expected. - [DataRow(true, true, false, DisplayName = "Both REST and GraphQL enabled.")] - [DataRow(true, false, false, DisplayName = "REST enabled, and GraphQL disabled.")] - [DataRow(false, true, false, DisplayName = "REST disabled, and GraphQL enabled.")] - [DataRow(false, false, true, DisplayName = "Both REST and GraphQL are disabled.")] + [DataRow(true, true, true, false, DisplayName = "REST, GraphQL, and MCP enabled.")] + [DataRow(true, true, false, false, DisplayName = "REST and GraphQL enabled, MCP disabled.")] + [DataRow(true, false, true, false, DisplayName = "REST enabled, GraphQL disabled, and MCP enabled.")] + [DataRow(true, false, false, false, DisplayName = "REST enabled, GraphQL and MCP disabled.")] + [DataRow(false, true, true, false, DisplayName = "REST disabled, GraphQL and MCP enabled.")] + [DataRow(false, true, false, false, DisplayName = "REST disabled, GraphQL enabled, and MCP disabled.")] + [DataRow(false, false, true, false, DisplayName = "REST and GraphQL disabled, MCP enabled.")] + [DataRow(false, false, false, true, DisplayName = "REST, GraphQL, and MCP disabled.")] [DataTestMethod] - public void EnsureFailureWhenBothRestAndGraphQLAreDisabled( + public void EnsureFailureWhenRestAndGraphQLAndMcpAreDisabled( bool restEnabled, bool graphqlEnabled, + bool mcpEnabled, bool expectError) { GraphQLRuntimeOptions graphQL = new(Enabled: graphqlEnabled); RestRuntimeOptions rest = new(Enabled: restEnabled); + McpRuntimeOptions mcp = new(Enabled: mcpEnabled); RuntimeConfig configuration = ConfigurationTests.InitMinimalRuntimeConfig( new(DatabaseType.MSSQL, "", Options: null), graphQL, - rest); - string expectedErrorMessage = "Both GraphQL and REST endpoints are disabled."; + rest, + mcp); + string expectedErrorMessage = "GraphQL, REST, and MCP endpoints are disabled."; try { @@ -1995,6 +2025,7 @@ public void ValidateRestMethodsForEntityInConfig( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null)), Entities: new(entityMap)); @@ -2068,6 +2099,7 @@ public void ValidateRestPathForEntityInConfig( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap) @@ -2138,6 +2170,7 @@ public void ValidateUniqueRestPathsForEntitiesInConfig( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(entityMap) @@ -2198,6 +2231,7 @@ public void ValidateRuntimeBaseRouteSettings( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: new(Provider: authenticationProvider, Jwt: null)), BaseRoute: runtimeBaseRoute ), @@ -2334,6 +2368,7 @@ public void TestRuntimeConfigSetupWithNonJsonConstructor() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new RuntimeEntities(entityMap), @@ -2405,6 +2440,7 @@ public void ValidatePaginationOptionsInConfig( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: null), Pagination: new PaginationOptions(defaultPageSize, maxPageSize, nextLinkRelative) ), @@ -2456,6 +2492,7 @@ public void ValidateMaxResponseSizeInConfig( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: null, MaxResponseSizeMB: providedMaxResponseSizeMB) ), Entities: new(new Dictionary())); diff --git a/src/Service.Tests/UnitTests/DbExceptionParserUnitTests.cs b/src/Service.Tests/UnitTests/DbExceptionParserUnitTests.cs index ba7f05251a..02801de3e2 100644 --- a/src/Service.Tests/UnitTests/DbExceptionParserUnitTests.cs +++ b/src/Service.Tests/UnitTests/DbExceptionParserUnitTests.cs @@ -38,6 +38,7 @@ public void VerifyCorrectErrorMessage(bool isDeveloperMode, string expected) Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null, isDeveloperMode ? HostMode.Development : HostMode.Production) ), Entities: new(new Dictionary()) @@ -80,6 +81,7 @@ public void TestIsTransientExceptionMethod(bool expected, int number) Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null, HostMode.Development) ), Entities: new(new Dictionary()) diff --git a/src/Service.Tests/UnitTests/MultiSourceQueryExecutionUnitTests.cs b/src/Service.Tests/UnitTests/MultiSourceQueryExecutionUnitTests.cs index dd76845a04..e06e140328 100644 --- a/src/Service.Tests/UnitTests/MultiSourceQueryExecutionUnitTests.cs +++ b/src/Service.Tests/UnitTests/MultiSourceQueryExecutionUnitTests.cs @@ -251,6 +251,7 @@ public async Task TestMultiSourceTokenSet() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: null) ), DefaultDataSourceName: DATA_SOURCE_NAME_1, @@ -312,6 +313,7 @@ private static RuntimeConfig GenerateMockRuntimeConfigForMultiDbScenario() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), // use prod mode to avoid having to mock config file watcher Host: new(Cors: null, Authentication: null, HostMode.Production) ), diff --git a/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs index 423234aa73..cbfef36664 100644 --- a/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs @@ -46,6 +46,7 @@ public async Task TestHandleManagedIdentityAccess( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) diff --git a/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs index f0db8b4742..ccaa90b353 100644 --- a/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs @@ -57,6 +57,7 @@ public async Task TestHandleManagedIdentityAccess( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) diff --git a/src/Service.Tests/UnitTests/RequestValidatorUnitTests.cs b/src/Service.Tests/UnitTests/RequestValidatorUnitTests.cs index a19823df18..186f254c51 100644 --- a/src/Service.Tests/UnitTests/RequestValidatorUnitTests.cs +++ b/src/Service.Tests/UnitTests/RequestValidatorUnitTests.cs @@ -356,6 +356,7 @@ public static void PerformTest( Runtime: new( Rest: new(Path: "/api"), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: null) ), Entities: new(new Dictionary() diff --git a/src/Service.Tests/UnitTests/RestServiceUnitTests.cs b/src/Service.Tests/UnitTests/RestServiceUnitTests.cs index 9d483bf1d2..1fa1a276ad 100644 --- a/src/Service.Tests/UnitTests/RestServiceUnitTests.cs +++ b/src/Service.Tests/UnitTests/RestServiceUnitTests.cs @@ -115,6 +115,7 @@ public static void InitializeTest(string restRoutePrefix, string entityName) Runtime: new( Rest: new(Path: restRoutePrefix), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 8d7dae0541..b98de993e2 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -259,7 +259,7 @@ public void TestNullableOptionalProps() TryParseAndAssertOnDefaults("{" + emptyRuntime, out _); // Test with empty sub properties of runtime - minJson.Append(@"{ ""rest"": { }, ""graphql"": { }, + minJson.Append(@"{ ""rest"": { }, ""graphql"": { }, ""mcp"": { }, ""base-route"" : """","); StringBuilder minJsonWithHostSubProps = new(minJson + @"""telemetry"" : { }, ""host"" : "); StringBuilder minJsonWithTelemetrySubProps = new(minJson + @"""host"" : { }, ""telemetry"" : "); @@ -423,6 +423,10 @@ public static string GetModifiedJsonString(string[] reps, string enumString) } } }, + ""mcp"": { + ""enabled"": true, + ""path"": """ + reps[++index % reps.Length] + @""" + }, ""host"": { ""mode"": ""development"", ""cors"": { @@ -506,6 +510,10 @@ public static string GetModifiedJsonString(string[] reps, string enumString) ""enabled"": true, ""path"": ""/graphql"" }, + ""mcp"": { + ""enabled"": true, + ""path"": ""/mcp"" + }, ""host"": { ""mode"": ""development"", ""cors"": { @@ -641,6 +649,8 @@ private static bool TryParseAndAssertOnDefaults(string json, out RuntimeConfig p Assert.AreEqual(RestRuntimeOptions.DEFAULT_PATH, parsedConfig.RestPath); Assert.IsTrue(parsedConfig.IsGraphQLEnabled); Assert.AreEqual(GraphQLRuntimeOptions.DEFAULT_PATH, parsedConfig.GraphQLPath); + Assert.IsTrue(parsedConfig.IsMcpEnabled); + Assert.AreEqual(McpRuntimeOptions.DEFAULT_PATH, parsedConfig.McpPath); Assert.IsTrue(parsedConfig.AllowIntrospection); Assert.IsFalse(parsedConfig.IsDevelopmentMode()); Assert.IsTrue(parsedConfig.IsStaticWebAppsIdentityProvider); diff --git a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs index 92b076107a..908b7019c4 100644 --- a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs @@ -80,6 +80,7 @@ public async Task TestHandleManagedIdentityAccess( Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) @@ -154,6 +155,7 @@ public async Task TestRetryPolicyExhaustingMaxAttempts() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) @@ -229,6 +231,7 @@ public void Test_DbCommandParameter_PopulatedWithCorrectDbTypes() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) @@ -344,6 +347,7 @@ public async Task TestHttpContextIsPopulatedWithDbExecutionTime() Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) @@ -446,6 +450,7 @@ public void TestToValidateLockingOfHttpContextObjectDuringCalcuationOfDbExecutio Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(null, null) ), Entities: new(new Dictionary()) @@ -512,6 +517,7 @@ public void ValidateStreamingLogicAsync(int readDataLoops, bool exceptionExpecte Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: null, MaxResponseSizeMB: 5) ), Entities: new(new Dictionary())); @@ -573,6 +579,7 @@ public void ValidateStreamingLogicForStoredProcedures(int readDataLoops, bool ex Runtime: new( Rest: new(), GraphQL: new(), + Mcp: new(), Host: new(Cors: null, Authentication: null, MaxResponseSizeMB: 4) ), Entities: new(new Dictionary())); diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index e57c7dce8c..d5e903d4f3 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -23,6 +23,11 @@ } } }, + "mcp": { + "enabled": true, + "path": "/mcp", + "dml-tools": true + }, "host": { "cors": { "origins": [ @@ -2314,7 +2319,8 @@ "Notebook": { "source": { "object": "notebooks", - "type": "table" + "type": "table", + "object-description": "Table containing notebook information" }, "graphql": { "enabled": true, @@ -3843,4 +3849,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index 9f1558e504..6ea9c8dad2 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -102,6 +102,7 @@ + diff --git a/src/Service/HealthCheck/HealthCheckHelper.cs b/src/Service/HealthCheck/HealthCheckHelper.cs index 9225c3aeb0..452cb803a9 100644 --- a/src/Service/HealthCheck/HealthCheckHelper.cs +++ b/src/Service/HealthCheck/HealthCheckHelper.cs @@ -140,6 +140,7 @@ private static void UpdateDabConfigurationDetails(ref ComprehensiveHealthCheckRe { Rest = runtimeConfig.IsRestEnabled, GraphQL = runtimeConfig.IsGraphQLEnabled, + Mcp = runtimeConfig.IsMcpEnabled, Caching = runtimeConfig.IsCachingEnabled, Telemetry = runtimeConfig?.Runtime?.Telemetry != null, Mode = runtimeConfig?.Runtime?.Host?.Mode ?? HostMode.Production, // Modify to runtimeConfig.HostMode in Roles PR diff --git a/src/Service/HealthCheck/Model/ConfigurationDetails.cs b/src/Service/HealthCheck/Model/ConfigurationDetails.cs index c3989e0167..9ff007754e 100644 --- a/src/Service/HealthCheck/Model/ConfigurationDetails.cs +++ b/src/Service/HealthCheck/Model/ConfigurationDetails.cs @@ -18,6 +18,9 @@ public record ConfigurationDetails [JsonPropertyName("graphql")] public bool GraphQL { get; init; } + [JsonPropertyName("mcp")] + public bool Mcp { get; init; } + [JsonPropertyName("caching")] public bool Caching { get; init; } diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index a23c23178a..48a39d31d0 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -24,6 +24,7 @@ using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Core.Telemetry; +using Azure.DataApiBuilder.Mcp.Core; using Azure.DataApiBuilder.Service.Controllers; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.HealthCheck; @@ -452,6 +453,9 @@ public void ConfigureServices(IServiceCollection services) } services.AddSingleton(); + + services.AddDabMcpServer(configProvider); + services.AddControllers(); } @@ -678,6 +682,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC { endpoints.MapControllers(); + // Special for MCP + endpoints.MapDabMcp(runtimeConfigProvider); + endpoints .MapGraphQL() .WithOptions(new GraphQLServerOptions