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
6 changes: 6 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@
# ServiceLabel: %tools-Core
# ServiceOwners: @alzimmermsft @anannya03 @anuchandy @g2vinay @xiangyan99 @microsoft/azure-mcp

# PRLabel: %tools-CostManagement
/tools/Azure.Mcp.Tools.CostManagement/ @francesco1501 @microsoft/azure-mcp

# ServiceLabel: %tools-CostManagement
# ServiceOwners: @francesco1501
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ServiceOwners line lists only the external contributor. Other toolsets in this repo have at least one Microsoft team member as ServiceOwner/CODEOWNER. Consider adding a team member alongside so code ownership doesn't depend entirely on an external contributor's availability.


# PRLabel: %tools-CosmosDB
/tools/Azure.Mcp.Tools.Cosmos/ @sajeetharan @xiangyan99 @microsoft/azure-mcp

Expand Down
1 change: 1 addition & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
"conv",
"copilotmd",
"cosell",
"costmanagement",
"csdevkit",
"cslschema",
"cvzf",
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageVersion Include="Azure.ResourceManager.DesktopVirtualization" Version="1.3.2" />
<PackageVersion Include="Azure.ResourceManager.DeviceRegistry" Version="1.1.0-beta.2" />
<PackageVersion Include="Azure.ResourceManager.ContainerService" Version="1.2.5" />
<PackageVersion Include="Azure.ResourceManager.CostManagement" Version="1.0.2" />
<PackageVersion Include="Azure.ResourceManager.EventGrid" Version="1.2.0-beta.2" />
<PackageVersion Include="Azure.ResourceManager.EventHubs" Version="1.2.1" />
<PackageVersion Include="Azure.ResourceManager.CognitiveServices" Version="1.5.1" />
Expand Down
8 changes: 8 additions & 0 deletions Microsoft.Mcp.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@
<Folder Name="/tools/Azure.Mcp.Tools.ContainerApps/tests/">
<Project Path="tools/Azure.Mcp.Tools.ContainerApps/tests/Azure.Mcp.Tools.ContainerApps.UnitTests/Azure.Mcp.Tools.ContainerApps.UnitTests.csproj" />
</Folder>
<Folder Name="/tools/Azure.Mcp.Tools.CostManagement/" />
<Folder Name="/tools/Azure.Mcp.Tools.CostManagement/src/">
<Project Path="tools/Azure.Mcp.Tools.CostManagement/src/Azure.Mcp.Tools.CostManagement.csproj" />
</Folder>
<Folder Name="/tools/Azure.Mcp.Tools.CostManagement/tests/">
<Project Path="tools/Azure.Mcp.Tools.CostManagement/tests/Azure.Mcp.Tools.CostManagement.LiveTests/Azure.Mcp.Tools.CostManagement.LiveTests.csproj" />
<Project Path="tools/Azure.Mcp.Tools.CostManagement/tests/Azure.Mcp.Tools.CostManagement.UnitTests/Azure.Mcp.Tools.CostManagement.UnitTests.csproj" />
</Folder>
<Folder Name="/tools/Azure.Mcp.Tools.Cosmos/" />
<Folder Name="/tools/Azure.Mcp.Tools.Cosmos/src/">
<Project Path="tools/Azure.Mcp.Tools.Cosmos/src/Azure.Mcp.Tools.Cosmos.csproj" />
Expand Down
8 changes: 8 additions & 0 deletions servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@
<Folder Name="/tools/Azure.Mcp.Tools.ContainerApps/tests/">
<Project Path="../../tools/Azure.Mcp.Tools.ContainerApps/tests/Azure.Mcp.Tools.ContainerApps.UnitTests/Azure.Mcp.Tools.ContainerApps.UnitTests.csproj" />
</Folder>
<Folder Name="/tools/Azure.Mcp.Tools.CostManagement/" />
<Folder Name="/tools/Azure.Mcp.Tools.CostManagement/src/">
<Project Path="../../tools/Azure.Mcp.Tools.CostManagement/src/Azure.Mcp.Tools.CostManagement.csproj" />
</Folder>
<Folder Name="/tools/Azure.Mcp.Tools.CostManagement/tests/">
<Project Path="../../tools/Azure.Mcp.Tools.CostManagement/tests/Azure.Mcp.Tools.CostManagement.LiveTests/Azure.Mcp.Tools.CostManagement.LiveTests.csproj" />
<Project Path="../../tools/Azure.Mcp.Tools.CostManagement/tests/Azure.Mcp.Tools.CostManagement.UnitTests/Azure.Mcp.Tools.CostManagement.UnitTests.csproj" />
</Folder>
<Folder Name="/tools/Azure.Mcp.Tools.Cosmos/" />
<Folder Name="/tools/Azure.Mcp.Tools.Cosmos/src/">
<Project Path="../../tools/Azure.Mcp.Tools.Cosmos/src/Azure.Mcp.Tools.Cosmos.csproj" />
Expand Down
7 changes: 7 additions & 0 deletions servers/Azure.Mcp.Server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,12 @@ Example prompts that generate Azure CLI commands:
* "Show me my container registries in the 'my-resource-group' resource group"
* "List all my Azure Container Registry repositories"

