Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion servers/Azure.Mcp.Server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,9 @@ Example prompts that generate Azure CLI commands:
* "Stop VM 'my-vm' in resource group 'my-rg'"
* "Deallocate VM 'my-vm' in resource group 'my-rg' to stop billing"
* "Restart VM 'my-vm' in resource group 'my-rg'"
* "Create an Azure Compute Gallery named 'myGallery' in resource group 'my-resource-group'"
* "Create a compute gallery 'myGallery' in resource group 'my-rg' in eastus with a description"
* "I want to publish a VM application, create a gallery 'myGallery' in resource group 'my-rg' first"
* "Delete virtual machine scale set 'my-vmss' in resource group 'my-resource-group'"
* "Force delete VMSS 'my-vmss' in resource group 'my-rg' using force-deletion"

Expand Down Expand Up @@ -1430,7 +1433,7 @@ The Azure MCP Server provides tools for interacting with **45+ Azure service are
- 🛡️ **Azure Best Practices** - Secure, production-grade guidance
- 🖥️ **Azure CLI Generate** - Generate Azure CLI commands from natural language
- 📞 **Azure Communication Services** - SMS messaging and communication
- � **Azure Compute** - Virtual Machine, Virtual Machine Scale Set, and Disk management
- � **Azure Compute** - Virtual Machine, Virtual Machine Scale Set, Disk, and Compute Gallery management
- �🔐 **Azure Confidential Ledger** - Tamper-proof ledger operations
- 📦 **Azure Container Apps** - Container hosting
- 📦 **Azure Container Registry (ACR)** - Container registry management
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Features Added"
description: "Added `azmcp compute gallery create` to create an Azure Compute Gallery for storing VM images and VM applications."
47 changes: 47 additions & 0 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -2434,6 +2434,53 @@ azmcp compute disk update --subscription <subscription> \
| `--disk-access` | No | Resource ID of the disk access resource for private endpoint connections |
| `--tier` | No | Performance tier for the disk (e.g., P30, P40, P50) |

#### Azure Compute Gallery

```bash
# Create an Azure Compute Gallery
# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp compute gallery create --subscription <subscription> \
--resource-group <resource-group> \
--gallery <gallery-name>

# Create an Azure Compute Gallery in a specific region with a description
# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp compute gallery create --subscription <subscription> \
--resource-group <resource-group> \
--gallery <gallery-name> \
--location <location> \
--description <description>

# Create an Azure Compute Gallery with tags
# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp compute gallery create --subscription <subscription> \
--resource-group <resource-group> \
--gallery <gallery-name> \
--tags <comma-separated-tags>
```

**Command Behavior:**
- Creates an Azure Compute Gallery (formerly Shared Image Gallery), the top-level container used to store and share VM image definitions and VM application definitions.
- Uses create-or-update (PUT) semantics. If a gallery with the same name already exists in the resource group, parameters that are not supplied are left unchanged.
- Supplying `--tags` replaces the gallery's entire tag set rather than merging, so any existing tag that is omitted is removed. Include every tag that should be kept.
- If `--location` is not specified, a new gallery is created in the resource group's location, and an existing gallery keeps the location it already has. A gallery's location cannot be changed after creation, so supplying a `--location` that differs from an existing gallery's region fails.
- Gallery names must be unique within the subscription, not just within the resource group.
- Gallery names must be 1-80 characters, contain only letters, digits, periods, and underscores, and start and end with a letter or digit. Unlike most Compute resources, hyphens are not allowed in gallery names.
- Create the gallery before publishing VM images or VM applications into it.

**Returns:**
- Created gallery information including name, resource ID, resource group, location, description, globally unique name, sharing permissions, tags, and provisioning state.

**Parameters:**
| Parameter | Required | Description |
|-----------|----------|-------------|
| `--subscription` | Yes | Azure subscription ID or name |
| `--resource-group`, `-g` | Yes | Resource group name |
| `--gallery`, `--name` | Yes | Name of the Azure Compute Gallery to create |
| `--location` | No | Azure region for the gallery (defaults to the resource group's location for a new gallery, or the existing gallery's location when updating) |
| `--description` | No | Description of the gallery |
| `--tags` | No | Comma-separated tags in key=value format (e.g., env=prod,team=compute) |

