22// Licensed under the MIT License.
33
44using System . Text . Json ;
5+ using Azure . DataApiBuilder . Auth ;
6+ using Azure . DataApiBuilder . Config . DatabasePrimitives ;
7+ using Azure . DataApiBuilder . Config . ObjectModel ;
8+ using Azure . DataApiBuilder . Core . Authorization ;
9+ using Azure . DataApiBuilder . Core . Configurations ;
10+ using Azure . DataApiBuilder . Core . Models ;
11+ using Azure . DataApiBuilder . Core . Resolvers ;
12+ using Azure . DataApiBuilder . Core . Resolvers . Factories ;
13+ using Azure . DataApiBuilder . Core . Services ;
14+ using Azure . DataApiBuilder . Core . Services . MetadataProviders ;
515using Azure . DataApiBuilder . Mcp . Model ;
16+ using Microsoft . AspNetCore . Http ;
17+ using Microsoft . AspNetCore . Mvc ;
18+ using Microsoft . Extensions . DependencyInjection ;
19+ using Microsoft . Extensions . Logging ;
620using ModelContextProtocol . Protocol ;
721using static Azure . DataApiBuilder . Mcp . Model . McpEnums ;
822
@@ -16,7 +30,7 @@ public Tool GetToolMetadata()
1630 {
1731 return new Tool
1832 {
19- Name = "create-record " ,
33+ Name = "create_record " ,
2034 Description = "Creates a new record in the specified entity." ,
2135 InputSchema = JsonSerializer . Deserialize < JsonElement > (
2236 @"{
@@ -37,51 +51,225 @@ public Tool GetToolMetadata()
3751 } ;
3852 }
3953
40- public Task < CallToolResult > ExecuteAsync (
54+ public async Task < CallToolResult > ExecuteAsync (
4155 JsonDocument ? arguments ,
4256 IServiceProvider serviceProvider ,
4357 CancellationToken cancellationToken = default )
4458 {
59+ ILogger < CreateRecordTool > ? logger = serviceProvider . GetService < ILogger < CreateRecordTool > > ( ) ;
4560 if ( arguments == null )
4661 {
47- return Task . FromResult ( new CallToolResult
48- {
49- Content = [ new TextContentBlock { Type = "text" , Text = "Error: No arguments provided" } ]
50- } ) ;
62+ return Utils . McpResponseBuilder . BuildErrorResult ( "Invalid Arguments" , "No arguments provided" , logger ) ;
63+ }
64+
65+ RuntimeConfigProvider runtimeConfigProvider = serviceProvider . GetRequiredService < RuntimeConfigProvider > ( ) ;
66+ if ( ! runtimeConfigProvider . TryGetConfig ( out RuntimeConfig ? runtimeConfig ) )
67+ {
68+ return Utils . McpResponseBuilder . BuildErrorResult ( "Invalid Configuration" , "Runtime configuration not available" , logger ) ;
69+ }
70+
71+ if ( runtimeConfig . McpDmlTools ? . CreateRecord != true )
72+ {
73+ return Utils . McpResponseBuilder . BuildErrorResult (
74+ "ToolDisabled" ,
75+ "The create_record tool is disabled in the configuration." ,
76+ logger ) ;
5177 }
5278
5379 try
5480 {
55- // Extract arguments
81+ cancellationToken . ThrowIfCancellationRequested ( ) ;
5682 JsonElement root = arguments . RootElement ;
5783
5884 if ( ! root . TryGetProperty ( "entity" , out JsonElement entityElement ) ||
5985 ! root . TryGetProperty ( "data" , out JsonElement dataElement ) )
6086 {
61- return Task . FromResult ( new CallToolResult
62- {
63- Content = [ new TextContentBlock { Type = "text" , Text = "Error: Missing required arguments 'entity' or 'data'" } ]
64- } ) ;
87+ return Utils . McpResponseBuilder . BuildErrorResult ( "InvalidArguments" , "Missing required arguments 'entity' or 'data'" , logger ) ;
6588 }
6689
6790 string entityName = entityElement . GetString ( ) ?? string . Empty ;
91+ if ( string . IsNullOrWhiteSpace ( entityName ) )
92+ {
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 ) ;
117+ }
68118
69- // TODO: Implement actual create logic using DAB's internal services
70- // For now, return a placeholder response
71- string result = $ "Would create record in entity '{ entityName } ' with data: { dataElement . GetRawText ( ) } ";
119+ // Create an HTTP context for authorization
120+ IHttpContextAccessor httpContextAccessor = serviceProvider . GetRequiredService < IHttpContextAccessor > ( ) ;
121+ HttpContext httpContext = httpContextAccessor . HttpContext ?? new DefaultHttpContext ( ) ;
122+ IAuthorizationResolver authorizationResolver = serviceProvider . GetRequiredService < IAuthorizationResolver > ( ) ;
72123
73- return Task . FromResult ( new CallToolResult
124+ if ( httpContext is null || ! authorizationResolver . IsValidRoleContext ( httpContext ) )
74125 {
75- Content = [ new TextContentBlock { Type = "text" , Text = result } ]
76- } ) ;
126+ return Utils . McpResponseBuilder . BuildErrorResult ( "PermissionDenied" , "Permission denied: Unable to resolve a valid role context for update operation." , logger ) ;
127+ }
128+
129+ // Validate that we have at least one role authorized for create
130+ if ( ! TryResolveAuthorizedRole ( httpContext , authorizationResolver , entityName , out string authError ) )
131+ {
132+ return Utils . McpResponseBuilder . BuildErrorResult ( "PermissionDenied" , authError , logger ) ;
133+ }
134+
135+ JsonElement insertPayloadRoot = dataElement . Clone ( ) ;
136+ InsertRequestContext insertRequestContext = new (
137+ entityName ,
138+ dbObject ,
139+ insertPayloadRoot ,
140+ EntityActionOperation . Insert ) ;
141+
142+ RequestValidator requestValidator = serviceProvider . GetRequiredService < RequestValidator > ( ) ;
143+
144+ // Only validate tables
145+ if ( dbObject . SourceType is EntitySourceType . Table )
146+ {
147+ try
148+ {
149+ requestValidator . ValidateInsertRequestContext ( insertRequestContext ) ;
150+ }
151+ catch ( Exception ex )
152+ {
153+ return Utils . McpResponseBuilder . BuildErrorResult ( "ValidationFailed" , $ "Request validation failed: { ex . Message } ", logger ) ;
154+ }
155+ }
156+ else
157+ {
158+ return Utils . McpResponseBuilder . BuildErrorResult (
159+ "InvalidCreateTarget" ,
160+ "The create_record tool is only available for tables." ,
161+ logger ) ;
162+ }
163+
164+ IMutationEngineFactory mutationEngineFactory = serviceProvider . GetRequiredService < IMutationEngineFactory > ( ) ;
165+ DatabaseType databaseType = sqlMetadataProvider . GetDatabaseType ( ) ;
166+ IMutationEngine mutationEngine = mutationEngineFactory . GetMutationEngine ( databaseType ) ;
167+
168+ IActionResult ? result = await mutationEngine . ExecuteAsync ( insertRequestContext ) ;
169+
170+ if ( result is CreatedResult createdResult )
171+ {
172+ return Utils . McpResponseBuilder . BuildSuccessResult (
173+ new Dictionary < string , object ? >
174+ {
175+ [ "entity" ] = entityName ,
176+ [ "result" ] = createdResult . Value ,
177+ [ "message" ] = $ "Successfully created record in entity '{ entityName } '"
178+ } ,
179+ logger ,
180+ $ "Successfully created record in entity '{ entityName } '") ;
181+ }
182+ else if ( result is ObjectResult objectResult )
183+ {
184+ bool isError = objectResult . StatusCode . HasValue && objectResult . StatusCode . Value >= 400 && objectResult . StatusCode . Value != 403 ;
185+ if ( isError )
186+ {
187+ return Utils . McpResponseBuilder . BuildErrorResult (
188+ "CreateFailed" ,
189+ $ "Failed to create record in entity '{ entityName } '. Error: { JsonSerializer . Serialize ( objectResult . Value ) } ",
190+ logger ) ;
191+ }
192+ else
193+ {
194+ return Utils . McpResponseBuilder . BuildSuccessResult (
195+ new Dictionary < string , object ? >
196+ {
197+ [ "entity" ] = entityName ,
198+ [ "result" ] = objectResult . Value ,
199+ [ "message" ] = $ "Successfully created record in entity '{ entityName } '. Unable to perform read-back of inserted records."
200+ } ,
201+ logger ,
202+ $ "Successfully created record in entity '{ entityName } '. Unable to perform read-back of inserted records.") ;
203+ }
204+ }
205+ else
206+ {
207+ if ( result is null )
208+ {
209+ return Utils . McpResponseBuilder . BuildErrorResult (
210+ "UnexpectedError" ,
211+ $ "Mutation engine returned null result for entity '{ entityName } '",
212+ logger ) ;
213+ }
214+ else
215+ {
216+ return Utils . McpResponseBuilder . BuildSuccessResult (
217+ new Dictionary < string , object ? >
218+ {
219+ [ "entity" ] = entityName ,
220+ [ "message" ] = $ "Create operation completed with unexpected result type: { result . GetType ( ) . Name } "
221+ } ,
222+ logger ,
223+ $ "Create operation completed for entity '{ entityName } ' with unexpected result type: { result . GetType ( ) . Name } ") ;
224+ }
225+ }
77226 }
78227 catch ( Exception ex )
79228 {
80- return Task . FromResult ( new CallToolResult
229+ return Utils . McpResponseBuilder . BuildErrorResult ( "Error" , $ "Error: { ex . Message } ", logger ) ;
230+ }
231+ }
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 )
81266 {
82- Content = [ new TextContentBlock { Type = "text" , Text = $ "Error: { ex . Message } " } ]
83- } ) ;
267+ return true ;
268+ }
84269 }
270+
271+ error = "You do not have permission to create records for this entity." ;
272+ return false ;
85273 }
86274 }
87275}
0 commit comments