Skip to content

Commit 9dad3d5

Browse files
Add entity-level MCP configuration support (#2989)
## Why make this change? This change allows entity-level MCP configuration to control which entities participate in MCP runtime tools, providing granular control over DML operations and custom tool exposure. - Closes on #2948 ## What is this change? This change introduces an optional mcp property at the entity level that controls participation in MCP's runtime tools. This is a prerequisite for custom tools support. The MCP property supports two formats: - **Boolean shorthand**: `"mcp": true` or `"mcp": false` - **Object format**: `{"dml-tools": boolean, "custom-tool": boolean}` Property Behavior: 1. Boolean Shorthand (`"mcp": true/false`) - `"mcp": true`: Enables DML tools only; custom tools remain disabled. - `"mcp": false`: Disables all MCP functionality for the entity. 2. Object Format `("mcp": { ... })` - `{ "dml-tools": true, "custom-tool": true }`: Enables both (valid only for stored procedures). - `{ "dml-tools": true, "custom-tool": false }`: DML only. - `{ "dml-tools": false, "custom-tool": true }`: Custom tool only (stored procedures). - `{ "dml-tools": false, "custom-tool": false }`: Fully disabled. Single-property cases: - `{"dml-tools": true}`: Enables DML only; auto-serializes to `"mcp": true`. - `{"custom-tool": true}`: Enables custom tool only; serializes as given. 3. No MCP Configuration in Entity (default) - `dml-tools` will still be enabled by default and no other change is behavior ## How was this tested? - [x] Unit Tests - [x] Integrations Tests - [x] CLI Command Testing Sample CLI commands: Add table with DML tools enabled `dab add Book --source books --permissions "anonymous:*" --mcp.dml-tools true` Add stored procedure with custom tool enabled `dab add GetBookById --source dbo.get_book_by_id --source.type stored-procedure --permissions "anonymous:execute" --mcp.custom-tool true` Add stored procedure with both properties `dab add UpdateBook --source dbo.update_book --source.type stored-procedure --permissions "anonymous:execute" --mcp.custom-tool true --mcp.dml-tools false`
1 parent d98a4ea commit 9dad3d5

25 files changed

+2033
-9
lines changed

schemas/dab.draft.schema.json

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@
695695
},
696696
"entities": {
697697
"type": "object",
698-
"description": "Entities that will be exposed via REST and/or GraphQL",
698+
"description": "Entities that will be exposed via REST, GraphQL and/or MCP",
699699
"patternProperties": {
700700
"^.*$": {
701701
"type": "object",
@@ -961,6 +961,31 @@
961961
"default": 5
962962
}
963963
}
964+
},
965+
"mcp": {
966+
"oneOf": [
967+
{
968+
"type": "boolean",
969+
"description": "Boolean shorthand: true enables dml-tools only (custom-tool remains false), false disables all MCP functionality."
970+
},
971+
{
972+
"type": "object",
973+
"additionalProperties": false,
974+
"properties": {
975+
"dml-tools": {
976+
"type": "boolean",
977+
"description": "Enable MCP DML (Data Manipulation Language) tools for this entity. Allows CRUD operations via MCP.",
978+
"default": true
979+
},
980+
"custom-tool": {
981+
"type": "boolean",
982+
"description": "Enable MCP custom tool for this entity. Only valid for stored procedures.",
983+
"default": false
984+
}
985+
}
986+
}
987+
],
988+
"description": "Model Context Protocol (MCP) configuration for this entity. Controls whether the entity is exposed via MCP tools."
964989
}
965990
},
966991
"if": {
@@ -1145,6 +1170,33 @@
11451170
]
11461171
}
11471172
}
1173+
},
1174+
{
1175+
"if": {
1176+
"properties": {
1177+
"mcp": {
1178+
"properties": {
1179+
"custom-tool": {
1180+
"const": true
1181+
}
1182+
}
1183+
}
1184+
},
1185+
"required": ["mcp"]
1186+
},
1187+
"then": {
1188+
"properties": {
1189+
"source": {
1190+
"properties": {
1191+
"type": {
1192+
"const": "stored-procedure"
1193+
}
1194+
},
1195+
"required": ["type"]
1196+
}
1197+
},
1198+
"errorMessage": "custom-tool can only be enabled for entities with source type 'stored-procedure'."
1199+
}
11481200
}
11491201
]
11501202
}

