Skip to content
This repository was archived by the owner on Sep 4, 2025. It is now read-only.

Commit 982b0fd

Browse files
authored
Add azmcp-storage-blob-upload command (#960)
* Add azmcp-storage-blob-upload command * Update return of upload tool and update tests * Result uses full file path * etag -> eTag * Change tool name
1 parent 24aeadf commit 982b0fd

File tree

14 files changed

+583
-34
lines changed

14 files changed

+583
-34
lines changed

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The Azure MCP Server updates automatically by default whenever a new release com
1616
- `azmcp-quota-usage-check` - Check Azure resource usage and quota information for specific resource types and regions.
1717
- Added support for listing Azure Function Apps via the command `azmcp-functionapp-list`. [[#863](https://github.com/Azure/azure-mcp/pull/863)]
1818
- Added support for importing existing certificates into Azure Key Vault via the command `azmcp-keyvault-certificate-import`. This command accepts PFX or PEM certificate data (file path, base64, or raw PEM) with optional password protection. [[#968](https://github.com/Azure/azure-mcp/issues/968)]
19+
- Added `azmcp-storage-blob-upload`: Upload a local file to an Azure Storage blob with the option to overwrite if the blob already exists. Returns blob metadata including name, container, uploaded file, last modified time, ETag, MD5 hash, and overwrite status. [[#960](https://github.com/Azure/azure-mcp/pull/960)]
1920

2021
### Breaking Changes
2122

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
8888
* "Create a new storage account in East US with Data Lake support"
8989
* "Show me the tables in my Storage account"
9090
* "Get details about my Storage container"
91+
* "Upload my file to the blob container"
9192
* "List paths in my Data Lake file system"
9293
* "List files and directories in my File Share"
9394
* "Send a message to my storage queue"
@@ -264,6 +265,7 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
264265
* List and create Storage accounts
265266
* Get detailed information about specific Storage accounts
266267
* Manage blob containers and blobs
268+
* Upload files to blob containers
267269
* List and query Storage tables
268270
* List paths in Data Lake file systems
269271
* Get container properties and metadata
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using AzureMcp.Core.Commands;
5+
using AzureMcp.Storage.Options;
6+
using AzureMcp.Storage.Options.Blob;
7+
using AzureMcp.Storage.Services;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace AzureMcp.Storage.Commands.Blob;
11+
12+
public sealed class BlobUploadCommand(ILogger<BlobUploadCommand> logger) : BaseBlobCommand<BlobUploadOptions>
13+
{
14+
private const string CommandTitle = "Upload Local File to Blob";
15+
private readonly ILogger<BlobUploadCommand> _logger = logger;
16+
17+
// Define options from OptionDefinitions
18+
private readonly Option<string> _localFilePathOption = StorageOptionDefinitions.LocalFilePath;
19+
private readonly Option<bool> _overwriteOption = StorageOptionDefinitions.Overwrite;
20+
21+
public override string Name => "upload";
22+
23+
public override string Description =>
24+
"""
25+
Uploads a local file to a blob in Azure Storage with the option to overwrite if the blob already exists.
26+
Returns details about the uploaded blob including last modified time, ETag, and content hash.
27+
""";
28+
29+
public override string Title => CommandTitle;
30+
31+
public override ToolMetadata Metadata => new()
32+
{
33+
Destructive = true,
34+
ReadOnly = false
35+
};
36+
37+
protected override void RegisterOptions(Command command)
38+
{
39+
base.RegisterOptions(command);
40+
command.AddOption(_localFilePathOption);
41+
command.AddOption(_overwriteOption);
42+
}
43+
44+
protected override BlobUploadOptions BindOptions(ParseResult parseResult)
45+
{
46+
var options = base.BindOptions(parseResult);
47+
options.LocalFilePath = parseResult.GetValueForOption(_localFilePathOption);
48+
options.Overwrite = parseResult.GetValueForOption(_overwriteOption);
49+
return options;
50+
}
51+
52+
public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
53+
{
54+
var options = BindOptions(parseResult);
55+
56+
try
57+
{
58+
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
59+
{
60+
return context.Response;
61+
}
62+
63+
var storageService = context.GetService<IStorageService>();
64+
65+
var result = await storageService.UploadBlob(
66+
options.Account!,
67+
options.Container!,
68+
options.Blob!,
69+
options.LocalFilePath!,
70+
options.Overwrite ?? false,
71+
options.Subscription!,
72+
options.Tenant,
73+
options.RetryPolicy);
74+
75+
context.Response.Results = ResponseResult.Create(result, StorageJsonContext.Default.BlobUploadResult);
76+
77+
_logger.LogInformation("Successfully uploaded file {LocalFilePath} to blob {Blob} in container {Container} (Overwrite: {Overwrite}).",
78+
options.LocalFilePath, options.Blob, options.Container, options.Overwrite);
79+
80+
return context.Response;
81+
}
82+
catch (Exception ex)
83+
{
84+
_logger.LogError(ex, "Error uploading file {LocalFilePath} to blob {Blob} in container {Container} (Overwrite {Overwrite}).",
85+
options.LocalFilePath, options.Blob, options.Container, options.Overwrite);
86+
HandleException(context, ex);
87+
return context.Response;
88+
}
89+
}
90+
}

‎areas/storage/src/AzureMcp.Storage/Commands/StorageJsonContext.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
using AzureMcp.Storage.Commands.Queue.Message;
1212
using AzureMcp.Storage.Commands.Share.File;
1313
using AzureMcp.Storage.Commands.Table;
14+
using AzureMcp.Storage.Models;
1415

1516
namespace AzureMcp.Storage.Commands;
1617

1718
[JsonSerializable(typeof(BlobListCommand.BlobListCommandResult))]
1819
[JsonSerializable(typeof(BlobDetailsCommand.BlobDetailsCommandResult))]
20+
[JsonSerializable(typeof(BlobUploadResult))]
1921
[JsonSerializable(typeof(BatchSetTierCommand.BatchSetTierCommandResult))]
2022
[JsonSerializable(typeof(AccountListCommand.AccountListCommandResult), TypeInfoPropertyName = "AccountListCommandResult")]
2123
[JsonSerializable(typeof(AccountDetailsCommand.AccountDetailsCommandResult), TypeInfoPropertyName = "AccountDetailsCommandResult")]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace AzureMcp.Storage.Models;
5+
6+
public record BlobUploadResult(
7+
string Blob,
8+
string Container,
9+
string UploadedFile,
10+
DateTimeOffset LastModified,
11+
string ETag,
12+
string? MD5Hash
13+
);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace AzureMcp.Storage.Options.Blob;
7+
8+
public class BlobUploadOptions : BaseBlobOptions
9+
{
10+
[JsonPropertyName(StorageOptionDefinitions.LocalFilePathName)]
11+
public string? LocalFilePath { get; set; }
12+
13+
[JsonPropertyName(StorageOptionDefinitions.OverwriteName)]
14+
public bool? Overwrite { get; set; }
15+
}

‎areas/storage/src/AzureMcp.Storage/Options/StorageOptionDefinitions.cs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public static class StorageOptionDefinitions
2222
public const string MessageContent = "message";
2323
public const string TimeToLiveInSeconds = "time-to-live-in-seconds";
2424
public const string VisibilityTimeoutInSeconds = "visibility-timeout-in-seconds";
25+
public const string LocalFilePathName = "local-file-path";
26+
public const string OverwriteName = "overwrite";
2527
public const string LocationName = "location";
2628
public const string SkuName = "sku";
2729
public const string KindName = "kind";
@@ -234,4 +236,20 @@ public static class StorageOptionDefinitions
234236
{
235237
IsRequired = false
236238
};
239+
240+
public static readonly Option<string> LocalFilePath = new(
241+
$"--{LocalFilePathName}",
242+
"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."
243+
)
244+
{
245+
IsRequired = true
246+
};
247+
248+
public static readonly Option<bool> Overwrite = new(
249+
$"--{OverwriteName}",
250+
"Whether to overwrite content if it already exists. Defaults to false."
251+
)
252+
{
253+
IsRequired = false
254+
};
237255
}

‎areas/storage/src/AzureMcp.Storage/Services/IStorageService.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,14 @@ Task<QueueMessageSendResult> SendQueueMessage(
120120
string subscription,
121121
string? tenant = null,
122122
RetryPolicyOptions? retryPolicy = null);
123+
124+
Task<BlobUploadResult> UploadBlob(
125+
string account,
126+
string container,
127+
string blob,
128+
string localFilePath,
129+
bool overwrite,
130+
string subscription,
131+
string? tenant = null,
132+
RetryPolicyOptions? retryPolicy = null);
123133
}

‎areas/storage/src/AzureMcp.Storage/Services/StorageService.cs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,4 +784,39 @@ private static StorageSkuName ParseStorageSkuName(string sku)
784784
_ => throw new ArgumentException($"Invalid access tier '{accessTier}'. Valid values are: Hot, Cool")
785785
};
786786
}
787+
788+
public async Task<BlobUploadResult> UploadBlob(
789+
string account,
790+
string container,
791+
string blob,
792+
string localFilePath,
793+
bool overwrite,
794+
string subscription,
795+
string? tenant = null,
796+
RetryPolicyOptions? retryPolicy = null)
797+
{
798+
ValidateRequiredParameters(account, container, blob, localFilePath, subscription);
799+
800+
if (!File.Exists(localFilePath))
801+
{
802+
throw new FileNotFoundException($"Local file not found: {localFilePath}");
803+
}
804+
805+
var blobServiceClient = await CreateBlobServiceClient(account, tenant, retryPolicy);
806+
var blobContainerClient = blobServiceClient.GetBlobContainerClient(container);
807+
var blobClient = blobContainerClient.GetBlobClient(blob);
808+
809+
// Upload the file
810+
using var fileStream = File.OpenRead(localFilePath);
811+
var response = await blobClient.UploadAsync(fileStream, overwrite);
812+
813+
return new BlobUploadResult(
814+
Blob: blob,
815+
Container: container,
816+
UploadedFile: localFilePath,
817+
LastModified: response.Value.LastModified,
818+
ETag: response.Value.ETag.ToString(),
819+
MD5Hash: response.Value.ContentHash != null ? Convert.ToBase64String(response.Value.ContentHash) : null
820+
);
821+
}
787822
}

‎areas/storage/src/AzureMcp.Storage/StorageSetup.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ access storage resources accessible to the authenticated user.
9898

9999
blobs.AddCommand("list", new BlobListCommand(loggerFactory.CreateLogger<BlobListCommand>()));
100100
blobs.AddCommand("details", new BlobDetailsCommand(loggerFactory.CreateLogger<BlobDetailsCommand>()));
101+
blobs.AddCommand("upload", new BlobUploadCommand(loggerFactory.CreateLogger<BlobUploadCommand>()));
101102

102103
batch.AddCommand("set-tier", new BatchSetTierCommand(loggerFactory.CreateLogger<BatchSetTierCommand>()));
103104

0 commit comments

Comments
 (0)