### 💰 Azure Cost Management

* "How much did we spend this month on subscription 'sub-name'?"
* "Show last month's Azure costs grouped by service"
* "Daily cost breakdown for resource group 'rg-prod' between 2026-04-01 and 2026-04-30"

### 📊 Azure Cosmos DB

* "Show me all my Cosmos DB databases"
Expand Down Expand Up @@ -1195,6 +1201,7 @@ The Azure MCP Server provides tools for interacting with **43+ Azure service are
- �🔐 **Azure Confidential Ledger** - Tamper-proof ledger operations
- 📦 **Azure Container Apps** - Container hosting
- 📦 **Azure Container Registry (ACR)** - Container registry management
- 💰 **Azure Cost Management** - Query actual Azure costs and usage by subscription, resource group, time range, and dimension
- 📊 **Azure Cosmos DB** - NoSQL database operations
- 🧮 **Azure Data Explorer** - Analytics queries and KQL
- 🐬 **Azure Database for MySQL** - MySQL database management
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Features Added"
description: "Added `azmcp costmanagement query run` tool to retrieve actual Azure costs and usage from the Azure Cost Management Query/Usage API for a subscription or resource group, with support for predefined timeframes (MonthToDate, BillingMonthToDate, TheLastBillingMonth, WeekToDate) and a custom range, optional Daily granularity, and grouping by a single dimension (ServiceName, ResourceGroupName, ResourceLocation, ResourceId, MeterCategory, MeterSubCategory, ChargeType, BillingPeriod). Closes the gap tracked in #1420 and supersedes #447."
15 changes: 15 additions & 0 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,21 @@ azmcp acr registry repository list --subscription <subscription> \
--registry <registry>
```

### Azure Cost Management Operations

```bash
# Query actual Azure costs for a subscription or resource group over a time period.
# Defaults to MonthToDate timeframe and a single aggregated total.
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp costmanagement query run --subscription <subscription> \
[--resource-group <resource-group>] \
[--timeframe <MonthToDate|BillingMonthToDate|TheLastBillingMonth|WeekToDate|Custom>] \
[--from <YYYY-MM-DD>] \
[--to <YYYY-MM-DD>] \
[--granularity <None|Daily>] \
[--group-by <ServiceName|ResourceGroupName|ResourceLocation|ResourceId|MeterCategory|MeterSubCategory|ChargeType|BillingPeriod>]
```

### Azure Cosmos DB Operations

```bash
Expand Down
15 changes: 15 additions & 0 deletions servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,21 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
| confidentialledger_entries_get | Get entry from Confidential Ledger for transaction <transaction_id> on ledger <ledger_name> |
| confidentialledger_entries_get | Get transaction <transaction_id> from ledger <ledger_name> |

## Azure Cost Management

| Tool Name | Test Prompt |
|:----------|:----------|
| costmanagement_query_run | How much did we spend this month on subscription <subscription>? |
| costmanagement_query_run | What are my Azure costs month-to-date for subscription <subscription>, broken down by service? |
| costmanagement_query_run | Show me last month's Azure spend on subscription <subscription> grouped by resource group |
| costmanagement_query_run | What are the top resource groups by cost in subscription <subscription> last billing month? |
| costmanagement_query_run | Give me a daily breakdown of costs for resource group <resource-group> in subscription <subscription> for the last month |
| costmanagement_query_run | How much did we spend in <resource-group> from 2026-04-01 to 2026-04-30? |
| costmanagement_query_run | Show monthly Azure costs for subscription <subscription> grouped by ServiceName |
| costmanagement_query_run | Compare this billing month vs the last billing month for subscription <subscription> |
| costmanagement_query_run | Which services drove the most cost in <subscription> week-to-date? |
| costmanagement_query_run | Get cost by ResourceLocation for subscription <subscription> month-to-date |

