Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
288 changes: 288 additions & 0 deletions src/Cli.Tests/AddEntityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -633,5 +633,293 @@ 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 (should serialize as boolean true)
/// </summary>
[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<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: null
);
return ExecuteVerifyTest(options);
}

/// <summary>
/// Test adding table entity with MCP dml-tools disabled (should serialize as boolean false)
/// </summary>
[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<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: null
);
return ExecuteVerifyTest(options);
}

/// <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
}
}
Loading