### Azure Confidential Ledger Operations

```bash
Expand Down
5 changes: 5 additions & 0 deletions servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,11 @@ The `Interaction` column describes whether a prompt can invoke its tool immediat
| compute_disk_update | Change the encryption type of disk <disk-name> in resource group <resource-group> to EncryptionAtRestWithPlatformAndCustomerKeys | investigation-required |
| compute_disk_update | Set disk access on disk <disk-name> in resource group <resource-group> to <disk-access-resource-id> with network access policy AllowPrivate | none |
| compute_disk_update | Update disk <disk-name> to Standard_LRS SKU with 512 GB size and tags env=dev | none |
| compute_gallery_create | Create an Azure Compute Gallery named <gallery-name> in resource group <resource-group> | none |
| compute_gallery_create | Create a compute gallery <gallery-name> in resource group <resource-group> in eastus | none |
| compute_gallery_create | Create a gallery named <gallery-name> in resource group <resource-group> with description "Shared images for the platform team" | none |
| compute_gallery_create | I want to publish a VM application, create a gallery <gallery-name> in resource group <resource-group> first | none |
| compute_gallery_create | Create a compute gallery <gallery-name> in resource group <resource-group> with tags env=prod,team=compute | none |

## Azure Confidential Ledger

Expand Down
33 changes: 33 additions & 0 deletions servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -2559,6 +2559,39 @@
"compute_disk_create"
]
},
{
"name": "create_azure_compute_gallery",
"description": "Create an Azure Compute Gallery (formerly Shared Image Gallery), the top-level container used to store and share VM image definitions and VM application definitions across subscriptions, regions, and tenants. Supports setting the region, description, and tags. Create a gallery before publishing VM images or VM applications.",
"toolMetadata": {
"destructive": {
"value": true,
"description": "This tool creates new resources which may incur costs."
},
"idempotent": {
"value": false,
"description": "Running this operation multiple times may create duplicate resources."
},
"openWorld": {
"value": false,
"description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities."
},
"readOnly": {
"value": false,
"description": "This tool creates new Azure resources."
},
"secret": {
"value": false,
"description": "This tool does not handle sensitive or secret information."
},
"localRequired": {
"value": false,
"description": "This tool is available in both local and remote server modes."
}
},
"mappedToolList": [
"compute_gallery_create"
]
},
{
"name": "update_azure_compute_vm_and_vmss",
"description": "Update Azure Virtual Machines (VMs) and Virtual Machine Scale Sets (VMSS). Modify VM properties like size, tags, license type, boot diagnostics, and user data. Update VMSS capacity, upgrade policy, overprovision settings, auto OS upgrade, and scale-in policies. Change VM power state (start, stop, deallocate, restart). For VM and VMSS update operations, PATCH semantics apply - only specified properties are updated; unspecified properties remain unchanged. Power-state actions are operation-based changes, not PATCH updates.",
Expand Down
8 changes: 8 additions & 0 deletions tools/Azure.Mcp.Tools.Compute/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ words:
- delvm
- delvms
- eaaaa
- envprod
- exactdisk
- galleryimageversionid
- leadingperiod
- leadingunderscore
- lnxvmss
- maaacbaj
- mysnapshot
Expand All @@ -31,10 +34,15 @@ words:
- otherdisk
- securedisk
- swingbench
- teamcompute
- testdatadisk
- testdisk
- testgallery
- testkey
- testvm
- trailinghyphen
- trailingperiod
- trailingunderscore
- uploaddisk
- uploadnosize
- uploadsec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Text.Json.Serialization;
using Azure.Mcp.Tools.Compute.Commands.Disk;
using Azure.Mcp.Tools.Compute.Commands.Gallery;
using Azure.Mcp.Tools.Compute.Commands.Vm;
using Azure.Mcp.Tools.Compute.Commands.Vmss;
using Azure.Mcp.Tools.Compute.Models;
Expand All @@ -18,6 +19,8 @@ namespace Azure.Mcp.Tools.Compute.Commands;
[JsonSerializable(typeof(DiskUpdateCommand.DiskUpdateCommandResult))]
[JsonSerializable(typeof(DiskInfo))]
[JsonSerializable(typeof(List<DiskInfo>))]
[JsonSerializable(typeof(GalleryCreateCommand.GalleryCreateCommandResult))]
[JsonSerializable(typeof(GalleryInfo))]
[JsonSerializable(typeof(VmCreateCommand.VmCreateCommandResult))]
[JsonSerializable(typeof(VmCreateResult))]
[JsonSerializable(typeof(VmUpdateCommand.VmUpdateCommandResult))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Core.Commands.Subscription;
using Azure.Mcp.Core.Services.Azure.Subscription;
using Azure.Mcp.Tools.Compute.Models;
using Azure.Mcp.Tools.Compute.Options.Gallery;
using Azure.Mcp.Tools.Compute.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Models.Command;

namespace Azure.Mcp.Tools.Compute.Commands.Gallery;

/// <summary>
/// Command to create an Azure Compute Gallery.
/// </summary>
[CommandMetadata(
Id = "7c4e1f2a-9b3d-4e5a-8c61-2d0f3a5b7e91",
Name = "create",
Title = "Create Azure Compute Gallery",
Description = """
Creates an Azure Compute Gallery (formerly Shared Image Gallery) in the specified resource group.
A gallery is the top-level container used to store and share VM image definitions and VM application
definitions across subscriptions, regions, and tenants. Create a gallery before publishing VM images
or VM applications. If --location is not specified, a new gallery is created in the resource group's
location and an existing gallery keeps its current location. This uses create-or-update semantics: if a
gallery with the same name already exists, values that are not supplied are left unchanged, but
supplying --tags replaces the gallery's entire tag set rather than merging, so include every tag that
should be kept. Gallery names must be unique within the subscription. Returns the
created gallery including its name, resource ID, location, description, globally unique name,
sharing permissions, tags, and provisioning state.
""",
OperationPlane = ToolOperationPlane.Control,
Destructive = true,
Idempotent = false,
OpenWorld = false,
ReadOnly = false,
Secret = false,
LocalRequired = false)]
public sealed class GalleryCreateCommand(ILogger<GalleryCreateCommand> logger, IComputeService computeService, ISubscriptionResolver subscriptionResolver)
: SubscriptionCommand<GalleryCreateOptions, GalleryCreateCommand.GalleryCreateCommandResult>(subscriptionResolver)
{
private const int MaxGalleryNameLength = 80;

private readonly ILogger<GalleryCreateCommand> _logger = logger;
private readonly IComputeService _computeService = computeService;

public override void ValidateOptions(GalleryCreateOptions options, ValidationResult validationResult)
{
base.ValidateOptions(options, validationResult);

if (!IsValidGalleryName(options.Gallery))
{
validationResult.Errors.Add(
$"Invalid gallery name '{options.Gallery}'. Gallery names must be 1-{MaxGalleryNameLength} characters, " +
"contain only letters, digits, periods, and underscores, and start and end with a letter or digit. " +
"Hyphens are not allowed in gallery names.");
}
}

// Unlike most Microsoft.Compute resources, galleries disallow hyphens and must end with an alphanumeric.
internal static bool IsValidGalleryName(string? gallery)
{
if (string.IsNullOrEmpty(gallery) || gallery.Length > MaxGalleryNameLength)
{
return false;
}

if (!char.IsAsciiLetterOrDigit(gallery[0]) || !char.IsAsciiLetterOrDigit(gallery[^1]))
{
return false;
}

foreach (var c in gallery)
{
if (!char.IsAsciiLetterOrDigit(c) && c is not '.' and not '_')
{
return false;
}
}

return true;
}

internal static Dictionary<string, string>? ParseTags(string? tags)
{
if (string.IsNullOrWhiteSpace(tags))
{
return null;
}

var parsed = new Dictionary<string, string>();
// Empty segments are kept so that leading, trailing, or consecutive commas fail validation
// below rather than being silently discarded.
foreach (var pair in tags.Split(',', StringSplitOptions.TrimEntries))
{
var parts = pair.Split('=', 2);
if (parts.Length != 2)
{
throw new ArgumentException($"Invalid tag '{pair}'. Tags must use the format 'key=value'.");
}

var key = parts[0].Trim();
var value = parts[1].Trim();

if (key.Length == 0)
{
throw new ArgumentException($"Invalid tag '{pair}'. Tag keys cannot be empty.");
}

if (value.Length == 0)
{
throw new ArgumentException($"Invalid tag '{pair}'. Tag values cannot be empty.");
}

parsed[key] = value;
}

return parsed.Count > 0 ? parsed : null;
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, GalleryCreateOptions options, CancellationToken cancellationToken)
{
try
{
context.Activity?.AddTag("subscription", options.Subscription);

// Parse tags before any Azure call so malformed input fails fast without creating a resource.
var tags = ParseTags(options.Tags);

var gallery = await _computeService.CreateGalleryAsync(
options.Gallery,
options.ResourceGroup,
options.Subscription!,
options.Location,
options.Description,
tags,
options.Tenant,
cancellationToken);

context.Response.Results = ResponseResult.Create(new(gallery), ComputeJsonContext.Default.GalleryCreateCommandResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error creating gallery. Gallery: {Gallery}, ResourceGroup: {ResourceGroup}, Location: {Location}.",
options.Gallery, options.ResourceGroup, options.Location ?? "(default)");
HandleException(context, ex);
}

return context.Response;
}

protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch
{
RequestFailedException reqEx => (HttpStatusCode)reqEx.Status,
Identity.AuthenticationFailedException => HttpStatusCode.Unauthorized,
ArgumentException => HttpStatusCode.BadRequest,
_ => base.GetStatusCode(ex)
};

protected override string GetErrorMessage(Exception ex) => ex switch
{
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound =>
"Resource group not found. Verify the resource group exists and you have access.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden =>
$"Authorization failed creating the gallery. Details: {reqEx.Message}",
RequestFailedException reqEx when reqEx.ErrorCode == "InvalidResourceLocation" =>
$"A gallery with this name already exists in a different region. A gallery's location cannot be changed, so omit --location to update the existing gallery in place, or choose a different gallery name to create one in the requested region. Details: {reqEx.Message}",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Conflict =>
$"Conflict creating the gallery. Another operation may be in progress on this gallery. Details: {reqEx.Message}",
RequestFailedException reqEx => reqEx.Message,
Identity.AuthenticationFailedException =>
"Authentication failed. Please run 'az login' to sign in.",
ArgumentException argEx => $"Invalid parameter: {argEx.Message}",
_ => base.GetErrorMessage(ex)
};

/// <summary>
/// Result record for the gallery create command.
/// </summary>
public record GalleryCreateCommandResult(GalleryInfo Gallery);
}
13 changes: 13 additions & 0 deletions tools/Azure.Mcp.Tools.Compute/src/ComputeSetup.cs
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.Compute.Commands.Disk;
using Azure.Mcp.Tools.Compute.Commands.Gallery;
using Azure.Mcp.Tools.Compute.Commands.Vm;
using Azure.Mcp.Tools.Compute.Commands.Vmss;
using Azure.Mcp.Tools.Compute.Services;
Expand Down Expand Up @@ -42,6 +43,9 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<DiskDeleteCommand>();
services.AddSingleton<DiskGetCommand>();
services.AddSingleton<DiskUpdateCommand>();

// Gallery commands
services.AddSingleton<GalleryCreateCommand>();
}

public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
Expand Down Expand Up @@ -92,6 +96,15 @@ Note that this tool requires appropriate Azure RBAC permissions and will only ac
disk.AddCommand<DiskGetCommand>(serviceProvider);
disk.AddCommand<DiskUpdateCommand>(serviceProvider);

// Create Gallery subgroup
var gallery = new CommandGroup(
"gallery",
"Azure Compute Gallery operations - Commands for managing Azure Compute Galleries, the containers used to store and share VM images and VM applications.");
compute.AddSubGroup(gallery);

// Register Gallery commands
gallery.AddCommand<GalleryCreateCommand>(serviceProvider);

return compute;
}
}
Loading
Loading