diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs index 4215705572..154b37ee80 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs @@ -2,11 +2,14 @@ // Licensed under the MIT License. using System.Text.Json; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Mcp.Model; using Azure.DataApiBuilder.Mcp.Utils; using Azure.DataApiBuilder.Service.Exceptions; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; @@ -80,8 +83,45 @@ public Task ExecuteAsync( logger)); } + // Get authorization services to determine current user's role + IAuthorizationResolver authResolver = serviceProvider.GetRequiredService(); + IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService(); + HttpContext? httpContext = httpContextAccessor.HttpContext; + + // Get current user's role for permission filtering + // For discovery tools like describe_entities, we use the first valid role from the header + // This differs from operation-specific tools that check permissions per entity per operation + string? currentUserRole = null; + if (httpContext != null && authResolver.IsValidRoleContext(httpContext)) + { + string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString(); + if (!string.IsNullOrWhiteSpace(roleHeader)) + { + string[] roles = roleHeader + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (roles.Length > 1) + { + logger?.LogWarning("Multiple roles detected in request header: [{Roles}]. Using first role '{FirstRole}' for entity discovery. " + + "Consider using a single role for consistent permission reporting.", + string.Join(", ", roles), roles[0]); + } + + // For discovery operations, take the first role from comma-separated list + // This provides a consistent view of available entities for the primary role + currentUserRole = roles.FirstOrDefault(); + } + } + (bool nameOnly, HashSet? entityFilter) = ParseArguments(arguments, logger); + if (currentUserRole == null) + { + logger?.LogWarning("Current user role could not be determined from HTTP context or role header. " + + "Entity permissions will be empty (no permissions shown) rather than using anonymous permissions. " + + "Ensure the '{RoleHeader}' header is properly set.", AuthorizationResolver.CLIENT_ROLE_HEADER); + } + List> entityList = new(); if (runtimeConfig.Entities != null) @@ -102,7 +142,7 @@ public Task ExecuteAsync( { Dictionary entityInfo = nameOnly ? BuildBasicEntityInfo(entityName, entity) - : BuildFullEntityInfo(entityName, entity); + : BuildFullEntityInfo(entityName, entity, currentUserRole); entityList.Add(entityInfo); } @@ -140,19 +180,14 @@ public Task ExecuteAsync( Dictionary responseData = new() { ["entities"] = finalEntityList, - ["count"] = finalEntityList.Count, - ["mode"] = nameOnly ? "basic" : "full" + ["count"] = finalEntityList.Count }; - if (entityFilter != null && entityFilter.Count > 0) - { - responseData["filter"] = entityFilter.ToArray(); - } - logger?.LogInformation( - "DescribeEntitiesTool returned {EntityCount} entities in {Mode} mode.", + "DescribeEntitiesTool returned {EntityCount} entities. Response type: {ResponseType} (nameOnly={NameOnly}).", finalEntityList.Count, - nameOnly ? "basic" : "full"); + nameOnly ? "lightweight summary (names and descriptions only)" : "full metadata with fields, parameters, and permissions", + nameOnly); return Task.FromResult(McpResponseBuilder.BuildSuccessResult( responseData, @@ -276,13 +311,18 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti /// /// The name of the entity to include in the dictionary. /// The entity object from which to extract additional information. - /// A dictionary with two keys: "name", containing the entity name, and "description", containing the entity's + /// A dictionary with two keys: "name", containing the entity alias (or name if no alias), and "description", containing the entity's /// description or an empty string if the description is null. private static Dictionary BuildBasicEntityInfo(string entityName, Entity entity) { + // Use GraphQL singular name as alias if available, otherwise use entity name + string displayName = !string.IsNullOrWhiteSpace(entity.GraphQL?.Singular) + ? entity.GraphQL.Singular + : entityName; + return new Dictionary { - ["name"] = entityName, + ["name"] = displayName, ["description"] = entity.Description ?? string.Empty }; } @@ -290,11 +330,22 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti /// /// Builds full entity info: name, description, fields, parameters (for stored procs), permissions. /// - private static Dictionary BuildFullEntityInfo(string entityName, Entity entity) + /// The name of the entity to include in the dictionary. + /// The entity object from which to extract additional information. + /// The role of the current user, used to determine permissions. + /// + /// A dictionary containing the entity's name, description, fields, parameters (if applicable), and permissions. + /// + private static Dictionary BuildFullEntityInfo(string entityName, Entity entity, string? currentUserRole) { + // Use GraphQL singular name as alias if available, otherwise use entity name + string displayName = !string.IsNullOrWhiteSpace(entity.GraphQL?.Singular) + ? entity.GraphQL.Singular + : entityName; + Dictionary info = new() { - ["name"] = entityName, + ["name"] = displayName, ["description"] = entity.Description ?? string.Empty, ["fields"] = BuildFieldMetadataInfo(entity.Fields), }; @@ -304,7 +355,7 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti info["parameters"] = BuildParameterMetadataInfo(entity.Source.Parameters); } - info["permissions"] = BuildPermissionsInfo(entity); + info["permissions"] = BuildPermissionsInfo(entity, currentUserRole); return info; } @@ -325,7 +376,7 @@ private static List BuildFieldMetadataInfo(List? fields) { result.Add(new { - name = field.Name, + name = field.Alias ?? field.Name, description = field.Description ?? string.Empty }); } @@ -338,7 +389,7 @@ private static List BuildFieldMetadataInfo(List? fields) /// Builds a list of parameter metadata objects containing information about each parameter. /// /// A list of objects representing the parameters to process. Can be null. - /// A list of anonymous objects, each containing the parameter's name, whether it is required, its default + /// A list of dictionaries, each containing the parameter's name, whether it is required, its default /// value, and its description. Returns an empty list if is null. private static List BuildParameterMetadataInfo(List? parameters) { @@ -348,13 +399,14 @@ private static List BuildParameterMetadataInfo(List? { foreach (ParameterMetadata param in parameters) { - result.Add(new + Dictionary paramInfo = new() { - name = param.Name, - required = param.Default == null, // required if no default - @default = param.Default, - description = param.Description ?? string.Empty - }); + ["name"] = param.Name, + ["required"] = param.Required, + ["default"] = param.Default, + ["description"] = param.Description ?? string.Empty + }; + result.Add(paramInfo); } } @@ -362,13 +414,14 @@ private static List BuildParameterMetadataInfo(List? } /// - /// Build a list of permission metadata info + /// Build a list of permission metadata info for the current user's role /// /// The entity object - /// A list of permissions available to the entity - private static string[] BuildPermissionsInfo(Entity entity) + /// The current user's role - if null, returns empty permissions + /// A list of permissions available to the current user's role for this entity + private static string[] BuildPermissionsInfo(Entity entity, string? currentUserRole) { - if (entity.Permissions == null) + if (entity.Permissions == null || string.IsNullOrWhiteSpace(currentUserRole)) { return Array.Empty(); } @@ -380,8 +433,15 @@ private static string[] BuildPermissionsInfo(Entity entity) HashSet permissions = new(StringComparer.OrdinalIgnoreCase); + // Only include permissions for the current user's role foreach (EntityPermission permission in entity.Permissions) { + // Check if this permission applies to the current user's role + if (!string.Equals(permission.Role, currentUserRole, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + foreach (EntityAction action in permission.Actions) { if (action.Action == EntityActionOperation.All)