From d9ceaf2473b8804f58ed8452d8e209061e70ca54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:23:16 +0000 Subject: [PATCH 1/6] Initial plan From 3fb4c4c68e3a4c75b908bb3f45c37313f2bed122 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:33:02 +0000 Subject: [PATCH 2/6] Add directory list-paths command structure and implementation Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- .../Directory/BaseDirectoryCommand.cs | 33 +++++++++ .../Directory/DirectoryListPathsCommand.cs | 68 +++++++++++++++++++ .../Storage/Commands/StorageJsonContext.cs | 2 + .../Options/DataLake/BaseDirectoryOptions.cs | 12 ++++ .../DataLake/Directory/ListPathsOptions.cs | 6 ++ .../Options/StorageOptionDefinitions.cs | 9 +++ src/Areas/Storage/Services/IStorageService.cs | 7 ++ src/Areas/Storage/Services/StorageService.cs | 37 ++++++++++ src/Areas/Storage/StorageSetup.cs | 8 +++ 9 files changed, 182 insertions(+) create mode 100644 src/Areas/Storage/Commands/DataLake/Directory/BaseDirectoryCommand.cs create mode 100644 src/Areas/Storage/Commands/DataLake/Directory/DirectoryListPathsCommand.cs create mode 100644 src/Areas/Storage/Options/DataLake/BaseDirectoryOptions.cs create mode 100644 src/Areas/Storage/Options/DataLake/Directory/ListPathsOptions.cs diff --git a/src/Areas/Storage/Commands/DataLake/Directory/BaseDirectoryCommand.cs b/src/Areas/Storage/Commands/DataLake/Directory/BaseDirectoryCommand.cs new file mode 100644 index 000000000..f26edc6f4 --- /dev/null +++ b/src/Areas/Storage/Commands/DataLake/Directory/BaseDirectoryCommand.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using AzureMcp.Areas.Storage.Commands; +using AzureMcp.Areas.Storage.Options; +using AzureMcp.Areas.Storage.Options.DataLake; +using AzureMcp.Commands; + +namespace AzureMcp.Areas.Storage.Commands.DataLake.Directory; + +public abstract class BaseDirectoryCommand< + [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions> + : BaseStorageCommand where TOptions : BaseDirectoryOptions, new() +{ + protected readonly Option _fileSystemOption = StorageOptionDefinitions.FileSystem; + protected readonly Option _directoryOption = StorageOptionDefinitions.Directory; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.AddOption(_fileSystemOption); + command.AddOption(_directoryOption); + } + + protected override TOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.FileSystem = parseResult.GetValueForOption(_fileSystemOption); + options.Directory = parseResult.GetValueForOption(_directoryOption); + return options; + } +} \ No newline at end of file diff --git a/src/Areas/Storage/Commands/DataLake/Directory/DirectoryListPathsCommand.cs b/src/Areas/Storage/Commands/DataLake/Directory/DirectoryListPathsCommand.cs new file mode 100644 index 000000000..73a41f590 --- /dev/null +++ b/src/Areas/Storage/Commands/DataLake/Directory/DirectoryListPathsCommand.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AzureMcp.Areas.Storage.Models; +using AzureMcp.Areas.Storage.Options.DataLake.Directory; +using AzureMcp.Areas.Storage.Services; +using AzureMcp.Commands.Storage; +using AzureMcp.Services.Telemetry; +using Microsoft.Extensions.Logging; + +namespace AzureMcp.Areas.Storage.Commands.DataLake.Directory; + +public sealed class DirectoryListPathsCommand(ILogger logger) : BaseDirectoryCommand +{ + private const string CommandTitle = "List Data Lake Directory Paths"; + private readonly ILogger _logger = logger; + + public override string Name => "list-paths"; + + public override string Description => + """ + List all paths in a Data Lake directory. This command retrieves and displays all paths (files and directories) + available in the specified directory within a Data Lake file system. Results include path names, + types (file or directory), and metadata, returned as a JSON array. Requires account-name, file-system-name, + and directory-name. + """; + + public override string Title => CommandTitle; + + [McpServerTool(Destructive = false, ReadOnly = true, Title = CommandTitle)] + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult) + { + var options = BindOptions(parseResult); + + try + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + context.Activity?.WithSubscriptionTag(options); + + var storageService = context.GetService(); + var paths = await storageService.ListDataLakeDirectoryPaths( + options.Account!, + options.FileSystem!, + options.Directory!, + options.Subscription!, + options.Tenant, + options.RetryPolicy); + + context.Response.Results = ResponseResult.Create( + new DirectoryListPathsCommandResult(paths ?? []), + StorageJsonContext.Default.DirectoryListPathsCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing Data Lake directory paths. Account: {Account}, FileSystem: {FileSystem}, Directory: {Directory}.", + options.Account, options.FileSystem, options.Directory); + HandleException(context, ex); + } + + return context.Response; + } + + internal record DirectoryListPathsCommandResult(List Paths); +} \ No newline at end of file diff --git a/src/Areas/Storage/Commands/StorageJsonContext.cs b/src/Areas/Storage/Commands/StorageJsonContext.cs index 31df77bd4..49c9a5f47 100644 --- a/src/Areas/Storage/Commands/StorageJsonContext.cs +++ b/src/Areas/Storage/Commands/StorageJsonContext.cs @@ -5,6 +5,7 @@ using AzureMcp.Areas.Storage.Commands.Account; using AzureMcp.Areas.Storage.Commands.Blob; using AzureMcp.Areas.Storage.Commands.Blob.Container; +using AzureMcp.Areas.Storage.Commands.DataLake.Directory; using AzureMcp.Areas.Storage.Commands.DataLake.FileSystem; using AzureMcp.Areas.Storage.Commands.Table; @@ -16,6 +17,7 @@ namespace AzureMcp.Commands.Storage; [JsonSerializable(typeof(ContainerListCommand.ContainerListCommandResult))] [JsonSerializable(typeof(ContainerDetailsCommand.ContainerDetailsCommandResult))] [JsonSerializable(typeof(FileSystemListPathsCommand.FileSystemListPathsCommandResult))] +[JsonSerializable(typeof(DirectoryListPathsCommand.DirectoryListPathsCommandResult))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] internal sealed partial class StorageJsonContext : JsonSerializerContext { diff --git a/src/Areas/Storage/Options/DataLake/BaseDirectoryOptions.cs b/src/Areas/Storage/Options/DataLake/BaseDirectoryOptions.cs new file mode 100644 index 000000000..f064c8e33 --- /dev/null +++ b/src/Areas/Storage/Options/DataLake/BaseDirectoryOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AzureMcp.Areas.Storage.Options.DataLake; + +public class BaseDirectoryOptions : BaseFileSystemOptions +{ + [JsonPropertyName(StorageOptionDefinitions.DirectoryName)] + public string? Directory { get; set; } +} \ No newline at end of file diff --git a/src/Areas/Storage/Options/DataLake/Directory/ListPathsOptions.cs b/src/Areas/Storage/Options/DataLake/Directory/ListPathsOptions.cs new file mode 100644 index 000000000..32aaf464a --- /dev/null +++ b/src/Areas/Storage/Options/DataLake/Directory/ListPathsOptions.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AzureMcp.Areas.Storage.Options.DataLake.Directory; + +public class ListPathsOptions : BaseDirectoryOptions; \ No newline at end of file diff --git a/src/Areas/Storage/Options/StorageOptionDefinitions.cs b/src/Areas/Storage/Options/StorageOptionDefinitions.cs index e52c76cf4..83fb37deb 100644 --- a/src/Areas/Storage/Options/StorageOptionDefinitions.cs +++ b/src/Areas/Storage/Options/StorageOptionDefinitions.cs @@ -9,6 +9,7 @@ public static class StorageOptionDefinitions public const string ContainerName = "container-name"; public const string TableName = "table-name"; public const string FileSystemName = "file-system-name"; + public const string DirectoryName = "directory-name"; public static readonly Option Account = new( $"--{AccountName}", @@ -41,4 +42,12 @@ public static class StorageOptionDefinitions { IsRequired = true }; + + public static readonly Option Directory = new( + $"--{DirectoryName}", + "The name of the directory within the Data Lake file system to access." + ) + { + IsRequired = true + }; } diff --git a/src/Areas/Storage/Services/IStorageService.cs b/src/Areas/Storage/Services/IStorageService.cs index 60cc09375..8103316d7 100644 --- a/src/Areas/Storage/Services/IStorageService.cs +++ b/src/Areas/Storage/Services/IStorageService.cs @@ -31,4 +31,11 @@ Task> ListDataLakePaths( string subscriptionId, string? tenant = null, RetryPolicyOptions? retryPolicy = null); + Task> ListDataLakeDirectoryPaths( + string accountName, + string fileSystemName, + string directoryName, + string subscriptionId, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null); } diff --git a/src/Areas/Storage/Services/StorageService.cs b/src/Areas/Storage/Services/StorageService.cs index 46ce97d08..70e363911 100644 --- a/src/Areas/Storage/Services/StorageService.cs +++ b/src/Areas/Storage/Services/StorageService.cs @@ -347,4 +347,41 @@ public async Task> ListDataLakePaths( return paths; } + + public async Task> ListDataLakeDirectoryPaths( + string accountName, + string fileSystemName, + string directoryName, + string subscriptionId, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null) + { + ValidateRequiredParameters(accountName, fileSystemName, directoryName, subscriptionId); + + var dataLakeServiceClient = await CreateDataLakeServiceClient(accountName, tenant, retryPolicy); + var fileSystemClient = dataLakeServiceClient.GetFileSystemClient(fileSystemName); + var directoryClient = fileSystemClient.GetDirectoryClient(directoryName); + var paths = new List(); + + try + { + await foreach (var pathItem in directoryClient.GetPathsAsync()) + { + var pathInfo = new DataLakePathInfo( + pathItem.Name, + pathItem.IsDirectory == true ? "directory" : "file", + pathItem.ContentLength, + pathItem.LastModified, + pathItem.ETag.ToString()); + + paths.Add(pathInfo); + } + } + catch (Exception ex) + { + throw new Exception($"Error listing Data Lake directory paths: {ex.Message}", ex); + } + + return paths; + } } diff --git a/src/Areas/Storage/StorageSetup.cs b/src/Areas/Storage/StorageSetup.cs index e36f52fdc..6ec592169 100644 --- a/src/Areas/Storage/StorageSetup.cs +++ b/src/Areas/Storage/StorageSetup.cs @@ -4,6 +4,7 @@ using AzureMcp.Areas.Storage.Commands.Account; using AzureMcp.Areas.Storage.Commands.Blob; using AzureMcp.Areas.Storage.Commands.Blob.Container; +using AzureMcp.Areas.Storage.Commands.DataLake.Directory; using AzureMcp.Areas.Storage.Commands.DataLake.FileSystem; using AzureMcp.Areas.Storage.Commands.Table; using AzureMcp.Areas.Storage.Services; @@ -48,6 +49,10 @@ public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactor var fileSystem = new CommandGroup("file-system", "Data Lake file system operations - Commands for managing file systems and paths in Azure Data Lake Storage Gen2."); dataLake.AddSubGroup(fileSystem); + // Create directory subgroup under datalake + var directory = new CommandGroup("directory", "Data Lake directory operations - Commands for managing directories and paths within Azure Data Lake Storage Gen2 file systems."); + dataLake.AddSubGroup(directory); + // Register Storage commands storageAccount.AddCommand("list", new AccountListCommand( loggerFactory.CreateLogger())); @@ -63,5 +68,8 @@ public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactor fileSystem.AddCommand("list-paths", new FileSystemListPathsCommand( loggerFactory.CreateLogger())); + + directory.AddCommand("list-paths", new DirectoryListPathsCommand( + loggerFactory.CreateLogger())); } } From 255e0e904138b5df824d2cce0c2a5917b6faa7e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:36:27 +0000 Subject: [PATCH 3/6] Add unit and integration tests for directory list-paths command Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- .../Storage/LiveTests/StorageCommandTests.cs | 18 ++ .../DirectoryListPathsCommandTests.cs | 169 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 tests/Areas/Storage/UnitTests/DataLake/Directory/DirectoryListPathsCommandTests.cs diff --git a/tests/Areas/Storage/LiveTests/StorageCommandTests.cs b/tests/Areas/Storage/LiveTests/StorageCommandTests.cs index 87606b19c..b5ef5b75d 100644 --- a/tests/Areas/Storage/LiveTests/StorageCommandTests.cs +++ b/tests/Areas/Storage/LiveTests/StorageCommandTests.cs @@ -225,5 +225,23 @@ public async Task Should_list_datalake_filesystem_paths() var actual = result.AssertProperty("paths"); Assert.Equal(JsonValueKind.Array, actual.ValueKind); } + + [Fact] + [Trait("Category", "Live")] + public async Task Should_list_datalake_directory_paths() + { + var result = await CallToolAsync( + "azmcp_storage_datalake_directory_list-paths", + new() + { + { "subscription", Settings.SubscriptionName }, + { "account-name", Settings.ResourceBaseName }, + { "file-system-name", "testfilesystem" }, + { "directory-name", "testdirectory" } + }); + + var actual = result.AssertProperty("paths"); + Assert.Equal(JsonValueKind.Array, actual.ValueKind); + } } } diff --git a/tests/Areas/Storage/UnitTests/DataLake/Directory/DirectoryListPathsCommandTests.cs b/tests/Areas/Storage/UnitTests/DataLake/Directory/DirectoryListPathsCommandTests.cs new file mode 100644 index 000000000..a2eab0b9b --- /dev/null +++ b/tests/Areas/Storage/UnitTests/DataLake/Directory/DirectoryListPathsCommandTests.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine.Parsing; +using System.Text.Json; +using System.Text.Json.Serialization; +using AzureMcp.Areas.Storage.Commands.DataLake.Directory; +using AzureMcp.Areas.Storage.Models; +using AzureMcp.Areas.Storage.Services; +using AzureMcp.Models.Command; +using AzureMcp.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace AzureMcp.Tests.Areas.Storage.UnitTests.DataLake.Directory; + +[Trait("Area", "Storage")] +public class DirectoryListPathsCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IStorageService _storageService; + private readonly ILogger _logger; + private readonly DirectoryListPathsCommand _command; + private readonly CommandContext _context; + private readonly Parser _parser; + private readonly string _knownAccountName = "account123"; + private readonly string _knownFileSystemName = "filesystem123"; + private readonly string _knownDirectoryName = "directory123"; + private readonly string _knownSubscriptionId = "sub123"; + + public DirectoryListPathsCommandTests() + { + _storageService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_storageService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _parser = new(_command.GetCommand()); + } + + [Fact] + public async Task ExecuteAsync_WithValidParameters_ReturnsPaths() + { + // Arrange + var expectedPaths = new List + { + new("file1.txt", "file", 1024, DateTimeOffset.Now, "\"etag1\""), + new("subdirectory1", "directory", null, DateTimeOffset.Now, "\"etag2\"") + }; + + _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownFileSystemName), + Arg.Is(_knownDirectoryName), Arg.Is(_knownSubscriptionId), Arg.Any(), Arg.Any()).Returns(expectedPaths); + + var args = _parser.Parse([ + "--account-name", _knownAccountName, + "--file-system-name", _knownFileSystemName, + "--directory-name", _knownDirectoryName, + "--subscription", _knownSubscriptionId + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json); + + Assert.NotNull(result); + Assert.Equal(expectedPaths.Count, result.Paths.Count); + Assert.Equal(expectedPaths[0].Name, result.Paths[0].Name); + Assert.Equal(expectedPaths[0].Type, result.Paths[0].Type); + } + + [Fact] + public async Task ExecuteAsync_ReturnsEmptyArray_WhenNoPaths() + { + // Arrange + _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownFileSystemName), + Arg.Is(_knownDirectoryName), Arg.Is(_knownSubscriptionId), Arg.Any(), Arg.Any()).Returns([]); + + var args = _parser.Parse([ + "--account-name", _knownAccountName, + "--file-system-name", _knownFileSystemName, + "--directory-name", _knownDirectoryName, + "--subscription", _knownSubscriptionId + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json); + + Assert.NotNull(result); + Assert.Empty(result.Paths); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + var expectedError = "Test error"; + + _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownFileSystemName), + Arg.Is(_knownDirectoryName), Arg.Is(_knownSubscriptionId), null, Arg.Any()).ThrowsAsync(new Exception(expectedError)); + + var args = _parser.Parse([ + "--account-name", _knownAccountName, + "--file-system-name", _knownFileSystemName, + "--directory-name", _knownDirectoryName, + "--subscription", _knownSubscriptionId + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.NotNull(response); + Assert.Equal(500, response.Status); + Assert.StartsWith(expectedError, response.Message); + } + + [Theory] + [InlineData("--file-system-name filesystem123 --directory-name directory123 --subscription sub123", false)] // Missing account + [InlineData("--account-name account123 --directory-name directory123 --subscription sub123", false)] // Missing file-system + [InlineData("--account-name account123 --file-system-name filesystem123 --subscription sub123", false)] // Missing directory + [InlineData("--account-name account123 --file-system-name filesystem123 --directory-name directory123", false)] // Missing subscription + [InlineData("--account-name account123 --file-system-name filesystem123 --directory-name directory123 --subscription sub123", true)] // Valid + public async Task ExecuteAsync_ValidatesRequiredParameters(string args, bool shouldSucceed) + { + // Arrange + if (shouldSucceed) + { + _storageService.ListDataLakeDirectoryPaths(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()).Returns([]); + } + + var parseResult = _parser.Parse(args.Split(' ')); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult); + + // Assert + Assert.Equal(shouldSucceed ? 200 : 400, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message.ToLower()); + } + } + + private class DirectoryListPathsResult + { + [JsonPropertyName("paths")] + public List Paths { get; set; } = []; + } +} \ No newline at end of file From a3c8985c913db7d210cdc6ea170876ec5452a9f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:40:29 +0000 Subject: [PATCH 4/6] Add comprehensive documentation for directory list-paths command Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ README.md | 1 + docs/azmcp-commands.md | 6 ++++++ e2eTests/e2eTestPrompts.md | 2 ++ 4 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f512ec1..6e30260e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## Unreleased + +### Features Added +- Support for Azure Data Lake Storage Gen2 directory operations - List paths in Data Lake directories via the command: `azmcp storage datalake directory list-paths`. + ## 0.4.1 (2025-07-17) ### Features Added diff --git a/README.md b/README.md index 1d985c7bd..72f6ac8ec 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some - "Show me the tables in my Storage account" - "Get details about my Storage container" - "List paths in my Data Lake file system" +- "List paths in my Data Lake directory" - "Show my key-value pairs in App Config" ### 🔧 Azure Resource Management diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index e1e0cb4ef..5b7076e13 100644 --- a/docs/azmcp-commands.md +++ b/docs/azmcp-commands.md @@ -665,6 +665,12 @@ azmcp storage blob container details --subscription \ azmcp storage datalake file-system list-paths --subscription \ --account-name \ --file-system-name + +# List paths in a Data Lake directory +azmcp storage datalake directory list-paths --subscription \ + --account-name \ + --file-system-name \ + --directory-name ``` ### Azure Subscription Management diff --git a/e2eTests/e2eTestPrompts.md b/e2eTests/e2eTestPrompts.md index fdf3d1f40..f5bfaea4f 100644 --- a/e2eTests/e2eTestPrompts.md +++ b/e2eTests/e2eTestPrompts.md @@ -272,6 +272,8 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | azmcp-storage-table-list | Show me the tables in the storage account | | azmcp-storage-datalake-file-system-list-paths | List all paths in the Data Lake file system in the storage account | | azmcp-storage-datalake-file-system-list-paths | Show me the paths in the Data Lake file system in the storage account | +| azmcp-storage-datalake-directory-list-paths | List all paths in the Data Lake directory in file system in the storage account | +| azmcp-storage-datalake-directory-list-paths | Show me the paths in the Data Lake directory in file system in the storage account | ## Azure Subscription Management From 6ca434199f4c03da4097fae41ebad02807df3c06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:48:18 +0000 Subject: [PATCH 5/6] Fix StorageService to use GetDirectoryClient directly as suggested in code review Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- src/Areas/Storage/Services/StorageService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Areas/Storage/Services/StorageService.cs b/src/Areas/Storage/Services/StorageService.cs index 70e363911..624b1c812 100644 --- a/src/Areas/Storage/Services/StorageService.cs +++ b/src/Areas/Storage/Services/StorageService.cs @@ -359,8 +359,7 @@ public async Task> ListDataLakeDirectoryPaths( ValidateRequiredParameters(accountName, fileSystemName, directoryName, subscriptionId); var dataLakeServiceClient = await CreateDataLakeServiceClient(accountName, tenant, retryPolicy); - var fileSystemClient = dataLakeServiceClient.GetFileSystemClient(fileSystemName); - var directoryClient = fileSystemClient.GetDirectoryClient(directoryName); + var directoryClient = dataLakeServiceClient.GetDirectoryClient(fileSystemName, directoryName); var paths = new List(); try From 00c83995a1a708aea2ffb3b987f3c36d39aee31f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:58:17 +0000 Subject: [PATCH 6/6] Remove fileSystemName parameter from directory list-paths command as requested in code review Co-authored-by: xiangyan99 <14350651+xiangyan99@users.noreply.github.com> --- docs/azmcp-commands.md | 3 +- e2eTests/e2eTestPrompts.md | 4 +-- .../Directory/BaseDirectoryCommand.cs | 3 -- .../Directory/DirectoryListPathsCommand.cs | 11 +++--- .../Options/DataLake/BaseDirectoryOptions.cs | 2 +- src/Areas/Storage/Services/IStorageService.cs | 3 +- src/Areas/Storage/Services/StorageService.cs | 15 ++++++-- .../Storage/LiveTests/StorageCommandTests.cs | 3 +- .../DirectoryListPathsCommandTests.cs | 35 ++++++++----------- 9 files changed, 38 insertions(+), 41 deletions(-) diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index 5b7076e13..334d11e52 100644 --- a/docs/azmcp-commands.md +++ b/docs/azmcp-commands.md @@ -669,8 +669,7 @@ azmcp storage datalake file-system list-paths --subscription \ # List paths in a Data Lake directory azmcp storage datalake directory list-paths --subscription \ --account-name \ - --file-system-name \ - --directory-name + --directory-name ``` ### Azure Subscription Management diff --git a/e2eTests/e2eTestPrompts.md b/e2eTests/e2eTestPrompts.md index f5bfaea4f..f7a9a5d99 100644 --- a/e2eTests/e2eTestPrompts.md +++ b/e2eTests/e2eTestPrompts.md @@ -272,8 +272,8 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | azmcp-storage-table-list | Show me the tables in the storage account | | azmcp-storage-datalake-file-system-list-paths | List all paths in the Data Lake file system in the storage account | | azmcp-storage-datalake-file-system-list-paths | Show me the paths in the Data Lake file system in the storage account | -| azmcp-storage-datalake-directory-list-paths | List all paths in the Data Lake directory in file system in the storage account | -| azmcp-storage-datalake-directory-list-paths | Show me the paths in the Data Lake directory in file system in the storage account | +| azmcp-storage-datalake-directory-list-paths | List all paths in the Data Lake directory in the storage account | +| azmcp-storage-datalake-directory-list-paths | Show me the paths in the Data Lake directory in the storage account | ## Azure Subscription Management diff --git a/src/Areas/Storage/Commands/DataLake/Directory/BaseDirectoryCommand.cs b/src/Areas/Storage/Commands/DataLake/Directory/BaseDirectoryCommand.cs index f26edc6f4..09093dd21 100644 --- a/src/Areas/Storage/Commands/DataLake/Directory/BaseDirectoryCommand.cs +++ b/src/Areas/Storage/Commands/DataLake/Directory/BaseDirectoryCommand.cs @@ -13,20 +13,17 @@ public abstract class BaseDirectoryCommand< [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions> : BaseStorageCommand where TOptions : BaseDirectoryOptions, new() { - protected readonly Option _fileSystemOption = StorageOptionDefinitions.FileSystem; protected readonly Option _directoryOption = StorageOptionDefinitions.Directory; protected override void RegisterOptions(Command command) { base.RegisterOptions(command); - command.AddOption(_fileSystemOption); command.AddOption(_directoryOption); } protected override TOptions BindOptions(ParseResult parseResult) { var options = base.BindOptions(parseResult); - options.FileSystem = parseResult.GetValueForOption(_fileSystemOption); options.Directory = parseResult.GetValueForOption(_directoryOption); return options; } diff --git a/src/Areas/Storage/Commands/DataLake/Directory/DirectoryListPathsCommand.cs b/src/Areas/Storage/Commands/DataLake/Directory/DirectoryListPathsCommand.cs index 73a41f590..e5aad8652 100644 --- a/src/Areas/Storage/Commands/DataLake/Directory/DirectoryListPathsCommand.cs +++ b/src/Areas/Storage/Commands/DataLake/Directory/DirectoryListPathsCommand.cs @@ -20,9 +20,9 @@ public sealed class DirectoryListPathsCommand(ILogger public override string Description => """ List all paths in a Data Lake directory. This command retrieves and displays all paths (files and directories) - available in the specified directory within a Data Lake file system. Results include path names, - types (file or directory), and metadata, returned as a JSON array. Requires account-name, file-system-name, - and directory-name. + available in the specified directory within a Data Lake storage account. The directory path should include the + file system name (e.g., 'filesystem/directory'). Results include path names, types (file or directory), + and metadata, returned as a JSON array. Requires account-name and directory-name. """; public override string Title => CommandTitle; @@ -44,7 +44,6 @@ public override async Task ExecuteAsync(CommandContext context, var storageService = context.GetService(); var paths = await storageService.ListDataLakeDirectoryPaths( options.Account!, - options.FileSystem!, options.Directory!, options.Subscription!, options.Tenant, @@ -56,8 +55,8 @@ public override async Task ExecuteAsync(CommandContext context, } catch (Exception ex) { - _logger.LogError(ex, "Error listing Data Lake directory paths. Account: {Account}, FileSystem: {FileSystem}, Directory: {Directory}.", - options.Account, options.FileSystem, options.Directory); + _logger.LogError(ex, "Error listing Data Lake directory paths. Account: {Account}, Directory: {Directory}.", + options.Account, options.Directory); HandleException(context, ex); } diff --git a/src/Areas/Storage/Options/DataLake/BaseDirectoryOptions.cs b/src/Areas/Storage/Options/DataLake/BaseDirectoryOptions.cs index f064c8e33..bb19ab7ed 100644 --- a/src/Areas/Storage/Options/DataLake/BaseDirectoryOptions.cs +++ b/src/Areas/Storage/Options/DataLake/BaseDirectoryOptions.cs @@ -5,7 +5,7 @@ namespace AzureMcp.Areas.Storage.Options.DataLake; -public class BaseDirectoryOptions : BaseFileSystemOptions +public class BaseDirectoryOptions : BaseStorageOptions { [JsonPropertyName(StorageOptionDefinitions.DirectoryName)] public string? Directory { get; set; } diff --git a/src/Areas/Storage/Services/IStorageService.cs b/src/Areas/Storage/Services/IStorageService.cs index 8103316d7..6a26c9ad6 100644 --- a/src/Areas/Storage/Services/IStorageService.cs +++ b/src/Areas/Storage/Services/IStorageService.cs @@ -33,8 +33,7 @@ Task> ListDataLakePaths( RetryPolicyOptions? retryPolicy = null); Task> ListDataLakeDirectoryPaths( string accountName, - string fileSystemName, - string directoryName, + string directoryPath, string subscriptionId, string? tenant = null, RetryPolicyOptions? retryPolicy = null); diff --git a/src/Areas/Storage/Services/StorageService.cs b/src/Areas/Storage/Services/StorageService.cs index 624b1c812..1d9a7942a 100644 --- a/src/Areas/Storage/Services/StorageService.cs +++ b/src/Areas/Storage/Services/StorageService.cs @@ -350,13 +350,22 @@ public async Task> ListDataLakePaths( public async Task> ListDataLakeDirectoryPaths( string accountName, - string fileSystemName, - string directoryName, + string directoryPath, string subscriptionId, string? tenant = null, RetryPolicyOptions? retryPolicy = null) { - ValidateRequiredParameters(accountName, fileSystemName, directoryName, subscriptionId); + ValidateRequiredParameters(accountName, directoryPath, subscriptionId); + + // Parse the directory path to extract file system and directory components + var pathParts = directoryPath.Split('/', 2, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 1) + { + throw new ArgumentException("Directory path must include at least the file system name", nameof(directoryPath)); + } + + var fileSystemName = pathParts[0]; + var directoryName = pathParts.Length > 1 ? pathParts[1] : ""; var dataLakeServiceClient = await CreateDataLakeServiceClient(accountName, tenant, retryPolicy); var directoryClient = dataLakeServiceClient.GetDirectoryClient(fileSystemName, directoryName); diff --git a/tests/Areas/Storage/LiveTests/StorageCommandTests.cs b/tests/Areas/Storage/LiveTests/StorageCommandTests.cs index b5ef5b75d..cc3e134e6 100644 --- a/tests/Areas/Storage/LiveTests/StorageCommandTests.cs +++ b/tests/Areas/Storage/LiveTests/StorageCommandTests.cs @@ -236,8 +236,7 @@ public async Task Should_list_datalake_directory_paths() { { "subscription", Settings.SubscriptionName }, { "account-name", Settings.ResourceBaseName }, - { "file-system-name", "testfilesystem" }, - { "directory-name", "testdirectory" } + { "directory-name", "testfilesystem/testdirectory" } }); var actual = result.AssertProperty("paths"); diff --git a/tests/Areas/Storage/UnitTests/DataLake/Directory/DirectoryListPathsCommandTests.cs b/tests/Areas/Storage/UnitTests/DataLake/Directory/DirectoryListPathsCommandTests.cs index a2eab0b9b..ea9e3dca8 100644 --- a/tests/Areas/Storage/UnitTests/DataLake/Directory/DirectoryListPathsCommandTests.cs +++ b/tests/Areas/Storage/UnitTests/DataLake/Directory/DirectoryListPathsCommandTests.cs @@ -27,8 +27,7 @@ public class DirectoryListPathsCommandTests private readonly CommandContext _context; private readonly Parser _parser; private readonly string _knownAccountName = "account123"; - private readonly string _knownFileSystemName = "filesystem123"; - private readonly string _knownDirectoryName = "directory123"; + private readonly string _knownDirectoryPath = "filesystem123/directory123"; private readonly string _knownSubscriptionId = "sub123"; public DirectoryListPathsCommandTests() @@ -54,13 +53,12 @@ public async Task ExecuteAsync_WithValidParameters_ReturnsPaths() new("subdirectory1", "directory", null, DateTimeOffset.Now, "\"etag2\"") }; - _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownFileSystemName), - Arg.Is(_knownDirectoryName), Arg.Is(_knownSubscriptionId), Arg.Any(), Arg.Any()).Returns(expectedPaths); + _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownDirectoryPath), + Arg.Is(_knownSubscriptionId), Arg.Any(), Arg.Any()).Returns(expectedPaths); var args = _parser.Parse([ "--account-name", _knownAccountName, - "--file-system-name", _knownFileSystemName, - "--directory-name", _knownDirectoryName, + "--directory-name", _knownDirectoryPath, "--subscription", _knownSubscriptionId ]); @@ -84,13 +82,12 @@ public async Task ExecuteAsync_WithValidParameters_ReturnsPaths() public async Task ExecuteAsync_ReturnsEmptyArray_WhenNoPaths() { // Arrange - _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownFileSystemName), - Arg.Is(_knownDirectoryName), Arg.Is(_knownSubscriptionId), Arg.Any(), Arg.Any()).Returns([]); + _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownDirectoryPath), + Arg.Is(_knownSubscriptionId), Arg.Any(), Arg.Any()).Returns([]); var args = _parser.Parse([ "--account-name", _knownAccountName, - "--file-system-name", _knownFileSystemName, - "--directory-name", _knownDirectoryName, + "--directory-name", _knownDirectoryPath, "--subscription", _knownSubscriptionId ]); @@ -114,13 +111,12 @@ public async Task ExecuteAsync_HandlesException() // Arrange var expectedError = "Test error"; - _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownFileSystemName), - Arg.Is(_knownDirectoryName), Arg.Is(_knownSubscriptionId), null, Arg.Any()).ThrowsAsync(new Exception(expectedError)); + _storageService.ListDataLakeDirectoryPaths(Arg.Is(_knownAccountName), Arg.Is(_knownDirectoryPath), + Arg.Is(_knownSubscriptionId), null, Arg.Any()).ThrowsAsync(new Exception(expectedError)); var args = _parser.Parse([ "--account-name", _knownAccountName, - "--file-system-name", _knownFileSystemName, - "--directory-name", _knownDirectoryName, + "--directory-name", _knownDirectoryPath, "--subscription", _knownSubscriptionId ]); @@ -134,17 +130,16 @@ public async Task ExecuteAsync_HandlesException() } [Theory] - [InlineData("--file-system-name filesystem123 --directory-name directory123 --subscription sub123", false)] // Missing account - [InlineData("--account-name account123 --directory-name directory123 --subscription sub123", false)] // Missing file-system - [InlineData("--account-name account123 --file-system-name filesystem123 --subscription sub123", false)] // Missing directory - [InlineData("--account-name account123 --file-system-name filesystem123 --directory-name directory123", false)] // Missing subscription - [InlineData("--account-name account123 --file-system-name filesystem123 --directory-name directory123 --subscription sub123", true)] // Valid + [InlineData("--directory-name filesystem123/directory123 --subscription sub123", false)] // Missing account + [InlineData("--account-name account123 --subscription sub123", false)] // Missing directory + [InlineData("--account-name account123 --directory-name filesystem123/directory123", false)] // Missing subscription + [InlineData("--account-name account123 --directory-name filesystem123/directory123 --subscription sub123", true)] // Valid public async Task ExecuteAsync_ValidatesRequiredParameters(string args, bool shouldSucceed) { // Arrange if (shouldSucceed) { - _storageService.ListDataLakeDirectoryPaths(Arg.Any(), Arg.Any(), Arg.Any(), + _storageService.ListDataLakeDirectoryPaths(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()).Returns([]); }