Skip to content
Merged
Changes from 19 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
203 changes: 183 additions & 20 deletions src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@
// 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 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;

Expand All @@ -16,7 +30,7 @@ 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 +51,200 @@ 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 Utils.McpResponseBuilder.BuildErrorResult("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);
}

if (runtimeConfig.McpDmlTools?.CreateRecord != true)
{
return Utils.McpResponseBuilder.BuildErrorResult(
"ToolDisabled",
"The create_record tool is disabled in the configuration.",
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 Utils.McpResponseBuilder.BuildErrorResult("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);
}

string dataSourceName;
try
{
dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
}
catch (Exception)
{
return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"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 Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"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 Utils.McpResponseBuilder.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 Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", authError, logger);
}

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

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

// Only validate tables
if (dbObject.SourceType is EntitySourceType.Table)
{
try
{
requestValidator.ValidateInsertRequestContext(insertRequestContext);
}
catch (Exception ex)
{
return Utils.McpResponseBuilder.BuildErrorResult("ValidationFailed", $"Request validation failed: {ex.Message}", logger);
}
}
else
{
return Utils.McpResponseBuilder.BuildErrorResult(
"InvalidCreateTarget",
"The create_record tool is only available for tables.",
logger);
}

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

// 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()}";
IActionResult? result = await mutationEngine.ExecuteAsync(insertRequestContext);

return Task.FromResult(new CallToolResult
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)
{
// Check if this is an error status code (400+ range)
return new CallToolResult
{
Content = [new TextContentBlock
{
Type = "text",
Text = $"Successfully created record in entity '{entityName}'. Result: {JsonSerializer.Serialize(objectResult.Value)}"
}]
};
}
else
{
Content = [new TextContentBlock { Type = "text", Text = result }]
});
return new CallToolResult
{
Content = [new TextContentBlock { Type = "text", Text = $"Successfully created record in entity '{entityName}'" }]
};
}
}
catch (Exception ex)
{
return Task.FromResult(new CallToolResult
return Utils.McpResponseBuilder.BuildErrorResult("Error", $"Error: {ex.Message}", logger);
}
}

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.Create);

if (allowed)
{
Content = [new TextContentBlock { Type = "text", Text = $"Error: {ex.Message}" }]
});
return true;
}
}

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