Skip to content

Commit 7b31e9a

Browse files
Honoring incoming request role in determining allowed permissions for describe-entities MCP tool (#2956)
## Why make this change? - Addresses follow ups to PR #2900 The `describe_entities` tool response format needed improvements to better align with MCP specifications and provide more accurate, user-scoped information. Key issues included non-specification compliant response fields, overly broad permission reporting across all roles, and inconsistent entity/field naming conventions that didn't prioritize user-friendly aliases. ## What is this change? - **Removed non-spec fields from response**: Eliminated `mode` and `filter` fields that were not part of the MCP specification - **Scoped permissions to current user's role**: Modified permissions logic to only return permissions available to the requesting user's role instead of all permissions across all roles - **Implemented entity alias support**: Updated entity name resolution to prefer GraphQL singular names (aliases) over configuration names, falling back to entity name only when alias is absent - **Fixed parameter metadata format**: Changed parameter default value key from `@default` to `default` in JSON response - **Enhanced field name resolution**: Updated field metadata to use field aliases when available, falling back to field names when aliases are absent - **Added proper authorization context**: Integrated HTTP context and authorization resolver to determine current user's role for permission filtering ## How was this tested? - [x] Manual Tests ## Sample Request(s) ``` POST http://localhost:5000/mcp { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "describe_entities" }, "id": 1 } ``` ``` POST http://localhost:5000/mcp { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "describe_entities", "arguments": { "nameOnly": true } }, "id": 2 } ``` ``` POST http://localhost:5000/mcp { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "describe_entities", "arguments": { "entities": ["Book", "Publisher"] } }, "id": 1 } ```
1 parent 3adf04f commit 7b31e9a

File tree

1 file changed

+87
-27
lines changed

1 file changed

+87
-27
lines changed

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

Lines changed: 87 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
// Licensed under the MIT License.
33

44
using System.Text.Json;
5+
using Azure.DataApiBuilder.Auth;
56
using Azure.DataApiBuilder.Config.ObjectModel;
7+
using Azure.DataApiBuilder.Core.Authorization;
68
using Azure.DataApiBuilder.Core.Configurations;
79
using Azure.DataApiBuilder.Mcp.Model;
810
using Azure.DataApiBuilder.Mcp.Utils;
911
using Azure.DataApiBuilder.Service.Exceptions;
12+
using Microsoft.AspNetCore.Http;
1013
using Microsoft.Extensions.DependencyInjection;
1114
using Microsoft.Extensions.Logging;
1215
using ModelContextProtocol.Protocol;
@@ -80,8 +83,45 @@ public Task<CallToolResult> ExecuteAsync(
8083
logger));
8184
}
8285

86+
// Get authorization services to determine current user's role
87+
IAuthorizationResolver authResolver = serviceProvider.GetRequiredService<IAuthorizationResolver>();
88+
IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
89+
HttpContext? httpContext = httpContextAccessor.HttpContext;
90+
91+
// Get current user's role for permission filtering
92+
// For discovery tools like describe_entities, we use the first valid role from the header
93+
// This differs from operation-specific tools that check permissions per entity per operation
94+
string? currentUserRole = null;
95+
if (httpContext != null && authResolver.IsValidRoleContext(httpContext))
96+
{
97+
string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString();
98+
if (!string.IsNullOrWhiteSpace(roleHeader))
99+
{
100+
string[] roles = roleHeader
101+
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
102+
103+
if (roles.Length > 1)
104+
{
105+
logger?.LogWarning("Multiple roles detected in request header: [{Roles}]. Using first role '{FirstRole}' for entity discovery. " +
106+
"Consider using a single role for consistent permission reporting.",
107+
string.Join(", ", roles), roles[0]);
108+
}
109+
110+
// For discovery operations, take the first role from comma-separated list
111+
// This provides a consistent view of available entities for the primary role
112+
currentUserRole = roles.FirstOrDefault();
113+
}
114+
}
115+
83116
(bool nameOnly, HashSet<string>? entityFilter) = ParseArguments(arguments, logger);
84117

