Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions eng/scripts/New-ChangelogEntry.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,8 @@ if ($Subsection) {
$yamlContent += " subsection: `"$Subsection`"`n"
}

# Remove trailing newline
$yamlContent = $yamlContent.TrimEnd("`n")
# Trim multiple trailing newlines and ensure file ends with a single newline
$yamlContent = $yamlContent.TrimEnd("`n") + "`n"

# Write YAML file
$yamlContent | Set-Content -Path $filepath -Encoding UTF8 -NoNewline
Expand Down
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. The prefix is ignored if a blob is specified.

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

Returns: blob name, size, lastModified, contentType, contentHash, 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 @@ -11,7 +13,6 @@
using Microsoft.Mcp.Core.Models.Option;

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

public sealed class ContainerGetCommand(ILogger<ContainerGetCommand> logger, IStorageService storageService) : BaseStorageCommand<ContainerGetOptions>()
{
private const string CommandTitle = "Get Storage Container Details";
Expand All @@ -23,8 +24,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. The prefix is ignored if a container is specified.

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

Returns: container name, lastModified, leaseStatus, publicAccess, metadata, and container properties.
Do not use this tool to list blobs in a container.
""";

public override string Title => CommandTitle;
Expand All @@ -43,12 +52,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 +78,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