## Azure Cosmos DB

| Tool Name | Test Prompt |
Expand Down
1 change: 1 addition & 0 deletions servers/Azure.Mcp.Server/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ private static IAreaSetup[] RegisterAreas()
new Azure.Mcp.Tools.Compute.ComputeSetup(),
new Azure.Mcp.Tools.ConfidentialLedger.ConfidentialLedgerSetup(),
new Azure.Mcp.Tools.ContainerApps.ContainerAppsSetup(),
new Azure.Mcp.Tools.CostManagement.CostManagementSetup(),
new Azure.Mcp.Tools.EventHubs.EventHubsSetup(),
new Azure.Mcp.Tools.FileShares.FileSharesSetup(),
new Azure.Mcp.Tools.FoundryExtensions.FoundryExtensionsSetup(),
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 @@ -769,6 +769,39 @@
"pricing_get"
]
},
{
"name": "query_azure_costs",
"description": "Query actual Azure costs and usage data for a subscription or resource group over a time period. Returns aggregated costs in the billing currency. Supports common predefined timeframes (MonthToDate, BillingMonthToDate, TheLastBillingMonth, WeekToDate) and a Custom range. Optional Daily granularity and grouping by a single dimension (ServiceName, ResourceGroupName, ResourceLocation, ResourceId, MeterCategory, MeterSubCategory, ChargeType, BillingPeriod). Caller must have 'Cost Management Reader' or 'Reader' role on the scope.",
"toolMetadata": {
"destructive": {
"value": false,
"description": "This tool performs only additive updates without deleting or modifying existing resources."
},
"idempotent": {
"value": true,
"description": "Running this operation multiple times with the same arguments produces the same result without additional effects."
},
"openWorld": {
"value": false,
"description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities."
},
"readOnly": {
"value": true,
"description": "This tool only performs read operations without modifying any state or data."
},
"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": [
"costmanagement_query_run"
]
},
{
"name": "generate_azure_cli_commands",
"description": "Generate Azure CLI commands from natural language descriptions to help answer questions about Azure environments and operations",
Expand Down
7 changes: 7 additions & 0 deletions tools/Azure.Mcp.Tools.CostManagement/src/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Azure.Mcp.Tools.CostManagement.UnitTests")]
[assembly: InternalsVisibleTo("Azure.Mcp.Tools.CostManagement.LiveTests")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="**\Resources\*.txt" />
<EmbeddedResource Include="**\Resources\*.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\core\Azure.Mcp.Core\src\Azure.Mcp.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.ResourceManager" />
<PackageReference Include="Azure.ResourceManager.CostManagement" />

<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using Azure.Mcp.Core.Commands.Subscription;
using Azure.Mcp.Tools.CostManagement.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Extensions;
using Microsoft.Mcp.Core.Models.Option;

namespace Azure.Mcp.Tools.CostManagement.Commands;

public abstract class BaseCostManagementCommand<
[DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions>(ILogger<BaseCostManagementCommand<TOptions>> logger)
: SubscriptionCommand<TOptions> where TOptions : BaseCostManagementOptions, new()
{
protected readonly ILogger<BaseCostManagementCommand<TOptions>> _logger = logger;

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsOptional());
}

protected override TOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.ResourceGroup ??= parseResult.GetValueOrDefault<string>(OptionDefinitions.Common.ResourceGroup.Name);
return options;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Azure.Mcp.Tools.CostManagement.Commands.Query;
using Azure.Mcp.Tools.CostManagement.Models;

namespace Azure.Mcp.Tools.CostManagement.Commands;

[JsonSerializable(typeof(QueryRunCommand.QueryRunCommandResult))]
[JsonSerializable(typeof(CostQueryResult))]
[JsonSerializable(typeof(CostQueryRow))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
internal partial class CostManagementJsonContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Tools.CostManagement.Models;
using Azure.Mcp.Tools.CostManagement.Options;
using Azure.Mcp.Tools.CostManagement.Options.Query;
using Azure.Mcp.Tools.CostManagement.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Models.Command;

namespace Azure.Mcp.Tools.CostManagement.Commands.Query;

[CommandMetadata(
Id = "f7c4b3a8-9e62-4d18-bc41-2a5d8e6f1b09",
Name = "run",
Title = "Query Azure Costs",
Description = """
Query actual Azure costs and usage data for a subscription or resource group over a time period.
Returns aggregated costs in the billing currency, optionally broken down daily and grouped by
a single Azure dimension. Caller must have 'Cost Management Reader' or 'Reader' on the scope.
""",
Destructive = false,
Idempotent = true,
OpenWorld = false,
ReadOnly = true,
Secret = false,
LocalRequired = false)]
public sealed class QueryRunCommand(ILogger<QueryRunCommand> logger, ICostManagementService costManagementService)
: BaseCostManagementCommand<QueryRunOptions>(logger)
{
private readonly ICostManagementService _costManagementService = costManagementService;

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(CostManagementOptionDefinitions.Timeframe);
command.Options.Add(CostManagementOptionDefinitions.From);
command.Options.Add(CostManagementOptionDefinitions.To);
command.Options.Add(CostManagementOptionDefinitions.Granularity);
command.Options.Add(CostManagementOptionDefinitions.GroupBy);

command.Validators.Add(result =>
{
var timeframe = result.GetValue(CostManagementOptionDefinitions.Timeframe);
if (timeframe == QueryTimeframe.Custom)
{
var from = result.GetValue(CostManagementOptionDefinitions.From);
var to = result.GetValue(CostManagementOptionDefinitions.To);
if (from is null || to is null)
{
result.AddError(
"When --timeframe is Custom, both --from and --to must be provided (ISO-8601 dates, UTC).");
}
else if (from > to)
{
result.AddError("--from must be earlier than or equal to --to.");
}
}
});
}

protected override QueryRunOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.Timeframe = parseResult.GetValue(CostManagementOptionDefinitions.Timeframe);
options.From = parseResult.GetValue(CostManagementOptionDefinitions.From);
options.To = parseResult.GetValue(CostManagementOptionDefinitions.To);
options.Granularity = parseResult.GetValue(CostManagementOptionDefinitions.Granularity);
options.GroupBy = parseResult.GetValue(CostManagementOptionDefinitions.GroupBy);
return options;
}

public override async Task<CommandResponse> ExecuteAsync(
CommandContext context,
ParseResult parseResult,
CancellationToken cancellationToken)
{
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
{
return context.Response;
}

var options = BindOptions(parseResult);

try
{
var timeframe = options.Timeframe ?? QueryTimeframe.MonthToDate;
var granularity = options.Granularity ?? QueryGranularity.None;

var result = await _costManagementService.QueryAsync(
subscription: options.Subscription!,
resourceGroup: options.ResourceGroup,
timeframe: timeframe,
from: options.From,
to: options.To,
granularity: granularity,
groupBy: options.GroupBy,
tenant: options.Tenant,
retryPolicy: options.RetryPolicy,
cancellationToken: cancellationToken);

context.Response.Results = ResponseResult.Create(
new QueryRunCommandResult(result),
CostManagementJsonContext.Default.QueryRunCommandResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error querying Cost Management. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, Timeframe: {Timeframe}, Granularity: {Granularity}, GroupBy: {GroupBy}.",
options.Subscription, options.ResourceGroup, options.Timeframe, options.Granularity, options.GroupBy);
HandleException(context, ex);
}

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound =>
"Cost data not found. Verify the subscription and resource group exist and are accessible.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden =>
$"Authorization failed querying Cost Management. The caller needs the 'Cost Management Reader' or 'Reader' role on the subscription or resource group. Details: {reqEx.Message}",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.TooManyRequests =>
$"Cost Management API throttled the request. Retry after the duration specified in the 'x-ms-ratelimit-microsoft.consumption-retry-after' response header. Details: {reqEx.Message}",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.BadRequest =>
$"Cost Management API rejected the query. Common causes: unsupported --group-by dimension; --timeframe value not allowed at this scope (subscription/resource group accept MonthToDate, BillingMonthToDate, TheLastBillingMonth, WeekToDate, Custom); --from later than --to; date range too large. Details: {reqEx.Message}",
RequestFailedException reqEx => reqEx.Message,
ArgumentException argEx => argEx.Message,
_ => base.GetErrorMessage(ex)
};

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

internal record QueryRunCommandResult(CostQueryResult Result);
}
Loading
Loading