-
Notifications
You must be signed in to change notification settings - Fork 366
Expand file tree
/
Copy pathCreateRecordTool.cs
More file actions
251 lines (228 loc) · 11.9 KB
/
Copy pathCreateRecordTool.cs
File metadata and controls
251 lines (228 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
// Copyright (c) Microsoft Corporation.
// 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.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.Mcp.Model;
using Azure.DataApiBuilder.Mcp.Utils;
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 bool IsEnabled(RuntimeConfig config) => config.McpDmlTools?.CreateRecord ?? true;
public Tool GetToolMetadata()
{
return new Tool
{
Name = "create_record",
Description = "STEP 1: describe_entities -> find entities with CREATE permission and their fields. STEP 2: call this tool with matching field names and values.",
InputSchema = JsonSerializer.Deserialize<JsonElement>(
@"{
""type"": ""object"",
""properties"": {
""entity"": {
""type"": ""string"",
""description"": ""Entity name with CREATE permission.""
},
""data"": {
""type"": ""object"",
""description"": ""Required fields and values for the new record.""
}
},
""required"": [""entity"", ""data""]
}"
)
};
}
public async Task<CallToolResult> ExecuteAsync(
JsonDocument? arguments,
IServiceProvider serviceProvider,
CancellationToken cancellationToken = default)
{
ILogger<CreateRecordTool>? logger = serviceProvider.GetService<ILogger<CreateRecordTool>>();
string toolName = GetToolMetadata().Name;
if (arguments == null)
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger);
}
RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService<RuntimeConfigProvider>();
if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", "Runtime configuration not available.", logger);
}
if (runtimeConfig.McpDmlTools?.CreateRecord != true)
{
return McpErrorHelpers.ToolDisabled(toolName, logger);
}
try
{
cancellationToken.ThrowIfCancellationRequested();
JsonElement root = arguments.RootElement;
if (!McpArgumentParser.TryParseEntityAndData(root, out string entityName, out JsonElement dataElement, out string parseError))
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
}
// Check entity-level DML tool configuration
if (runtimeConfig.Entities?.TryGetValue(entityName, out Entity? entity) == true &&
entity.Mcp?.DmlToolEnabled == false)
{
return McpErrorHelpers.ToolDisabled(toolName, logger, $"DML tools are disabled for entity '{entityName}'.");
}
if (!McpMetadataHelper.TryResolveMetadata(
entityName,
runtimeConfig,
serviceProvider,
out ISqlMetadataProvider sqlMetadataProvider,
out DatabaseObject dbObject,
out string dataSourceName,
out string metadataError))
{
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger);
}
// Create an HTTP context for authorization
IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
HttpContext httpContext = httpContextAccessor.HttpContext ?? new DefaultHttpContext();
IAuthorizationResolver authorizationResolver = serviceProvider.GetRequiredService<IAuthorizationResolver>();
if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authorizationResolver, out string roleCtxError))
{
return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", roleCtxError, logger);
}
if (!McpAuthorizationHelper.TryResolveAuthorizedRole(
httpContext,
authorizationResolver,
entityName,
EntityActionOperation.Create,
out string? effectiveRole,
out string authError))
{
return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", authError, logger);
}
// Column-level authorization: ensure the caller's effective role is permitted to write
// every column present in the request payload (fields.include/fields.exclude enforcement).
IEnumerable<string> requestedColumns = dataElement.ValueKind == JsonValueKind.Object
? dataElement.EnumerateObject().Select(property => property.Name)
: Enumerable.Empty<string>();
try
{
if (!McpAuthorizationHelper.AreColumnsAuthorizedForOperation(
authorizationResolver,
entityName,
effectiveRole!,
EntityActionOperation.Create,
requestedColumns,
out string columnAuthError))
{
return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", columnAuthError, logger);
}
}
catch (Azure.DataApiBuilder.Service.Exceptions.DataApiBuilderException dabEx)
{
return McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {dabEx.Message}", logger);
}
JsonElement insertPayloadRoot = dataElement.Clone();
// Validate it's a table or view - stored procedures use execute_entity
if (dbObject.SourceType != EntitySourceType.Table && dbObject.SourceType != EntitySourceType.View)
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entityName}' is not a table or view. For stored procedures, use the execute_entity tool instead.", logger);
}
InsertRequestContext insertRequestContext = new(
entityName,
dbObject,
insertPayloadRoot,
EntityActionOperation.Insert);
// Only validate tables. For views, skip validation and let the database handle any errors.
if (dbObject.SourceType is EntitySourceType.Table)
{
RequestValidator requestValidator = serviceProvider.GetRequiredService<RequestValidator>();
try
{
requestValidator.ValidateInsertRequestContext(insertRequestContext);
}
catch (Exception ex)
{
return McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger);
}
}
IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService<IMutationEngineFactory>();
DatabaseType databaseType = sqlMetadataProvider.GetDatabaseType();
IMutationEngine mutationEngine = mutationEngineFactory.GetMutationEngine(databaseType);
IActionResult? result = await mutationEngine.ExecuteAsync(insertRequestContext);
if (result is CreatedResult createdResult)
{
return McpResponseBuilder.BuildSuccessResult(
new Dictionary<string, object?>
{
["entity"] = entityName,
["result"] = createdResult.Value,
["message"] = $"Successfully created record in entity '{entityName}'"
},
logger,
$"Successfully created record in entity '{entityName}'");
}
else if (result is ObjectResult objectResult)
{
bool isError = objectResult.StatusCode.HasValue && objectResult.StatusCode.Value >= 400 && objectResult.StatusCode.Value != 403;
if (isError)
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"CreateFailed",
$"Failed to create record in entity '{entityName}'. Error: {JsonSerializer.Serialize(objectResult.Value)}",
logger);
}
else
{
return McpResponseBuilder.BuildSuccessResult(
new Dictionary<string, object?>
{
["entity"] = entityName,
["result"] = objectResult.Value,
["message"] = $"Successfully created record in entity '{entityName}'. Unable to perform read-back of inserted records."
},
logger,
$"Successfully created record in entity '{entityName}'. Unable to perform read-back of inserted records.");
}
}
else
{
if (result is null)
{
return McpResponseBuilder.BuildErrorResult(
toolName,
"UnexpectedError",
$"Mutation engine returned null result for entity '{entityName}'",
logger);
}
else
{
return McpResponseBuilder.BuildSuccessResult(
new Dictionary<string, object?>
{
["entity"] = entityName,
["message"] = $"Create operation completed with unexpected result type: {result.GetType().Name}"
},
logger,
$"Create operation completed for entity '{entityName}' with unexpected result type: {result.GetType().Name}");
}
}
}
catch (Exception ex)
{
return McpResponseBuilder.BuildErrorResult(toolName, "Error", $"Error: {ex.Message}", logger);
}
}
}
}