-
Notifications
You must be signed in to change notification settings - Fork 281
[MCP] Adding delete-record #2889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
souvikghosh04
wants to merge
84
commits into
main
Choose a base branch
from
Usr/sogh/mcpdev
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
84 commits
Select commit
Hold shift + click to select a range
2898076
Just before logging
17e9728
Rearrange
6e1abe5
git should be ignored
26b40c1
Testing GQL Schema
a017772
broken but nice
e81dbeb
basic working
a1f3d75
Just before we format the schema
b4a4447
Working with LIST
123f5ca
PRE
ad64ad0
Working again
8d015c0
Updated health
db72d87
JSON based Dynamic tools configuration based
souvikghosh04 3948822
Tweaking tooling adding tests
04b19f9
Updated mege
66bba27
Revert "Updated mege"
souvikghosh04 ba6909f
Revert "Merge branch 'jerry-mcp-core' of https://github.com/Azure/dat…
souvikghosh04 ddca78b
DAB MCP Runtime (#2866)
souvikghosh04 5e06a6e
Delete dab_aci_deploy.ps1
souvikghosh04 6ef8a3a
Delete dab_aca_deploy.ps1
souvikghosh04 55e3e61
Backmerge
souvikghosh04 4cb06b0
Merge branch 'main' into jerry-mcp-core
RubenCerna2079 d63d999
Added mechanism to implement tools and configure them in generic way
souvikghosh04 deac452
Merge branch 'jerry-mcp-core' of https://github.com/Azure/data-api-bu…
souvikghosh04 046d04a
Auto registration of tools and refactoring
souvikghosh04 0796e32
Refactored unwanted files and logic
souvikghosh04 78baf4a
Refactoring and removed gitignore, installcredprovider.ps1
souvikghosh04 2362084
Refactoring file structures
souvikghosh04 c212bdd
Refactoring- files structures, unwanted changes
souvikghosh04 540a025
Fix test formatting errors
RubenCerna2079 07e5945
Fix formatting for tests part 2
RubenCerna2079 00916ed
Fix ConfigValidationUnitTests
RubenCerna2079 ae99b9a
Fix RuntimeConfigValidator tests
RubenCerna2079 78809e4
Backmerge, removed health check and schema logic due to errors
souvikghosh04 f093688
Merge branch 'jerry-mcp-core' of https://github.com/Azure/data-api-bu…
souvikghosh04 be77230
Add MCP Runtime serialization/deserialization and fix tests related t…
RubenCerna2079 9a70ac3
Fix MCP runtime config
souvikghosh04 68a7119
Revert "Add MCP Runtime serialization/deserialization and fix tests r…
souvikghosh04 89777bf
Fixed MCP runtime configs
souvikghosh04 6f3b494
Backmerge and fixes on MCP runtime config
souvikghosh04 590ddd9
Merge branch 'main' into jerry-mcp-core
souvikghosh04 c757d8e
PR reviews and fixes
souvikghosh04 416b5e4
Merge branch 'jerry-mcp-core' of https://github.com/Azure/data-api-bu…
souvikghosh04 9c7c95b
Fix CLI commands
RubenCerna2079 8985d5e
Fix formatting error
RubenCerna2079 395f765
[MCP] Clean up for Cli and Cli.Tests packages (#2879)
anushakolan fd5926f
rename -record to -entity
souvikghosh04 54ef980
Addressed some review comments
souvikghosh04 b139945
Update execute-entity and rest to -record
souvikghosh04 b71c382
set default true for all and write to JSON if user modified
souvikghosh04 2423646
rename execute-record to execute-entity
souvikghosh04 3f0cdd7
review comments- nits, null checks
souvikghosh04 4f18f6e
Update src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
souvikghosh04 f71232a
use DEFAULT_PATH constant for mcp endpoint
souvikghosh04 95ff107
Merge branch 'jerry-mcp-core' of https://github.com/Azure/data-api-bu…
souvikghosh04 629d3fb
Fixed unit tests
souvikghosh04 b3040ad
fix formatting
souvikghosh04 b623589
Fix the incorrectly changed log messages
Aniruddh25 64b1371
Remove nullable Path property
Aniruddh25 f0e6e85
Apply suggestions from code review
Aniruddh25 3b893c8
Keeping the parameters UpperCases to align with other RuntimeOptions …
Aniruddh25 2eacd64
Draft PR: Service Tests Fix (#2884)
RubenCerna2079 a29485c
Fix mssql snapshot
RubenCerna2079 8080c83
Addressed review comments. mostly nits
souvikghosh04 b4d7bd6
Fix failing test by reverting to original version
souvikghosh04 fd9d163
Fix formatting errors
souvikghosh04 44a59f5
fixed the summary
souvikghosh04 96d8f8f
fix formatting
souvikghosh04 30120d8
added execute-entity
souvikghosh04 87a0570
Backmerge
souvikghosh04 11f3b0b
Added delete-record and execute-entity tools
souvikghosh04 1af357c
revert local dev changes
souvikghosh04 81e719e
revert local dev changes
souvikghosh04 5567fa4
Delete src/McpRuntimeConfig.cs
souvikghosh04 db05a89
revert local dev changes
souvikghosh04 38cbb72
Merge branch 'Usr/sogh/mcpdev' of https://github.com/Azure/data-api-b…
souvikghosh04 9028270
Backmerge
souvikghosh04 cbc83ff
Refactoring DeleteRecordTool
souvikghosh04 3165308
Remove ExecuteEntityTool
souvikghosh04 8aec84a
remove var
souvikghosh04 747f018
Fix whitespace formatting
souvikghosh04 23e6f95
Update src/Azure.DataApiBuilder.Mcp/Utils/McpJsonHelper.cs
souvikghosh04 986b39f
Update src/Azure.DataApiBuilder.Mcp/Utils/McpArgumentParser.cs
souvikghosh04 64a684b
Update src/Azure.DataApiBuilder.Mcp/Utils/McpArgumentParser.cs
souvikghosh04 9ab7ba9
Addressed review comments
souvikghosh04 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
350 changes: 350 additions & 0 deletions
350
src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,350 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
using System.Data.Common; | ||
using System.Text.Json; | ||
using Azure.DataApiBuilder.Auth; | ||
using Azure.DataApiBuilder.Config.DatabasePrimitives; | ||
using Azure.DataApiBuilder.Config.ObjectModel; | ||
using Azure.DataApiBuilder.Core.Configurations; | ||
using Azure.DataApiBuilder.Core.Models; | ||
using Azure.DataApiBuilder.Core.Resolvers; | ||
using Azure.DataApiBuilder.Core.Resolvers.Factories; | ||
using Azure.DataApiBuilder.Core.Services; | ||
using Azure.DataApiBuilder.Core.Services.MetadataProviders; | ||
using Azure.DataApiBuilder.Mcp.Model; | ||
using Azure.DataApiBuilder.Mcp.Utils; | ||
using Azure.DataApiBuilder.Service.Exceptions; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.Data.SqlClient; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using ModelContextProtocol.Protocol; | ||
using static Azure.DataApiBuilder.Mcp.Model.McpEnums; | ||
|
||
namespace Azure.DataApiBuilder.Mcp.BuiltInTools | ||
{ | ||
/// <summary> | ||
/// Tool to delete records from a table/view entity configured in DAB. | ||
/// Supports both simple and composite primary keys. | ||
/// </summary> | ||
public class DeleteRecordTool : IMcpTool | ||
{ | ||
/// <summary> | ||
/// Gets the type of the tool, which is BuiltIn for this implementation. | ||
/// </summary> | ||
public ToolType ToolType { get; } = ToolType.BuiltIn; | ||
|
||
/// <summary> | ||
/// Gets the metadata for the delete-record tool, including its name, description, and input schema. | ||
/// </summary> | ||
public Tool GetToolMetadata() | ||
{ | ||
return new Tool | ||
{ | ||
Name = "delete-record", | ||
Description = "Deletes a record from a table based on primary key or composite key", | ||
InputSchema = JsonSerializer.Deserialize<JsonElement>( | ||
@"{ | ||
""type"": ""object"", | ||
""properties"": { | ||
""entity"": { | ||
""type"": ""string"", | ||
""description"": ""The name of the entity (table) as configured in dab-config. Required."" | ||
}, | ||
""keys"": { | ||
""type"": ""object"", | ||
""description"": ""Primary key values to identify the record to delete. For composite keys, provide all key columns as properties. Required."" | ||
} | ||
}, | ||
""required"": [""entity"", ""keys""] | ||
}" | ||
) | ||
}; | ||
} | ||
|
||
/// <summary> | ||
/// Executes the delete-record tool, deleting an existing record in the specified entity using provided keys. | ||
/// </summary> | ||
public async Task<CallToolResult> ExecuteAsync( | ||
JsonDocument? arguments, | ||
IServiceProvider serviceProvider, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
ILogger<DeleteRecordTool>? logger = serviceProvider.GetService<ILogger<DeleteRecordTool>>(); | ||
|
||
try | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
// 1) Parsing & basic argument validation | ||
if (arguments is null) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult("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); | ||
} | ||
|
||
// 2) Resolve required services & configuration | ||
RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService<RuntimeConfigProvider>(); | ||
RuntimeConfig config = runtimeConfigProvider.GetConfig(); | ||
|
||
IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>(); | ||
IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService<IMutationEngineFactory>(); | ||
|
||
// 3) Resolve metadata for entity existence check | ||
string dataSourceName; | ||
ISqlMetadataProvider sqlMetadataProvider; | ||
|
||
try | ||
{ | ||
dataSourceName = config.GetDataSourceNameFromEntityName(entityName); | ||
sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName); | ||
} | ||
catch (Exception) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult("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); | ||
} | ||
|
||
// 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); | ||
} | ||
|
||
// 4) Authorization | ||
IAuthorizationResolver authResolver = serviceProvider.GetRequiredService<IAuthorizationResolver>(); | ||
IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); | ||
HttpContext? httpContext = httpContextAccessor.HttpContext; | ||
|
||
if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError)) | ||
souvikghosh04 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {roleError}", logger); | ||
} | ||
|
||
if (!McpAuthorizationHelper.TryResolveAuthorizedRole( | ||
httpContext!, | ||
authResolver, | ||
entityName, | ||
EntityActionOperation.Delete, | ||
out string? effectiveRole, | ||
out string authError)) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {authError}", logger); | ||
} | ||
|
||
// 5) Build and validate Delete context | ||
RequestValidator requestValidator = new(metadataProviderFactory, runtimeConfigProvider); | ||
|
||
DeleteRequestContext context = new( | ||
entityName: entityName, | ||
dbo: dbObject, | ||
isList: false); | ||
|
||
foreach (KeyValuePair<string, object?> kvp in keys) | ||
{ | ||
if (kvp.Value is null) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger); | ||
} | ||
|
||
context.PrimaryKeyValuePairs[kvp.Key] = kvp.Value; | ||
} | ||
|
||
requestValidator.ValidatePrimaryKey(context); | ||
|
||
// 6) Execute | ||
DatabaseType dbType = config.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType; | ||
IMutationEngine mutationEngine = mutationEngineFactory.GetMutationEngine(dbType); | ||
|
||
IActionResult? mutationResult = null; | ||
try | ||
{ | ||
mutationResult = await mutationEngine.ExecuteAsync(context).ConfigureAwait(false); | ||
} | ||
catch (DataApiBuilderException dabEx) | ||
{ | ||
// Handle specific DAB exceptions | ||
logger?.LogError(dabEx, "Data API Builder error deleting record from {Entity}", entityName); | ||
|
||
string message = dabEx.Message; | ||
|
||
// Check for specific error patterns | ||
if (message.Contains("Could not find item with", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
string keyDetails = McpJsonHelper.FormatKeyDetails(keys); | ||
return McpResponseBuilder.BuildErrorResult( | ||
"RecordNotFound", | ||
$"No record found with the specified primary key: {keyDetails}", | ||
logger); | ||
} | ||
else if (message.Contains("violates foreign key constraint", StringComparison.OrdinalIgnoreCase) || | ||
message.Contains("REFERENCE constraint", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult( | ||
"ConstraintViolation", | ||
"Cannot delete record due to foreign key constraint. Other records depend on this record.", | ||
logger); | ||
} | ||
else if (message.Contains("permission", StringComparison.OrdinalIgnoreCase) || | ||
message.Contains("authorization", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult( | ||
"PermissionDenied", | ||
"You do not have permission to delete this record.", | ||
logger); | ||
} | ||
else if (message.Contains("invalid", StringComparison.OrdinalIgnoreCase) && | ||
message.Contains("type", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult( | ||
"InvalidArguments", | ||
"Invalid data type for one or more key values.", | ||
logger); | ||
} | ||
|
||
// For any other DAB exceptions, return the message as-is | ||
return McpResponseBuilder.BuildErrorResult( | ||
"DataApiBuilderError", | ||
dabEx.Message, | ||
logger); | ||
} | ||
catch (SqlException sqlEx) | ||
{ | ||
// Handle SQL Server specific errors | ||
logger?.LogError(sqlEx, "SQL Server error deleting record from {Entity}", entityName); | ||
string errorMessage = sqlEx.Number switch | ||
{ | ||
547 => "Cannot delete record due to foreign key constraint. Other records depend on this record.", | ||
2627 or 2601 => "Cannot delete record due to unique constraint violation.", | ||
229 or 262 => $"Permission denied to delete from table '{dbObject.FullName}'.", | ||
208 => $"Table '{dbObject.FullName}' not found in the database.", | ||
_ => $"Database error: {sqlEx.Message}" | ||
}; | ||
return McpResponseBuilder.BuildErrorResult("DatabaseError", errorMessage, logger); | ||
} | ||
catch (DbException dbEx) | ||
{ | ||
// Handle generic database exceptions (works for PostgreSQL, MySQL, etc.) | ||
logger?.LogError(dbEx, "Database error deleting record from {Entity}", entityName); | ||
|
||
// Check for common patterns in error messages | ||
string errorMsg = dbEx.Message.ToLowerInvariant(); | ||
if (errorMsg.Contains("foreign key") || errorMsg.Contains("constraint")) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult( | ||
"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( | ||
"RecordNotFound", | ||
"No record found with the specified primary key.", | ||
logger); | ||
} | ||
|
||
return McpResponseBuilder.BuildErrorResult("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); | ||
} | ||
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); | ||
} | ||
catch (Exception ex) | ||
{ | ||
string errorMsg = ex.Message ?? string.Empty; | ||
|
||
if (errorMsg.Contains("Could not find", StringComparison.OrdinalIgnoreCase) || | ||
errorMsg.Contains("record not found", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
string keyDetails = McpJsonHelper.FormatKeyDetails(keys); | ||
return McpResponseBuilder.BuildErrorResult( | ||
"RecordNotFound", | ||
$"No entity found with the given key {keyDetails}.", | ||
logger); | ||
} | ||
else | ||
{ | ||
// Re-throw unexpected exceptions | ||
throw; | ||
} | ||
} | ||
|
||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
// 7) Build response | ||
return BuildDeleteSuccessResponse(entityName, keys, mutationResult, logger); | ||
} | ||
catch (OperationCanceledException) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult("OperationCanceled", "The delete operation was canceled.", logger); | ||
} | ||
catch (ArgumentException argEx) | ||
{ | ||
return McpResponseBuilder.BuildErrorResult("InvalidArguments", argEx.Message, logger); | ||
} | ||
catch (Exception ex) | ||
souvikghosh04 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
souvikghosh04 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ILogger<DeleteRecordTool>? innerLogger = serviceProvider.GetService<ILogger<DeleteRecordTool>>(); | ||
innerLogger?.LogError(ex, "Unexpected error in DeleteRecordTool."); | ||
|
||
return McpResponseBuilder.BuildErrorResult( | ||
"UnexpectedError", | ||
"An unexpected error occurred during the delete operation.", | ||
logger); | ||
} | ||
} | ||
|
||
private static CallToolResult BuildDeleteSuccessResponse( | ||
string entityName, | ||
Dictionary<string, object?> keys, | ||
IActionResult? mutationResult, | ||
ILogger? logger) | ||
{ | ||
Dictionary<string, object?> responseData = new() | ||
{ | ||
["entity"] = entityName, | ||
["keyDetails"] = McpJsonHelper.FormatKeyDetails(keys), | ||
["message"] = "Record deleted successfully" | ||
}; | ||
|
||
// Handle different result types | ||
if (mutationResult is OkObjectResult okObjectResult) | ||
{ | ||
string rawPayloadJson = McpResponseBuilder.ExtractResultJson(okObjectResult); | ||
using JsonDocument resultDoc = JsonDocument.Parse(rawPayloadJson); | ||
JsonElement root = resultDoc.RootElement; | ||
|
||
Dictionary<string, object?> extractedData = McpJsonHelper.ExtractValuesFromEngineResult(root); | ||
if (extractedData.Count > 0) | ||
{ | ||
responseData["result"] = extractedData; | ||
} | ||
} | ||
|
||
return McpResponseBuilder.BuildSuccessResult( | ||
responseData, | ||
logger, | ||
$"DeleteRecordTool success for entity {entityName}." | ||
); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we agreed that the separations would be done with underscore
_
. I am wrong?