From 3f97416a5518ad498446b18e6917da60cd58fba4 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 4 Dec 2025 16:15:41 +0530 Subject: [PATCH 01/11] Add entity-level MCP configuration support - Add EntityMcpOptions class with custom-tool and dml-tools properties - Add JSON converter supporting boolean and object formats - Add CLI support for --mcp.dml-tools and --mcp.custom-tool flags - Add schema validation restricting custom-tool to stored procedures - Entity.Mcp property is optional (default null) to avoid test cascade Only 9 files changed in this minimal implementation. --- schemas/dab.draft.schema.json | 52 +++++++ src/Cli/Commands/AddOptions.cs | 6 +- src/Cli/Commands/EntityOptions.cs | 12 +- src/Cli/Commands/UpdateOptions.cs | 6 +- src/Cli/ConfigGenerator.cs | 27 +++- src/Cli/Utils.cs | 50 +++++++ .../EntityMcpOptionsConverterFactory.cs | 127 ++++++++++++++++++ src/Config/ObjectModel/Entity.cs | 8 +- src/Config/ObjectModel/EntityMcpOptions.cs | 62 +++++++++ 9 files changed, 344 insertions(+), 6 deletions(-) create mode 100644 src/Config/Converters/EntityMcpOptionsConverterFactory.cs create mode 100644 src/Config/ObjectModel/EntityMcpOptions.cs diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 80cfd953ad..54db9dbaba 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -961,6 +961,31 @@ "default": 5 } } + }, + "mcp": { + "oneOf": [ + { + "type": "boolean", + "description": "Boolean shorthand: true enables dml-tools, false disables dml-tools." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "dml-tools": { + "type": "boolean", + "description": "Enable MCP DML (Data Manipulation Language) tools for this entity. Allows CRUD operations via MCP.", + "default": true + }, + "custom-tool": { + "type": "boolean", + "description": "Enable MCP custom tool for this entity. Only valid for stored procedures.", + "default": false + } + } + } + ], + "description": "Model Context Protocol (MCP) configuration for this entity. Controls whether the entity is exposed via MCP tools." } }, "if": { @@ -1145,6 +1170,33 @@ ] } } + }, + { + "if": { + "properties": { + "mcp": { + "properties": { + "custom-tool": { + "const": true + } + } + } + }, + "required": ["mcp"] + }, + "then": { + "properties": { + "source": { + "properties": { + "type": { + "const": "stored-procedure" + } + }, + "required": ["type"] + } + }, + "errorMessage": "custom-tool can only be enabled for entities with source type 'stored-procedure'" + } } ] } diff --git a/src/Cli/Commands/AddOptions.cs b/src/Cli/Commands/AddOptions.cs index b7d9fbeb08..e7e378d94b 100644 --- a/src/Cli/Commands/AddOptions.cs +++ b/src/Cli/Commands/AddOptions.cs @@ -43,7 +43,9 @@ public AddOptions( IEnumerable? fieldsAliasCollection, IEnumerable? fieldsDescriptionCollection, IEnumerable? fieldsPrimaryKeyCollection, - string? config + string? mcpDmlTools = null, + string? mcpCustomTool = null, + string? config = null ) : base( entity, @@ -69,6 +71,8 @@ public AddOptions( fieldsAliasCollection, fieldsDescriptionCollection, fieldsPrimaryKeyCollection, + mcpDmlTools, + mcpCustomTool, config ) { diff --git a/src/Cli/Commands/EntityOptions.cs b/src/Cli/Commands/EntityOptions.cs index 7f26816800..700bb051eb 100644 --- a/src/Cli/Commands/EntityOptions.cs +++ b/src/Cli/Commands/EntityOptions.cs @@ -34,7 +34,9 @@ public EntityOptions( IEnumerable? fieldsAliasCollection, IEnumerable? fieldsDescriptionCollection, IEnumerable? fieldsPrimaryKeyCollection, - string? config + string? mcpDmlTools = null, + string? mcpCustomTool = null, + string? config = null ) : base(config) { @@ -61,6 +63,8 @@ public EntityOptions( FieldsAliasCollection = fieldsAliasCollection; FieldsDescriptionCollection = fieldsDescriptionCollection; FieldsPrimaryKeyCollection = fieldsPrimaryKeyCollection; + McpDmlTools = mcpDmlTools; + McpCustomTool = mcpCustomTool; } // Entity is required but we have made required as false to have custom error message (more user friendly), if not provided. @@ -132,5 +136,11 @@ public EntityOptions( [Option("fields.primary-key", Required = false, Separator = ',', HelpText = "Set this field as a primary key.")] public IEnumerable? FieldsPrimaryKeyCollection { get; } + + [Option("mcp.dml-tools", Required = false, HelpText = "Enable MCP DML (Data Manipulation Language) tools for this entity. Allows CRUD operations via MCP.")] + public string? McpDmlTools { get; } + + [Option("mcp.custom-tool", Required = false, HelpText = "Enable MCP custom tool for this entity. Only valid for stored procedures.")] + public string? McpCustomTool { get; } } } diff --git a/src/Cli/Commands/UpdateOptions.cs b/src/Cli/Commands/UpdateOptions.cs index fe1664c5bb..050afa2ddb 100644 --- a/src/Cli/Commands/UpdateOptions.cs +++ b/src/Cli/Commands/UpdateOptions.cs @@ -51,7 +51,9 @@ public UpdateOptions( IEnumerable? fieldsAliasCollection, IEnumerable? fieldsDescriptionCollection, IEnumerable? fieldsPrimaryKeyCollection, - string? config) + string? mcpDmlTools = null, + string? mcpCustomTool = null, + string? config = null) : base(entity, sourceType, sourceParameters, @@ -75,6 +77,8 @@ public UpdateOptions( fieldsAliasCollection, fieldsDescriptionCollection, fieldsPrimaryKeyCollection, + mcpDmlTools, + mcpCustomTool, config) { Source = source; diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 7c35335089..a2802c28c2 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -449,6 +449,18 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt EntityRestOptions restOptions = ConstructRestOptions(options.RestRoute, SupportedRestMethods, initialRuntimeConfig.DataSource.DatabaseType == DatabaseType.CosmosDB_NoSQL); EntityGraphQLOptions graphqlOptions = ConstructGraphQLTypeDetails(options.GraphQLType, graphQLOperationsForStoredProcedures); EntityCacheOptions? cacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); + + if (options.McpDmlTools is not null || options.McpCustomTool is not null) + { + EntityMcpOptions? mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); + if (mcpOptions is null) + { + _logger.LogError("Failed to construct MCP options."); + return false; + } + } + + EntityMcpOptions? mcpOptionsToUse = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); // Create new entity. Entity entity = new( @@ -460,7 +472,8 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt Relationships: null, Mappings: null, Cache: cacheOptions, - Description: string.IsNullOrWhiteSpace(options.Description) ? null : options.Description); + Description: string.IsNullOrWhiteSpace(options.Description) ? null : options.Description, + Mcp: mcpOptionsToUse); // Add entity to existing runtime config. IDictionary entities = new Dictionary(initialRuntimeConfig.Entities.Entities) @@ -1620,6 +1633,15 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig EntityActionPolicy? updatedPolicy = GetPolicyForOperation(options.PolicyRequest, options.PolicyDatabase); EntityActionFields? updatedFields = GetFieldsForOperation(options.FieldsToInclude, options.FieldsToExclude); EntityCacheOptions? updatedCacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); + + // Determine if the entity is or will be a stored procedure + bool isStoredProcedureAfterUpdate = doOptionsRepresentStoredProcedure || (isCurrentEntityStoredProcedure && options.SourceType is null); + EntityMcpOptions? updatedMcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate); + // If MCP options were provided, use them; otherwise keep existing MCP options + if (updatedMcpOptions is null) + { + updatedMcpOptions = entity.Mcp; + } if (!updatedGraphQLDetails.Enabled) { @@ -1857,7 +1879,8 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig Relationships: updatedRelationships, Mappings: updatedMappings, Cache: updatedCacheOptions, - Description: string.IsNullOrWhiteSpace(options.Description) ? entity.Description : options.Description + Description: string.IsNullOrWhiteSpace(options.Description) ? entity.Description : options.Description, + Mcp: updatedMcpOptions ); IDictionary entities = new Dictionary(initialConfig.Entities.Entities) { diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs index 451c330503..5a52d4cb0d 100644 --- a/src/Cli/Utils.cs +++ b/src/Cli/Utils.cs @@ -892,6 +892,56 @@ public static EntityGraphQLOptions ConstructGraphQLTypeDetails(string? graphQL, return cacheOptions with { Enabled = isEnabled, TtlSeconds = ttl, UserProvidedTtlOptions = isCacheTtlUserProvided }; } + /// + /// Constructs the EntityMcpOptions for Add/Update. + /// + /// String value that defines if DML tools are enabled for MCP. + /// String value that defines if custom tool is enabled for MCP. + /// Whether the entity is a stored procedure. + /// EntityMcpOptions if values are provided, null otherwise. + public static EntityMcpOptions? ConstructMcpOptions(string? mcpDmlTools, string? mcpCustomTool, bool isStoredProcedure) + { + if (mcpDmlTools is null && mcpCustomTool is null) + { + return null; + } + + bool? dmlToolsEnabled = null; + bool? customToolEnabled = null; + + // Parse dml-tools option + if (mcpDmlTools is not null) + { + if (!bool.TryParse(mcpDmlTools, out bool dmlValue)) + { + _logger.LogError("Invalid format for --mcp.dml-tools. Accepted values are true/false."); + return null; + } + dmlToolsEnabled = dmlValue; + } + + // Parse custom-tool option + if (mcpCustomTool is not null) + { + if (!bool.TryParse(mcpCustomTool, out bool customValue)) + { + _logger.LogError("Invalid format for --mcp.custom-tool. Accepted values are true/false."); + return null; + } + + // Validate that custom-tool can only be used with stored procedures + if (customValue && !isStoredProcedure) + { + _logger.LogError("--mcp.custom-tool can only be enabled for stored procedures."); + return null; + } + + customToolEnabled = customValue; + } + + return new EntityMcpOptions(customToolEnabled, dmlToolsEnabled); + } + /// /// Check if add/update command has Entity provided. Return false otherwise. /// diff --git a/src/Config/Converters/EntityMcpOptionsConverterFactory.cs b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs new file mode 100644 index 0000000000..25d0c9487f --- /dev/null +++ b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs @@ -0,0 +1,127 @@ +// 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; + +/// +/// Factory for creating EntityMcpOptions converters. +/// +internal class EntityMcpOptionsConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(EntityMcpOptions); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return new EntityMcpOptionsConverter(); + } + + /// + /// Converter for EntityMcpOptions that handles both boolean and object representations. + /// When boolean: true enables dml-tools, false disables dml-tools. + /// When object: can specify individual properties (custom-tool and dml-tools). + /// + private class EntityMcpOptionsConverter : JsonConverter + { + public override EntityMcpOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + // Handle boolean shorthand: true/false + if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False) + { + bool value = reader.GetBoolean(); + // Boolean true means: dml-tools=true, custom-tool=false (default) + // Boolean false means: dml-tools=false, custom-tool=false + return new EntityMcpOptions( + customToolEnabled: false, + dmlToolsEnabled: value + ); + } + + // Handle object representation + if (reader.TokenType == JsonTokenType.StartObject) + { + bool? customToolEnabled = null; + bool? dmlToolsEnabled = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + reader.Read(); // Move to the value + + if (propertyName == "custom-tool") + { + customToolEnabled = reader.TokenType == JsonTokenType.True; + } + else if (propertyName == "dml-tools") + { + dmlToolsEnabled = reader.TokenType == JsonTokenType.True; + } + } + } + + return new EntityMcpOptions(customToolEnabled, dmlToolsEnabled); + } + + throw new JsonException($"Unexpected token type {reader.TokenType} for EntityMcpOptions"); + } + + public override void Write(Utf8JsonWriter writer, EntityMcpOptions value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + // Check if we should write as boolean shorthand + // Write as boolean if: only dml-tools is set (or custom-tool is default false) + bool writeAsBoolean = !value.UserProvidedCustomToolEnabled && value.UserProvidedDmlToolsEnabled; + + if (writeAsBoolean) + { + // Write as boolean shorthand + writer.WriteBooleanValue(value.DmlToolEnabled ?? true); + } + else if (value.UserProvidedCustomToolEnabled || value.UserProvidedDmlToolsEnabled) + { + // Write as object + writer.WriteStartObject(); + + if (value.UserProvidedCustomToolEnabled) + { + writer.WriteBoolean("custom-tool", value.CustomToolEnabled ?? false); + } + + if (value.UserProvidedDmlToolsEnabled) + { + writer.WriteBoolean("dml-tools", value.DmlToolEnabled ?? true); + } + + writer.WriteEndObject(); + } + else + { + // Nothing provided, write null (will be omitted by DefaultIgnoreCondition) + writer.WriteNullValue(); + } + } + } +} diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index c9f247e0f6..1e8c5a6dba 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.HealthCheck; namespace Azure.DataApiBuilder.Config.ObjectModel; @@ -39,6 +40,9 @@ public record Entity public EntityCacheOptions? Cache { get; init; } public EntityHealthCheckConfig? Health { get; init; } + [JsonConverter(typeof(EntityMcpOptionsConverterFactory))] + public EntityMcpOptions? Mcp { get; init; } + [JsonIgnore] public bool IsLinkingEntity { get; init; } @@ -54,7 +58,8 @@ public Entity( EntityCacheOptions? Cache = null, bool IsLinkingEntity = false, EntityHealthCheckConfig? Health = null, - string? Description = null) + string? Description = null, + EntityMcpOptions? Mcp = null) { this.Health = Health; this.Source = Source; @@ -67,6 +72,7 @@ public Entity( this.Cache = Cache; this.IsLinkingEntity = IsLinkingEntity; this.Description = Description; + this.Mcp = Mcp; } /// diff --git a/src/Config/ObjectModel/EntityMcpOptions.cs b/src/Config/ObjectModel/EntityMcpOptions.cs new file mode 100644 index 0000000000..d231e2ab0c --- /dev/null +++ b/src/Config/ObjectModel/EntityMcpOptions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel +{ + /// + /// Options for Model Context Protocol (MCP) tools at the entity level. + /// + public record EntityMcpOptions + { + /// + /// Indicates whether custom tools are enabled for this entity. + /// Only applicable for stored procedures. + /// + [JsonPropertyName("custom-tool")] + public bool? CustomToolEnabled { get; init; } = false; + + /// + /// Indicates whether DML tools are enabled for this entity. + /// + [JsonPropertyName("dml-tools")] + public bool? DmlToolEnabled { get; init; } = true; + + /// + /// Flag which informs CLI and JSON serializer whether to write the CustomToolEnabled + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public bool UserProvidedCustomToolEnabled { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write the DmlToolEnabled + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public bool UserProvidedDmlToolsEnabled { get; init; } = false; + + /// + /// Constructor for EntityMcpOptions + /// + /// The custom tool enabled flag. + /// The DML tools enabled flag. + public EntityMcpOptions(bool? customToolEnabled, bool? dmlToolsEnabled) + { + if (customToolEnabled is not null) + { + this.CustomToolEnabled = customToolEnabled; + this.UserProvidedCustomToolEnabled = true; + } + + if (dmlToolsEnabled is not null) + { + this.DmlToolEnabled = dmlToolsEnabled; + this.UserProvidedDmlToolsEnabled = true; + } + else + { + this.DmlToolEnabled = true; + } + } + } +} From 8bde11e6a10bd983956dd97a2b6b1a774bb850d4 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 4 Dec 2025 17:18:57 +0530 Subject: [PATCH 02/11] Address PR review comments - Update EntityMcpOptions documentation to clarify custom-tool behavior in boolean mode - Replace if-else with switch-case in converter for better extensibility - Remove unnecessary null writes in serializer - Change CustomToolEnabled and DmlToolEnabled from nullable to non-nullable bool - Fix boolean shorthand deserialization to not mark custom-tool as user-provided - Add consistent else block in constructor for symmetry All 530 tests passing. Functionality verified with manual testing. --- schemas/dab.draft.schema.json | 2 +- src/Cli/ConfigGenerator.cs | 7 ++-- src/Cli/Utils.cs | 1 + .../EntityMcpOptionsConverterFactory.cs | 32 +++++++++---------- src/Config/ObjectModel/EntityMcpOptions.cs | 16 ++++------ 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 54db9dbaba..01e3a874f0 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -695,7 +695,7 @@ }, "entities": { "type": "object", - "description": "Entities that will be exposed via REST and/or GraphQL", + "description": "Entities that will be exposed via REST, GraphQL and/or MCP", "patternProperties": { "^.*$": { "type": "object", diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index a2802c28c2..c3d747680c 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -449,7 +449,7 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt EntityRestOptions restOptions = ConstructRestOptions(options.RestRoute, SupportedRestMethods, initialRuntimeConfig.DataSource.DatabaseType == DatabaseType.CosmosDB_NoSQL); EntityGraphQLOptions graphqlOptions = ConstructGraphQLTypeDetails(options.GraphQLType, graphQLOperationsForStoredProcedures); EntityCacheOptions? cacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); - + if (options.McpDmlTools is not null || options.McpCustomTool is not null) { EntityMcpOptions? mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); @@ -459,7 +459,7 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt return false; } } - + EntityMcpOptions? mcpOptionsToUse = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); // Create new entity. @@ -1633,10 +1633,11 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig EntityActionPolicy? updatedPolicy = GetPolicyForOperation(options.PolicyRequest, options.PolicyDatabase); EntityActionFields? updatedFields = GetFieldsForOperation(options.FieldsToInclude, options.FieldsToExclude); EntityCacheOptions? updatedCacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); - + // Determine if the entity is or will be a stored procedure bool isStoredProcedureAfterUpdate = doOptionsRepresentStoredProcedure || (isCurrentEntityStoredProcedure && options.SourceType is null); EntityMcpOptions? updatedMcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate); + // If MCP options were provided, use them; otherwise keep existing MCP options if (updatedMcpOptions is null) { diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs index 5a52d4cb0d..48edd4411c 100644 --- a/src/Cli/Utils.cs +++ b/src/Cli/Utils.cs @@ -917,6 +917,7 @@ public static EntityGraphQLOptions ConstructGraphQLTypeDetails(string? graphQL, _logger.LogError("Invalid format for --mcp.dml-tools. Accepted values are true/false."); return null; } + dmlToolsEnabled = dmlValue; } diff --git a/src/Config/Converters/EntityMcpOptionsConverterFactory.cs b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs index 25d0c9487f..2c4d4d44df 100644 --- a/src/Config/Converters/EntityMcpOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs @@ -24,7 +24,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer /// /// Converter for EntityMcpOptions that handles both boolean and object representations. - /// When boolean: true enables dml-tools, false disables dml-tools. + /// When boolean: true enables dml-tools and custom-tool remains false (default), false disables dml-tools and custom-tool remains false. /// When object: can specify individual properties (custom-tool and dml-tools). /// private class EntityMcpOptionsConverter : JsonConverter @@ -42,8 +42,9 @@ private class EntityMcpOptionsConverter : JsonConverter bool value = reader.GetBoolean(); // Boolean true means: dml-tools=true, custom-tool=false (default) // Boolean false means: dml-tools=false, custom-tool=false + // Pass null for customToolEnabled to keep it as default (not user-provided) return new EntityMcpOptions( - customToolEnabled: false, + customToolEnabled: null, dmlToolsEnabled: value ); } @@ -66,13 +67,16 @@ private class EntityMcpOptionsConverter : JsonConverter string? propertyName = reader.GetString(); reader.Read(); // Move to the value - if (propertyName == "custom-tool") + switch (propertyName) { - customToolEnabled = reader.TokenType == JsonTokenType.True; - } - else if (propertyName == "dml-tools") - { - dmlToolsEnabled = reader.TokenType == JsonTokenType.True; + case "custom-tool": + customToolEnabled = reader.TokenType == JsonTokenType.True; + break; + case "dml-tools": + dmlToolsEnabled = reader.TokenType == JsonTokenType.True; + break; + default: + throw new JsonException($"Unknown property '{propertyName}' in EntityMcpOptions"); } } } @@ -87,7 +91,6 @@ public override void Write(Utf8JsonWriter writer, EntityMcpOptions value, JsonSe { if (value == null) { - writer.WriteNullValue(); return; } @@ -98,7 +101,7 @@ public override void Write(Utf8JsonWriter writer, EntityMcpOptions value, JsonSe if (writeAsBoolean) { // Write as boolean shorthand - writer.WriteBooleanValue(value.DmlToolEnabled ?? true); + writer.WriteBooleanValue(value.DmlToolEnabled); } else if (value.UserProvidedCustomToolEnabled || value.UserProvidedDmlToolsEnabled) { @@ -107,21 +110,16 @@ public override void Write(Utf8JsonWriter writer, EntityMcpOptions value, JsonSe if (value.UserProvidedCustomToolEnabled) { - writer.WriteBoolean("custom-tool", value.CustomToolEnabled ?? false); + writer.WriteBoolean("custom-tool", value.CustomToolEnabled); } if (value.UserProvidedDmlToolsEnabled) { - writer.WriteBoolean("dml-tools", value.DmlToolEnabled ?? true); + writer.WriteBoolean("dml-tools", value.DmlToolEnabled); } writer.WriteEndObject(); } - else - { - // Nothing provided, write null (will be omitted by DefaultIgnoreCondition) - writer.WriteNullValue(); - } } } } diff --git a/src/Config/ObjectModel/EntityMcpOptions.cs b/src/Config/ObjectModel/EntityMcpOptions.cs index d231e2ab0c..b72d1d3a09 100644 --- a/src/Config/ObjectModel/EntityMcpOptions.cs +++ b/src/Config/ObjectModel/EntityMcpOptions.cs @@ -15,13 +15,13 @@ public record EntityMcpOptions /// Only applicable for stored procedures. /// [JsonPropertyName("custom-tool")] - public bool? CustomToolEnabled { get; init; } = false; + public bool CustomToolEnabled { get; init; } = false; /// /// Indicates whether DML tools are enabled for this entity. /// [JsonPropertyName("dml-tools")] - public bool? DmlToolEnabled { get; init; } = true; + public bool DmlToolEnabled { get; init; } = true; /// /// Flag which informs CLI and JSON serializer whether to write the CustomToolEnabled @@ -42,21 +42,17 @@ public record EntityMcpOptions /// The DML tools enabled flag. public EntityMcpOptions(bool? customToolEnabled, bool? dmlToolsEnabled) { - if (customToolEnabled is not null) + if (customToolEnabled.HasValue) { - this.CustomToolEnabled = customToolEnabled; + this.CustomToolEnabled = customToolEnabled.Value; this.UserProvidedCustomToolEnabled = true; } - if (dmlToolsEnabled is not null) + if (dmlToolsEnabled.HasValue) { - this.DmlToolEnabled = dmlToolsEnabled; + this.DmlToolEnabled = dmlToolsEnabled.Value; this.UserProvidedDmlToolsEnabled = true; } - else - { - this.DmlToolEnabled = true; - } } } } From c186dc280c0fbeb16a50bb13a3435fb04ba78784 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 5 Dec 2025 10:11:56 +0530 Subject: [PATCH 03/11] Validate MCP options if provided --- src/Cli/ConfigGenerator.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index c3d747680c..957c7903b7 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -1636,6 +1636,19 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig // Determine if the entity is or will be a stored procedure bool isStoredProcedureAfterUpdate = doOptionsRepresentStoredProcedure || (isCurrentEntityStoredProcedure && options.SourceType is null); + + // Validate MCP options if provided + if (options.McpDmlTools is not null || options.McpCustomTool is not null) + { + EntityMcpOptions? mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate); + + if (mcpOptions is null) + { + _logger.LogError("Failed to construct MCP options."); + return false; + } + } + EntityMcpOptions? updatedMcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate); // If MCP options were provided, use them; otherwise keep existing MCP options From 0514f58627d52749e1863a913012a0bb63bfb9af Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 5 Dec 2025 15:02:37 +0530 Subject: [PATCH 04/11] Copilot review fixes --- src/Cli/ConfigGenerator.cs | 22 ++++++++----------- .../EntityMcpOptionsConverterFactory.cs | 4 ++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 957c7903b7..f8fdd66560 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -450,18 +450,17 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt EntityGraphQLOptions graphqlOptions = ConstructGraphQLTypeDetails(options.GraphQLType, graphQLOperationsForStoredProcedures); EntityCacheOptions? cacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); + EntityMcpOptions? mcpOptionsToUse = null; if (options.McpDmlTools is not null || options.McpCustomTool is not null) { - EntityMcpOptions? mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); - if (mcpOptions is null) + mcpOptionsToUse = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); + if (mcpOptionsToUse is null) { _logger.LogError("Failed to construct MCP options."); return false; } } - EntityMcpOptions? mcpOptionsToUse = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); - // Create new entity. Entity entity = new( Source: source, @@ -1637,23 +1636,20 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig // Determine if the entity is or will be a stored procedure bool isStoredProcedureAfterUpdate = doOptionsRepresentStoredProcedure || (isCurrentEntityStoredProcedure && options.SourceType is null); - // Validate MCP options if provided + // Construct and validate MCP options if provided + EntityMcpOptions? updatedMcpOptions = null; if (options.McpDmlTools is not null || options.McpCustomTool is not null) { - EntityMcpOptions? mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate); - - if (mcpOptions is null) + updatedMcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate); + if (updatedMcpOptions is null) { _logger.LogError("Failed to construct MCP options."); return false; } } - - EntityMcpOptions? updatedMcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate); - - // If MCP options were provided, use them; otherwise keep existing MCP options - if (updatedMcpOptions is null) + else { + // Keep existing MCP options if no updates provided updatedMcpOptions = entity.Mcp; } diff --git a/src/Config/Converters/EntityMcpOptionsConverterFactory.cs b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs index 2c4d4d44df..1cab0b9cc9 100644 --- a/src/Config/Converters/EntityMcpOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs @@ -70,10 +70,10 @@ private class EntityMcpOptionsConverter : JsonConverter switch (propertyName) { case "custom-tool": - customToolEnabled = reader.TokenType == JsonTokenType.True; + customToolEnabled = reader.GetBoolean(); break; case "dml-tools": - dmlToolsEnabled = reader.TokenType == JsonTokenType.True; + dmlToolsEnabled = reader.GetBoolean(); break; default: throw new JsonException($"Unknown property '{propertyName}' in EntityMcpOptions"); From ac12ece9efe3c3fc3fc0e7db4156e765af9350d6 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 8 Dec 2025 14:58:21 +0530 Subject: [PATCH 05/11] Added tests for Entity MCP config --- src/Cli.Tests/AddEntityTests.cs | 288 +++++++++++ ...rocedureWithBothMcpProperties.verified.txt | 63 +++ ...eWithBothMcpPropertiesEnabled.verified.txt | 63 +++ ...edureWithMcpCustomToolEnabled.verified.txt | 63 +++ ...EntityWithMcpDmlToolsDisabled.verified.txt | 59 +++ ...eEntityWithMcpDmlToolsEnabled.verified.txt | 59 +++ ...rocedureWithBothMcpProperties.verified.txt | 66 +++ ...eWithBothMcpPropertiesEnabled.verified.txt | 66 +++ ...edureWithMcpCustomToolEnabled.verified.txt | 66 +++ ...EntityWithMcpDmlToolsDisabled.verified.txt | 63 +++ ...eEntityWithMcpDmlToolsEnabled.verified.txt | 63 +++ src/Cli.Tests/UpdateEntityTests.cs | 464 +++++++++++++++++- 12 files changed, 1381 insertions(+), 2 deletions(-) create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsDisabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsDisabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsEnabled.verified.txt diff --git a/src/Cli.Tests/AddEntityTests.cs b/src/Cli.Tests/AddEntityTests.cs index 9386916f7f..f4836a1cca 100644 --- a/src/Cli.Tests/AddEntityTests.cs +++ b/src/Cli.Tests/AddEntityTests.cs @@ -633,5 +633,293 @@ private Task ExecuteVerifyTest(AddOptions options, string config = INITIAL_CONFI return Verify(updatedRuntimeConfig, settings); } + + #region MCP Entity Configuration Tests + + /// + /// Test adding table entity with MCP dml-tools enabled (should serialize as boolean true) + /// + [TestMethod] + public Task AddTableEntityWithMcpDmlToolsEnabled() + { + AddOptions options = new( + source: "books", + permissions: new string[] { "anonymous", "*" }, + entity: "Book", + description: null, + sourceType: "table", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: "true", + mcpCustomTool: null + ); + return ExecuteVerifyTest(options); + } + + /// + /// Test adding table entity with MCP dml-tools disabled (should serialize as boolean false) + /// + [TestMethod] + public Task AddTableEntityWithMcpDmlToolsDisabled() + { + AddOptions options = new( + source: "authors", + permissions: new string[] { "anonymous", "*" }, + entity: "Author", + description: null, + sourceType: "table", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: "false", + mcpCustomTool: null + ); + return ExecuteVerifyTest(options); + } + + /// + /// Test adding stored procedure with MCP custom-tool enabled (should serialize as object) + /// + [TestMethod] + public Task AddStoredProcedureWithMcpCustomToolEnabled() + { + AddOptions options = new( + source: "dbo.GetBookById", + permissions: new string[] { "anonymous", "execute" }, + entity: "GetBookById", + description: null, + sourceType: "stored-procedure", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: null, + mcpCustomTool: "true" + ); + return ExecuteVerifyTest(options); + } + + /// + /// Test adding stored procedure with both MCP properties set to different values (should serialize as object with both) + /// + [TestMethod] + public Task AddStoredProcedureWithBothMcpProperties() + { + AddOptions options = new( + source: "dbo.UpdateBook", + permissions: new string[] { "anonymous", "execute" }, + entity: "UpdateBook", + description: null, + sourceType: "stored-procedure", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: "false", + mcpCustomTool: "true" + ); + return ExecuteVerifyTest(options); + } + + /// + /// Test adding stored procedure with both MCP properties enabled (common use case) + /// + [TestMethod] + public Task AddStoredProcedureWithBothMcpPropertiesEnabled() + { + AddOptions options = new( + source: "dbo.GetAllBooks", + permissions: new string[] { "anonymous", "execute" }, + entity: "GetAllBooks", + description: null, + sourceType: "stored-procedure", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: "true", + mcpCustomTool: "true" + ); + return ExecuteVerifyTest(options); + } + + /// + /// Test that adding table entity with custom-tool fails validation + /// + [TestMethod] + public void AddTableEntityWithInvalidMcpCustomTool() + { + AddOptions options = new( + source: "reviews", + permissions: new string[] { "anonymous", "*" }, + entity: "Review", + description: null, + sourceType: "table", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: null, + mcpCustomTool: "true" + ); + + RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig); + + Assert.IsFalse(TryAddNewEntity(options, runtimeConfig!, out RuntimeConfig _), + "Should fail to add table entity with custom-tool enabled"); + } + + /// + /// Test that invalid MCP option value fails + /// + [DataTestMethod] + [DataRow("invalid", null, DisplayName = "Invalid dml-tools value")] + [DataRow(null, "invalid", DisplayName = "Invalid custom-tool value")] + [DataRow("yes", "no", DisplayName = "Invalid boolean-like values")] + public void AddEntityWithInvalidMcpOptions(string? mcpDmlTools, string? mcpCustomTool) + { + AddOptions options = new( + source: "MyTable", + permissions: new string[] { "anonymous", "*" }, + entity: "MyEntity", + description: null, + sourceType: "table", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: mcpDmlTools, + mcpCustomTool: mcpCustomTool + ); + + RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig); + + Assert.IsFalse(TryAddNewEntity(options, runtimeConfig!, out RuntimeConfig _), + "Should fail with invalid MCP option values"); + } + + #endregion MCP Entity Configuration Tests } } diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt new file mode 100644 index 0000000000..38dfae6840 --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt @@ -0,0 +1,63 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [ + { + UpdateBook: { + Source: { + Object: dbo.UpdateBook, + Type: stored-procedure + }, + GraphQL: { + Singular: UpdateBook, + Plural: UpdateBooks, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: false, + UserProvidedCustomToolEnabled: true, + UserProvidedDmlToolsEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt new file mode 100644 index 0000000000..f87a181a24 --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt @@ -0,0 +1,63 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [ + { + GetAllBooks: { + Source: { + Object: dbo.GetAllBooks, + Type: stored-procedure + }, + GraphQL: { + Singular: GetAllBooks, + Plural: GetAllBooks, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: true, + UserProvidedCustomToolEnabled: true, + UserProvidedDmlToolsEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt new file mode 100644 index 0000000000..0b81ce23b1 --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -0,0 +1,63 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [ + { + GetBookById: { + Source: { + Object: dbo.GetBookById, + Type: stored-procedure + }, + GraphQL: { + Singular: GetBookById, + Plural: GetBookByIds, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: true, + UserProvidedCustomToolEnabled: true, + UserProvidedDmlToolsEnabled: false + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsDisabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsDisabled.verified.txt new file mode 100644 index 0000000000..384fdbb80c --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsDisabled.verified.txt @@ -0,0 +1,59 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [ + { + Author: { + Source: { + Object: authors, + Type: Table + }, + GraphQL: { + Singular: Author, + Plural: Authors, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Mcp: { + CustomToolEnabled: false, + DmlToolEnabled: false, + UserProvidedCustomToolEnabled: false, + UserProvidedDmlToolsEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsEnabled.verified.txt new file mode 100644 index 0000000000..e08eb2e4b3 --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsEnabled.verified.txt @@ -0,0 +1,59 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps + } + } + }, + Entities: [ + { + Book: { + Source: { + Object: books, + Type: Table + }, + GraphQL: { + Singular: Book, + Plural: Books, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Mcp: { + CustomToolEnabled: false, + DmlToolEnabled: true, + UserProvidedCustomToolEnabled: false, + UserProvidedDmlToolsEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt new file mode 100644 index 0000000000..e784664b66 --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt @@ -0,0 +1,66 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + UpdateBook: { + Source: { + Type: stored-procedure + }, + GraphQL: { + Singular: UpdateBook, + Plural: UpdateBooks, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: false, + UserProvidedCustomToolEnabled: true, + UserProvidedDmlToolsEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt new file mode 100644 index 0000000000..7c9fb41700 --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt @@ -0,0 +1,66 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + GetAllBooks: { + Source: { + Type: stored-procedure + }, + GraphQL: { + Singular: GetAllBooks, + Plural: GetAllBooks, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: true, + UserProvidedCustomToolEnabled: true, + UserProvidedDmlToolsEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt new file mode 100644 index 0000000000..ad87c99f15 --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -0,0 +1,66 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + GetBookById: { + Source: { + Type: stored-procedure + }, + GraphQL: { + Singular: GetBookById, + Plural: GetBookByIds, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: true, + UserProvidedCustomToolEnabled: true, + UserProvidedDmlToolsEnabled: false + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsDisabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsDisabled.verified.txt new file mode 100644 index 0000000000..f22dee731f --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsDisabled.verified.txt @@ -0,0 +1,63 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + MyEntity: { + Source: { + Object: MyTable, + Type: Table + }, + GraphQL: { + Singular: MyEntity, + Plural: MyEntities, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Mcp: { + CustomToolEnabled: false, + DmlToolEnabled: false, + UserProvidedCustomToolEnabled: false, + UserProvidedDmlToolsEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsEnabled.verified.txt new file mode 100644 index 0000000000..9c3eca020a --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsEnabled.verified.txt @@ -0,0 +1,63 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: StaticWebApps, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + MyEntity: { + Source: { + Object: MyTable, + Type: Table + }, + GraphQL: { + Singular: MyEntity, + Plural: MyEntities, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Mcp: { + CustomToolEnabled: false, + DmlToolEnabled: true, + UserProvidedCustomToolEnabled: false, + UserProvidedDmlToolsEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/UpdateEntityTests.cs b/src/Cli.Tests/UpdateEntityTests.cs index 3a106c0adc..9c6b81536a 100644 --- a/src/Cli.Tests/UpdateEntityTests.cs +++ b/src/Cli.Tests/UpdateEntityTests.cs @@ -1160,7 +1160,9 @@ private static UpdateOptions GenerateBaseUpdateOptions( string? graphQLOperationForStoredProcedure = null, string? cacheEnabled = null, string? cacheTtl = null, - string? description = null + string? description = null, + string? mcpDmlTools = null, + string? mcpCustomTool = null ) { return new( @@ -1197,7 +1199,9 @@ private static UpdateOptions GenerateBaseUpdateOptions( fieldsNameCollection: null, fieldsAliasCollection: null, fieldsDescriptionCollection: null, - fieldsPrimaryKeyCollection: null + fieldsPrimaryKeyCollection: null, + mcpDmlTools: mcpDmlTools, + mcpCustomTool: mcpCustomTool ); } @@ -1211,5 +1215,461 @@ private Task ExecuteVerifyTest(string initialConfig, UpdateOptions options, Veri return Verify(updatedRuntimeConfig, settings); } + + #region MCP Entity Configuration Tests + + /// + /// Test updating table entity with MCP dml-tools enabled + /// + [TestMethod] + public Task TestUpdateTableEntityWithMcpDmlToolsEnabled() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "MyEntity", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: "true", + mcpCustomTool: null + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""MyEntity"": { + ""source"": ""MyTable"", + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""*""] + } + ] + } + } + }"; + + return ExecuteVerifyTest(initialConfig, options); + } + + /// + /// Test updating table entity with MCP dml-tools disabled + /// + [TestMethod] + public Task TestUpdateTableEntityWithMcpDmlToolsDisabled() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "MyEntity", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: "false", + mcpCustomTool: null + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""MyEntity"": { + ""source"": ""MyTable"", + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""*""] + } + ] + } + } + }"; + + return ExecuteVerifyTest(initialConfig, options); + } + + /// + /// Test updating stored procedure with MCP custom-tool enabled + /// + [TestMethod] + public Task TestUpdateStoredProcedureWithMcpCustomToolEnabled() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "GetBookById", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: null, + mcpCustomTool: "true" + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""GetBookById"": { + ""source"": ""dbo.GetBookById"", + ""source"": { + ""type"": ""stored-procedure"" + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""execute""] + } + ] + } + } + }"; + + return ExecuteVerifyTest(initialConfig, options); + } + + /// + /// Test updating stored procedure with both MCP properties + /// + [TestMethod] + public Task TestUpdateStoredProcedureWithBothMcpProperties() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "UpdateBook", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: "false", + mcpCustomTool: "true" + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""UpdateBook"": { + ""source"": ""dbo.UpdateBook"", + ""source"": { + ""type"": ""stored-procedure"" + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""execute""] + } + ] + } + } + }"; + + return ExecuteVerifyTest(initialConfig, options); + } + + /// + /// Test updating stored procedure with both MCP properties enabled + /// + [TestMethod] + public Task TestUpdateStoredProcedureWithBothMcpPropertiesEnabled() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "GetAllBooks", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: "true", + mcpCustomTool: "true" + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""GetAllBooks"": { + ""source"": ""dbo.GetAllBooks"", + ""source"": { + ""type"": ""stored-procedure"" + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""execute""] + } + ] + } + } + }"; + + return ExecuteVerifyTest(initialConfig, options); + } + + /// + /// Test that updating table entity with custom-tool fails validation + /// + [TestMethod] + public void TestUpdateTableEntityWithInvalidMcpCustomTool() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "MyEntity", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: null, + mcpCustomTool: "true" + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""MyEntity"": { + ""source"": ""MyTable"", + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""*""] + } + ] + } + } + }"; + + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(initialConfig, out RuntimeConfig? runtimeConfig)); + + Assert.IsFalse(TryUpdateExistingEntity(options, runtimeConfig!, out RuntimeConfig _), + "Should fail to update table entity with custom-tool enabled"); + } + + /// + /// Test that invalid MCP option value fails + /// + [DataTestMethod] + [DataRow("invalid", null, DisplayName = "Invalid dml-tools value")] + [DataRow(null, "invalid", DisplayName = "Invalid custom-tool value")] + [DataRow("yes", "no", DisplayName = "Invalid boolean-like values")] + public void TestUpdateEntityWithInvalidMcpOptions(string? mcpDmlTools, string? mcpCustomTool) + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "MyEntity", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: mcpDmlTools, + mcpCustomTool: mcpCustomTool + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""MyEntity"": { + ""source"": ""MyTable"", + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""*""] + } + ] + } + } + }"; + + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(initialConfig, out RuntimeConfig? runtimeConfig)); + + Assert.IsFalse(TryUpdateExistingEntity(options, runtimeConfig!, out RuntimeConfig _), + $"Should fail to update entity with invalid MCP options: dml-tools={mcpDmlTools}, custom-tool={mcpCustomTool}"); + } + + #endregion MCP Entity Configuration Tests } } From f30ef6fd9c327b276879cc7e65f84a73ed281619 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 8 Dec 2025 15:50:29 +0530 Subject: [PATCH 06/11] Added additional tests and fixes --- ...edureWithMcpCustomToolEnabled.verified.txt | 2 +- ...edureWithMcpCustomToolEnabled.verified.txt | 2 +- src/Config/ObjectModel/EntityMcpOptions.cs | 3 +- .../EntityMcpConfigurationTests.cs | 504 ++++++++++++++++++ 4 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt index 0b81ce23b1..81628df772 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -53,7 +53,7 @@ ], Mcp: { CustomToolEnabled: true, - DmlToolEnabled: true, + DmlToolEnabled: false, UserProvidedCustomToolEnabled: true, UserProvidedDmlToolsEnabled: false } diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt index ad87c99f15..7461ea4ae5 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -56,7 +56,7 @@ ], Mcp: { CustomToolEnabled: true, - DmlToolEnabled: true, + DmlToolEnabled: false, UserProvidedCustomToolEnabled: true, UserProvidedDmlToolsEnabled: false } diff --git a/src/Config/ObjectModel/EntityMcpOptions.cs b/src/Config/ObjectModel/EntityMcpOptions.cs index b72d1d3a09..5485926e39 100644 --- a/src/Config/ObjectModel/EntityMcpOptions.cs +++ b/src/Config/ObjectModel/EntityMcpOptions.cs @@ -19,9 +19,10 @@ public record EntityMcpOptions /// /// Indicates whether DML tools are enabled for this entity. + /// Defaults to false when not explicitly provided. /// [JsonPropertyName("dml-tools")] - public bool DmlToolEnabled { get; init; } = true; + public bool DmlToolEnabled { get; init; } = false; /// /// Flag which informs CLI and JSON serializer whether to write the CustomToolEnabled diff --git a/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs new file mode 100644 index 0000000000..ea6f5f9e34 --- /dev/null +++ b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs @@ -0,0 +1,504 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.Configuration +{ + /// + /// Tests for entity-level MCP configuration deserialization and validation. + /// Validates that EntityMcpOptions are correctly deserialized from runtime config JSON. + /// + [TestClass] + public class EntityMcpConfigurationTests + { + /// + /// Test that deserializing boolean 'true' shorthand correctly sets dml-tools enabled. + /// + [TestMethod] + public void DeserializeConfig_McpBooleanTrue_EnablesDmlToolsOnly() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": true + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + + Entity bookEntity = runtimeConfig.Entities["Book"]; + Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); + Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be enabled"); + Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled (default)"); + } + + /// + /// Test that deserializing boolean 'false' shorthand correctly sets dml-tools disabled. + /// + [TestMethod] + public void DeserializeConfig_McpBooleanFalse_DisablesDmlTools() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": false + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + + Entity bookEntity = runtimeConfig.Entities["Book"]; + Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); + Assert.IsFalse(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled"); + Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled (default)"); + } + + /// + /// Test that deserializing object format with both properties works correctly. + /// + [TestMethod] + public void DeserializeConfig_McpObject_SetsBothProperties() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true, + ""dml-tools"": false + } + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + + Entity spEntity = runtimeConfig.Entities["GetBook"]; + Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); + Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); + Assert.IsFalse(spEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled"); + } + + /// + /// Test that deserializing object format with only dml-tools works. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithDmlToolsOnly_WorksCorrectly() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": { + ""dml-tools"": true + } + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + + Entity bookEntity = runtimeConfig.Entities["Book"]; + Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); + Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be enabled"); + Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled (default)"); + } + + /// + /// Test that entity without MCP configuration has null MCP options. + /// + [TestMethod] + public void DeserializeConfig_NoMcp_HasNullMcpOptions() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }] + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + + Entity bookEntity = runtimeConfig.Entities["Book"]; + Assert.IsNull(bookEntity.Mcp, "MCP options should be null when not specified"); + } + + /// + /// Test that deserializing object format with both properties set to true works correctly. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithBothTrue_SetsCorrectly() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true, + ""dml-tools"": true + } + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + + Entity spEntity = runtimeConfig.Entities["GetBook"]; + Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); + Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); + Assert.IsTrue(spEntity.Mcp.DmlToolEnabled, "DmlTools should be enabled"); + } + + /// + /// Test that deserializing object format with both properties set to false works correctly. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithBothFalse_SetsCorrectly() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": false, + ""dml-tools"": false + } + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + + Entity spEntity = runtimeConfig.Entities["GetBook"]; + Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); + Assert.IsFalse(spEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled"); + Assert.IsFalse(spEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled"); + } + + /// + /// Test that deserializing object format with only custom-tool works. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithCustomToolOnly_WorksCorrectly() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true + } + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + + Entity spEntity = runtimeConfig.Entities["GetBook"]; + Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); + Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); + Assert.IsFalse(spEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled (default is false)"); + } + + /// + /// Test that deserializing config with multiple entities having different MCP settings works. + /// + [TestMethod] + public void DeserializeConfig_MultipleEntitiesWithDifferentMcpSettings_WorksCorrectly() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": true + }, + ""Author"": { + ""source"": ""authors"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": false + }, + ""Publisher"": { + ""source"": ""publishers"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }] + }, + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true, + ""dml-tools"": false + } + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + + // Book: mcp = true + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + Entity bookEntity = runtimeConfig.Entities["Book"]; + Assert.IsNotNull(bookEntity.Mcp); + Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled); + Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled); + + // Author: mcp = false + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Author")); + Entity authorEntity = runtimeConfig.Entities["Author"]; + Assert.IsNotNull(authorEntity.Mcp); + Assert.IsFalse(authorEntity.Mcp.DmlToolEnabled); + Assert.IsFalse(authorEntity.Mcp.CustomToolEnabled); + + // Publisher: no mcp + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Publisher")); + Entity publisherEntity = runtimeConfig.Entities["Publisher"]; + Assert.IsNull(publisherEntity.Mcp); + + // GetBook: mcp object + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + Entity spEntity = runtimeConfig.Entities["GetBook"]; + Assert.IsNotNull(spEntity.Mcp); + Assert.IsTrue(spEntity.Mcp.CustomToolEnabled); + Assert.IsFalse(spEntity.Mcp.DmlToolEnabled); + } + + /// + /// Test that deserializing invalid MCP value (non-boolean, non-object) fails gracefully. + /// + [TestMethod] + public void DeserializeConfig_InvalidMcpValue_FailsGracefully() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": ""invalid"" + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsFalse(success, "Config parsing should fail with invalid MCP value"); + } + + /// + /// Test that deserializing MCP object with unknown property fails gracefully. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithUnknownProperty_FailsGracefully() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": { + ""dml-tools"": true, + ""unknown-property"": true + } + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsFalse(success, "Config parsing should fail with unknown MCP property"); + } + } +} From f0329637f5d489415472c0278449bc8c9105a4d0 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 8 Dec 2025 16:23:44 +0530 Subject: [PATCH 07/11] Fix formattings --- .../EntityMcpConfigurationTests.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs index ea6f5f9e34..cf0edc4ece 100644 --- a/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs +++ b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs @@ -48,7 +48,7 @@ public void DeserializeConfig_McpBooleanTrue_EnablesDmlToolsOnly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - + Entity bookEntity = runtimeConfig.Entities["Book"]; Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be enabled"); @@ -89,7 +89,7 @@ public void DeserializeConfig_McpBooleanFalse_DisablesDmlTools() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - + Entity bookEntity = runtimeConfig.Entities["Book"]; Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); Assert.IsFalse(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled"); @@ -133,7 +133,7 @@ public void DeserializeConfig_McpObject_SetsBothProperties() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - + Entity spEntity = runtimeConfig.Entities["GetBook"]; Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); @@ -176,7 +176,7 @@ public void DeserializeConfig_McpObjectWithDmlToolsOnly_WorksCorrectly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - + Entity bookEntity = runtimeConfig.Entities["Book"]; Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be enabled"); @@ -216,7 +216,7 @@ public void DeserializeConfig_NoMcp_HasNullMcpOptions() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - + Entity bookEntity = runtimeConfig.Entities["Book"]; Assert.IsNull(bookEntity.Mcp, "MCP options should be null when not specified"); } @@ -258,7 +258,7 @@ public void DeserializeConfig_McpObjectWithBothTrue_SetsCorrectly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - + Entity spEntity = runtimeConfig.Entities["GetBook"]; Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); @@ -302,7 +302,7 @@ public void DeserializeConfig_McpObjectWithBothFalse_SetsCorrectly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - + Entity spEntity = runtimeConfig.Entities["GetBook"]; Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); Assert.IsFalse(spEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled"); @@ -345,7 +345,7 @@ public void DeserializeConfig_McpObjectWithCustomToolOnly_WorksCorrectly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - + Entity spEntity = runtimeConfig.Entities["GetBook"]; Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); @@ -402,26 +402,26 @@ public void DeserializeConfig_MultipleEntitiesWithDifferentMcpSettings_WorksCorr // Assert Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); - + // Book: mcp = true Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); Entity bookEntity = runtimeConfig.Entities["Book"]; Assert.IsNotNull(bookEntity.Mcp); Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled); Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled); - + // Author: mcp = false Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Author")); Entity authorEntity = runtimeConfig.Entities["Author"]; Assert.IsNotNull(authorEntity.Mcp); Assert.IsFalse(authorEntity.Mcp.DmlToolEnabled); Assert.IsFalse(authorEntity.Mcp.CustomToolEnabled); - + // Publisher: no mcp Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Publisher")); Entity publisherEntity = runtimeConfig.Entities["Publisher"]; Assert.IsNull(publisherEntity.Mcp); - + // GetBook: mcp object Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); Entity spEntity = runtimeConfig.Entities["GetBook"]; @@ -458,7 +458,7 @@ public void DeserializeConfig_InvalidMcpValue_FailsGracefully() }"; // Act - bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + bool success = RuntimeConfigLoader.TryParseConfig(config, out _); // Assert Assert.IsFalse(success, "Config parsing should fail with invalid MCP value"); @@ -495,7 +495,7 @@ public void DeserializeConfig_McpObjectWithUnknownProperty_FailsGracefully() }"; // Act - bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + bool success = RuntimeConfigLoader.TryParseConfig(config, out _); // Assert Assert.IsFalse(success, "Config parsing should fail with unknown MCP property"); From dc286961430c1a5a2ae6c6006ffac21499001994 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 11 Dec 2025 18:44:34 +0530 Subject: [PATCH 08/11] nits and suggesstions --- schemas/dab.draft.schema.json | 4 ++-- src/Cli/ConfigGenerator.cs | 11 ++++++----- .../Converters/EntityMcpOptionsConverterFactory.cs | 4 +--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 01e3a874f0..61ab7474b7 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -966,7 +966,7 @@ "oneOf": [ { "type": "boolean", - "description": "Boolean shorthand: true enables dml-tools, false disables dml-tools." + "description": "Boolean shorthand: true enables dml-tools only (custom-tool remains false), false disables all MCP functionality." }, { "type": "object", @@ -1195,7 +1195,7 @@ "required": ["type"] } }, - "errorMessage": "custom-tool can only be enabled for entities with source type 'stored-procedure'" + "errorMessage": "custom-tool can only be enabled for entities with source type 'stored-procedure'." } } ] diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index cd532a9552..1031c5968f 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -449,12 +449,13 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt EntityRestOptions restOptions = ConstructRestOptions(options.RestRoute, SupportedRestMethods, initialRuntimeConfig.DataSource.DatabaseType == DatabaseType.CosmosDB_NoSQL); EntityGraphQLOptions graphqlOptions = ConstructGraphQLTypeDetails(options.GraphQLType, graphQLOperationsForStoredProcedures); EntityCacheOptions? cacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); - - EntityMcpOptions? mcpOptionsToUse = null; + EntityMcpOptions? mcpOptions = null; + if (options.McpDmlTools is not null || options.McpCustomTool is not null) { - mcpOptionsToUse = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); - if (mcpOptionsToUse is null) + mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); + + if (mcpOptions is null) { _logger.LogError("Failed to construct MCP options."); return false; @@ -472,7 +473,7 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt Mappings: null, Cache: cacheOptions, Description: string.IsNullOrWhiteSpace(options.Description) ? null : options.Description, - Mcp: mcpOptionsToUse); + Mcp: mcpOptions); // Add entity to existing runtime config. IDictionary entities = new Dictionary(initialRuntimeConfig.Entities.Entities) diff --git a/src/Config/Converters/EntityMcpOptionsConverterFactory.cs b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs index 1cab0b9cc9..b4ad0e9170 100644 --- a/src/Config/Converters/EntityMcpOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs @@ -59,7 +59,7 @@ private class EntityMcpOptionsConverter : JsonConverter { if (reader.TokenType == JsonTokenType.EndObject) { - break; + return new EntityMcpOptions(customToolEnabled, dmlToolsEnabled); } if (reader.TokenType == JsonTokenType.PropertyName) @@ -80,8 +80,6 @@ private class EntityMcpOptionsConverter : JsonConverter } } } - - return new EntityMcpOptions(customToolEnabled, dmlToolsEnabled); } throw new JsonException($"Unexpected token type {reader.TokenType} for EntityMcpOptions"); From 7e764d22cb63eefc4bafbb2d86bc5b5ec6641827 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 11 Dec 2025 19:38:06 +0530 Subject: [PATCH 09/11] fix formatting --- src/Cli/ConfigGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 1031c5968f..8fcc7cae31 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -450,11 +450,11 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt EntityGraphQLOptions graphqlOptions = ConstructGraphQLTypeDetails(options.GraphQLType, graphQLOperationsForStoredProcedures); EntityCacheOptions? cacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); EntityMcpOptions? mcpOptions = null; - + if (options.McpDmlTools is not null || options.McpCustomTool is not null) { mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); - + if (mcpOptions is null) { _logger.LogError("Failed to construct MCP options."); From 8252a85b304f5e2b47ae31d85345697c78e30c66 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 15 Dec 2025 12:46:51 +0530 Subject: [PATCH 10/11] Refactor MCP properties in entity configurations - Updated MCP DML tools default value to true in EntityMcpOptions. - Simplified MCP properties in various test snapshots by removing unnecessary fields. - Consolidated test methods for updating table entities with MCP DML tools into a single parameterized test. - Removed obsolete test snapshots related to MCP DML tools. - Enhanced MCP configuration tests to assert default values and behavior more effectively. - Updated CLI command options help text to reflect new default values for MCP properties. --- src/Cli.Tests/AddEntityTests.cs | 56 +-- src/Cli.Tests/ModuleInitializer.cs | 4 + ...rocedureWithBothMcpProperties.verified.txt | 4 +- ...eWithBothMcpPropertiesEnabled.verified.txt | 4 +- ...edureWithMcpCustomToolEnabled.verified.txt | 4 +- ...mlTools=false_source=authors.verified.txt} | 4 +- ...cpDmlTools=true_source=books.verified.txt} | 4 +- ...rocedureWithBothMcpProperties.verified.txt | 4 +- ...eWithBothMcpPropertiesEnabled.verified.txt | 4 +- ...edureWithMcpCustomToolEnabled.verified.txt | 4 +- ...mlTools_newMcpDmlTools=false.verified.txt} | 4 +- ...DmlTools_newMcpDmlTools=true.verified.txt} | 4 +- src/Cli.Tests/UpdateEntityTests.cs | 102 ++--- src/Cli/Commands/EntityOptions.cs | 4 +- src/Config/ObjectModel/EntityMcpOptions.cs | 4 +- src/Config/RuntimeConfigLoader.cs | 1 + .../EntityMcpConfigurationTests.cs | 371 ++++++------------ 17 files changed, 188 insertions(+), 394 deletions(-) rename src/Cli.Tests/Snapshots/{AddEntityTests.AddTableEntityWithMcpDmlToolsDisabled.verified.txt => AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt} (88%) rename src/Cli.Tests/Snapshots/{AddEntityTests.AddTableEntityWithMcpDmlToolsEnabled.verified.txt => AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt} (88%) rename src/Cli.Tests/Snapshots/{UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsEnabled.verified.txt => UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt} (88%) rename src/Cli.Tests/Snapshots/{UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsDisabled.verified.txt => UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt} (88%) diff --git a/src/Cli.Tests/AddEntityTests.cs b/src/Cli.Tests/AddEntityTests.cs index f4836a1cca..e96d131880 100644 --- a/src/Cli.Tests/AddEntityTests.cs +++ b/src/Cli.Tests/AddEntityTests.cs @@ -637,15 +637,17 @@ private Task ExecuteVerifyTest(AddOptions options, string config = INITIAL_CONFI #region MCP Entity Configuration Tests /// - /// Test adding table entity with MCP dml-tools enabled (should serialize as boolean true) + /// Test adding table entity with MCP dml-tools enabled or disabled /// - [TestMethod] - public Task AddTableEntityWithMcpDmlToolsEnabled() + [DataTestMethod] + [DataRow("true", "books", "Book", DisplayName = "AddTableEntityWithMcpDmlToolsEnabled")] + [DataRow("false", "authors", "Author", DisplayName = "AddTableEntityWithMcpDmlToolsDisabled")] + public Task AddTableEntityWithMcpDmlTools(string mcpDmlTools, string source, string entity) { AddOptions options = new( - source: "books", + source: source, permissions: new string[] { "anonymous", "*" }, - entity: "Book", + entity: entity, description: null, sourceType: "table", sourceParameters: null, @@ -669,49 +671,13 @@ public Task AddTableEntityWithMcpDmlToolsEnabled() fieldsAliasCollection: [], fieldsDescriptionCollection: [], fieldsPrimaryKeyCollection: [], - mcpDmlTools: "true", + mcpDmlTools: mcpDmlTools, mcpCustomTool: null ); - return ExecuteVerifyTest(options); - } - /// - /// Test adding table entity with MCP dml-tools disabled (should serialize as boolean false) - /// - [TestMethod] - public Task AddTableEntityWithMcpDmlToolsDisabled() - { - AddOptions options = new( - source: "authors", - permissions: new string[] { "anonymous", "*" }, - entity: "Author", - description: null, - sourceType: "table", - sourceParameters: null, - sourceKeyFields: null, - restRoute: null, - graphQLType: null, - fieldsToInclude: Array.Empty(), - fieldsToExclude: Array.Empty(), - policyRequest: null, - policyDatabase: null, - cacheEnabled: null, - cacheTtl: null, - config: TEST_RUNTIME_CONFIG_FILE, - restMethodsForStoredProcedure: null, - graphQLOperationForStoredProcedure: null, - parametersNameCollection: null, - parametersDescriptionCollection: null, - parametersRequiredCollection: null, - parametersDefaultCollection: null, - fieldsNameCollection: [], - fieldsAliasCollection: [], - fieldsDescriptionCollection: [], - fieldsPrimaryKeyCollection: [], - mcpDmlTools: "false", - mcpCustomTool: null - ); - return ExecuteVerifyTest(options); + VerifySettings settings = new(); + settings.UseParameters(mcpDmlTools, source); + return ExecuteVerifyTest(options, settings: settings); } /// diff --git a/src/Cli.Tests/ModuleInitializer.cs b/src/Cli.Tests/ModuleInitializer.cs index e00dc00a89..a0c882ae74 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -65,6 +65,10 @@ public static void Init() VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); + // Ignore the UserProvidedCustomToolEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory. + VerifierSettings.IgnoreMember(mcpOptions => mcpOptions.UserProvidedCustomToolEnabled); + // Ignore the UserProvidedDmlToolsEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory. + VerifierSettings.IgnoreMember(mcpOptions => mcpOptions.UserProvidedDmlToolsEnabled); // 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. diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt index 38dfae6840..d7d3ed0056 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt @@ -53,9 +53,7 @@ ], Mcp: { CustomToolEnabled: true, - DmlToolEnabled: false, - UserProvidedCustomToolEnabled: true, - UserProvidedDmlToolsEnabled: true + DmlToolEnabled: false } } } diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt index f87a181a24..aa30025561 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt @@ -53,9 +53,7 @@ ], Mcp: { CustomToolEnabled: true, - DmlToolEnabled: true, - UserProvidedCustomToolEnabled: true, - UserProvidedDmlToolsEnabled: true + DmlToolEnabled: true } } } diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt index 81628df772..576e84f6d8 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -53,9 +53,7 @@ ], Mcp: { CustomToolEnabled: true, - DmlToolEnabled: false, - UserProvidedCustomToolEnabled: true, - UserProvidedDmlToolsEnabled: false + DmlToolEnabled: true } } } diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsDisabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt similarity index 88% rename from src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsDisabled.verified.txt rename to src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt index 384fdbb80c..51a278d2a3 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsDisabled.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt @@ -49,9 +49,7 @@ ], Mcp: { CustomToolEnabled: false, - DmlToolEnabled: false, - UserProvidedCustomToolEnabled: false, - UserProvidedDmlToolsEnabled: true + DmlToolEnabled: false } } } diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt similarity index 88% rename from src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsEnabled.verified.txt rename to src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt index e08eb2e4b3..4dc41a4d45 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlToolsEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt @@ -49,9 +49,7 @@ ], Mcp: { CustomToolEnabled: false, - DmlToolEnabled: true, - UserProvidedCustomToolEnabled: false, - UserProvidedDmlToolsEnabled: true + DmlToolEnabled: true } } } diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt index e784664b66..627e8e9e01 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt @@ -56,9 +56,7 @@ ], Mcp: { CustomToolEnabled: true, - DmlToolEnabled: false, - UserProvidedCustomToolEnabled: true, - UserProvidedDmlToolsEnabled: true + DmlToolEnabled: false } } } diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt index 7c9fb41700..47d181d59d 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt @@ -56,9 +56,7 @@ ], Mcp: { CustomToolEnabled: true, - DmlToolEnabled: true, - UserProvidedCustomToolEnabled: true, - UserProvidedDmlToolsEnabled: true + DmlToolEnabled: true } } } diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt index 7461ea4ae5..4cb7fb45ef 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -56,9 +56,7 @@ ], Mcp: { CustomToolEnabled: true, - DmlToolEnabled: false, - UserProvidedCustomToolEnabled: true, - UserProvidedDmlToolsEnabled: false + DmlToolEnabled: true } } } diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt similarity index 88% rename from src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsEnabled.verified.txt rename to src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt index 9c3eca020a..42cb419190 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt @@ -53,9 +53,7 @@ ], Mcp: { CustomToolEnabled: false, - DmlToolEnabled: true, - UserProvidedCustomToolEnabled: false, - UserProvidedDmlToolsEnabled: true + DmlToolEnabled: false } } } diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsDisabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt similarity index 88% rename from src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsDisabled.verified.txt rename to src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt index f22dee731f..4084b29397 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlToolsDisabled.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt @@ -53,9 +53,7 @@ ], Mcp: { CustomToolEnabled: false, - DmlToolEnabled: false, - UserProvidedCustomToolEnabled: false, - UserProvidedDmlToolsEnabled: true + DmlToolEnabled: true } } } diff --git a/src/Cli.Tests/UpdateEntityTests.cs b/src/Cli.Tests/UpdateEntityTests.cs index 9c6b81536a..2cc03dd8f8 100644 --- a/src/Cli.Tests/UpdateEntityTests.cs +++ b/src/Cli.Tests/UpdateEntityTests.cs @@ -1219,72 +1219,13 @@ private Task ExecuteVerifyTest(string initialConfig, UpdateOptions options, Veri #region MCP Entity Configuration Tests /// - /// Test updating table entity with MCP dml-tools enabled + /// Test updating table entity with MCP dml-tools from false to true, or true to false + /// Tests actual update scenario where existing MCP config is modified /// - [TestMethod] - public Task TestUpdateTableEntityWithMcpDmlToolsEnabled() - { - UpdateOptions options = new( - source: null, - permissions: null, - entity: "MyEntity", - sourceType: null, - sourceParameters: null, - sourceKeyFields: null, - restRoute: null, - graphQLType: null, - fieldsToInclude: null, - fieldsToExclude: null, - policyRequest: null, - policyDatabase: null, - relationship: null, - cardinality: null, - targetEntity: null, - linkingObject: null, - linkingSourceFields: null, - linkingTargetFields: null, - relationshipFields: null, - map: null, - cacheEnabled: null, - cacheTtl: null, - config: TEST_RUNTIME_CONFIG_FILE, - restMethodsForStoredProcedure: null, - graphQLOperationForStoredProcedure: null, - description: null, - parametersNameCollection: null, - parametersDescriptionCollection: null, - parametersRequiredCollection: null, - parametersDefaultCollection: null, - fieldsNameCollection: null, - fieldsAliasCollection: null, - fieldsDescriptionCollection: null, - fieldsPrimaryKeyCollection: null, - mcpDmlTools: "true", - mcpCustomTool: null - ); - - string initialConfig = GetInitialConfigString() + "," + @" - ""entities"": { - ""MyEntity"": { - ""source"": ""MyTable"", - ""permissions"": [ - { - ""role"": ""anonymous"", - ""actions"": [""*""] - } - ] - } - } - }"; - - return ExecuteVerifyTest(initialConfig, options); - } - - /// - /// Test updating table entity with MCP dml-tools disabled - /// - [TestMethod] - public Task TestUpdateTableEntityWithMcpDmlToolsDisabled() + [DataTestMethod] + [DataRow("true", "false", DisplayName = "TestUpdateTableEntityWithMcpDmlToolsEnabled")] + [DataRow("false", "true", DisplayName = "TestUpdateTableEntityWithMcpDmlToolsDisabled")] + public Task TestUpdateTableEntityWithMcpDmlTools(string newMcpDmlTools, string initialMcpDmlTools) { UpdateOptions options = new( source: null, @@ -1321,7 +1262,7 @@ public Task TestUpdateTableEntityWithMcpDmlToolsDisabled() fieldsAliasCollection: null, fieldsDescriptionCollection: null, fieldsPrimaryKeyCollection: null, - mcpDmlTools: "false", + mcpDmlTools: newMcpDmlTools, mcpCustomTool: null ); @@ -1334,16 +1275,20 @@ public Task TestUpdateTableEntityWithMcpDmlToolsDisabled() ""role"": ""anonymous"", ""actions"": [""*""] } - ] + ], + ""mcp"": " + initialMcpDmlTools + @" } } }"; - return ExecuteVerifyTest(initialConfig, options); + VerifySettings settings = new(); + settings.UseParameters(newMcpDmlTools); + return ExecuteVerifyTest(initialConfig, options, settings: settings); } /// - /// Test updating stored procedure with MCP custom-tool enabled + /// Test updating stored procedure with MCP custom-tool from false to true + /// Tests actual update scenario where existing MCP config is modified /// [TestMethod] public Task TestUpdateStoredProcedureWithMcpCustomToolEnabled() @@ -1399,7 +1344,10 @@ public Task TestUpdateStoredProcedureWithMcpCustomToolEnabled() ""role"": ""anonymous"", ""actions"": [""execute""] } - ] + ], + ""mcp"": { + ""custom-tool"": false + } } } }"; @@ -1409,6 +1357,7 @@ public Task TestUpdateStoredProcedureWithMcpCustomToolEnabled() /// /// Test updating stored procedure with both MCP properties + /// Updates from both true to custom-tool=true, dml-tools=false /// [TestMethod] public Task TestUpdateStoredProcedureWithBothMcpProperties() @@ -1464,7 +1413,11 @@ public Task TestUpdateStoredProcedureWithBothMcpProperties() ""role"": ""anonymous"", ""actions"": [""execute""] } - ] + ], + ""mcp"": { + ""custom-tool"": false, + ""dml-tools"": true + } } } }"; @@ -1474,6 +1427,7 @@ public Task TestUpdateStoredProcedureWithBothMcpProperties() /// /// Test updating stored procedure with both MCP properties enabled + /// Updates from both false to both true /// [TestMethod] public Task TestUpdateStoredProcedureWithBothMcpPropertiesEnabled() @@ -1529,7 +1483,11 @@ public Task TestUpdateStoredProcedureWithBothMcpPropertiesEnabled() ""role"": ""anonymous"", ""actions"": [""execute""] } - ] + ], + ""mcp"": { + ""custom-tool"": false, + ""dml-tools"": false + } } } }"; diff --git a/src/Cli/Commands/EntityOptions.cs b/src/Cli/Commands/EntityOptions.cs index 700bb051eb..3b2b77d9b2 100644 --- a/src/Cli/Commands/EntityOptions.cs +++ b/src/Cli/Commands/EntityOptions.cs @@ -137,10 +137,10 @@ public EntityOptions( [Option("fields.primary-key", Required = false, Separator = ',', HelpText = "Set this field as a primary key.")] public IEnumerable? FieldsPrimaryKeyCollection { get; } - [Option("mcp.dml-tools", Required = false, HelpText = "Enable MCP DML (Data Manipulation Language) tools for this entity. Allows CRUD operations via MCP.")] + [Option("mcp.dml-tools", Required = false, HelpText = "Enable MCP DML (Data Manipulation Language) tools for this entity. Allows CRUD operations via MCP. Default value is true.")] public string? McpDmlTools { get; } - [Option("mcp.custom-tool", Required = false, HelpText = "Enable MCP custom tool for this entity. Only valid for stored procedures.")] + [Option("mcp.custom-tool", Required = false, HelpText = "Enable MCP custom tool for this entity. Only valid for stored procedures. Default value is false.")] public string? McpCustomTool { get; } } } diff --git a/src/Config/ObjectModel/EntityMcpOptions.cs b/src/Config/ObjectModel/EntityMcpOptions.cs index 5485926e39..ad928a21ab 100644 --- a/src/Config/ObjectModel/EntityMcpOptions.cs +++ b/src/Config/ObjectModel/EntityMcpOptions.cs @@ -19,10 +19,10 @@ public record EntityMcpOptions /// /// Indicates whether DML tools are enabled for this entity. - /// Defaults to false when not explicitly provided. + /// Defaults to true when not explicitly provided. /// [JsonPropertyName("dml-tools")] - public bool DmlToolEnabled { get; init; } = false; + public bool DmlToolEnabled { get; init; } = true; /// /// Flag which informs CLI and JSON serializer whether to write the CustomToolEnabled diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index bad5aa8680..6d6cf6d51b 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -314,6 +314,7 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new EntityMcpOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory()); options.Converters.Add(new MultipleCreateOptionsConverter()); diff --git a/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs index cf0edc4ece..5ce34c9355 100644 --- a/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs +++ b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs @@ -14,6 +14,51 @@ namespace Azure.DataApiBuilder.Service.Tests.Configuration [TestClass] public class EntityMcpConfigurationTests { + private const string BASE_CONFIG_TEMPLATE = @"{{ + ""$schema"": ""test-schema"", + ""data-source"": {{ + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }}, + ""runtime"": {{ + ""rest"": {{ ""enabled"": true, ""path"": ""/api"" }}, + ""graphql"": {{ ""enabled"": true, ""path"": ""/graphql"" }}, + ""host"": {{ ""mode"": ""development"" }} + }}, + ""entities"": {{ + {0} + }} + }}"; + + /// + /// Helper method to create a config with specified entities JSON + /// + private static string CreateConfig(string entitiesJson) + { + return string.Format(BASE_CONFIG_TEMPLATE, entitiesJson); + } + + /// + /// Helper method to assert entity MCP configuration + /// + private static void AssertEntityMcp(Entity entity, bool? expectedDmlTools, bool? expectedCustomTool, string message = null) + { + if (expectedDmlTools == null && expectedCustomTool == null) + { + Assert.IsNull(entity.Mcp, "MCP options should be null when not specified"); + return; + } + + Assert.IsNotNull(entity.Mcp, message ?? "MCP options should be present"); + + bool actualDmlTools = entity.Mcp?.DmlToolEnabled ?? true; // Default is true + bool actualCustomTool = entity.Mcp?.CustomToolEnabled ?? false; // Default is false + + Assert.AreEqual(expectedDmlTools ?? true, actualDmlTools, + $"DmlToolEnabled should be {expectedDmlTools ?? true}"); + Assert.AreEqual(expectedCustomTool ?? false, actualCustomTool, + $"CustomToolEnabled should be {expectedCustomTool ?? false}"); + } /// /// Test that deserializing boolean 'true' shorthand correctly sets dml-tools enabled. /// @@ -21,25 +66,13 @@ public class EntityMcpConfigurationTests public void DeserializeConfig_McpBooleanTrue_EnablesDmlToolsOnly() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""Book"": { - ""source"": ""books"", - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], - ""mcp"": true - } + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": true } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); @@ -48,11 +81,7 @@ public void DeserializeConfig_McpBooleanTrue_EnablesDmlToolsOnly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - - Entity bookEntity = runtimeConfig.Entities["Book"]; - Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); - Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be enabled"); - Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled (default)"); + AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false); } /// @@ -62,25 +91,13 @@ public void DeserializeConfig_McpBooleanTrue_EnablesDmlToolsOnly() public void DeserializeConfig_McpBooleanFalse_DisablesDmlTools() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""Book"": { - ""source"": ""books"", - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], - ""mcp"": false - } + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": false } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); @@ -89,11 +106,7 @@ public void DeserializeConfig_McpBooleanFalse_DisablesDmlTools() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - - Entity bookEntity = runtimeConfig.Entities["Book"]; - Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); - Assert.IsFalse(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled"); - Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled (default)"); + AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: false, expectedCustomTool: false); } /// @@ -103,28 +116,16 @@ public void DeserializeConfig_McpBooleanFalse_DisablesDmlTools() public void DeserializeConfig_McpObject_SetsBothProperties() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""GetBook"": { - ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], - ""mcp"": { - ""custom-tool"": true, - ""dml-tools"": false - } + string config = CreateConfig(@" + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true, + ""dml-tools"": false } } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); @@ -133,11 +134,7 @@ public void DeserializeConfig_McpObject_SetsBothProperties() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - - Entity spEntity = runtimeConfig.Entities["GetBook"]; - Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); - Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); - Assert.IsFalse(spEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled"); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: true); } /// @@ -147,27 +144,15 @@ public void DeserializeConfig_McpObject_SetsBothProperties() public void DeserializeConfig_McpObjectWithDmlToolsOnly_WorksCorrectly() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""Book"": { - ""source"": ""books"", - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], - ""mcp"": { - ""dml-tools"": true - } + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": { + ""dml-tools"": true } } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); @@ -176,38 +161,22 @@ public void DeserializeConfig_McpObjectWithDmlToolsOnly_WorksCorrectly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - - Entity bookEntity = runtimeConfig.Entities["Book"]; - Assert.IsNotNull(bookEntity.Mcp, "MCP options should be present"); - Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled, "DmlTools should be enabled"); - Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled (default)"); + AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false); } /// - /// Test that entity without MCP configuration has null MCP options. + /// Test that entity without MCP configuration has null Mcp property. /// [TestMethod] public void DeserializeConfig_NoMcp_HasNullMcpOptions() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""Book"": { - ""source"": ""books"", - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }] - } + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }] } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); @@ -216,9 +185,7 @@ public void DeserializeConfig_NoMcp_HasNullMcpOptions() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - - Entity bookEntity = runtimeConfig.Entities["Book"]; - Assert.IsNull(bookEntity.Mcp, "MCP options should be null when not specified"); + Assert.IsNull(runtimeConfig.Entities["Book"].Mcp, "MCP options should be null when not specified"); } /// @@ -228,28 +195,16 @@ public void DeserializeConfig_NoMcp_HasNullMcpOptions() public void DeserializeConfig_McpObjectWithBothTrue_SetsCorrectly() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""GetBook"": { - ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], - ""mcp"": { - ""custom-tool"": true, - ""dml-tools"": true - } + string config = CreateConfig(@" + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true, + ""dml-tools"": true } } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); @@ -258,11 +213,7 @@ public void DeserializeConfig_McpObjectWithBothTrue_SetsCorrectly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - - Entity spEntity = runtimeConfig.Entities["GetBook"]; - Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); - Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); - Assert.IsTrue(spEntity.Mcp.DmlToolEnabled, "DmlTools should be enabled"); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: true, expectedCustomTool: true); } /// @@ -272,28 +223,16 @@ public void DeserializeConfig_McpObjectWithBothTrue_SetsCorrectly() public void DeserializeConfig_McpObjectWithBothFalse_SetsCorrectly() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""GetBook"": { - ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], - ""mcp"": { - ""custom-tool"": false, - ""dml-tools"": false - } + string config = CreateConfig(@" + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": false, + ""dml-tools"": false } } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); @@ -302,11 +241,7 @@ public void DeserializeConfig_McpObjectWithBothFalse_SetsCorrectly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - - Entity spEntity = runtimeConfig.Entities["GetBook"]; - Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); - Assert.IsFalse(spEntity.Mcp.CustomToolEnabled, "CustomTool should be disabled"); - Assert.IsFalse(spEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled"); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: false); } /// @@ -316,27 +251,15 @@ public void DeserializeConfig_McpObjectWithBothFalse_SetsCorrectly() public void DeserializeConfig_McpObjectWithCustomToolOnly_WorksCorrectly() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""GetBook"": { - ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], - ""mcp"": { - ""custom-tool"": true - } + string config = CreateConfig(@" + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true } } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); @@ -345,11 +268,7 @@ public void DeserializeConfig_McpObjectWithCustomToolOnly_WorksCorrectly() Assert.IsTrue(success, "Config should parse successfully"); Assert.IsNotNull(runtimeConfig); Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - - Entity spEntity = runtimeConfig.Entities["GetBook"]; - Assert.IsNotNull(spEntity.Mcp, "MCP options should be present"); - Assert.IsTrue(spEntity.Mcp.CustomToolEnabled, "CustomTool should be enabled"); - Assert.IsFalse(spEntity.Mcp.DmlToolEnabled, "DmlTools should be disabled (default is false)"); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: true, expectedCustomTool: true); } /// @@ -405,29 +324,19 @@ public void DeserializeConfig_MultipleEntitiesWithDifferentMcpSettings_WorksCorr // Book: mcp = true Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); - Entity bookEntity = runtimeConfig.Entities["Book"]; - Assert.IsNotNull(bookEntity.Mcp); - Assert.IsTrue(bookEntity.Mcp.DmlToolEnabled); - Assert.IsFalse(bookEntity.Mcp.CustomToolEnabled); + AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false); // Author: mcp = false Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Author")); - Entity authorEntity = runtimeConfig.Entities["Author"]; - Assert.IsNotNull(authorEntity.Mcp); - Assert.IsFalse(authorEntity.Mcp.DmlToolEnabled); - Assert.IsFalse(authorEntity.Mcp.CustomToolEnabled); + AssertEntityMcp(runtimeConfig.Entities["Author"], expectedDmlTools: false, expectedCustomTool: false); - // Publisher: no mcp + // Publisher: no mcp (null) Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Publisher")); - Entity publisherEntity = runtimeConfig.Entities["Publisher"]; - Assert.IsNull(publisherEntity.Mcp); + Assert.IsNull(runtimeConfig.Entities["Publisher"].Mcp, "Mcp should be null when not specified"); // GetBook: mcp object Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); - Entity spEntity = runtimeConfig.Entities["GetBook"]; - Assert.IsNotNull(spEntity.Mcp); - Assert.IsTrue(spEntity.Mcp.CustomToolEnabled); - Assert.IsFalse(spEntity.Mcp.DmlToolEnabled); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: true); } /// @@ -437,25 +346,13 @@ public void DeserializeConfig_MultipleEntitiesWithDifferentMcpSettings_WorksCorr public void DeserializeConfig_InvalidMcpValue_FailsGracefully() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""Book"": { - ""source"": ""books"", - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], - ""mcp"": ""invalid"" - } + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": ""invalid"" } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out _); @@ -471,28 +368,16 @@ public void DeserializeConfig_InvalidMcpValue_FailsGracefully() public void DeserializeConfig_McpObjectWithUnknownProperty_FailsGracefully() { // Arrange - string config = @"{ - ""$schema"": ""test-schema"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""test"" - }, - ""runtime"": { - ""rest"": { ""enabled"": true, ""path"": ""/api"" }, - ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, - ""host"": { ""mode"": ""development"" } - }, - ""entities"": { - ""Book"": { - ""source"": ""books"", - ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], - ""mcp"": { - ""dml-tools"": true, - ""unknown-property"": true - } + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": { + ""dml-tools"": true, + ""unknown-property"": true } } - }"; + "); // Act bool success = RuntimeConfigLoader.TryParseConfig(config, out _); From e7dab7bc3fb746dc1b6d83e66963f136313d30b2 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 17 Dec 2025 00:14:53 +0530 Subject: [PATCH 11/11] Ignore user provided poperty flag for service test --- src/Service.Tests/ModuleInitializer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Service.Tests/ModuleInitializer.cs b/src/Service.Tests/ModuleInitializer.cs index ba0407ecd5..f0c3984a72 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -69,6 +69,10 @@ public static void Init() VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); + // Ignore the UserProvidedCustomToolEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory. + VerifierSettings.IgnoreMember(mcpOptions => mcpOptions.UserProvidedCustomToolEnabled); + // Ignore the UserProvidedDmlToolsEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory. + VerifierSettings.IgnoreMember(mcpOptions => mcpOptions.UserProvidedDmlToolsEnabled); // Ignore the CosmosDataSourceUsed as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.CosmosDataSourceUsed); // Ignore the IsRequestBodyStrict as that's unimportant from a test standpoint.