Skip to content
Merged
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2fc665d
allow unused cache-control options without error
aaronburtle Jun 20, 2025
50e54aa
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Jun 25, 2025
455ff18
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Jun 26, 2025
e0574bf
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Jul 11, 2025
033eeb0
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Jul 23, 2025
c7ad08f
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Aug 27, 2025
049b23e
...
aaronburtle Sep 11, 2025
802eb53
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Sep 26, 2025
79ba263
ue rest objects for create tool
aaronburtle Sep 29, 2025
9366f39
update auth and factor out error messages
aaronburtle Oct 9, 2025
4e71c25
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Oct 9, 2025
bd95b3e
Merge branch 'main' into dev/aaronburtle/CreateRecordMCPTool
aaronburtle Oct 9, 2025
4d4824f
address comments, fix error messaging, better response, format
aaronburtle Oct 17, 2025
0481048
Merge branch 'main' into dev/aaronburtle/CreateRecordMCPTool
Aniruddh25 Oct 21, 2025
c3dabe3
Merge branch 'main' of github.com:Azure/data-api-builder
aaronburtle Oct 21, 2025
e900cb1
Merge branch 'main' into dev/aaronburtle/CreateRecordMCPTool
aaronburtle Oct 21, 2025
59bc70e
addressing comments
aaronburtle Oct 21, 2025
7561588
Merge branch 'dev/aaronburtle/CreateRecordMCPTool' of github.com:Azur…
aaronburtle Oct 21, 2025
eda47b4
align create and objectresult message
aaronburtle Oct 21, 2025
61f60c9
handle edge cases in returned result
aaronburtle Oct 22, 2025
e0f2a11
format
aaronburtle Oct 22, 2025
90b7ebd
format
aaronburtle Oct 22, 2025
d049d53
use built in response function
aaronburtle Oct 23, 2025
7f9f8f4
align messaging
aaronburtle Oct 23, 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
221 changes: 202 additions & 19 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,37 @@
// Licensed under the MIT License.

using System.Text.Json;
using Azure.DataApiBuilder.Auth;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Authorization;
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.Service.Exceptions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using static Azure.DataApiBuilder.Mcp.Model.McpEnums;

