Skip to content

Commit dd72498

Browse files
authored
Refactor response for Read and Update MCP tools to use built in utility functions (#2984)
## Why make this change? Closes #2919 ## What is this change? Refactors the `Read` and `Update` built in MCP tools so that they use the common `BuildErrorResult` and `BuildSuccessResult` functiosn in the Utils, aligning their usage with the other tools. ## How was this tested? Manually tested using MCP Inspector tool, and run against normal test suite. * DESCRIBE_ENTITIES <img width="395" height="660" alt="image" src="https://github.com/user-attachments/assets/a7e86256-a2ee-4623-9cc7-7126993a2e12" /> * CREATE <img width="1181" height="672" alt="image" src="https://github.com/user-attachments/assets/9042bd14-83da-48d2-a24f-4d95873a54c5" /> * READ <img width="1210" height="658" alt="image" src="https://github.com/user-attachments/assets/d800d9ed-0b03-4173-bf44-cadc7b612e62" /> * UPDATE <img width="1300" height="593" alt="image" src="https://github.com/user-attachments/assets/6aa38f25-80ab-47e6-aba6-38e80397ae4b" /> * DELETE <img width="1178" height="605" alt="image" src="https://github.com/user-attachments/assets/363fca1d-1ec9-42cd-85c0-b965463c9b80" /> ## Sample Request(s) N/A
1 parent 08ca05f commit dd72498

File tree

7 files changed

+165
-299
lines changed

7 files changed

+165
-299
lines changed

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,22 @@ public async Task<CallToolResult> ExecuteAsync(
5757
CancellationToken cancellationToken = default)
5858
{
5959
ILogger<CreateRecordTool>? logger = serviceProvider.GetService<ILogger<CreateRecordTool>>();
60+
string toolName = GetToolMetadata().Name;
6061
if (arguments == null)
6162
{
62-
return Utils.McpResponseBuilder.BuildErrorResult("Invalid Arguments", "No arguments provided", logger);
63+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Invalid Arguments", "No arguments provided", logger);
6364
}
6465

6566
RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService<RuntimeConfigProvider>();
6667
if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
6768
{
68-
return Utils.McpResponseBuilder.BuildErrorResult("Invalid Configuration", "Runtime configuration not available", logger);
69+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Invalid Configuration", "Runtime configuration not available", logger);
6970
}
7071

7172
if (runtimeConfig.McpDmlTools?.CreateRecord != true)
7273
{
7374
return Utils.McpResponseBuilder.BuildErrorResult(
75+
toolName,
7476
"ToolDisabled",
7577
"The create_record tool is disabled in the configuration.",
7678
logger);
@@ -84,13 +86,13 @@ public async Task<CallToolResult> ExecuteAsync(
8486
if (!root.TryGetProperty("entity", out JsonElement entityElement) ||
8587
!root.TryGetProperty("data", out JsonElement dataElement))
8688
{
87-
return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Missing required arguments 'entity' or 'data'", logger);
89+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Missing required arguments 'entity' or 'data'", logger);
8890
}
8991

9092
string entityName = entityElement.GetString() ?? string.Empty;
9193
if (string.IsNullOrWhiteSpace(entityName))
9294
{
93-
return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Entity name cannot be empty", logger);
95+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Entity name cannot be empty", logger);
9496
}
9597

9698
string dataSourceName;
@@ -100,7 +102,7 @@ public async Task<CallToolResult> ExecuteAsync(
100102
}
101103
catch (Exception)
102104
{
103-
return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger);
105+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger);
104106
}
105107

106108
IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>();
@@ -113,7 +115,7 @@ public async Task<CallToolResult> ExecuteAsync(
113115
}
114116
catch (Exception)
115117
{
116-
return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger);
118+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger);
117119
}
118120

119121
// Create an HTTP context for authorization
@@ -123,13 +125,13 @@ public async Task<CallToolResult> ExecuteAsync(
123125

124126
if (httpContext is null || !authorizationResolver.IsValidRoleContext(httpContext))
125127
{
126-
return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger);
128+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger);
127129
}
128130

129131
// Validate that we have at least one role authorized for create
130132
if (!TryResolveAuthorizedRole(httpContext, authorizationResolver, entityName, out string authError))
131133
{
132-
return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", authError, logger);
134+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", authError, logger);
133135
}
134136

