Skip to content

Commit 72c20d2

Browse files
aaronburtleCopilotAniruddh25souvikghosh04
authored
Cherry pick variable replacement and MCP refactor for release 1.7 (#3014)
## Why make this change? This change cherry picks the commits associated with the updating of how we do variable replacement during deserialization, adding AKV to those variables that we look for. It further includes the commits for refactoring the MCP project so that the tools have their common code shared in utility functions, and have their responses aligned. ## What is this change? Cherry picked PRs: #### Azure Key Vault Support: * #2882 * #2957 * #3004 #### MCP Refactoring: * #2986 * #2984 ## How was this tested? The PRs in question were tested against the regular test suite and had tests added to cover new code changes, as well as being manually tested e.g. mcp inspector tool. ## Sample Request(s) N/A --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Aniruddh Munde <[email protected]> Co-authored-by: Souvik Ghosh <[email protected]>
1 parent c7927fa commit 72c20d2

48 files changed

Lines changed: 1598 additions & 1064 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs

Lines changed: 37 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55
using Azure.DataApiBuilder.Auth;
66
using Azure.DataApiBuilder.Config.DatabasePrimitives;
77
using Azure.DataApiBuilder.Config.ObjectModel;
8-
using Azure.DataApiBuilder.Core.Authorization;
98
using Azure.DataApiBuilder.Core.Configurations;
109
using Azure.DataApiBuilder.Core.Models;
1110
using Azure.DataApiBuilder.Core.Resolvers;
1211
using Azure.DataApiBuilder.Core.Resolvers.Factories;
1312
using Azure.DataApiBuilder.Core.Services;
14-
using Azure.DataApiBuilder.Core.Services.MetadataProviders;
1513
using Azure.DataApiBuilder.Mcp.Model;
14+
using Azure.DataApiBuilder.Mcp.Utils;
1615
using Microsoft.AspNetCore.Http;
1716
using Microsoft.AspNetCore.Mvc;
1817
using Microsoft.Extensions.DependencyInjection;
@@ -57,79 +56,64 @@ public async Task<CallToolResult> ExecuteAsync(
5756
CancellationToken cancellationToken = default)
5857
{
5958
ILogger<CreateRecordTool>? logger = serviceProvider.GetService<ILogger<CreateRecordTool>>();
59+
string toolName = GetToolMetadata().Name;
6060
if (arguments == null)
6161
{
62-
return Utils.McpResponseBuilder.BuildErrorResult("Invalid Arguments", "No arguments provided", logger);
62+
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger);
6363
}
6464

6565
RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService<RuntimeConfigProvider>();
6666
if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
6767
{
68-
return Utils.McpResponseBuilder.BuildErrorResult("Invalid Configuration", "Runtime configuration not available", logger);
68+
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", "Runtime configuration not available.", logger);
6969
}
7070

7171
if (runtimeConfig.McpDmlTools?.CreateRecord != true)
7272
{
73-
return Utils.McpResponseBuilder.BuildErrorResult(
74-
"ToolDisabled",
75-
"The create_record tool is disabled in the configuration.",
76-
logger);
73+
return McpErrorHelpers.ToolDisabled(toolName, logger);
7774
}
7875

7976
try
8077
{
8178
cancellationToken.ThrowIfCancellationRequested();
8279
JsonElement root = arguments.RootElement;
8380

84-
if (!root.TryGetProperty("entity", out JsonElement entityElement) ||
85-
!root.TryGetProperty("data", out JsonElement dataElement))
81+
if (!McpArgumentParser.TryParseEntityAndData(root, out string entityName, out JsonElement dataElement, out string parseError))
8682
{
87-
return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Missing required arguments 'entity' or 'data'", logger);
83+
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
8884
}
8985

90-
string entityName = entityElement.GetString() ?? string.Empty;
91-
if (string.IsNullOrWhiteSpace(entityName))
86+
if (!McpMetadataHelper.TryResolveMetadata(
87+
entityName,
88+
runtimeConfig,
89+
serviceProvider,
90+
out ISqlMetadataProvider sqlMetadataProvider,
91+
out DatabaseObject dbObject,
92+
out string dataSourceName,
93+
out string metadataError))
9294
{
93-
return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Entity name cannot be empty", logger);
94-
}
95-
96-
string dataSourceName;
97-
try
98-
{
99-
dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
100-
}
101-
catch (Exception)
102-
{
103-
return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger);
104-
}
105-
106-
IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>();
107-
ISqlMetadataProvider sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName);
108-
109-
DatabaseObject dbObject;
110-
try
111-
{
112-
dbObject = sqlMetadataProvider.GetDatabaseObjectByKey(entityName);
113-
}
114-
catch (Exception)
115-
{
116-
return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger);
95+
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger);
11796
}
11897

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

