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
25 changes: 15 additions & 10 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,22 @@ public async Task<CallToolResult> ExecuteAsync(
CancellationToken cancellationToken = default)
{
ILogger<CreateRecordTool>? logger = serviceProvider.GetService<ILogger<CreateRecordTool>>();
string toolName = GetToolMetadata().Name;
if (arguments == null)
{
return Utils.McpResponseBuilder.BuildErrorResult("Invalid Arguments", "No arguments provided", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Invalid Arguments", "No arguments provided", logger);
}

RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService<RuntimeConfigProvider>();
if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
{
return Utils.McpResponseBuilder.BuildErrorResult("Invalid Configuration", "Runtime configuration not available", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Invalid Configuration", "Runtime configuration not available", logger);
}

if (runtimeConfig.McpDmlTools?.CreateRecord != true)
{
return Utils.McpResponseBuilder.BuildErrorResult(
toolName,
"ToolDisabled",
"The create_record tool is disabled in the configuration.",
logger);
Expand All @@ -84,13 +86,13 @@ public async Task<CallToolResult> ExecuteAsync(
if (!root.TryGetProperty("entity", out JsonElement entityElement) ||
!root.TryGetProperty("data", out JsonElement dataElement))
{
return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Missing required arguments 'entity' or 'data'", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Missing required arguments 'entity' or 'data'", logger);
}

string entityName = entityElement.GetString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(entityName))
{
return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Entity name cannot be empty", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Entity name cannot be empty", logger);
}

string dataSourceName;
Expand All @@ -100,7 +102,7 @@ public async Task<CallToolResult> ExecuteAsync(
}
catch (Exception)
{
return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger);
}

IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>();
Expand All @@ -113,7 +115,7 @@ public async Task<CallToolResult> ExecuteAsync(
}
catch (Exception)
{
return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger);
}

// Create an HTTP context for authorization
Expand All @@ -123,13 +125,13 @@ public async Task<CallToolResult> ExecuteAsync(

if (httpContext is null || !authorizationResolver.IsValidRoleContext(httpContext))
{
return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger);
}

// Validate that we have at least one role authorized for create
if (!TryResolveAuthorizedRole(httpContext, authorizationResolver, entityName, out string authError))
{
return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", authError, logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", authError, logger);
}

