Azure FinOps MCP Server
Azure FinOps MCP: cost, budgets, forecasting and optimization for Claude, VS Code and Cursor.
Open source Open in the app JSON README (API)
About
Azure FinOps MCP: cost, budgets, forecasting and optimization for Claude, VS Code and Cursor.
Details
- Kind
- MCP servers
- Topic
- Cloud & DevOps
- Publisher
- raviteja-pegata
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.1.1
- Last push
- 2026-05-25T22:51:41Z
- Repository state
- ativo
- Language
- Python
- License
- MIT
- Added
- 2026-08-29 04:01:16
- Updated
- 2026-08-29 04:01:16
- Origin id
io.github.raviteja-pegata/azure-finops-mcp
README
# Azure FinOps MCP Server
<!-- mcp-name: io.github.raviteja-pegata/azure-finops-mcp -->
<!-- google-site-verification: BG8dYih08oraoJjFa-qBpVZE5W4tuJ0A4qTylUWzjrI -->
An MCP server that gives LLM clients (Claude Desktop, Claude Code, VS Code, Cursor)
conversational access to Azure cost analysis, budget tracking, forecasting, and resource
optimization — across multiple subscriptions.
## Tools
### Discovery
| Tool | Purpose |
|---|---|
| `list_subscriptions` | List allowed subscriptions with friendly names |
### Cost Analysis
| Tool | Purpose |
|---|---|
| `get_cost_summary` | Total cost for a date range (single sub) |
| `get_cost_by_dimension` | Cost breakdown by service / RG / location / meter |
| `get_cost_by_tag` | Cost grouped by tag value (showback/chargeback) |
| `get_month_to_date_cost` | Current-month spend (single sub) |
| `get_portfolio_month_to_date_cost` | Current-month spend across ALL subs |
### Budgets
| Tool | Purpose |
|---|---|
| `get_budget_status` | Budget consumption for a single sub |
| `get_portfolio_budget_status` | Budget status across ALL subs |
### Optimization
| Tool | Purpose |
|---|---|
| `find_idle_resources` | Unattached disks, stranded IPs/NICs, stopped VMs |
| `find_idle_resources_portfolio` | Idle resources across ALL subs |
| `get_advisor_recommendations` | Azure Advisor cost recs with annual savings |
| `get_vm_utilization` | CPU stats to validate rightsizing |
### Forecasting
| Tool | Purpose |
|---|---|
| `forecast_month_end_spend` | Predicted month-end cost (single sub) |
| `forecast_portfolio_month_end_spend` | Predicted month-end cost across ALL subs |
## Quick Install
```bash
pip install azure-finops-mcp
```
Then add it to your MCP client config using the installed command — no cloning needed:
```json
{
"mcpServers": {
"azure-finops": {
"command": "azure-finops-mcp",
"env": {
"AZURE_ALLOWED_SUBSCRIPTIONS": "sub-id-1,sub-id-2",
"AZURE_DEFAULT_SUBSCRIPTION": "sub-id-1"
}
}
}
}
```
See the [Client Configuration](#client-configuration) section below for per-client config file locations.
---
## Prerequisites
- Python 3.11+
- Azure CLI installed and logged in (`az login`)
## Azure RBAC Permissions
The identity running this server (your user, a service principal, or a managed identity)
needs three roles assigned on **each subscription** you want to query:
| Role | Purpose |
|---|---|
| **Cost Management Reader** | Cost analysis, forecasting, budget queries |
| **Reader** | Resource inventory via Resource Graph |
| **Monitoring Reader** | VM utilization metrics via Azure Monitor |
### Assign via Azure CLI
```bash
SUBSCRIPTION_ID="<your-subscription-id>"
PRINCIPAL_ID="<object-id-of-user-sp-or-managed-identity>"
for ROLE in "Cost Management Reader" "Reader" "Monitoring Reader"; do
az role assignment create \
--assignee "$PRINCIPAL_ID" \
--role "$ROLE" \
--scope "/subscriptions/$SUBSCRIPTION_ID"
done
```
Repeat for each subscription listed in `AZURE_ALLOWED_SUBSCRIPTIONS`.
### Local development (your own user)
```bash
az login
az account set --subscription "<your-subscription-id>"
# Check your object ID
az ad signed-in-user show --query id -o tsv
```
Your user already has these roles if you're a subscription Owner or Contributor.
If not, ask your Azure admin to assign them.
### Managed Identity (Container Apps deployment)
After deploying with `deploy.sh`, the script automatically assigns these three roles
to the Container App's system-assigned managed identity on each allowed subscription.
No credentials or secrets are needed — `DefaultAzureCredential` picks up the
managed identity automatically at runtime.
## Install
```bash
git clone <your-repo-url> azure-finops-mcp
cd azure-finops-mcp
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
cp .env.example .env
# Edit .env: set your subscription IDs
```
## Configure .env
```bash
# Required: comma-separated subscription IDs the server may query
AZURE_ALLOWED_SUBSCRIPTIONS=sub-id-1,sub-id-2,sub-id-3
# Required: default subscription (must be in the list above)
AZURE_DEFAULT_SUBSCRIPTION=sub-id-1
```
## Test with MCP Inspector
The Inspector is a web UI that lets you call tools interactively and see
raw JSON-RPC messages. Always test here before connecting to Claude Desktop.
```bash
# Use the venv's python3 explicitly — the Inspector launches a subprocess
# and needs the binary that has mcp + azure SDKs installed.
npx @modelcontextprotocol/inspector $(which python3) -m azure_finops_mcp.server
```
In the Inspector UI:
1. Verify Transport Type is **STDIO**
2. Click **Connect** — should succeed and show "azure-finops" as the server name
3. Navigate to Tools, click List Tools — you should see all 15 tools
4. Try `list_subscriptions` first (no arguments needed)
5. Try `get_month_to_date_cost` (no arguments needed — uses default sub)
## Client Configuration
Find the absolute path to your venv's Python first — you'll need it in every config below:
```bash
# With your venv activated:
which python3
# e.g. /Users/yourname/azure-finops-mcp/.venv/bin/python3
```
---
### Claude Desktop
Edit `claude_desktop_config.json`:
| OS | Path |
|---|---|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
```json
{
"mcpServers": {
"azure-finops": {
"command": "/absolute/path/to/.venv/bin/python3",
"args": ["-m", "azure_finops_mcp.server"],
"env": {
"AZURE_ALLOWED_SUBSCRIPTIONS": "sub-1,sub-2,sub-3",
"AZURE_DEFAULT_SUBSCRIPTION": "sub-1",
"FINOPS_CACHE_TTL_SECONDS": "900"
}
}
}
}
```
Restart Claude Desktop. A tool icon in the chat input confirms the server connected.
---
### VS Code (GitHub Copilot / Agent mode)
Create `.vscode/mcp.json` in your workspace (or add to user `settings.json` under `"mcp"`):
```json
{
"servers": {
"azure-finops": {
"type": "stdio",
"command": "/absolute/path/to/.venv/bin/python3",
"args": ["-m", "azure_finops_mcp.server"],
"env": {
"AZURE_ALLOWED_SUBSCRIPTIONS": "sub-1,sub-2,sub-3",
"AZURE_DEFAULT_SUBSCRIPTION": "sub-1",
"FINOPS_CACHE_TTL_SECONDS": "900"
}
}
}
}
```
Requires VS Code 1.99+ with the **GitHub Copilot** extension. Open the Chat panel,
switch to **Agent** mode, and the `azure-finops` tools will appear automatically.
---
### Cursor
Create or edit `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"azure-finops": {
"command": "/absolute/path/to/.venv/bin/python3",
"args": ["-m", "azure_finops_mcp.server"],
"env": {
"AZURE_ALLOWED_SUBSCRIPTIONS": "sub-1,sub-2,sub-3",
"AZURE_DEFAULT_SUBSCRIPTION": "sub-1",
"FINOPS_CACHE_TTL_SECONDS": "900"
}
}
}
}
```
Or add it via **Cursor Settings → MCP → Add new global MCP server**.
Restart Cursor. The tools appear in Cursor's Agent/Composer panel.
---
### Claude Code (CLI)
```bash
claude mcp add azure-finops \
/absolute/path/to/.venv/bin/python3 \
-m azure_finops_mcp.server \
-e AZURE_ALLOWED_SUBSCRIPTIONS=sub-1,sub-2,sub-3 \
-e AZURE_DEFAULT_SUBSCRIPTION=sub-1
```
---
### Remote HTTP (after deploying to Azure Container Apps)
All clients support connecting to the deployed server over HTTP — no local Python needed:
**Claude Desktop / Cursor** — add to the same config files above:
```json
{
"mcpServers": {
"azure-finops": {
"type": "http",
"url": "https://<your-container-app-fqdn>/mcp"
}
}
}
```
**VS Code** — in `.vscode/mcp.json`:
```json
{
"servers": {
"azure-finops": {
"type": "http",
"url": "https://<your-container-app-fqdn>/mcp"
}
}
}
```
**Claude Web** — Settings → Integrations → Add → `https://<your-container-app-fqdn>/mcp`
## Example Prompts
Try these once connected:
- "What are our allowed subscriptions?"
- "What's our total month-to-date spend across all subscriptions?"
- "Which 10 services cost the most on our prod subscription last month?"
- "Break down last quarter's spend by the `costcenter` tag."
- "Are any budgets close to breaching?"
- "Show me idle resources across all our subscriptions."
- "What does Azure Advisor recommend for cost savings?"
- "Is VM `my-analytics-vm` actually being used? Check its CPU over 14 days."
- "Compare our forecast for this month against our budgets."
## Architecture
```
Claude Desktop ◄─┐
VS Code ◄─┤
Cursor ◄─┼──stdio / HTTP──► Azure FinOps MCP Server ◄──REST──► Azure APIs
Claude Code ◄─┤ │
Claude Web ◄─┘ ├── config.py ← env + allowlist
├── azure_clients.py ← shared credential
├── cache.py ← TTL cache
├── server.py ← FastMCP + registration
└── tools/
├── subscriptions ← discovery
├── cost ← queries + portfolio
├── budgets ← budget status
├── optimization ← idle + advisor + metrics
└── forecast ← predictions
```
### Key design decisions
**Narrow tools over flexible tools.** The LLM picks among well-named tools
far better than it constructs complex query objects. 15 purpose-built tools
beats 3 configurable ones.
**Subscription allowlist.** A frozenset loaded from env. Every tool calls
`resolve_subscription()` which refuses any ID not in the list. Prevents the
LLM from querying unauthorized subscriptions — important for prompt injection
defense.
**Portfolio tools catch per-sub errors.** When querying 5+ subscriptions, one
might have different RBAC or be in a weird state. Portfolio tools (`get_portfolio_*`)
wrap each sub in try/except so partial results are returned with errors listed
separately.
**Cache on Cost Management only.** Cost queries are expensive and rate-limited
(~30 req/min per tenant). Cost data updates hourly at best. Default 15-minute
TTL trades almost nothing in freshness for significant rate-limit headroom.
Resource Graph and Advisor are fast and cheap — no caching needed.
**Structured returns, not prose.** Tools return dicts with columns/rows/metadata.
The LLM narrates them naturally. This avoids encoding English into tool responses
(which makes them brittle to prompt changes).
## Deploying to Azure (Remote Mode)
For team-wide access, deploy as a remote HTTP server:
1. **Transport swap** in `server.py`:
```python
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
```
2. **Dockerfile:**
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -e .
CMD ["azure-finops-mcp"]
```
3. **Deploy to Azure Container Apps** with a user-assigned managed identity.
4. **Grant RBAC** to the managed identity (same 3 roles: Cost Management Reader,
Reader, Monitoring Reader) on each subscription.
5. **Add auth** via APIM or Azure Front Door + Entra ID.
MCP supports OAuth for remote servers.
6. `DefaultAzureCredential` picks up the managed identity automatically —
no code changes needed.
## Troubleshooting
| Problem | Fix |
|---|---|
| `DefaultAzureCredential` auth errors | Run `az login` and verify with `az account show` |
| 429 throttling on Cost Management | Increase `FINOPS_CACHE_TTL_SECONDS` |
| Empty budget list | Budgets must exist in the portal — the API doesn't create them |
| `find_idle_resources` errors | You need `Reader` RBAC at subscription scope |
| Inspector "Connection Error" | Use absolute path to venv's python3 in Command field |
| `print()` breaks the server | Never use `print()` in MCP tools — it corrupts the stdio JSON stream. Use `logging` instead |