135137
JsonElement insertPayloadRoot = dataElement.Clone();
@@ -150,12 +152,13 @@ public async Task<CallToolResult> ExecuteAsync(
150152
}
151153
catch (Exception ex)
152154
{
153-
return Utils.McpResponseBuilder.BuildErrorResult("ValidationFailed", $"Request validation failed: {ex.Message}", logger);
155+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger);
154156
}
155157
}
156158
else
157159
{
158160
return Utils.McpResponseBuilder.BuildErrorResult(
161+
toolName,
159162
"InvalidCreateTarget",
160163
"The create_record tool is only available for tables.",
161164
logger);
@@ -185,6 +188,7 @@ public async Task<CallToolResult> ExecuteAsync(
185188
if (isError)
186189
{
187190
return Utils.McpResponseBuilder.BuildErrorResult(
191+
toolName,
188192
"CreateFailed",
189193
$"Failed to create record in entity '{entityName}'. Error: {JsonSerializer.Serialize(objectResult.Value)}",
190194
logger);
@@ -207,6 +211,7 @@ public async Task<CallToolResult> ExecuteAsync(
207211
if (result is null)
208212
{
209213
return Utils.McpResponseBuilder.BuildErrorResult(
214+
toolName,
210215
"UnexpectedError",
211216
$"Mutation engine returned null result for entity '{entityName}'",
212217
logger);
@@ -226,7 +231,7 @@ public async Task<CallToolResult> ExecuteAsync(
226231
}
227232
catch (Exception ex)
228233
{
229-
return Utils.McpResponseBuilder.BuildErrorResult("Error", $"Error: {ex.Message}", logger);
234+
return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Error", $"Error: {ex.Message}", logger);
230235
}
231236
}
232237

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public async Task<CallToolResult> ExecuteAsync(
7373
CancellationToken cancellationToken = default)
7474
{
7575
ILogger<DeleteRecordTool>? logger = serviceProvider.GetService<ILogger<DeleteRecordTool>>();
76+
string toolName = GetToolMetadata().Name;
7677

7778
try
7879
{
@@ -87,6 +88,7 @@ public async Task<CallToolResult> ExecuteAsync(
8788
if (config.McpDmlTools?.DeleteRecord != true)
8889
{
8990
return McpResponseBuilder.BuildErrorResult(
91+
toolName,
9092
"ToolDisabled",
9193
$"The {this.GetToolMetadata().Name} tool is disabled in the configuration.",
9294
logger);
@@ -95,12 +97,12 @@ public async Task<CallToolResult> ExecuteAsync(
9597
// 3) Parsing & basic argument validation
9698
if (arguments is null)
9799
{
98-
return McpResponseBuilder.BuildErrorResult("InvalidArguments", "No arguments provided.", logger);
100+
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger);
99101
}
100102

101103
if (!McpArgumentParser.TryParseEntityAndKeys(arguments.RootElement, out string entityName, out Dictionary<string, object?> keys, out string parseError))
102104
{
103-
return McpResponseBuilder.BuildErrorResult("InvalidArguments", parseError, logger);
105+
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger);
104106
}
105107

106108
IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService<IMetadataProviderFactory>();
@@ -117,18 +119,18 @@ public async Task<CallToolResult> ExecuteAsync(
117119
}
118120
catch (Exception)
119121
{
120-
return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
122+
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
121123
}
122124

123125
if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null)
124126
{
125-
return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
127+
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
126128
}
127129

128130
// Validate it's a table or view
129131
if (dbObject.SourceType != EntitySourceType.Table && dbObject.SourceType != EntitySourceType.View)
130132
{
131-
return McpResponseBuilder.BuildErrorResult("InvalidEntity", $"Entity '{entityName}' is not a table or view. Use 'execute-entity' for stored procedures.", logger);
133+
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entityName}' is not a table or view. Use 'execute-entity' for stored procedures.", logger);
132134
}
133135

134136
// 5) Authorization
@@ -138,7 +140,7 @@ public async Task<CallToolResult> ExecuteAsync(
138140

139141
if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError))
140142
{
141-
return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {roleError}", logger);
143+
return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {roleError}", logger);
142144
}
143145

144146
if (!McpAuthorizationHelper.TryResolveAuthorizedRole(
@@ -149,7 +151,7 @@ public async Task<CallToolResult> ExecuteAsync(
149151
out string? effectiveRole,
150152
out string authError))
151153
{
152-
return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {authError}", logger);
154+
return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {authError}", logger);
153155
}
154156

155157
// 6) Build and validate Delete context
@@ -164,7 +166,7 @@ public async Task<CallToolResult> ExecuteAsync(
164166
{
165167
if (kvp.Value is null)
166168
{
167-
return McpResponseBuilder.BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
169+
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
168170
}
169171

170172
context.PrimaryKeyValuePairs[kvp.Key] = kvp.Value;
@@ -195,6 +197,7 @@ public async Task<CallToolResult> ExecuteAsync(
195197
{
196198
string keyDetails = McpJsonHelper.FormatKeyDetails(keys);
197199
return McpResponseBuilder.BuildErrorResult(
200+
toolName,
198201
"RecordNotFound",
199202
$"No record found with the specified primary key: {keyDetails}",
200203
logger);
@@ -203,6 +206,7 @@ public async Task<CallToolResult> ExecuteAsync(
203206
message.Contains("REFERENCE constraint", StringComparison.OrdinalIgnoreCase))
204207
{
205208
return McpResponseBuilder.BuildErrorResult(
209+
toolName,
206210
"ConstraintViolation",
207211
"Cannot delete record due to foreign key constraint. Other records depend on this record.",
208212
logger);
@@ -211,6 +215,7 @@ public async Task<CallToolResult> ExecuteAsync(
211215
message.Contains("authorization", StringComparison.OrdinalIgnoreCase))
212216
{
213217
return McpResponseBuilder.BuildErrorResult(
218+
toolName,
214219
"PermissionDenied",
215220
"You do not have permission to delete this record.",
216221
logger);
@@ -219,13 +224,15 @@ public async Task<CallToolResult> ExecuteAsync(
219224
message.Contains("type", StringComparison.OrdinalIgnoreCase))
220225
{
221226
return McpResponseBuilder.BuildErrorResult(
227+
toolName,
222228
"InvalidArguments",
223229
"Invalid data type for one or more key values.",
224230
logger);
225231
}
226232

227233
// For any other DAB exceptions, return the message as-is
228234
return McpResponseBuilder.BuildErrorResult(
235+
toolName,
229236
"DataApiBuilderError",
230237
dabEx.Message,
231238
logger);
@@ -242,7 +249,7 @@ public async Task<CallToolResult> ExecuteAsync(
242249
208 => $"Table '{dbObject.FullName}' not found in the database.",
243250
_ => $"Database error: {sqlEx.Message}"
244251
};
245-
return McpResponseBuilder.BuildErrorResult("DatabaseError", errorMessage, logger);
252+
return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", errorMessage, logger);
246253
}
247254
catch (DbException dbEx)
248255
{
@@ -254,31 +261,33 @@ public async Task<CallToolResult> ExecuteAsync(
254261
if (errorMsg.Contains("foreign key") || errorMsg.Contains("constraint"))
255262
{
256263
return McpResponseBuilder.BuildErrorResult(
264+
toolName,
257265
"ConstraintViolation",
258266
"Cannot delete record due to foreign key constraint. Other records depend on this record.",
259267
logger);
260268
}
261269
else if (errorMsg.Contains("not found") || errorMsg.Contains("does not exist"))
262270
{
263271
return McpResponseBuilder.BuildErrorResult(
272+
toolName,
264273
"RecordNotFound",
265274
"No record found with the specified primary key.",
266275
logger);
267276
}
268277

269-
return McpResponseBuilder.BuildErrorResult("DatabaseError", $"Database error: {dbEx.Message}", logger);
278+
return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", $"Database error: {dbEx.Message}", logger);
270279
}
271280
catch (InvalidOperationException ioEx) when (ioEx.Message.Contains("connection", StringComparison.OrdinalIgnoreCase))
272281
{
273282
// Handle connection-related issues
274283
logger?.LogError(ioEx, "Database connection error");
275-
return McpResponseBuilder.BuildErrorResult("ConnectionError", "Failed to connect to the database.", logger);
284+
return McpResponseBuilder.BuildErrorResult(toolName, "ConnectionError", "Failed to connect to the database.", logger);
276285
}
277286
catch (TimeoutException timeoutEx)
278287
{
279288
// Handle query timeout
280289
logger?.LogError(timeoutEx, "Delete operation timeout for {Entity}", entityName);
281-
return McpResponseBuilder.BuildErrorResult("TimeoutError", "The delete operation timed out.", logger);
290+
return McpResponseBuilder.BuildErrorResult(toolName, "TimeoutError", "The delete operation timed out.", logger);
282291
}
283292
catch (Exception ex)
284293
{
@@ -289,6 +298,7 @@ public async Task<CallToolResult> ExecuteAsync(
289298
{
290299
string keyDetails = McpJsonHelper.FormatKeyDetails(keys);
291300
return McpResponseBuilder.BuildErrorResult(
301+
toolName,
292302
"RecordNotFound",
293303
$"No entity found with the given key {keyDetails}.",
294304
logger);
@@ -325,18 +335,19 @@ public async Task<CallToolResult> ExecuteAsync(
325335
}
326336
catch (OperationCanceledException)
327337
{
328-
return McpResponseBuilder.BuildErrorResult("OperationCanceled", "The delete operation was canceled.", logger);
338+
return McpResponseBuilder.BuildErrorResult(toolName, "OperationCanceled", "The delete operation was canceled.", logger);
329339
}
330340
catch (ArgumentException argEx)
331341
{
332-
return McpResponseBuilder.BuildErrorResult("InvalidArguments", argEx.Message, logger);
342+
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", argEx.Message, logger);
333343
}
334344
catch (Exception ex)
335345
{
336346
ILogger<DeleteRecordTool>? innerLogger = serviceProvider.GetService<ILogger<DeleteRecordTool>>();
337347
innerLogger?.LogError(ex, "Unexpected error in DeleteRecordTool.");
338348

339349
return McpResponseBuilder.BuildErrorResult(
350+
toolName,
340351
"UnexpectedError",
341352
"An unexpected error occurred during the delete operation.",
342353
logger);

0 commit comments

Comments
 (0)