Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
2898076
Just before logging
Aug 27, 2025
17e9728
Rearrange
Aug 28, 2025
6e1abe5
git should be ignored
Aug 28, 2025
26b40c1
Testing GQL Schema
Aug 28, 2025
a017772
broken but nice
Aug 28, 2025
e81dbeb
basic working
Aug 28, 2025
a1f3d75
Just before we format the schema
Aug 28, 2025
b4a4447
Working with LIST
Aug 28, 2025
123f5ca
PRE
Aug 28, 2025
ad64ad0
Working again
Aug 29, 2025
8d015c0
Updated health
Aug 29, 2025
db72d87
JSON based Dynamic tools configuration based
souvikghosh04 Sep 3, 2025
3948822
Tweaking tooling adding tests
Sep 8, 2025
04b19f9
Updated mege
Sep 8, 2025
66bba27
Revert "Updated mege"
souvikghosh04 Sep 9, 2025
ba6909f
Revert "Merge branch 'jerry-mcp-core' of https://github.com/Azure/dat…
souvikghosh04 Sep 9, 2025
ddca78b
DAB MCP Runtime (#2866)
souvikghosh04 Sep 9, 2025
5e06a6e
Delete dab_aci_deploy.ps1
souvikghosh04 Sep 9, 2025
6ef8a3a
Delete dab_aca_deploy.ps1
souvikghosh04 Sep 9, 2025
55e3e61
Backmerge
souvikghosh04 Sep 9, 2025
4cb06b0
Merge branch 'main' into jerry-mcp-core
RubenCerna2079 Sep 10, 2025
d63d999
Added mechanism to implement tools and configure them in generic way
souvikghosh04 Sep 10, 2025
deac452
Merge branch 'jerry-mcp-core' of https://github.com/Azure/data-api-bu…
souvikghosh04 Sep 10, 2025
046d04a
Auto registration of tools and refactoring
souvikghosh04 Sep 11, 2025
0796e32
Refactored unwanted files and logic
souvikghosh04 Sep 11, 2025
78baf4a
Refactoring and removed gitignore, installcredprovider.ps1
souvikghosh04 Sep 15, 2025
2362084
Refactoring file structures
souvikghosh04 Sep 15, 2025
c212bdd
Refactoring- files structures, unwanted changes
souvikghosh04 Sep 15, 2025
540a025
Fix test formatting errors
RubenCerna2079 Sep 11, 2025
07e5945
Fix formatting for tests part 2
RubenCerna2079 Sep 11, 2025
00916ed
Fix ConfigValidationUnitTests
RubenCerna2079 Sep 12, 2025
ae99b9a
Fix RuntimeConfigValidator tests
RubenCerna2079 Sep 17, 2025
78809e4
Backmerge, removed health check and schema logic due to errors
souvikghosh04 Sep 17, 2025
f093688
Merge branch 'jerry-mcp-core' of https://github.com/Azure/data-api-bu…
souvikghosh04 Sep 17, 2025
be77230
Add MCP Runtime serialization/deserialization and fix tests related t…
RubenCerna2079 Sep 19, 2025
9a70ac3
Fix MCP runtime config
souvikghosh04 Sep 19, 2025
68a7119
Revert "Add MCP Runtime serialization/deserialization and fix tests r…
souvikghosh04 Sep 19, 2025
89777bf
Fixed MCP runtime configs
souvikghosh04 Sep 19, 2025
6f3b494
Backmerge and fixes on MCP runtime config
souvikghosh04 Sep 19, 2025
590ddd9
Merge branch 'main' into jerry-mcp-core
souvikghosh04 Sep 19, 2025
c757d8e
PR reviews and fixes
souvikghosh04 Sep 19, 2025
416b5e4
Merge branch 'jerry-mcp-core' of https://github.com/Azure/data-api-bu…
souvikghosh04 Sep 19, 2025
9c7c95b
Fix CLI commands
RubenCerna2079 Sep 19, 2025
8985d5e
Fix formatting error
RubenCerna2079 Sep 19, 2025
395f765
[MCP] Clean up for Cli and Cli.Tests packages (#2879)
anushakolan Sep 22, 2025
fd5926f
rename -record to -entity
souvikghosh04 Sep 22, 2025
54ef980
Addressed some review comments
souvikghosh04 Sep 22, 2025
b139945
Update execute-entity and rest to -record
souvikghosh04 Sep 23, 2025
b71c382
set default true for all and write to JSON if user modified
souvikghosh04 Sep 23, 2025
2423646
rename execute-record to execute-entity
souvikghosh04 Sep 23, 2025
3f0cdd7
review comments- nits, null checks
souvikghosh04 Sep 24, 2025
4f18f6e
Update src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
souvikghosh04 Sep 24, 2025
f71232a
use DEFAULT_PATH constant for mcp endpoint
souvikghosh04 Sep 24, 2025
95ff107
Merge branch 'jerry-mcp-core' of https://github.com/Azure/data-api-bu…
souvikghosh04 Sep 24, 2025
629d3fb
Fixed unit tests
souvikghosh04 Sep 24, 2025
b3040ad
fix formatting
souvikghosh04 Sep 24, 2025
b623589
Fix the incorrectly changed log messages
Aniruddh25 Sep 24, 2025
64b1371
Remove nullable Path property
Aniruddh25 Sep 24, 2025
f0e6e85
Apply suggestions from code review
Aniruddh25 Sep 24, 2025
3b893c8
Keeping the parameters UpperCases to align with other RuntimeOptions …
Aniruddh25 Sep 25, 2025
2eacd64
Draft PR: Service Tests Fix (#2884)
RubenCerna2079 Sep 25, 2025
a29485c
Fix mssql snapshot
RubenCerna2079 Sep 25, 2025
8080c83
Addressed review comments. mostly nits
souvikghosh04 Sep 25, 2025
b4d7bd6
Fix failing test by reverting to original version
souvikghosh04 Sep 25, 2025
fd9d163
Fix formatting errors
souvikghosh04 Sep 25, 2025
44a59f5
fixed the summary
souvikghosh04 Sep 25, 2025
96d8f8f
fix formatting
souvikghosh04 Sep 25, 2025
30120d8
added execute-entity
souvikghosh04 Sep 26, 2025
87a0570
Backmerge
souvikghosh04 Sep 26, 2025
11f3b0b
Added delete-record and execute-entity tools
souvikghosh04 Sep 26, 2025
1af357c
revert local dev changes
souvikghosh04 Sep 26, 2025
81e719e
revert local dev changes
souvikghosh04 Sep 26, 2025
5567fa4
Delete src/McpRuntimeConfig.cs
souvikghosh04 Sep 26, 2025
db05a89
revert local dev changes
souvikghosh04 Sep 26, 2025
38cbb72
Merge branch 'Usr/sogh/mcpdev' of https://github.com/Azure/data-api-b…
souvikghosh04 Sep 26, 2025
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
285 changes: 285 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Mcp.Model;
using Azure.DataApiBuilder.Service.Exceptions;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
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.
/// </summary>
public class DeleteRecordTool : IMcpTool
{
public ToolType ToolType { get; } = ToolType.BuiltIn;

public Tool GetToolMetadata()
{
return new Tool
{
Name = "delete-record",
Description = "Deletes records from a table or view based on specified conditions",
InputSchema = JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
entityName = new
{
type = "string",
description = "Name of the entity (table/view) as configured in dab-config"
},
primaryKey = new
{
type = "object",
description = "Primary key values to identify the record(s) to delete",
additionalProperties = true
},
filter = new
{
type = "string",
description = "Optional WHERE clause conditions (e.g., 'age > 18 AND status = \"active\"')"
}
},
required = new[] { "entityName" },
oneOf = new[]
{
new { required = new[] { "primaryKey" } },
new { required = new[] { "filter" } }
}
})
};
}

public async Task<CallToolResult> ExecuteAsync(
JsonDocument? arguments,
IServiceProvider serviceProvider,
CancellationToken cancellationToken = default)
{
try
{
if (arguments?.RootElement.ValueKind != JsonValueKind.Object)
{
return CreateErrorResult("Invalid arguments: expected object");
}

// Extract entity name
if (!arguments.RootElement.TryGetProperty("entityName", out JsonElement entityNameElement) ||
entityNameElement.ValueKind != JsonValueKind.String)
{
return CreateErrorResult("Missing or invalid 'entityName' parameter");
}

string entityName = entityNameElement.GetString()!;

// Get runtime configuration
RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService<RuntimeConfigProvider>();
RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig();

// Validate entity exists
if (!runtimeConfig.Entities.TryGetValue(entityName, out Entity? entity))
{
return CreateErrorResult($"Entity '{entityName}' not found in configuration");
}

// Validate it's a table or view
if (entity.Source.Type != EntitySourceType.Table && entity.Source.Type != EntitySourceType.View)
{
return CreateErrorResult($"Entity '{entityName}' is not a table or view. Use 'execute-entity' for stored procedures.");
}

// Check permissions for delete action
bool hasDeletePermission = entity.Permissions?.Any(p =>
p.Actions?.Any(a => a.Action == EntityActionOperation.Delete) == true) == true;

if (!hasDeletePermission)
{
return CreateErrorResult($"Delete operation is not permitted for entity '{entityName}'");
}

// Get table/view name from entity configuration
string tableName = entity.Source.Object!;

// Build WHERE clause
string whereClause;
Dictionary<string, object?> parameters = new();

bool hasPrimaryKey = arguments.RootElement.TryGetProperty("primaryKey", out JsonElement primaryKeyElement) &&
primaryKeyElement.ValueKind == JsonValueKind.Object;
bool hasFilter = arguments.RootElement.TryGetProperty("filter", out JsonElement filterElement) &&
filterElement.ValueKind == JsonValueKind.String;

if (!hasPrimaryKey && !hasFilter)
{
return CreateErrorResult("Either 'primaryKey' or 'filter' must be provided");
}

if (hasPrimaryKey && hasFilter)
{
return CreateErrorResult("Cannot specify both 'primaryKey' and 'filter'. Use one or the other.");
}

if (hasPrimaryKey)
{
// Build WHERE clause from primary key
List<string> conditions = new();
foreach (JsonProperty prop in primaryKeyElement.EnumerateObject())
{
string paramName = $"pk_{prop.Name}";
conditions.Add($"[{prop.Name}] = @{paramName}");
parameters[paramName] = GetParameterValue(prop.Value);
}

whereClause = string.Join(" AND ", conditions);
}
else
{
// Use the provided filter
whereClause = filterElement.GetString()!;

// Basic SQL injection prevention - check for dangerous patterns
string[] dangerousPatterns = { "--", "/*", "*/", "xp_", "sp_", "exec", "execute", "drop", "create", "alter" };
string filterLower = whereClause.ToLower();
foreach (string pattern in dangerousPatterns)
{
if (filterLower.Contains(pattern))
{
return CreateErrorResult($"Filter contains potentially dangerous SQL pattern: '{pattern}'");
}
}
}

// Get the database connection string
string dataSourceName = runtimeConfig.DefaultDataSourceName;
DataSource? dataSource = runtimeConfig.GetDataSourceFromDataSourceName(dataSourceName);
string connectionString = dataSource.ConnectionString;

// Execute delete operation
int rowsAffected = 0;
List<Dictionary<string, object?>> deletedRecords = new();

using (SqlConnection connection = new(connectionString))
{
await connection.OpenAsync(cancellationToken);

// First, get a preview of what will be deleted (for response)
string selectQuery = $"SELECT * FROM [{tableName}] WHERE {whereClause}";

using (SqlCommand selectCommand = new(selectQuery, connection))
{
// Add parameters if using primary key
foreach (KeyValuePair<string, object?> param in parameters)
{
selectCommand.Parameters.AddWithValue($"@{param.Key}", param.Value ?? DBNull.Value);
}

using (SqlDataReader reader = await selectCommand.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
Dictionary<string, object?> row = new();

for (int i = 0; i < reader.FieldCount; i++)
{
string columnName = reader.GetName(i);
object? value = reader.IsDBNull(i) ? null : reader.GetValue(i);
row[columnName] = value;
}

deletedRecords.Add(row);
}
}
}

// Now execute the delete
string deleteQuery = $"DELETE FROM [{tableName}] WHERE {whereClause}";

using (SqlCommand deleteCommand = new(deleteQuery, connection))
{
// Add parameters if using primary key
foreach (KeyValuePair<string, object?> param in parameters)
{
deleteCommand.Parameters.AddWithValue($"@{param.Key}", param.Value ?? DBNull.Value);
}

rowsAffected = await deleteCommand.ExecuteNonQueryAsync(cancellationToken);
}
}

// Format the response
var response = new
{
success = true,
entity = entityName,
table = tableName,
rowsDeleted = rowsAffected,
deletedRecords = deletedRecords
};

return new CallToolResult
{
Content = new List<ContentBlock>
{
new TextContentBlock
{
Type = "text",
Text = JsonSerializer.Serialize(response, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
})
}
}
};
}
catch (SqlException ex)
{
return CreateErrorResult($"Database error: {ex.Message}");
}
catch (DataApiBuilderException ex)
{
return CreateErrorResult($"DAB Error: {ex.Message}");
}
catch (Exception ex)
{
return CreateErrorResult($"Unexpected error: {ex.Message}");
}
}

private static object? GetParameterValue(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt32(out int intValue) ? intValue : element.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => element.ToString()
};
}

private static CallToolResult CreateErrorResult(string errorMessage)
{
return new CallToolResult
{
Content = new List<ContentBlock>
{
new TextContentBlock
{
Type = "text",
Text = JsonSerializer.Serialize(new { error = errorMessage })
}
},
IsError = true
};
}
}
}
Loading
Loading