Skip to content

Add Start-FinOpsMultitool cmdlet — interactive GUI for tenant-wide FinOps scanning#2155

Draft
z-larsen wants to merge 65 commits into
microsoft:devfrom
z-larsen:feature/finops-multitool
Draft

Add Start-FinOpsMultitool cmdlet — interactive GUI for tenant-wide FinOps scanning#2155
z-larsen wants to merge 65 commits into
microsoft:devfrom
z-larsen:feature/finops-multitool

Conversation

@z-larsen

@z-larsen z-larsen commented May 19, 2026

Copy link
Copy Markdown

🛠️ Description

Adds the Azure FinOps Multitool to the FinOps toolkit. Discussed with @MSBrett, who suggested contributing the tool into the official toolkit.

The Multitool scans an Azure tenant for cost optimization, governance, and FinOps insights — cost trends, orphaned resources, idle VMs, tag hygiene, reservation/savings-plan utilization, AHB opportunities, budgets, anomaly alerts, and policy compliance. Analysis scans are read-only (Reader / Cost Management Reader) and ground their findings in the customer's live resource state. Four write/remediation tools (delete orphaned resource, deallocate idle VM, enable AHB, set cost allocation rule) are dry-run by default, gated by a configurable write-safety policy, and disabled unless FINOPS_WRITE_MODE is set — the server defaults to ReadOnly.

This PR delivers two interfaces over one shared scanner engine, so the same scan logic is reused everywhere:

Interface Entry point Platform Best for
TUI Invoke-FinOpsMultitool Windows, macOS, Linux Terminal-only / cross-platform, CI-friendly
MCP server Start-McpServer.ps1 Any (stdio JSON-RPC) AI agents calling scans as tools

A separate WPF GUI is maintained outside this repo; this PR contributes the TUI and MCP server only.

Shared scanner modules (modules/)

30 modular scanners (one per category): orphaned resources, idle VMs, storage tier advice, AHB, tag inventory/recommendations, policy inventory/recommendations, cost data/trend/by-tag, resource costs, reservation advice, commitment utilization, savings realized, budget status, anomaly alerts, Advisor optimization advice, billing structure, contract info, tenant hierarchy, and more.

FinOps Hub data paths

When a FinOps Hub is present, cost scans prefer the hub's Kusto database — an Azure Data Explorer / Fabric cluster (auto-discovered via Resource Graph) or a local ftklocal emulator (FINOPS_HUB_KUSTO_URI) — and push aggregation into the engine, returning only summarized results. This scales to large hubs (tens of GB / hundreds of millions of rows) without loading raw cost rows into PowerShell. The storage-export reader remains as a small-dataset convenience fallback, used only when no Kusto cluster is reachable.

MCP server + agent skills

Start-McpServer.ps1 exposes the scanners as 40 MCP tools — 36 read-only (30 scan_*, plus run_full_scan, detect_cost_data_source, get_azure_context, and other helpers) and 4 gated write/remediation tools — over the 2024-11-05 MCP protocol via stdio. .vscode/mcp.json registers the server for VS Code, and Test-McpServer.ps1 provides protocol-level unit tests.

A companion agent-skill ecosystem (src/templates/agent-skills/) teaches AI agents to use the server proactively and to route findings into the wider FinOps practice. The finops-multitool skill acts as the hub, handing off to 11 FinOps-adjacent skills: power-bi-finops, cost-allocation, azure-policy-governance, unit-economics, finops-reporting, azure-workbooks-finops, forecasting-budgeting, anomaly-investigation, focus-data-quality, sustainability-carbon, and rate-optimization-portfolio.

📦 Files added / changed

Path Purpose
Public/Start-FinOpsMultitool.ps1 Public cmdlet — launches the cross-platform TUI
Invoke-FinOpsMultitool.ps1 + FinOpsMultitool.psm1 Cross-platform TUI + module loader
Start-McpServer.ps1 MCP server — 40 tools (36 read-only + 4 gated write)
Test-McpServer.ps1 MCP protocol unit tests
modules/ 30 shared scanner modules + write/remediation modules
helpers/Get-FOHubProvider.ps1 + Invoke-FOHubKustoQuery.ps1 Scalable FinOps Hub Kusto data path (ADX / Fabric / ftklocal)
helpers/Confirm-WriteAction.ps1 Write-safety policy gate (modes, guardrails, audit log)
agent-skills/finops-multitool/ Hub skill for the MCP server
agent-skills/cost-data-source/ Cost data-source routing skill (Kusto vs storage vs API)
agent-skills/{power-bi-finops, cost-allocation, …}/ 11 FinOps-adjacent skills
.vscode/mcp.json MCP server registration for VS Code
Tests/Unit/Start-FinOpsMultitool.Tests.ps1 + FOHubProvider.Tests.ps1 Pester unit tests
docs-mslearn/.../powershell/multitool/ + docs/multitool.md Documentation (command reference, landing page, TOC, changelog)

