Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Features Added"
description: "Add prefix support to storage_blob_get and storage_blob_container_get"
12 changes: 7 additions & 5 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -3240,8 +3240,8 @@ azmcp storage account create --subscription <subscription> \
# Get detailed properties of Storage accounts
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp storage account get --subscription <subscription> \
[--account <account>] \
[--tenant <tenant>]
[--account <account>] \
[--tenant <tenant>]
```

#### Blob Storage
Expand All @@ -3256,15 +3256,17 @@ azmcp storage blob container create --subscription <subscription> \
# Get detailed properties of Storage containers
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp storage blob container get --subscription <subscription> \
--account <account> \
[--container <container>]
--account <account> \
[--container <container>] \
[--prefix <prefix>]

# Get detailed properties of Storage blobs
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp storage blob get --subscription <subscription> \
--account <account> \
--container <container> \
[--blob <blob>]
[--blob <blob>] \
[--prefix <prefix>]

# Upload a file to a Storage blob
# ❌ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ✅ LocalRequired
Expand Down
2 changes: 2 additions & 0 deletions servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -908,10 +908,12 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
| storage_blob_container_create | Create the storage container mycontainer in storage account <account> |
| storage_blob_container_get | Show me the properties of the storage container <container> in the storage account <account> |
| storage_blob_container_get | List all blob containers in the storage account <account> |
| storage_blob_container_get | List all blob containers in the storage account <account> with prefix <prefix> |
| storage_blob_container_get | Show me the containers in the storage account <account> |
| storage_blob_get | Show me the properties for blob <blob> in container <container> in storage account <account> |
| storage_blob_get | Get the details about blob <blob> in the container <container> in storage account <account> |
| storage_blob_get | List all blobs in the blob container <container> in the storage account <account> |
| storage_blob_get | List all blobs in the blob container <container> in the storage account <account> with prefix <prefix> |
| storage_blob_get | Show me the blobs in the blob container <container> in the storage account <account> |
| storage_blob_upload | Upload file <local-file-path> to storage blob <blob> in container <container> in storage account <account> |
| storage_table_list | List all tables in the storage account <account> |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Azure.Mcp.Tools.Storage.Commands.Blob.Container;
using Azure.Mcp.Tools.Storage.Models;
using Azure.Mcp.Tools.Storage.Options;
using Azure.Mcp.Tools.Storage.Options.Blob;
using Azure.Mcp.Tools.Storage.Services;
Expand All @@ -24,8 +25,16 @@ public sealed class BlobGetCommand(ILogger<BlobGetCommand> logger, IStorageServi
public override string Name => "get";

public override string Description =>
$"""
List/get/show blobs in a blob container in Storage account. Use this tool to list the blobs in a container or get details for a specific blob. Shows blob properties including metadata, size, last modification time, and content properties. If no blob specified, lists all blobs present in the container. Required: account, container <container>, subscription <subscription>. Optional: blob <blob>, tenant <tenant>. Returns: blob name, size, lastModified, contentType, contentMD5, metadata, and blob properties. Do not use this tool to list containers in the storage account.
"""
List/get/show blobs in a blob container in Storage account. Use this tool to list the blobs in a container or
get details for a specific blob. If no blob specified, lists all blobs present in the container, optionally
filtering on a prefix.

Required: --account, --container, --subscription
Optional: --blob, --tenant, --prefix

Returns: blob name, size, lastModified, contentType, contentMD5, metadata, and blob properties.
Do not use this tool to list containers in the storage account.
""";

public override string Title => CommandTitle;
Expand All @@ -44,12 +53,14 @@ protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(StorageOptionDefinitions.Blob.AsOptional());
command.Options.Add(StorageOptionDefinitions.BlobPrefix.AsOptional());
}

protected override BlobGetOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.Blob = parseResult.GetValueOrDefault<string>(StorageOptionDefinitions.Blob.Name);
options.Prefix = parseResult.GetValueOrDefault<string>(StorageOptionDefinitions.BlobPrefix.Name);
return options;
}

Expand All @@ -69,6 +80,7 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
options.Container!,
options.Blob,
options.Subscription!,
options.Prefix,
options.Tenant,
options.RetryPolicy,
cancellationToken
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Tools.Storage.Models;
using Azure.Mcp.Tools.Storage.Options.Blob.Container;
using Azure.Mcp.Tools.Storage.Services;
using Microsoft.Extensions.Logging;
Expand All @@ -21,7 +22,13 @@ public sealed class ContainerCreateCommand(ILogger<ContainerCreateCommand> logge

public override string Description =>
"""
Create/provision a new Azure Storage blob container in a storage account. Required: --account <account>, --container <container>, --subscription <subscription>. Optional: --tenant <tenant>. Returns: container name, lastModified, eTag, leaseStatus, publicAccessLevel, hasImmutabilityPolicy, hasLegalHold. Creates a logical container for organizing blobs within a storage account.
Create/provision a new Azure Storage blob container in a storage account.

Required: --account, --container, --subscription
Optional: --tenant

Returns: container name, lastModified, eTag, leaseStatus, publicAccessLevel, hasImmutabilityPolicy, hasLegalHold.
Creates a logical container for organizing blobs within a storage account.
""";

public override string Title => CommandTitle;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.


using Azure.Mcp.Tools.Storage.Models;
using Azure.Mcp.Tools.Storage.Options;
using Azure.Mcp.Tools.Storage.Options.Blob.Container;
using Azure.Mcp.Tools.Storage.Services;
Expand All @@ -23,8 +25,16 @@ public sealed class ContainerGetCommand(ILogger<ContainerGetCommand> logger, ISt
public override string Name => "get";

public override string Description =>
$"""
Show/list containers in a storage account. Use this tool to list all blob containers in the storage account or show details for a specific Storage container. Displays container properties including access policies, lease status, and metadata. If no container specified, shows all containers in the storage account. Required: account <account>, subscription <subscription>. Optional: container <container>, tenant <tenant>. Returns: container name, lastModified, leaseStatus, publicAccessLevel, metadata, and container properties. Do not use this tool to list blobs in a container.
"""
Show/list containers in a storage account. Use this tool to list all blob containers in the storage account or
show details for a specific Storage container. If no container specified, shows all containers in the storage
account, optionally filtering on a prefix.
Required: --account, --subscription
Optional: --container, --tenant, --prefix
Returns: container name, lastModified, leaseStatus, publicAccessLevel, metadata, and container properties.
Do not use this tool to list blobs in a container.
""";

public override string Title => CommandTitle;
Expand All @@ -43,12 +53,14 @@ protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(StorageOptionDefinitions.Container.AsOptional());
command.Options.Add(StorageOptionDefinitions.ContainerPrefix.AsOptional());
}

protected override ContainerGetOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.Container = parseResult.GetValueOrDefault<string>(StorageOptionDefinitions.Container.Name);
options.Prefix = parseResult.GetValueOrDefault<string>(StorageOptionDefinitions.ContainerPrefix.Name);
return options;
}

Expand All @@ -67,6 +79,7 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
options.Account!,
options.Container,
options.Subscription!,
options.Prefix,
options.Tenant,
options.RetryPolicy,
cancellationToken
Expand Down
2 changes: 2 additions & 0 deletions tools/Azure.Mcp.Tools.Storage/src/Models/BlobInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System.Text.Json.Serialization;

namespace Azure.Mcp.Tools.Storage.Models;

// Lightweight projection of BlobProperties with commonly useful metadata.
// Keep property names stable; only add new nullable properties to extend.
public sealed record BlobInfo(
Expand Down
2 changes: 2 additions & 0 deletions tools/Azure.Mcp.Tools.Storage/src/Models/ContainerInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System.Text.Json.Serialization;

namespace Azure.Mcp.Tools.Storage.Models;

// Lightweight projection of ContainerProperties with commonly useful metadata.
// Keep property names stable; only add new nullable properties to extend.
public sealed record ContainerInfo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.Mcp.Tools.Storage.Options.Blob;

public class BlobGetOptions : BaseBlobOptions;
public class BlobGetOptions : BaseBlobOptions
{
[JsonPropertyName(StorageOptionDefinitions.PrefixName)]
public string? Prefix { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.Mcp.Tools.Storage.Options.Blob.Container;

public class ContainerGetOptions : BaseContainerOptions;
public class ContainerGetOptions : BaseContainerOptions
{
[JsonPropertyName(StorageOptionDefinitions.PrefixName)]
public string? Prefix { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class StorageOptionDefinitions
public const string SkuName = "sku";
public const string AccessTierName = "access-tier";
public const string EnableHierarchicalNamespaceName = "enable-hierarchical-namespace";
public const string PrefixName = "prefix";

public static readonly Option<string> Account = new($"--{AccountName}")
{
Expand Down Expand Up @@ -69,4 +70,16 @@ public static class StorageOptionDefinitions
Description = "The local file path to read content from or to write content to. This should be the full path to the file on your local system.",
Required = true
};

public static readonly Option<string> BlobPrefix = new($"--{PrefixName}")
{
Description = "The prefix to filter blobs when listing blobs in a container. Only blobs whose names start with the specified prefix will be listed.",
Required = false
};

public static readonly Option<string> ContainerPrefix = new($"--{PrefixName}")
{
Description = "The prefix to filter containers when listing containers in a storage account. Only containers whose names start with the specified prefix will be listed.",
Required = false
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Task<List<BlobInfo>> GetBlobDetails(
string container,
string? blob,
string subscription,
string? prefix = null,
string? tenant = null,
RetryPolicyOptions? retryPolicy = null,
CancellationToken cancellationToken = default);
Expand All @@ -41,6 +42,7 @@ Task<List<ContainerInfo>> GetContainerDetails(
string account,
string? container,
string subscription,
string? prefix = null,
string? tenant = null,
RetryPolicyOptions? retryPolicy = null,
CancellationToken cancellationToken = default);
Expand Down
45 changes: 21 additions & 24 deletions tools/Azure.Mcp.Tools.Storage/src/Services/StorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ public class StorageService(
private readonly ITenantService _tenantService = tenantService ?? throw new ArgumentNullException(nameof(tenantService));
private readonly ILogger<StorageService> _logger = logger ?? throw new ArgumentNullException(nameof(logger));

private static readonly HashSet<string> s_validSkus = new(StringComparer.OrdinalIgnoreCase)
{
"Standard_LRS", "Standard_GRS", "Standard_RAGRS", "Standard_ZRS", "Premium_LRS", "Premium_ZRS",
"Standard_GZRS", "Standard_RAGZRS", "StandardV2_LRS", "StandardV2_GRS", "StandardV2_ZRS", "StandardV2_GZRS",
"PremiumV2_LRS", "PremiumV2_ZRS"
};

private static readonly HashSet<string> s_validTiers = new(StringComparer.OrdinalIgnoreCase) { "hot", "cool", "premium", "cold" };

public async Task<ResourceQueryResults<StorageAccountInfo>> GetAccountDetails(
string? account,
string subscription,
Expand Down Expand Up @@ -62,12 +71,7 @@ public async Task<ResourceQueryResults<StorageAccountInfo>> GetAccountDetails(
converter: ConvertToAccountInfoModel,
additionalFilter: $"name =~ '{EscapeKqlString(account)}'",
tenant: tenant,
cancellationToken: cancellationToken);

if (storageAccount == null)
{
throw new KeyNotFoundException($"Storage account '{account}' not found in subscription '{subscription}'.");
}
cancellationToken: cancellationToken) ?? throw new KeyNotFoundException($"Storage account '{account}' not found in subscription '{subscription}'.");

return new([storageAccount], false);
}
Expand All @@ -92,7 +96,7 @@ public async Task<StorageAccountResult> CreateStorageAccount(
(nameof(subscription), subscription));

// Create ArmClient for deployments
ArmClient armClient = await CreateArmClientWithApiVersionAsync("Microsoft.Storage/storageAccounts", "2024-01-01", null, retryPolicy, cancellationToken);
ArmClient armClient = await CreateArmClientWithApiVersionAsync("Microsoft.Storage/storageAccounts", "2024-01-01", tenant, retryPolicy, cancellationToken);

// Prepare data
ResourceIdentifier accountId = new($"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Storage/storageAccounts/{account}");
Expand Down Expand Up @@ -150,11 +154,12 @@ public async Task<StorageAccountResult> CreateStorageAccount(
}
}

public async Task<List<BlobInfo>> GetBlobDetails(
public async Task<List<Storage.Models.BlobInfo>> GetBlobDetails(
string account,
string container,
string? blob,
string subscription,
string? prefix = null,
string? tenant = null,
RetryPolicyOptions? retryPolicy = null,
CancellationToken cancellationToken = default)
Expand All @@ -167,10 +172,10 @@ public async Task<List<BlobInfo>> GetBlobDetails(
var blobServiceClient = await CreateBlobServiceClient(account, tenant, retryPolicy, cancellationToken);
var containerClient = blobServiceClient.GetBlobContainerClient(container);

var blobInfos = new List<BlobInfo>();
var blobInfos = new List<Storage.Models.BlobInfo>();
if (string.IsNullOrEmpty(blob))
{
await foreach (var blobItem in containerClient.GetBlobsAsync(cancellationToken: cancellationToken))
await foreach (var blobItem in containerClient.GetBlobsAsync(prefix: prefix, cancellationToken: cancellationToken))
{
blobInfos.Add(new(
blobItem.Name,
Expand Down Expand Up @@ -231,6 +236,7 @@ public async Task<List<ContainerInfo>> GetContainerDetails(
string account,
string? container,
string subscription,
string? prefix = null,
string? tenant = null,
RetryPolicyOptions? retryPolicy = null,
CancellationToken cancellationToken = default)
Expand All @@ -242,7 +248,7 @@ public async Task<List<ContainerInfo>> GetContainerDetails(

if (string.IsNullOrEmpty(container))
{
await foreach (var containerItem in blobServiceClient.GetBlobContainersAsync(cancellationToken: cancellationToken))
await foreach (var containerItem in blobServiceClient.GetBlobContainersAsync(prefix: prefix, cancellationToken: cancellationToken))
{
var properties = containerItem.Properties;
containers.Add(new(
Expand Down Expand Up @@ -340,28 +346,19 @@ private static string ParseStorageSkuName(string sku)
throw new ArgumentException("Storage SKU cannot be null or empty.");
}

var validSkus = new[]
{
"Standard_LRS", "Standard_GRS", "Standard_RAGRS", "Standard_ZRS",
"Premium_LRS", "Premium_ZRS", "Standard_GZRS", "Standard_RAGZRS",
"StandardV2_LRS", "StandardV2_GRS", "StandardV2_ZRS", "StandardV2_GZRS",
"PremiumV2_LRS", "PremiumV2_ZRS"
};

if (!validSkus.Contains(sku, StringComparer.OrdinalIgnoreCase))
if (!s_validSkus.Contains(sku))
{
throw new ArgumentException($"Invalid storage SKU '{sku}'. Valid values are: {string.Join(", ", validSkus)}.");
throw new ArgumentException($"Invalid storage SKU '{sku}'. Valid values are: {string.Join(", ", s_validSkus)}.");
}

return sku;
}

private static string ParseAccessTier(string accessTier)
{
var validTiers = new[] { "hot", "cool", "premium", "cold" };
if (!validTiers.Contains(accessTier.ToLowerInvariant()))
if (!s_validTiers.Contains(accessTier))
{
throw new ArgumentException($"Invalid access tier '{accessTier}'. Valid values are: {string.Join(", ", validTiers)}.");
throw new ArgumentException($"Invalid access tier '{accessTier}'. Valid values are: {string.Join(", ", s_validTiers)}.");
}

return accessTier;
Expand Down
Loading
Loading