namespace Azure.DataApiBuilder.Mcp.BuiltInTools
{
public class CreateRecordTool : IMcpTool

{
public ToolType ToolType { get; } = ToolType.BuiltIn;

public Tool GetToolMetadata()
{
return new Tool
{
Name = "create-record",
Name = "create_record",
Description = "Creates a new record in the specified entity.",
InputSchema = JsonSerializer.Deserialize<JsonElement>(
@"{
Expand All @@ -37,51 +53,218 @@ public Tool GetToolMetadata()
};
}

public Task<CallToolResult> ExecuteAsync(
public async Task<CallToolResult> ExecuteAsync(
JsonDocument? arguments,
IServiceProvider serviceProvider,
CancellationToken cancellationToken = default)
{
ILogger<CreateRecordTool>? logger = serviceProvider.GetService<ILogger<CreateRecordTool>>();
if (arguments == null)
{
return Task.FromResult(new CallToolResult
{
Content = [new TextContentBlock { Type = "text", Text = "Error: No arguments provided" }]
});
return BuildErrorResult("Invalid Arguments", "No arguments provided", logger);
}

try
{
// Extract arguments
cancellationToken.ThrowIfCancellationRequested();
JsonElement root = arguments.RootElement;

if (!root.TryGetProperty("entity", out JsonElement entityElement) ||
!root.TryGetProperty("data", out JsonElement dataElement))
{
return Task.FromResult(new CallToolResult
{
Content = [new TextContentBlock { Type = "text", Text = "Error: Missing required arguments 'entity' or 'data'" }]
});
return BuildErrorResult("Invalid Arguments", "Missing required arguments 'entity' or 'data'", logger);
}

string entityName = entityElement.GetString() ?? string.Empty;
if (string.IsNullOrEmpty(entityName))
{
return BuildErrorResult("Invalid Arguments", "Entity name cannot be empty", logger);
}

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

string dataSourceName;
try
{
dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
}
catch (DataApiBuilderException)
{
return BuildErrorResult("Invalid Configuration", $"Entity '{entityName}' not found in configuration", logger);
}

IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>();
ISqlMetadataProvider sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName);

DatabaseObject dbObject;
try
{
dbObject = sqlMetadataProvider.GetDatabaseObjectByKey(entityName);
}
catch (Exception)
{
return BuildErrorResult("Invalid Configuration", $"Database object for entity '{entityName}' not found", logger);
}


// Create an HTTP context for authorization
IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext httpContext = httpContextAccessor.HttpContext ?? new DefaultHttpContext();
IAuthorizationResolver authorizationResolver = serviceProvider.GetRequiredService<IAuthorizationResolver>();

if (httpContext is null || !authorizationResolver.IsValidRoleContext(httpContext))
{
return BuildErrorResult("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 BuildErrorResult("PermissionDenied", authError, logger);
}

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

// TODO: Implement actual create logic using DAB's internal services
// For now, return a placeholder response
string result = $"Would create record in entity '{entityName}' with data: {dataElement.GetRawText()}";
RequestValidator requestValidator = serviceProvider.GetRequiredService<RequestValidator>();

// Only validate tables
if (dbObject.SourceType is EntitySourceType.Table)
{
try
{
requestValidator.ValidateInsertRequestContext(insertRequestContext);
}
catch (DataApiBuilderException ex)
{
return new CallToolResult
{
Content = [new TextContentBlock { Type = "text", Text = $"Error: Request validation failed: {ex.Message}" }]
};
}
}

IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService<IMutationEngineFactory>();
DatabaseType databaseType = sqlMetadataProvider.GetDatabaseType();
IMutationEngine mutationEngine = mutationEngineFactory.GetMutationEngine(databaseType);

return Task.FromResult(new CallToolResult
IActionResult? result = await mutationEngine.ExecuteAsync(insertRequestContext);

if (result is CreatedResult createdResult)
{
return new CallToolResult
{
Content = [new TextContentBlock
{
Type = "text",
Text = $"Successfully created record in entity '{entityName}'. Result: {JsonSerializer.Serialize(createdResult.Value)}"
}]
};
}
else if (result is ObjectResult objectResult)
{
Content = [new TextContentBlock { Type = "text", Text = result }]
});
return new CallToolResult
{
Content = [new TextContentBlock
{
Type = "text",
Text = $"Record creation completed with status {objectResult.StatusCode}. Result: {JsonSerializer.Serialize(objectResult.Value)}"
}]
};
}
else
{
return new CallToolResult
{
Content = [new TextContentBlock { Type = "text", Text = $"Successfully created record in entity '{entityName}'" }]
};
}
}
catch (Exception ex)
{
return Task.FromResult(new CallToolResult
return new CallToolResult
{
Content = [new TextContentBlock { Type = "text", Text = $"Error: {ex.Message}" }]
});
};
}
}

private static bool TryResolveAuthorizedRole(
HttpContext httpContext,
IAuthorizationResolver authorizationResolver,
string entityName,
out string error)
{
error = string.Empty;

string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString();

if (string.IsNullOrWhiteSpace(roleHeader))
{
error = "Client role header is missing or empty.";
return false;
}

string[] roles = roleHeader
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();

if (roles.Length == 0)
{
error = "Client role header is missing or empty.";
return false;
}

foreach (string role in roles)
{
bool allowed = authorizationResolver.AreRoleAndOperationDefinedForEntity(
entityName, role, EntityActionOperation.Insert);

if (allowed)
{
return true;
}
}

error = "You do not have permission to create records for this entity.";
return false;
}

private static CallToolResult BuildErrorResult(
string errorType,
string message,
ILogger? logger)
{
Dictionary<string, object?> errorObj = new()
{
["status"] = "error",
["error"] = new Dictionary<string, object?>
{
["type"] = errorType,
["message"] = message
}
};

string output = JsonSerializer.Serialize(errorObj);

logger?.LogWarning("UpdateRecordTool error {ErrorType}: {Message}", errorType, message);

return new CallToolResult
{
Content =
[
new TextContentBlock { Type = "text", Text = output }
]
};
}
}
}
Loading