📸 Screenshots

Screenshots are in the public repo README.

📋 Checklist

🧪 How did you test this change?

  • 🧹 Lint tests
  • 👍 PS -WhatIf / az validate
  • 🔌 Manually deployed + verified
  • 🧪 Unit tests (TUI + MCP protocol)
  • 👀 Integration tests

🐳 Deploy to test?

N/A — standalone PowerShell tooling (TUI / MCP server), not a template deployment.

🏷️ Do any of the following that apply?

  • 🚨 This is a breaking change.
  • 🐣 The change is less than 20 lines of code.

📄 Did you update docs/changelog.md?

  • ✅ Updated changelog
  • ❌ Log not needed (small/internal change)

📖 Did you update documentation?

  • ✅ Documentation updated — FinOps Multitool reference under docs-mslearn/.../powershell/multitool/, a Jekyll landing page, overview/TOC/changelog entries, and the module README + finops-multitool / cost-data-source skills.
  • ❌ Docs not needed (small/internal change)

… GUI

Adds the Azure FinOps Multitool as a new PowerShell cmdlet in the FinOps toolkit. The Multitool is a WPF-based GUI that scans an Azure tenant for cost optimization, governance, and FinOps insights including cost trends, orphaned resources, idle VMs, tag hygiene, reservation/savings plan utilization, AHB opportunities, budgets, anomaly alerts, and policy compliance.

- Public/Start-FinOpsMultitool.ps1: thin launcher cmdlet with comment-based help

- Private/FinOpsMultitool/: full implementation (24 scanner modules, WPF GUI, Power BI template)

- Tests/Unit/Start-FinOpsMultitool.Tests.ps1: Pester unit tests

Windows-only (requires WPF support).
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Documentation 📝 PR needs a changelog entry, public docs, or internal docs before it can be closed Needs: Review 👀 PR that is ready to be reviewed labels May 19, 2026
@microsoft-github-policy-service microsoft-github-policy-service Bot added the Tool: PowerShell PowerShell scripts and automation label May 19, 2026
@z-larsen

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Microsoft"

@flanakin

Copy link
Copy Markdown
Collaborator

@z-larsen This is exciting! I don't know much about the tool, but would love to learn more. Can you join us at the contributor sync next Wednesday to share?

https://aka.ms/ftk/contrib-sync

@z-larsen

Copy link
Copy Markdown
Author

Thanks, Michael! Would love to join.

Zac Larsen added 15 commits May 26, 2026 22:11
…info

- Add contract-aware cost access warning banner (EA/MCA/CSP) on Overview tab
- Add contract-specific billing tab messages when billing access unavailable
- Add MG hierarchy unavailable info node in tree view with role guidance
- Fix tag cost queries: use TagKey grouping type (not Tag/Dimension)
- Add batched TagKey+TagValue query attempt with per-tag fallback
- Clear skipSubs between batched and per-tag strategies
- Add throttle pacing (2s every 2 queries) to avoid 429s
- Add EA/MCA cost access detection in Get-CostData
- Add runspace pool for API call parallelization

@MSBrett MSBrett left a comment

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.

Review: FinOps Multitool (TUI / CLI / MCP)

Thanks for this — it's an ambitious, genuinely useful contribution and the breadth of capabilities is impressive. I ran hands-on UAT on the three core surfaces and did a code + security pass with a couple of reviewers. Good news up front: the TUI, CLI, and MCP server all run. Module imports cleanly (352 commands), the TUI parses and launches with 0 errors, and the MCP server starts and responds to tools/list / tools/call. Since TUI/CLI/MCP are the features that matter for this release, the WPF/GUI piece can be dropped without blocking — but the current entry point still points at it, which is the first must-fix below.