src/Cli.Tests/AddEntityTests.cs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,5 +633,259 @@ private Task ExecuteVerifyTest(AddOptions options, string config = INITIAL_CONFI
633633

634634
return Verify(updatedRuntimeConfig, settings);
635635
}
636+
637+
#region MCP Entity Configuration Tests
638+
639+
/// <summary>
640+
/// Test adding table entity with MCP dml-tools enabled or disabled
641+
/// </summary>
642+
[DataTestMethod]
643+
[DataRow("true", "books", "Book", DisplayName = "AddTableEntityWithMcpDmlToolsEnabled")]
644+
[DataRow("false", "authors", "Author", DisplayName = "AddTableEntityWithMcpDmlToolsDisabled")]
645+
public Task AddTableEntityWithMcpDmlTools(string mcpDmlTools, string source, string entity)
646+
{
647+
AddOptions options = new(
648+
source: source,
649+
permissions: new string[] { "anonymous", "*" },
650+
entity: entity,
651+
description: null,
652+
sourceType: "table",
653+
sourceParameters: null,
654+
sourceKeyFields: null,
655+
restRoute: null,
656+
graphQLType: null,
657+
fieldsToInclude: Array.Empty<string>(),
658+
fieldsToExclude: Array.Empty<string>(),
659+
policyRequest: null,
660+
policyDatabase: null,
661+
cacheEnabled: null,
662+
cacheTtl: null,
663+
config: TEST_RUNTIME_CONFIG_FILE,
664+
restMethodsForStoredProcedure: null,
665+
graphQLOperationForStoredProcedure: null,
666+
parametersNameCollection: null,
667+
parametersDescriptionCollection: null,
668+
parametersRequiredCollection: null,
669+
parametersDefaultCollection: null,
670+
fieldsNameCollection: [],
671+
fieldsAliasCollection: [],
672+
fieldsDescriptionCollection: [],
673+
fieldsPrimaryKeyCollection: [],
674+
mcpDmlTools: mcpDmlTools,
675+
mcpCustomTool: null
676+
);
677+
678+
VerifySettings settings = new();
679+
settings.UseParameters(mcpDmlTools, source);
680+
return ExecuteVerifyTest(options, settings: settings);
681+
}
682+
683+
/// <summary>
684+
/// Test adding stored procedure with MCP custom-tool enabled (should serialize as object)
685+
/// </summary>
686+
[TestMethod]
687+
public Task AddStoredProcedureWithMcpCustomToolEnabled()
688+
{
689+
AddOptions options = new(
690+
source: "dbo.GetBookById",
691+
permissions: new string[] { "anonymous", "execute" },
692+
entity: "GetBookById",
693+
description: null,
694+
sourceType: "stored-procedure",
695+
sourceParameters: null,
696+
sourceKeyFields: null,
697+
restRoute: null,
698+
graphQLType: null,
699+
fieldsToInclude: Array.Empty<string>(),
700+
fieldsToExclude: Array.Empty<string>(),
701+
policyRequest: null,
702+
policyDatabase: null,
703+
cacheEnabled: null,
704+
cacheTtl: null,
705+
config: TEST_RUNTIME_CONFIG_FILE,
706+
restMethodsForStoredProcedure: null,
707+
graphQLOperationForStoredProcedure: null,
708+
parametersNameCollection: null,
709+
parametersDescriptionCollection: null,
710+
parametersRequiredCollection: null,
711+
parametersDefaultCollection: null,
712+
fieldsNameCollection: [],
713+
fieldsAliasCollection: [],
714+
fieldsDescriptionCollection: [],
715+
fieldsPrimaryKeyCollection: [],
716+
mcpDmlTools: null,
717+
mcpCustomTool: "true"
718+
);
719+
return ExecuteVerifyTest(options);
720+
}
721+
722+
/// <summary>
723+
/// Test adding stored procedure with both MCP properties set to different values (should serialize as object with both)
724+
/// </summary>
725+
[TestMethod]
726+
public Task AddStoredProcedureWithBothMcpProperties()
727+
{
728+
AddOptions options = new(
729+
source: "dbo.UpdateBook",
730+
permissions: new string[] { "anonymous", "execute" },
731+
entity: "UpdateBook",
732+
description: null,
733+
sourceType: "stored-procedure",
734+
sourceParameters: null,
735+
sourceKeyFields: null,
736+
restRoute: null,
737+
graphQLType: null,
738+
fieldsToInclude: Array.Empty<string>(),
739+
fieldsToExclude: Array.Empty<string>(),
740+
policyRequest: null,
741+
policyDatabase: null,
742+
cacheEnabled: null,
743+
cacheTtl: null,
744+
config: TEST_RUNTIME_CONFIG_FILE,
745+
restMethodsForStoredProcedure: null,
746+
graphQLOperationForStoredProcedure: null,
747+
parametersNameCollection: null,
748+
parametersDescriptionCollection: null,
749+
parametersRequiredCollection: null,
750+
parametersDefaultCollection: null,
751+
fieldsNameCollection: [],
752+
fieldsAliasCollection: [],
753+
fieldsDescriptionCollection: [],
754+
fieldsPrimaryKeyCollection: [],
755+
mcpDmlTools: "false",
756+
mcpCustomTool: "true"
757+
);
758+
return ExecuteVerifyTest(options);
759+
}
760+
761+
/// <summary>
762+
/// Test adding stored procedure with both MCP properties enabled (common use case)
763+
/// </summary>
764+
[TestMethod]
765+
public Task AddStoredProcedureWithBothMcpPropertiesEnabled()
766+
{
767+
AddOptions options = new(
768+
source: "dbo.GetAllBooks",
769+
permissions: new string[] { "anonymous", "execute" },
770+
entity: "GetAllBooks",
771+
description: null,
772+
sourceType: "stored-procedure",
773+
sourceParameters: null,
774+
sourceKeyFields: null,
775+
restRoute: null,
776+
graphQLType: null,
777+
fieldsToInclude: Array.Empty<string>(),
778+
fieldsToExclude: Array.Empty<string>(),
779+
policyRequest: null,
780+
policyDatabase: null,
781+
cacheEnabled: null,
782+
cacheTtl: null,
783+
config: TEST_RUNTIME_CONFIG_FILE,
784+
restMethodsForStoredProcedure: null,
785+
graphQLOperationForStoredProcedure: null,
786+
parametersNameCollection: null,
787+
parametersDescriptionCollection: null,
788+
parametersRequiredCollection: null,
789+
parametersDefaultCollection: null,
790+
fieldsNameCollection: [],
791+
fieldsAliasCollection: [],
792+
fieldsDescriptionCollection: [],
793+
fieldsPrimaryKeyCollection: [],
794+
mcpDmlTools: "true",
795+
mcpCustomTool: "true"
796+
);
797+
return ExecuteVerifyTest(options);
798+
}
799+
800+
/// <summary>
801+
/// Test that adding table entity with custom-tool fails validation
802+
/// </summary>
803+
[TestMethod]
804+
public void AddTableEntityWithInvalidMcpCustomTool()
805+
{
806+
AddOptions options = new(
807+
source: "reviews",
808+
permissions: new string[] { "anonymous", "*" },
809+
entity: "Review",
810+
description: null,
811+
sourceType: "table",
812+
sourceParameters: null,
813+
sourceKeyFields: null,
814+
restRoute: null,
815+
graphQLType: null,
816+
fieldsToInclude: Array.Empty<string>(),
817+
fieldsToExclude: Array.Empty<string>(),
818+
policyRequest: null,
819+
policyDatabase: null,
820+
cacheEnabled: null,
821+
cacheTtl: null,
822+
config: TEST_RUNTIME_CONFIG_FILE,
823+
restMethodsForStoredProcedure: null,
824+
graphQLOperationForStoredProcedure: null,
825+
parametersNameCollection: null,
826+
parametersDescriptionCollection: null,
827+
parametersRequiredCollection: null,
828+
parametersDefaultCollection: null,
829+
fieldsNameCollection: [],
830+
fieldsAliasCollection: [],
831+
fieldsDescriptionCollection: [],
832+
fieldsPrimaryKeyCollection: [],
833+
mcpDmlTools: null,
834+
mcpCustomTool: "true"
835+
);
836+
837+
RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig);
838+
839+
Assert.IsFalse(TryAddNewEntity(options, runtimeConfig!, out RuntimeConfig _),
840+
"Should fail to add table entity with custom-tool enabled");
841+
}
842+
843+
/// <summary>
844+
/// Test that invalid MCP option value fails
845+
/// </summary>
846+
[DataTestMethod]
847+
[DataRow("invalid", null, DisplayName = "Invalid dml-tools value")]
848+
[DataRow(null, "invalid", DisplayName = "Invalid custom-tool value")]
849+
[DataRow("yes", "no", DisplayName = "Invalid boolean-like values")]
850+
public void AddEntityWithInvalidMcpOptions(string? mcpDmlTools, string? mcpCustomTool)
851+
{
852+
AddOptions options = new(
853+
source: "MyTable",
854+
permissions: new string[] { "anonymous", "*" },
855+
entity: "MyEntity",
856+
description: null,
857+
sourceType: "table",
858+
sourceParameters: null,
859+
sourceKeyFields: null,
860+
restRoute: null,
861+
graphQLType: null,
862+
fieldsToInclude: Array.Empty<string>(),
863+
fieldsToExclude: Array.Empty<string>(),
864+
policyRequest: null,
865+
policyDatabase: null,
866+
cacheEnabled: null,
867+
cacheTtl: null,
868+
config: TEST_RUNTIME_CONFIG_FILE,
869+
restMethodsForStoredProcedure: null,
870+
graphQLOperationForStoredProcedure: null,
871+
parametersNameCollection: null,
872+
parametersDescriptionCollection: null,
873+
parametersRequiredCollection: null,
874+
parametersDefaultCollection: null,
875+
fieldsNameCollection: [],
876+
fieldsAliasCollection: [],
877+
fieldsDescriptionCollection: [],
878+
fieldsPrimaryKeyCollection: [],
879+
mcpDmlTools: mcpDmlTools,
880+
mcpCustomTool: mcpCustomTool
881+
);
882+
883+
RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig);
884+
885+
Assert.IsFalse(TryAddNewEntity(options, runtimeConfig!, out RuntimeConfig _),
886+
"Should fail with invalid MCP option values");
887+
}
888+
889+
#endregion MCP Entity Configuration Tests
636890
}
637891
}

src/Cli.Tests/ModuleInitializer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ public static void Init()
6565
VerifierSettings.IgnoreMember<Entity>(entity => entity.IsLinkingEntity);
6666
// Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
6767
VerifierSettings.IgnoreMember<EntityCacheOptions>(cacheOptions => cacheOptions.UserProvidedTtlOptions);
68+
// Ignore the UserProvidedCustomToolEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory.
69+
VerifierSettings.IgnoreMember<EntityMcpOptions>(mcpOptions => mcpOptions.UserProvidedCustomToolEnabled);
70+
// Ignore the UserProvidedDmlToolsEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory.
71+
VerifierSettings.IgnoreMember<EntityMcpOptions>(mcpOptions => mcpOptions.UserProvidedDmlToolsEnabled);
6872
// Ignore the IsRequestBodyStrict as that's unimportant from a test standpoint.
6973
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.IsRequestBodyStrict);
7074
// Ignore the IsGraphQLEnabled as that's unimportant from a test standpoint.

0 commit comments

Comments
 (0)