Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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'."
}
}
]
}
Expand Down
254 changes: 254 additions & 0 deletions src/Cli.Tests/AddEntityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -633,5 +633,259 @@ private Task ExecuteVerifyTest(AddOptions options, string config = INITIAL_CONFI

return Verify(updatedRuntimeConfig, settings);
}

#region MCP Entity Configuration Tests

/// <summary>
/// Test adding table entity with MCP dml-tools enabled or disabled
/// </summary>
[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<string>(),
fieldsToExclude: Array.Empty<string>(),
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);
}

/// <summary>
/// Test adding stored procedure with MCP custom-tool enabled (should serialize as object)
/// </summary>
[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<string>(),
fieldsToExclude: Array.Empty<string>(),
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);
}

/// <summary>
/// Test adding stored procedure with both MCP properties set to different values (should serialize as object with both)
/// </summary>
[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<string>(),
fieldsToExclude: Array.Empty<string>(),
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);
}

/// <summary>
/// Test adding stored procedure with both MCP properties enabled (common use case)
/// </summary>
[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<string>(),
fieldsToExclude: Array.Empty<string>(),
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);
}

/// <summary>
/// Test that adding table entity with custom-tool fails validation
/// </summary>
[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<string>(),
fieldsToExclude: Array.Empty<string>(),
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");
}

/// <summary>
/// Test that invalid MCP option value fails
/// </summary>
[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<string>(),
fieldsToExclude: Array.Empty<string>(),
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
}
}
4 changes: 4 additions & 0 deletions src/Cli.Tests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ public static void Init()
VerifierSettings.IgnoreMember<Entity>(entity => entity.IsLinkingEntity);
// Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter.
VerifierSettings.IgnoreMember<EntityCacheOptions>(cacheOptions => cacheOptions.UserProvidedTtlOptions);
// Ignore the UserProvidedCustomToolEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory.
VerifierSettings.IgnoreMember<EntityMcpOptions>(mcpOptions => mcpOptions.UserProvidedCustomToolEnabled);
// Ignore the UserProvidedDmlToolsEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory.
VerifierSettings.IgnoreMember<EntityMcpOptions>(mcpOptions => mcpOptions.UserProvidedDmlToolsEnabled);
// Ignore the IsRequestBodyStrict as that's unimportant from a test standpoint.
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.IsRequestBodyStrict);
// Ignore the IsGraphQLEnabled as that's unimportant from a test standpoint.
Expand Down
Loading
Loading