124-
if (httpContext is null || !authorizationResolver.IsValidRoleContext(httpContext))
103+
if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authorizationResolver, out string roleCtxError))
125104
{
126-
return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger);
105+
return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", roleCtxError, logger);
127106
}
128107

129-
// Validate that we have at least one role authorized for create
130-
if (!TryResolveAuthorizedRole(httpContext, authorizationResolver, entityName, out string authError))
108+
if (!McpAuthorizationHelper.TryResolveAuthorizedRole(
109+
httpContext,
110+
authorizationResolver,
111+
entityName,
112+
EntityActionOperation.Create,
113+
out string? effectiveRole,
114+
out string authError))
131115
{
132-
return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", authError, logger);
116+
return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", authError, logger);
133117
}
134118

135119
JsonElement insertPayloadRoot = dataElement.Clone();
@@ -150,12 +134,13 @@ public async Task<CallToolResult> ExecuteAsync(
150134
}
151135
catch (Exception ex)
152136
{
153-
return Utils.McpResponseBuilder.BuildErrorResult("ValidationFailed", $"Request validation failed: {ex.Message}", logger);
137+
return McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger);
154138
}
155139
}
156140
else
157141
{
158-
return Utils.McpResponseBuilder.BuildErrorResult(
142+
return McpResponseBuilder.BuildErrorResult(
143+
toolName,
159144
"InvalidCreateTarget",
160145
"The create_record tool is only available for tables.",
161146
logger);
@@ -169,7 +154,7 @@ public async Task<CallToolResult> ExecuteAsync(
169154

170155
if (result is CreatedResult createdResult)
171156
{
172-
return Utils.McpResponseBuilder.BuildSuccessResult(
157+
return McpResponseBuilder.BuildSuccessResult(
173158
new Dictionary<string, object?>
174159
{
175160
["entity"] = entityName,
@@ -184,14 +169,15 @@ public async Task<CallToolResult> ExecuteAsync(
184169
bool isError = objectResult.StatusCode.HasValue && objectResult.StatusCode.Value >= 400 && objectResult.StatusCode.Value != 403;
185170
if (isError)
186171
{
187-
return Utils.McpResponseBuilder.BuildErrorResult(
172+
return McpResponseBuilder.BuildErrorResult(
173+
toolName,
188174
"CreateFailed",
189175
$"Failed to create record in entity '{entityName}'. Error: {JsonSerializer.Serialize(objectResult.Value)}",
190176
logger);
191177
}
192178
else
193179
{
194-
return Utils.McpResponseBuilder.BuildSuccessResult(
180+
return McpResponseBuilder.BuildSuccessResult(
195181
new Dictionary<string, object?>
196182
{
197183
["entity"] = entityName,
@@ -206,14 +192,15 @@ public async Task<CallToolResult> ExecuteAsync(
206192
{
207193
if (result is null)
208194
{
209-
return Utils.McpResponseBuilder.BuildErrorResult(
195+
return McpResponseBuilder.BuildErrorResult(
196+
toolName,
210197
"UnexpectedError",
211198
$"Mutation engine returned null result for entity '{entityName}'",
212199
logger);
213200
}
214201
else
215202
{
216-
return Utils.McpResponseBuilder.BuildSuccessResult(
203+
return McpResponseBuilder.BuildSuccessResult(
217204
new Dictionary<string, object?>
218205
{
219206
["entity"] = entityName,
@@ -226,50 +213,8 @@ public async Task<CallToolResult> ExecuteAsync(
226213
}
227214
catch (Exception ex)
228215
{
229-
return Utils.McpResponseBuilder.BuildErrorResult("Error", $"Error: {ex.Message}", logger);
216+
return McpResponseBuilder.BuildErrorResult(toolName, "Error", $"Error: {ex.Message}", logger);
230217
}
231218
}
232-
233-
private static bool TryResolveAuthorizedRole(
234-
HttpContext httpContext,
235-
IAuthorizationResolver authorizationResolver,
236-
string entityName,
237-
out string error)
238-
{
239-
error = string.Empty;
240-
241-
string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString();
242-
243-
if (string.IsNullOrWhiteSpace(roleHeader))
244-
{
245-
error = "Client role header is missing or empty.";
246-
return false;
247-
}
248-
249-
string[] roles = roleHeader
250-
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
251-
.Distinct(StringComparer.OrdinalIgnoreCase)
252-
.ToArray();
253-
254-
if (roles.Length == 0)
255-
{
256-
error = "Client role header is missing or empty.";
257-
return false;
258-
}
259-
260-
foreach (string role in roles)
261-
{
262-
bool allowed = authorizationResolver.AreRoleAndOperationDefinedForEntity(
263-
entityName, role, EntityActionOperation.Create);
264-
265-
if (allowed)
266-
{
267-
return true;
268-
}
269-
}
270-
271-
error = "You do not have permission to create records for this entity.";
272-
return false;
273-
}
274219
}
275220
}

0 commit comments

Comments
 (0)