I'm marking this as comment rather than request-changes since it's a draft, but there are a few hard blockers I'd want resolved before merge. Grouped by severity.

🔴 Blockers

1. Public entry point is dead on arrival (Public/Start-FinOpsMultitool.ps1).
Line 45 builds a path to Private/FinOpsMultitool/Start-FinOpsMultitool.ps1 and invokes it (line 54), but that file doesn't exist — there's no gui/ and no MainWindow.xaml. The Test-Path guard at line 47 means the public cmdlet emits "FinOps Multitool files not found..." and returns. Only the TUI (Private/.../Invoke-FinOpsMultitool.ps1) actually exists. Since WPF is cut, please repoint the public cmdlet at the TUI and update its help (lines 9, 19 still describe a "WPF-based GUI"). Right now the only public command is non-functional.

2. The "read-only" contract is not true — there are 4 live Azure write tools.
The PR description and SKILL.md state all tools are read-only / Reader-scope, but these perform real ARM mutations via Invoke-AzRestMethodWithRetry:

  • Remove-OrphanedResource.ps1:236 — DELETE
  • Stop-IdleVm.ps1:130 — deallocate POST
  • Enable-HybridBenefit.ps1:151 — PATCH
  • Set-CostAllocationRule.ps1:238 — PUT

All four are advertised unfiltered over tools/list. Also worth noting: the server exposes 40 tools, not the 21/22 claimed in the docs and tests. The read-only framing needs to be corrected everywhere (PR description, SKILL.md lines 5/13/15/133), or these tools need to be gated/opt-in.

3. Set-CostAllocationRule bypasses the write-safety gate entirely. (most material issue)
Every other write tool routes through Resolve-WriteDecision (e.g. Remove-OrphanedResource.ps1:188), which enforces ReadOnly/Enforced mode, the confirmation token, protected-tag/sub/RG guardrails, impact caps, and the audit log. Set-CostAllocationRule has no ConfirmationToken param and never calls Resolve-WriteDecision — it rolls its own -Apply dry-run (line 217) then PUTs directly (line 238). Consequences I verified:

  • Setting FINOPS_WRITE_MODE=ReadOnly does not stop this tool (ReadOnly is only enforced inside Resolve-WriteDecision). An operator who believes they're in read-only mode can still have chargeback rules written.
  • These writes are never audited — no apply entry hits the audit log, so the "every apply is logged" guarantee is false for cost-allocation writes.

Please route this tool through the same gate as the other three.

4. String JSON-RPC id terminates the server (Start-McpServer.ps1).
All handlers and Send-Result / Send-Error type the id as [int] (lines 1530–1626), and the dispatch switch (line 1684) sits outside the inner try/catch — the outer try (1664) has a finally but no catch. A spec-legal string id ("abc") throws an argument-transformation error that abandons the while($true) loop and exits the process. Many MCP clients use string ids, so this will hard-crash the server in practice. Suggest typing ids as [object]/[string] and echoing them back verbatim.

🟠 High

5. Default write mode is Interactive, which one-shots writes without a token.
Confirm-WriteAction.ps1:51-52 defaults the mode to Interactive; the Apply path only requires a confirmation token when Mode -eq 'Enforced' (line 259). .vscode/mcp.json sets no FINOPS_WRITE_MODE, so the default applies — an agent can perform an irreversible delete with a single apply=true call. To be fair: dry-run is the genuine default (apply must be explicitly set) and the protected-tag list + orphan-type allowlist still apply, so this is partly by-design "low friction." But I'd strongly recommend defaulting to ReadOnly (or requiring a token in Interactive too), especially for an agent-driven surface.

6. SKILL.md misrepresents the safety contract to the agent.
Lines 5/13/15/133 tell the LLM all tools are read-only and "never modify resources." Because this is the agent-facing contract an LLM uses to decide an action is safe, the misstatement is higher-risk than a normal doc bug — please fix alongside #2.

🟡 Medium

7. run_full_scan invokes remediation tools.
Invoke-FullScan (Start-McpServer.ps1:1373) selects every tool except _full_scan with no category filter, so remediation tools (and set_cost_allocation_rule) get called in the loop. To be precise on impact: missing mandatory params raise ParameterBindingException which is caught at line 1500 and recorded in $errors — so the scan does not crash or mutate anything (Apply is never passed). The real effect is noisy per-tool errors / junk in results. Please filter the scan to read-only/diagnostic tools by category.