JsonElement insertPayloadRoot = dataElement.Clone();
Expand All @@ -150,12 +152,13 @@ public async Task<CallToolResult> ExecuteAsync(
}
catch (Exception ex)
{
return Utils.McpResponseBuilder.BuildErrorResult("ValidationFailed", $"Request validation failed: {ex.Message}", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger);
}
}
else
{
return Utils.McpResponseBuilder.BuildErrorResult(
toolName,
"InvalidCreateTarget",
"The create_record tool is only available for tables.",
logger);
Expand Down Expand Up @@ -185,6 +188,7 @@ public async Task<CallToolResult> ExecuteAsync(
if (isError)
{
return Utils.McpResponseBuilder.BuildErrorResult(
toolName,
"CreateFailed",
$"Failed to create record in entity '{entityName}'. Error: {JsonSerializer.Serialize(objectResult.Value)}",
logger);
Expand All @@ -207,6 +211,7 @@ public async Task<CallToolResult> ExecuteAsync(
if (result is null)
{
return Utils.McpResponseBuilder.BuildErrorResult(
toolName,
"UnexpectedError",
$"Mutation engine returned null result for entity '{entityName}'",
logger);
Expand All @@ -226,7 +231,7 @@ public async Task<CallToolResult> ExecuteAsync(
}
catch (Exception ex)
{
return Utils.McpResponseBuilder.BuildErrorResult("Error", $"Error: {ex.Message}", logger);
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Error", $"Error: {ex.Message}", logger);
}
}

Expand Down
39 changes: 25 additions & 14 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public async Task<CallToolResult> ExecuteAsync(
CancellationToken cancellationToken = default)
{
ILogger<DeleteRecordTool>? logger = serviceProvider.GetService<ILogger<DeleteRecordTool>>();
string toolName = GetToolMetadata().Name;

try
{
Expand All @@ -87,6 +88,7 @@ public async Task<CallToolResult> ExecuteAsync(
if (config.McpDmlTools?.DeleteRecord != true)
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"ToolDisabled",
$"The {this.GetToolMetadata().Name} tool is disabled in the configuration.",
logger);
Expand All @@ -95,12 +97,12 @@ public async Task<CallToolResult> ExecuteAsync(
// 3) Parsing & basic argument validation
if (arguments is null)
{
return McpResponseBuilder.BuildErrorResult("InvalidArguments", "No arguments provided.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger);
}

if (!McpArgumentParser.TryParseEntityAndKeys(arguments.RootElement, out string entityName, out Dictionary<string, object?> keys, out string parseError))
{
return McpResponseBuilder.BuildErrorResult("InvalidArguments", parseError, logger);
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
}

IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>();
Expand All @@ -117,18 +119,18 @@ public async Task<CallToolResult> ExecuteAsync(
}
catch (Exception)
{
return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
}

if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null)
{
return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
}

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

// 5) Authorization
Expand All @@ -138,7 +140,7 @@ public async Task<CallToolResult> ExecuteAsync(

if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError))
{
return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {roleError}", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {roleError}", logger);
}

if (!McpAuthorizationHelper.TryResolveAuthorizedRole(
Expand All @@ -149,7 +151,7 @@ public async Task<CallToolResult> ExecuteAsync(
out string? effectiveRole,
out string authError))
{
return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {authError}", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {authError}", logger);
}

// 6) Build and validate Delete context
Expand All @@ -164,7 +166,7 @@ public async Task<CallToolResult> ExecuteAsync(
{
if (kvp.Value is null)
{
return McpResponseBuilder.BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
}

context.PrimaryKeyValuePairs[kvp.Key] = kvp.Value;
Expand Down Expand Up @@ -195,6 +197,7 @@ public async Task<CallToolResult> ExecuteAsync(
{
string keyDetails = McpJsonHelper.FormatKeyDetails(keys);
return McpResponseBuilder.BuildErrorResult(
toolName,
"RecordNotFound",
$"No record found with the specified primary key: {keyDetails}",
logger);
Expand All @@ -203,6 +206,7 @@ public async Task<CallToolResult> ExecuteAsync(
message.Contains("REFERENCE constraint", StringComparison.OrdinalIgnoreCase))
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"ConstraintViolation",
"Cannot delete record due to foreign key constraint. Other records depend on this record.",
logger);
Expand All @@ -211,6 +215,7 @@ public async Task<CallToolResult> ExecuteAsync(
message.Contains("authorization", StringComparison.OrdinalIgnoreCase))
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"PermissionDenied",
"You do not have permission to delete this record.",
logger);
Expand All @@ -219,13 +224,15 @@ public async Task<CallToolResult> ExecuteAsync(
message.Contains("type", StringComparison.OrdinalIgnoreCase))
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"InvalidArguments",
"Invalid data type for one or more key values.",
logger);
}

// For any other DAB exceptions, return the message as-is
return McpResponseBuilder.BuildErrorResult(
toolName,
"DataApiBuilderError",
dabEx.Message,
logger);
Expand All @@ -242,7 +249,7 @@ public async Task<CallToolResult> ExecuteAsync(
208 => $"Table '{dbObject.FullName}' not found in the database.",
_ => $"Database error: {sqlEx.Message}"
};
return McpResponseBuilder.BuildErrorResult("DatabaseError", errorMessage, logger);
return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", errorMessage, logger);
}
catch (DbException dbEx)
{
Expand All @@ -254,31 +261,33 @@ public async Task<CallToolResult> ExecuteAsync(
if (errorMsg.Contains("foreign key") || errorMsg.Contains("constraint"))
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"ConstraintViolation",
"Cannot delete record due to foreign key constraint. Other records depend on this record.",
logger);
}
else if (errorMsg.Contains("not found") || errorMsg.Contains("does not exist"))
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"RecordNotFound",
"No record found with the specified primary key.",
logger);
}

return McpResponseBuilder.BuildErrorResult("DatabaseError", $"Database error: {dbEx.Message}", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", $"Database error: {dbEx.Message}", logger);
}
catch (InvalidOperationException ioEx) when (ioEx.Message.Contains("connection", StringComparison.OrdinalIgnoreCase))
{
// Handle connection-related issues
logger?.LogError(ioEx, "Database connection error");
return McpResponseBuilder.BuildErrorResult("ConnectionError", "Failed to connect to the database.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "ConnectionError", "Failed to connect to the database.", logger);
}
catch (TimeoutException timeoutEx)
{
// Handle query timeout
logger?.LogError(timeoutEx, "Delete operation timeout for {Entity}", entityName);
return McpResponseBuilder.BuildErrorResult("TimeoutError", "The delete operation timed out.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "TimeoutError", "The delete operation timed out.", logger);
}
catch (Exception ex)
{
Expand All @@ -289,6 +298,7 @@ public async Task<CallToolResult> ExecuteAsync(
{
string keyDetails = McpJsonHelper.FormatKeyDetails(keys);
return McpResponseBuilder.BuildErrorResult(
toolName,
"RecordNotFound",
$"No entity found with the given key {keyDetails}.",
logger);
Expand Down Expand Up @@ -325,18 +335,19 @@ public async Task<CallToolResult> ExecuteAsync(
}
catch (OperationCanceledException)
{
return McpResponseBuilder.BuildErrorResult("OperationCanceled", "The delete operation was canceled.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "OperationCanceled", "The delete operation was canceled.", logger);
}
catch (ArgumentException argEx)
{
return McpResponseBuilder.BuildErrorResult("InvalidArguments", argEx.Message, logger);
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", argEx.Message, logger);
}
catch (Exception ex)
{
ILogger<DeleteRecordTool>? innerLogger = serviceProvider.GetService<ILogger<DeleteRecordTool>>();
innerLogger?.LogError(ex, "Unexpected error in DeleteRecordTool.");

return McpResponseBuilder.BuildErrorResult(
toolName,
"UnexpectedError",
"An unexpected error occurred during the delete operation.",
logger);
Expand Down
Loading
Loading