Skip to content
Open
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"]
},
Comment on lines +1175 to +1186
"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
27 changes: 16 additions & 11 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ public async Task<CallToolResult> ExecuteAsync(
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
}

// Check entity-level DML tool configuration
if (runtimeConfig.Entities?.TryGetValue(entityName, out Entity? entity) == true &&
entity.Mcp?.DmlToolEnabled == false)
{
return McpErrorHelpers.ToolDisabled(toolName, logger, $"DML tools are disabled for entity '{entityName}'.");
}

if (!McpMetadataHelper.TryResolveMetadata(
entityName,
runtimeConfig,
Expand Down Expand Up @@ -117,17 +124,23 @@ public async Task<CallToolResult> ExecuteAsync(
}

JsonElement insertPayloadRoot = dataElement.Clone();

// Validate it's a table or view - stored procedures use execute_entity
if (dbObject.SourceType != EntitySourceType.Table && dbObject.SourceType != EntitySourceType.View)
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entityName}' is not a table or view. For stored procedures, use the execute_entity tool instead.", logger);
}

InsertRequestContext insertRequestContext = new(
entityName,
dbObject,
insertPayloadRoot,
EntityActionOperation.Insert);

RequestValidator requestValidator = serviceProvider.GetRequiredService<RequestValidator>();

// Only validate tables
// Only validate tables. For views, skip validation and let the database handle any errors.
if (dbObject.SourceType is EntitySourceType.Table)
{
RequestValidator requestValidator = serviceProvider.GetRequiredService<RequestValidator>();
try
{
requestValidator.ValidateInsertRequestContext(insertRequestContext);
Expand All @@ -137,14 +150,6 @@ public async Task<CallToolResult> ExecuteAsync(
return McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger);
}
}
else
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"InvalidCreateTarget",
"The create_record tool is only available for tables.",
logger);
}

IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService<IMutationEngineFactory>();
DatabaseType databaseType = sqlMetadataProvider.GetDatabaseType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ public async Task<CallToolResult> ExecuteAsync(
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
}

// Check entity-level DML tool configuration
if (config.Entities?.TryGetValue(entityName, out Entity? entity) == true &&
entity.Mcp?.DmlToolEnabled == false)
{
return McpErrorHelpers.ToolDisabled(toolName, logger, $"DML tools are disabled for entity '{entityName}'.");
}

// 4) Resolve metadata for entity existence
if (!McpMetadataHelper.TryResolveMetadata(
entityName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,18 @@ public async Task<CallToolResult> ExecuteAsync(
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Entity is required", logger);
}

// Check entity-level DML tool configuration early (before metadata resolution)
if (config.Entities?.TryGetValue(entity, out Entity? entityForCheck) == true &&
entityForCheck.Mcp?.DmlToolEnabled == false)
{
return McpErrorHelpers.ToolDisabled(toolName, logger, $"DML tools are disabled for entity '{entity}'.");
}

IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>();
IQueryEngineFactory queryEngineFactory = serviceProvider.GetRequiredService<IQueryEngineFactory>();

// 4) Validate entity exists and is a stored procedure
if (!config.Entities.TryGetValue(entity, out Entity? entityConfig))
if (config.Entities is null || !config.Entities.TryGetValue(entity, out Entity? entityConfig))
{
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entity}' not found in configuration.", logger);
}
Expand Down
13 changes: 13 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ public async Task<CallToolResult> ExecuteAsync(
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
}

// Check entity-level DML tool configuration
if (runtimeConfig.Entities?.TryGetValue(entityName, out Entity? entity) == true &&
entity.Mcp?.DmlToolEnabled == false)
{
return McpErrorHelpers.ToolDisabled(toolName, logger, $"DML tools are disabled for entity '{entityName}'.");
}

if (root.TryGetProperty("select", out JsonElement selectElement))
{
select = selectElement.GetString();
Expand Down Expand Up @@ -151,6 +158,12 @@ public async Task<CallToolResult> ExecuteAsync(
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger);
}

// Validate it's a table or view
if (dbObject.SourceType != EntitySourceType.Table && dbObject.SourceType != EntitySourceType.View)
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entityName}' is not a table or view. For stored procedures, use the execute_entity tool instead.", logger);
}

// Authorization check in the existing entity
IAuthorizationResolver authResolver = serviceProvider.GetRequiredService<IAuthorizationResolver>();
IAuthorizationService authorizationService = serviceProvider.GetRequiredService<IAuthorizationService>();
Expand Down
13 changes: 13 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ public async Task<CallToolResult> ExecuteAsync(
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
}

// Check entity-level DML tool configuration
if (config.Entities?.TryGetValue(entityName, out Entity? entity) == true &&
entity.Mcp?.DmlToolEnabled == false)
{
return McpErrorHelpers.ToolDisabled(toolName, logger, $"DML tools are disabled for entity '{entityName}'.");
}

IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>();
IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService<IMutationEngineFactory>();

Expand All @@ -130,6 +137,12 @@ public async Task<CallToolResult> ExecuteAsync(
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger);
}

// Validate it's a table or view
if (dbObject.SourceType != EntitySourceType.Table && dbObject.SourceType != EntitySourceType.View)
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entityName}' is not a table or view. For stored procedures, use the execute_entity tool instead.", logger);
}

// 5) Authorization after we have a known entity
IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext? httpContext = httpContextAccessor.HttpContext;
Expand Down
Loading
Loading