118+
if (currentUserRole == null)
119+
{
120+
logger?.LogWarning("Current user role could not be determined from HTTP context or role header. " +
121+
"Entity permissions will be empty (no permissions shown) rather than using anonymous permissions. " +
122+
"Ensure the '{RoleHeader}' header is properly set.", AuthorizationResolver.CLIENT_ROLE_HEADER);
123+
}
124+
85125
List<Dictionary<string, object?>> entityList = new();
86126

87127
if (runtimeConfig.Entities != null)
@@ -102,7 +142,7 @@ public Task<CallToolResult> ExecuteAsync(
102142
{
103143
Dictionary<string, object?> entityInfo = nameOnly
104144
? BuildBasicEntityInfo(entityName, entity)
105-
: BuildFullEntityInfo(entityName, entity);
145+
: BuildFullEntityInfo(entityName, entity, currentUserRole);
106146

107147
entityList.Add(entityInfo);
108148
}
@@ -140,19 +180,14 @@ public Task<CallToolResult> ExecuteAsync(
140180
Dictionary<string, object?> responseData = new()
141181
{
142182
["entities"] = finalEntityList,
143-
["count"] = finalEntityList.Count,
144-
["mode"] = nameOnly ? "basic" : "full"
183+
["count"] = finalEntityList.Count
145184
};
146185

147-
if (entityFilter != null && entityFilter.Count > 0)
148-
{
149-
responseData["filter"] = entityFilter.ToArray();
150-
}
151-
152186
logger?.LogInformation(
153-
"DescribeEntitiesTool returned {EntityCount} entities in {Mode} mode.",
187+
"DescribeEntitiesTool returned {EntityCount} entities. Response type: {ResponseType} (nameOnly={NameOnly}).",
154188
finalEntityList.Count,
155-
nameOnly ? "basic" : "full");
189+
nameOnly ? "lightweight summary (names and descriptions only)" : "full metadata with fields, parameters, and permissions",
190+
nameOnly);
156191

157192
return Task.FromResult(McpResponseBuilder.BuildSuccessResult(
158193
responseData,
@@ -276,25 +311,41 @@ private static bool ShouldIncludeEntity(string entityName, HashSet<string>? enti
276311
/// </summary>
277312
/// <param name="entityName">The name of the entity to include in the dictionary.</param>
278313
/// <param name="entity">The entity object from which to extract additional information.</param>
279-
/// <returns>A dictionary with two keys: "name", containing the entity name, and "description", containing the entity's
314+
/// <returns>A dictionary with two keys: "name", containing the entity alias (or name if no alias), and "description", containing the entity's
280315
/// description or an empty string if the description is null.</returns>
281316
private static Dictionary<string, object?> BuildBasicEntityInfo(string entityName, Entity entity)
282317
{
318+
// Use GraphQL singular name as alias if available, otherwise use entity name
319+
string displayName = !string.IsNullOrWhiteSpace(entity.GraphQL?.Singular)
320+
? entity.GraphQL.Singular
321+
: entityName;
322+
283323
return new Dictionary<string, object?>
284324
{
285-
["name"] = entityName,
325+
["name"] = displayName,
286326
["description"] = entity.Description ?? string.Empty
287327
};
288328
}
289329

290330
/// <summary>
291331
/// Builds full entity info: name, description, fields, parameters (for stored procs), permissions.
292332
/// </summary>
293-
private static Dictionary<string, object?> BuildFullEntityInfo(string entityName, Entity entity)
333+
/// <param name="entityName">The name of the entity to include in the dictionary.</param>
334+
/// <param name="entity">The entity object from which to extract additional information.</param>
335+
/// <param name="currentUserRole">The role of the current user, used to determine permissions.</param>
336+
/// <returns>
337+
/// A dictionary containing the entity's name, description, fields, parameters (if applicable), and permissions.
338+
/// </returns>
339+
private static Dictionary<string, object?> BuildFullEntityInfo(string entityName, Entity entity, string? currentUserRole)
294340
{
341+
// Use GraphQL singular name as alias if available, otherwise use entity name
342+
string displayName = !string.IsNullOrWhiteSpace(entity.GraphQL?.Singular)
343+
? entity.GraphQL.Singular
344+
: entityName;
345+
295346
Dictionary<string, object?> info = new()
296347
{
297-
["name"] = entityName,
348+
["name"] = displayName,
298349
["description"] = entity.Description ?? string.Empty,
299350
["fields"] = BuildFieldMetadataInfo(entity.Fields),
300351
};
@@ -304,7 +355,7 @@ private static bool ShouldIncludeEntity(string entityName, HashSet<string>? enti
304355
info["parameters"] = BuildParameterMetadataInfo(entity.Source.Parameters);
305356
}
306357

307-
info["permissions"] = BuildPermissionsInfo(entity);
358+
info["permissions"] = BuildPermissionsInfo(entity, currentUserRole);
308359

309360
return info;
310361
}
@@ -325,7 +376,7 @@ private static List<object> BuildFieldMetadataInfo(List<FieldMetadata>? fields)
325376
{
326377
result.Add(new
327378
{
328-
name = field.Name,
379+
name = field.Alias ?? field.Name,
329380
description = field.Description ?? string.Empty
330381
});
331382
}
@@ -338,7 +389,7 @@ private static List<object> BuildFieldMetadataInfo(List<FieldMetadata>? fields)
338389
/// Builds a list of parameter metadata objects containing information about each parameter.
339390
/// </summary>
340391
/// <param name="parameters">A list of <see cref="ParameterMetadata"/> objects representing the parameters to process. Can be null.</param>
341-
/// <returns>A list of anonymous objects, each containing the parameter's name, whether it is required, its default
392+
/// <returns>A list of dictionaries, each containing the parameter's name, whether it is required, its default
342393
/// value, and its description. Returns an empty list if <paramref name="parameters"/> is null.</returns>
343394
private static List<object> BuildParameterMetadataInfo(List<ParameterMetadata>? parameters)
344395
{
@@ -348,27 +399,29 @@ private static List<object> BuildParameterMetadataInfo(List<ParameterMetadata>?
348399
{
349400
foreach (ParameterMetadata param in parameters)
350401
{
351-
result.Add(new
402+
Dictionary<string, object?> paramInfo = new()
352403
{
353-
name = param.Name,
354-
required = param.Default == null, // required if no default
355-
@default = param.Default,
356-
description = param.Description ?? string.Empty
357-
});
404+
["name"] = param.Name,
405+
["required"] = param.Required,
406+
["default"] = param.Default,
407+
["description"] = param.Description ?? string.Empty
408+
};
409+
result.Add(paramInfo);
358410
}
359411
}
360412

361413
return result;
362414
}
363415

364416
/// <summary>
365-
/// Build a list of permission metadata info
417+
/// Build a list of permission metadata info for the current user's role
366418
/// </summary>
367419
/// <param name="entity">The entity object</param>
368-
/// <returns>A list of permissions available to the entity</returns>
369-
private static string[] BuildPermissionsInfo(Entity entity)
420+
/// <param name="currentUserRole">The current user's role - if null, returns empty permissions</param>
421+
/// <returns>A list of permissions available to the current user's role for this entity</returns>
422+
private static string[] BuildPermissionsInfo(Entity entity, string? currentUserRole)
370423
{
371-
if (entity.Permissions == null)
424+
if (entity.Permissions == null || string.IsNullOrWhiteSpace(currentUserRole))
372425
{
373426
return Array.Empty<string>();
374427
}
@@ -380,8 +433,15 @@ private static string[] BuildPermissionsInfo(Entity entity)
380433

381434
HashSet<string> permissions = new(StringComparer.OrdinalIgnoreCase);
382435

436+
// Only include permissions for the current user's role
383437
foreach (EntityPermission permission in entity.Permissions)
384438
{
439+
// Check if this permission applies to the current user's role
440+
if (!string.Equals(permission.Role, currentUserRole, StringComparison.OrdinalIgnoreCase))
441+
{
442+
continue;
443+
}
444+
385445
foreach (EntityAction action in permission.Actions)
386446
{
387447
if (action.Action == EntityActionOperation.All)

0 commit comments

Comments
 (0)