diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json
index 80cfd953ad..61ab7474b7 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",
@@ -961,6 +961,31 @@
"default": 5
}
}
+ },
+ "mcp": {
+ "oneOf": [
+ {
+ "type": "boolean",
+ "description": "Boolean shorthand: true enables dml-tools only (custom-tool remains false), false disables all MCP functionality."
+ },
+ {
+ "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.Tests/AddEntityTests.cs b/src/Cli.Tests/AddEntityTests.cs
index 9386916f7f..e96d131880 100644
--- a/src/Cli.Tests/AddEntityTests.cs
+++ b/src/Cli.Tests/AddEntityTests.cs
@@ -633,5 +633,259 @@ 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 or disabled
+ ///
+ [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: source,
+ permissions: new string[] { "anonymous", "*" },
+ entity: entity,
+ 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: null
+ );
+
+ VerifySettings settings = new();
+ settings.UseParameters(mcpDmlTools, source);
+ return ExecuteVerifyTest(options, settings: settings);
+ }
+
+ ///
+ /// 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/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
new file mode 100644
index 0000000000..d7d3ed0056
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt
@@ -0,0 +1,61 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ 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..aa30025561
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt
@@ -0,0 +1,61 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ 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..576e84f6d8
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt
@@ -0,0 +1,61 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt
new file mode 100644
index 0000000000..51a278d2a3
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt
@@ -0,0 +1,57 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt
new file mode 100644
index 0000000000..4dc41a4d45
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt
@@ -0,0 +1,57 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ 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..627e8e9e01
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt
@@ -0,0 +1,64 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ 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..47d181d59d
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt
@@ -0,0 +1,64 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ 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..4cb7fb45ef
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt
@@ -0,0 +1,64 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt
new file mode 100644
index 0000000000..42cb419190
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt
@@ -0,0 +1,61 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt
new file mode 100644
index 0000000000..4084b29397
--- /dev/null
+++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt
@@ -0,0 +1,61 @@
+{
+ 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
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Cli.Tests/UpdateEntityTests.cs b/src/Cli.Tests/UpdateEntityTests.cs
index 3a106c0adc..2cc03dd8f8 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,419 @@ 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 from false to true, or true to false
+ /// Tests actual update scenario where existing MCP config is modified
+ ///
+ [DataTestMethod]
+ [DataRow("true", "false", DisplayName = "TestUpdateTableEntityWithMcpDmlToolsEnabled")]
+ [DataRow("false", "true", DisplayName = "TestUpdateTableEntityWithMcpDmlToolsDisabled")]
+ public Task TestUpdateTableEntityWithMcpDmlTools(string newMcpDmlTools, string initialMcpDmlTools)
+ {
+ 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: newMcpDmlTools,
+ mcpCustomTool: null
+ );
+
+ string initialConfig = GetInitialConfigString() + "," + @"
+ ""entities"": {
+ ""MyEntity"": {
+ ""source"": ""MyTable"",
+ ""permissions"": [
+ {
+ ""role"": ""anonymous"",
+ ""actions"": [""*""]
+ }
+ ],
+ ""mcp"": " + initialMcpDmlTools + @"
+ }
+ }
+ }";
+
+ VerifySettings settings = new();
+ settings.UseParameters(newMcpDmlTools);
+ return ExecuteVerifyTest(initialConfig, options, settings: settings);
+ }
+
+ ///
+ /// 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()
+ {
+ 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""]
+ }
+ ],
+ ""mcp"": {
+ ""custom-tool"": false
+ }
+ }
+ }
+ }";
+
+ return ExecuteVerifyTest(initialConfig, options);
+ }
+
+ ///
+ /// Test updating stored procedure with both MCP properties
+ /// Updates from both true to custom-tool=true, dml-tools=false
+ ///
+ [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""]
+ }
+ ],
+ ""mcp"": {
+ ""custom-tool"": false,
+ ""dml-tools"": true
+ }
+ }
+ }
+ }";
+
+ return ExecuteVerifyTest(initialConfig, options);
+ }
+
+ ///
+ /// Test updating stored procedure with both MCP properties enabled
+ /// Updates from both false to both true
+ ///
+ [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""]
+ }
+ ],
+ ""mcp"": {
+ ""custom-tool"": false,
+ ""dml-tools"": false
+ }
+ }
+ }
+ }";
+
+ 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
}
}
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..3b2b77d9b2 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. 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. Default value is false.")]
+ 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 1d673c11e3..8fcc7cae31 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);
+ 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.");
+ return false;
+ }
+ }
// 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: mcpOptions);
// Add entity to existing runtime config.
IDictionary entities = new Dictionary(initialRuntimeConfig.Entities.Entities)
@@ -1621,6 +1634,26 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig
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);
+
+ // Construct and validate MCP options if provided
+ EntityMcpOptions? updatedMcpOptions = null;
+ if (options.McpDmlTools is not null || options.McpCustomTool is not null)
+ {
+ updatedMcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate);
+ if (updatedMcpOptions is null)
+ {
+ _logger.LogError("Failed to construct MCP options.");
+ return false;
+ }
+ }
+ else
+ {
+ // Keep existing MCP options if no updates provided
+ updatedMcpOptions = entity.Mcp;
+ }
+
if (!updatedGraphQLDetails.Enabled)
{
_logger.LogWarning("Disabling GraphQL for this entity will restrict its usage in relationships");
@@ -1857,7 +1890,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..48edd4411c 100644
--- a/src/Cli/Utils.cs
+++ b/src/Cli/Utils.cs
@@ -892,6 +892,57 @@ 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..b4ad0e9170
--- /dev/null
+++ b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs
@@ -0,0 +1,123 @@
+// 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 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
+ {
+ 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
+ // Pass null for customToolEnabled to keep it as default (not user-provided)
+ return new EntityMcpOptions(
+ customToolEnabled: null,
+ dmlToolsEnabled: value
+ );
+ }
+
+ // Handle object representation
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ bool? customToolEnabled = null;
+ bool? dmlToolsEnabled = null;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ {
+ return new EntityMcpOptions(customToolEnabled, dmlToolsEnabled);
+ }
+
+ if (reader.TokenType == JsonTokenType.PropertyName)
+ {
+ string? propertyName = reader.GetString();
+ reader.Read(); // Move to the value
+
+ switch (propertyName)
+ {
+ case "custom-tool":
+ customToolEnabled = reader.GetBoolean();
+ break;
+ case "dml-tools":
+ dmlToolsEnabled = reader.GetBoolean();
+ break;
+ default:
+ throw new JsonException($"Unknown property '{propertyName}' in EntityMcpOptions");
+ }
+ }
+ }
+ }
+
+ throw new JsonException($"Unexpected token type {reader.TokenType} for EntityMcpOptions");
+ }
+
+ public override void Write(Utf8JsonWriter writer, EntityMcpOptions value, JsonSerializerOptions options)
+ {
+ if (value == null)
+ {
+ 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);
+ }
+ else if (value.UserProvidedCustomToolEnabled || value.UserProvidedDmlToolsEnabled)
+ {
+ // Write as object
+ writer.WriteStartObject();
+
+ if (value.UserProvidedCustomToolEnabled)
+ {
+ writer.WriteBoolean("custom-tool", value.CustomToolEnabled);
+ }
+
+ if (value.UserProvidedDmlToolsEnabled)
+ {
+ writer.WriteBoolean("dml-tools", value.DmlToolEnabled);
+ }
+
+ writer.WriteEndObject();
+ }
+ }
+ }
+}
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..ad928a21ab
--- /dev/null
+++ b/src/Config/ObjectModel/EntityMcpOptions.cs
@@ -0,0 +1,59 @@
+// 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.
+ /// Defaults to true when not explicitly provided.
+ ///
+ [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.HasValue)
+ {
+ this.CustomToolEnabled = customToolEnabled.Value;
+ this.UserProvidedCustomToolEnabled = true;
+ }
+
+ if (dmlToolsEnabled.HasValue)
+ {
+ this.DmlToolEnabled = dmlToolsEnabled.Value;
+ this.UserProvidedDmlToolsEnabled = true;
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000000..5ce34c9355
--- /dev/null
+++ b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs
@@ -0,0 +1,389 @@
+// 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
+ {
+ 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.
+ ///
+ [TestMethod]
+ public void DeserializeConfig_McpBooleanTrue_EnablesDmlToolsOnly()
+ {
+ // Arrange
+ string config = CreateConfig(@"
+ ""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"));
+ AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false);
+ }
+
+ ///
+ /// Test that deserializing boolean 'false' shorthand correctly sets dml-tools disabled.
+ ///
+ [TestMethod]
+ public void DeserializeConfig_McpBooleanFalse_DisablesDmlTools()
+ {
+ // Arrange
+ string config = CreateConfig(@"
+ ""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"));
+ AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: false, expectedCustomTool: false);
+ }
+
+ ///
+ /// Test that deserializing object format with both properties works correctly.
+ ///
+ [TestMethod]
+ public void DeserializeConfig_McpObject_SetsBothProperties()
+ {
+ // Arrange
+ 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);
+
+ // Assert
+ Assert.IsTrue(success, "Config should parse successfully");
+ Assert.IsNotNull(runtimeConfig);
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook"));
+ AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: true);
+ }
+
+ ///
+ /// Test that deserializing object format with only dml-tools works.
+ ///
+ [TestMethod]
+ public void DeserializeConfig_McpObjectWithDmlToolsOnly_WorksCorrectly()
+ {
+ // Arrange
+ string config = CreateConfig(@"
+ ""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"));
+ AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false);
+ }
+
+ ///
+ /// Test that entity without MCP configuration has null Mcp property.
+ ///
+ [TestMethod]
+ public void DeserializeConfig_NoMcp_HasNullMcpOptions()
+ {
+ // Arrange
+ string config = CreateConfig(@"
+ ""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"));
+ Assert.IsNull(runtimeConfig.Entities["Book"].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 = 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);
+
+ // Assert
+ Assert.IsTrue(success, "Config should parse successfully");
+ Assert.IsNotNull(runtimeConfig);
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook"));
+ AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: true, expectedCustomTool: true);
+ }
+
+ ///
+ /// Test that deserializing object format with both properties set to false works correctly.
+ ///
+ [TestMethod]
+ public void DeserializeConfig_McpObjectWithBothFalse_SetsCorrectly()
+ {
+ // Arrange
+ 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);
+
+ // Assert
+ Assert.IsTrue(success, "Config should parse successfully");
+ Assert.IsNotNull(runtimeConfig);
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook"));
+ AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: false);
+ }
+
+ ///
+ /// Test that deserializing object format with only custom-tool works.
+ ///
+ [TestMethod]
+ public void DeserializeConfig_McpObjectWithCustomToolOnly_WorksCorrectly()
+ {
+ // Arrange
+ 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);
+
+ // Assert
+ Assert.IsTrue(success, "Config should parse successfully");
+ Assert.IsNotNull(runtimeConfig);
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook"));
+ AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: true, expectedCustomTool: true);
+ }
+
+ ///
+ /// 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"));
+ AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false);
+
+ // Author: mcp = false
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Author"));
+ AssertEntityMcp(runtimeConfig.Entities["Author"], expectedDmlTools: false, expectedCustomTool: false);
+
+ // Publisher: no mcp (null)
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Publisher"));
+ Assert.IsNull(runtimeConfig.Entities["Publisher"].Mcp, "Mcp should be null when not specified");
+
+ // GetBook: mcp object
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook"));
+ AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: true);
+ }
+
+ ///
+ /// Test that deserializing invalid MCP value (non-boolean, non-object) fails gracefully.
+ ///
+ [TestMethod]
+ public void DeserializeConfig_InvalidMcpValue_FailsGracefully()
+ {
+ // Arrange
+ string config = CreateConfig(@"
+ ""Book"": {
+ ""source"": ""books"",
+ ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }],
+ ""mcp"": ""invalid""
+ }
+ ");
+
+ // Act
+ bool success = RuntimeConfigLoader.TryParseConfig(config, out _);
+
+ // 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 = CreateConfig(@"
+ ""Book"": {
+ ""source"": ""books"",
+ ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }],
+ ""mcp"": {
+ ""dml-tools"": true,
+ ""unknown-property"": true
+ }
+ }
+ ");
+
+ // Act
+ bool success = RuntimeConfigLoader.TryParseConfig(config, out _);
+
+ // Assert
+ Assert.IsFalse(success, "Config parsing should fail with unknown MCP property");
+ }
+ }
+}
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.