8. nuget.exe is downloaded from a mutable URL and executed unverified (Read-FinOpsHubData.ps1:72-77,159).
Pulls from .../latest/nuget.exe, executes it, then Add-Type-loads the fetched DLLs with no hash/signature check. Mitigating context: it's an official Microsoft TLS endpoint and the package version is pinned (Parquet.Net -Version 4.24.0), and nuget.exe is Authenticode-signed — but the signature isn't verified here. Pin/verify the nuget.exe hash or validate the Authenticode signature before execution.

9. Bundled tests fail as written.
Test-McpServer.ps1:139 asserts 21 tools (actual: 40); Start-FinOpsMultitool.Tests.ps1:23,28 assert the nonexistent Start-FinOpsMultitool.ps1 / gui/MainWindow.xaml. These need updating once the entry point and tool count are settled. The write modules also have 0 SupportsShouldProcess, which the repo lint may flag.

Minor

  • Audit log defaults to %TEMP% (Confirm-WriteAction.ps1:63) — an "append-only" trail in a user-clearable temp dir weakens the audit story. Consider a more durable default location.

Net: the core surfaces work and the design of the write-safety gate is sound — the problem is that one write tool skips it, the default mode is permissive, and the docs/entry-point don't match reality. Fix #1#4 and correct the read-only claims and I think this is in good shape. Happy to re-review once those land, and thanks again for the contribution.

@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Attention 👋 Issue or PR needs to be reviewed by the author or it will be closed due to no activity and removed Needs: Review 👀 PR that is ready to be reviewed labels Jun 17, 2026
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Review 👀 PR that is ready to be reviewed and removed Needs: Attention 👋 Issue or PR needs to be reviewed by the author or it will be closed due to no activity labels Jun 18, 2026
@MSBrett

MSBrett commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

I did another pass specifically against a large local FinOps hub dataset and want to call out a scaling concern with the current hub-data path.

The current "FinOps Hub" fast path appears to be PowerShell over storage exports: it detects a hub storage account, downloads ingestion Parquet / msexports CSV files, materializes rows into PowerShell objects, then aggregates those objects in-process. That is useful for small samples, but it will not scale to large customer delivery datasets.

For reference, the local hub dataset we just validated was ~41.9 GB of exported files, 734 Parquet files, ~322M source rows, and 644M rows across raw + transformed Kusto tables. Loading that shape as [PSCustomObject] rows in PowerShell is the wrong execution model; it will be memory-heavy, slow, and likely fail well before useful analysis completes.

For larger hubs, the tool should prefer querying the ADX/Fabric/Kusto Hub database directly and push aggregation/filtering into the engine (Costs_v*, Prices_v*, recommendations, etc.), then return only summarized result sets to PowerShell. Storage-export reading can remain a fallback or small-dataset path, but it should not be presented as the scalable hub path.

This also matters for the new local-hub/on-own-hardware scenario: the local Kusto endpoint can query the full dataset successfully, but this PR currently has no direct Kusto/Hub query path to use it.

@MSBrett

MSBrett commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

One clarification to the recommendation above: if the goal is offline/local analysis and you want to keep the data on your own machine, the better scalable path is the ftklocal approach — load the exports into the local Kusto emulator and query the local Hub database there.

So I would frame the options as:

  • Online / deployed hub: query ADX/Fabric/Kusto Hub database directly.
  • Offline / own hardware: use the ftklocal Kusto emulator approach and query the local Hub database directly.
  • Storage export reader: keep only as a small-dataset fallback or convenience path, not the scalable analysis engine.

The important point is the same in both deployed and local cases: avoid loading tens of GB / hundreds of millions of rows into PowerShell objects. Push the aggregation into Kusto and bring back summarized results.

@flanakin flanakin added this to the v15 milestone Jun 24, 2026
@microsoft-github-policy-service microsoft-github-policy-service Bot removed the Needs: Documentation 📝 PR needs a changelog entry, public docs, or internal docs before it can be closed label Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Review 👀 PR that is ready to be reviewed Tool: PowerShell PowerShell scripts and automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants