Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
173aedc
feat(plugins): add FinOps Toolkit agent plugins
Jun 3, 2026
347d23f
fix(plugins): remove markdown trailing whitespace
Jun 3, 2026
f666bfe
fix(plugins): address Copilot review comments
Jun 3, 2026
1f86c8f
fix(plugins): tighten merge review follow-ups
Jun 4, 2026
b1acc13
fix(plugins): scrub identifiers, restore versions, add missing Copilo…
Jun 17, 2026
11ec9a5
fix(plugins): correct catalog count, fiscal-year examples, and knowle…
MSBrett Jun 17, 2026
efd2e9b
fix(plugins): address agent plugin review feedback
Jun 18, 2026
e41f083
docs: add GitHub Copilot CLI plugin changelog entry for v15
MSBrett Jun 21, 2026
8caedff
Merge remote-tracking branch 'origin/dev' into features/sre-agent-plu…
MSBrett Jun 21, 2026
c106483
feat(plugins): consolidate to single agent-plugin template
MSBrett Jun 21, 2026
0de7dad
fix(plugins): align manifests with shared mcp config
MSBrett Jun 22, 2026
c7d65f6
fix(plugins): finalize single-artifact SRE parity
MSBrett Jun 22, 2026
3fcae3e
fix(plugins): unpin azure mcp package version
MSBrett Jun 22, 2026
14ad1b3
fix(docs): update mslearn changelog date
MSBrett Jun 22, 2026
4296ae7
fix(plugins): address final review feedback
MSBrett Jul 29, 2026
0c1ac28
merge: sync dev into features/sre-agent-plugins
MSBrett Jul 30, 2026
6457912
fix(plugins): harden packaging and manifests
MSBrett Jul 31, 2026
b20cb97
docs(plugins): clarify deferred skill scope
MSBrett Jul 31, 2026
19963c5
fix(plugins): address latest review feedback
MSBrett Aug 2, 2026
6d27c61
docs: update changelog date
MSBrett Aug 5, 2026
fe9251b
Merge remote-tracking branch 'origin/dev' into features/sre-agent-plu…
MSBrett Aug 6, 2026
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: 3 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"plugins": [
{
"name": "microsoft-finops-toolkit",
"version": "15.0-dev",
"source": "./src/templates/claude-plugin",
"description": "AI-powered cloud financial management for Azure. Analyze costs with KQL queries against FinOps hubs, get CFO-level reporting, and access Azure Cost Management insights.",
"version": "15.0.0",
Comment thread
MSBrett marked this conversation as resolved.
"source": "./plugins/microsoft-finops-toolkit",
"description": "AI-powered cloud financial management for Azure. Analyze FinOps hub cost data with KQL, get CFO-level reporting, and assess Azure capacity.",
"category": "finops",
"homepage": "https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/finops-toolkit-overview"
},
Expand Down
30 changes: 30 additions & 0 deletions .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "finops-toolkit",
"owner": {
"name": "Microsoft"
},
"metadata": {
"description": "Microsoft FinOps Toolkit plugins for AI-powered cloud financial management.",
"version": "15.0.0"
},
"plugins": [
{
"name": "microsoft-finops-toolkit",
Comment thread
MSBrett marked this conversation as resolved.
"version": "15.0.0",
"source": "./plugins/microsoft-finops-toolkit",
"description": "AI-powered cloud financial management for Azure. Analyze FinOps hub cost data with KQL, get CFO-level reporting, and assess Azure capacity.",
"category": "finops",
"homepage": "https://learn.microsoft.com/en-us/cloud-computing/finops/toolkit/finops-toolkit-overview"
},
{
Comment thread
MSBrett marked this conversation as resolved.
"name": "microsoft-learn",
"source": {
"source": "url",
"url": "https://github.com/microsoftdocs/mcp.git"
},
"description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more.",
"category": "documentation",
"homepage": "https://learn.microsoft.com"
}
]
}
1 change: 1 addition & 0 deletions .plugin
10 changes: 5 additions & 5 deletions docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: FinOps toolkit changelog
description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more.
author: MSBrett
ms.author: brettwil
ms.date: 07/30/2026
ms.date: 08/06/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
Expand Down Expand Up @@ -65,12 +65,12 @@ _Released June 2026_
- **Fixed**
- Corrected stale and incorrect descriptions for `BilledCost`, `EffectiveCost`, `BillingCurrency`, `BillingProfileId`, `BillingProfileName`, `CommitmentDiscountQuantity`, `ListUnitPrice`, `PricingQuantity`, `PricingUnitDescription`, and `TotalSavingsRunningTotal` in the [data dictionary](help/data-dictionary.md) to align with FOCUS 1.2 ([#2112](https://github.com/microsoft/finops-toolkit/pull/2112)).

### Claude Code plugin v15
### FinOps Toolkit agent plugins v15

- **Added**
- Added Claude Code plugin with skills for FinOps hubs and Azure Cost Management ([#2043](https://github.com/microsoft/finops-toolkit/pull/2043)).
- Added 4 agents (CFO, FinOps practitioner, database query, hubs agent), 5 commands (`/ftk-hubs-connect`, `/ftk-hubs-healthCheck`, `/ftk-mom-report`, `/ftk-ytd-report`, `/ftk-cost-optimization`), and an output style.
- Linked to the existing KQL query catalog in `src/queries/` from the plugin.
- Added a shared FinOps Toolkit plugin for Claude Code and GitHub Copilot CLI with 5 agents (CFO, FinOps practitioner, database query, hubs agent, and Azure capacity manager), 4 commands (`/ftk/hubs-connect`, `/ftk/hubs-health-check`, `/ftk/mom-report`, and `/ftk/ytd-report`), a FinOps hubs KQL skill, and a read-only Azure MCP server ([#2043](https://github.com/microsoft/finops-toolkit/pull/2043), [#2167](https://github.com/microsoft/finops-toolkit/pull/2167)).
- Published marketplace entries and repository discovery so the plugin installs with `copilot plugin install microsoft/finops-toolkit`.
- This release includes only the FinOps Toolkit skill. Azure Cost Management and capacity-management skills will be added in subsequent PRs.

### [FinOps hubs](hubs/finops-hubs-overview.md) v15

Expand Down
1 change: 1 addition & 0 deletions plugins/microsoft-finops-toolkit
174 changes: 174 additions & 0 deletions src/powershell/Tests/Unit/AgentPlugins.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

BeforeAll {
$script:RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../../..')).Path
$script:Plugin = Join-Path $script:RepoRoot 'src/templates/agent-plugin'
$script:PluginSource = './plugins/microsoft-finops-toolkit'
}

Describe 'Agent plugin manifest' {
It 'Ships a root plugin.json for GitHub Copilot CLI' {
Join-Path $script:Plugin 'plugin.json' | Should -Exist
}

It 'Ships a .claude-plugin/plugin.json for Claude' {
Join-Path $script:Plugin '.claude-plugin/plugin.json' | Should -Exist
}

It 'Uses a consistent plugin name across both manifests' {
$root = Get-Content (Join-Path $script:Plugin 'plugin.json') -Raw | ConvertFrom-Json
$claude = Get-Content (Join-Path $script:Plugin '.claude-plugin/plugin.json') -Raw | ConvertFrom-Json
$root.name | Should -Be $claude.name
}

It 'Uses consistent shared manifest metadata and intentional platform-specific fields' {
$root = Get-Content (Join-Path $script:Plugin 'plugin.json') -Raw | ConvertFrom-Json
$claude = Get-Content (Join-Path $script:Plugin '.claude-plugin/plugin.json') -Raw | ConvertFrom-Json

$root.description | Should -Be $claude.description
$root.version | Should -Be $claude.version
$root.license | Should -Be $claude.license
$root.repository | Should -Be $claude.repository
$root.commands.TrimStart('./') | Should -Be $claude.commands.TrimStart('./')
$root.skills[0].TrimStart('./') | Should -Be $claude.skills[0].TrimStart('./')
$root.agents | Should -Be './agents/'
$root.mcpServers | Should -Be '.mcp.json'
$claude.agents | Should -BeNullOrEmpty
$claude.mcpServers | Should -BeNullOrEmpty
$claude.outputStyles | Should -Be './output-styles/'
}

It 'Points the agents field at a directory that exists' {
$root = Get-Content (Join-Path $script:Plugin 'plugin.json') -Raw | ConvertFrom-Json
Join-Path $script:Plugin ($root.agents -replace '^\./', '') | Should -Exist
}

It 'Loads MCP servers from .mcp.json in the plugin root' {
$root = Get-Content (Join-Path $script:Plugin 'plugin.json') -Raw | ConvertFrom-Json
$claude = Get-Content (Join-Path $script:Plugin '.claude-plugin/plugin.json') -Raw | ConvertFrom-Json

$root.mcpServers | Should -Be '.mcp.json'
$claude.mcpServers | Should -BeNullOrEmpty
$claude.agents | Should -BeNullOrEmpty
Join-Path $script:Plugin '.mcp.json' | Should -Exist
}

It 'Uses unpinned Azure MCP latest package in .mcp.json' {
$mcp = Get-Content (Join-Path $script:Plugin '.mcp.json') -Raw | ConvertFrom-Json
$args = $mcp.mcpServers.'azure-mcp-server'.args
$packageArg = $args | Where-Object { $_ -like '@azure/mcp@*' } | Select-Object -First 1

$packageArg | Should -Not -BeNullOrEmpty
$packageArg | Should -Be '@azure/mcp@latest'
}
}

Describe 'Agent plugin components' {
It 'Ships agent definitions as NAME.agent.md files' {
$agents = Get-ChildItem (Join-Path $script:Plugin 'agents') -Filter '*.agent.md'
$agents.Count | Should -BeGreaterThan 0
}

It 'Gives every agent a name and description in front matter' {
Get-ChildItem (Join-Path $script:Plugin 'agents') -Filter '*.agent.md' | ForEach-Object {
$content = Get-Content $_.FullName -Raw
$content | Should -Match '(?ms)^---\s.*^name:\s*\S.*^description:\s*\S.*^---'
}
}

It 'Ships slash commands under the ftk namespace' {
$commands = Get-ChildItem (Join-Path $script:Plugin 'commands/ftk') -Filter '*.md'
$commands.Count | Should -BeGreaterThan 0
}

It 'Requires explicit invocation for commands that access hub data or write reports' {
foreach ($command in @('hubs-connect.md', 'hubs-health-check.md', 'mom-report.md', 'ytd-report.md'))
{
$content = Get-Content (Join-Path $script:Plugin "commands/ftk/$command") -Raw
$content | Should -Match '(?m)^disable-model-invocation:\s*true\s*$'
}
}

It 'Resolves linked template content from its full source path' {
$buildScript = Get-Content (Join-Path $script:RepoRoot 'src/scripts/Build-Toolkit.ps1') -Raw
$buildScript | Should -Match '\$target = Join-Path \(Split-Path -Path \$source\.FullName -Parent\) \$target'
}

It 'Uses the kebab-case health-check command name throughout the plugin' {
Join-Path $script:Plugin 'commands/ftk/hubs-health-check.md' | Should -Exist
Join-Path $script:Plugin 'skills/finops-toolkit/references/workflows/ftk-hubs-health-check.md' | Should -Exist

$content = (Get-ChildItem $script:Plugin -Recurse -File | Get-Content -Raw) -join [Environment]::NewLine
$content | Should -Not -Match 'hubs-healthCheck|ftk-hubs-healthCheck'
}

It 'Does not reference the deprecated Azure MCP command name' {
$content = (Get-ChildItem $script:Plugin -Recurse -File | Get-Content -Raw) -join [Environment]::NewLine
$content | Should -Not -Match '#?azmcp-kusto-query'
}

It 'Ships the finops-toolkit skill' {
Join-Path $script:Plugin 'skills/finops-toolkit/SKILL.md' | Should -Exist
}

It 'Documents required kusto_query parameters in skill docs' {
foreach ($doc in @('skills/finops-toolkit/SKILL.md', 'skills/finops-toolkit/README.md'))
{
$content = Get-Content (Join-Path $script:Plugin $doc) -Raw
$content | Should -Match 'azure-mcp-server'
$content | Should -Match 'kusto_query'
$content | Should -Match '"subscription"\s*:'
}
}

It 'Ships plugin root README for marketplace onboarding' {
Join-Path $script:Plugin 'README.md' | Should -Exist
}

It 'Bundles only Markdown Learn documentation' {
$bundle = Join-Path $TestDrive 'agent-plugin'
New-Item (Join-Path $bundle 'skills/finops-toolkit') -ItemType Directory -Force | Out-Null

& (Join-Path $script:RepoRoot 'src/scripts/Build-AgentPlugin.ps1') -DestDir $bundle

$docs = Get-ChildItem (Join-Path $bundle 'skills/finops-toolkit/references/docs-mslearn') -File -Recurse
$docs.Count | Should -BeGreaterThan 0
$docs.Extension | Select-Object -Unique | Should -Be @('.md')
}
}

Describe 'Deprecated azure-cost-management skill' {
It 'No longer ships the azure-cost-management skill' {
Join-Path $script:Plugin 'skills/azure-cost-management' | Should -Not -Exist
}

It 'Is not referenced by any plugin manifest' {
foreach ($manifest in @('plugin.json', '.claude-plugin/plugin.json'))
{
$content = Get-Content (Join-Path $script:Plugin $manifest) -Raw
$content | Should -Not -Match 'azure-cost-management'
}
}
}

Describe 'Plugin discovery and marketplaces' {
It 'Resolves the .plugin discovery pointer to a plugin manifest' {
$pluginRoot = Join-Path $script:RepoRoot '.plugin'
if (-not (Test-Path $pluginRoot -PathType Container))
{
$pluginRoot = Join-Path $script:RepoRoot (Get-Content $pluginRoot -Raw).Trim()
}

Join-Path $pluginRoot 'plugin.json' | Should -Exist
}

It 'Points every marketplace source at the plugin directory' {
foreach ($marketplace in @('.github/plugin/marketplace.json', '.claude-plugin/marketplace.json'))
{
$json = Get-Content (Join-Path $script:RepoRoot $marketplace) -Raw | ConvertFrom-Json
$entry = $json.plugins | Where-Object { $_.name -eq 'microsoft-finops-toolkit' }
$entry.source | Should -Be $script:PluginSource
}
}
}
55 changes: 55 additions & 0 deletions src/scripts/Build-AgentPlugin.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$DestDir
)

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
$skillsDir = Join-Path $DestDir 'skills'

function Remove-PluginBundle([string]$Path)
{
if (-not (Test-Path -LiteralPath $Path))
{
return
}

$item = Get-Item -LiteralPath $Path -Force
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
{
Remove-Item -LiteralPath $Path -Force -ErrorAction Stop
}
else
{
Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop
}
}

if (-not (Test-Path $skillsDir))
{
return
}

$finopsSkill = Join-Path $skillsDir 'finops-toolkit'
if (Test-Path $finopsSkill)
{
$queryDest = Join-Path $finopsSkill 'references/queries'
Remove-PluginBundle $queryDest
Copy-Item (Join-Path $repoRoot 'src/queries') -Destination $queryDest -Recurse -Force
Comment thread
MSBrett marked this conversation as resolved.

$docsDest = Join-Path $finopsSkill 'references/docs-mslearn'
Remove-PluginBundle $docsDest
$docsSource = Join-Path $repoRoot 'docs-mslearn'
Get-ChildItem $docsSource -File -Recurse -Filter '*.md' | ForEach-Object {
$relativePath = $_.FullName.Substring($docsSource.Length).TrimStart([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)
$destination = Join-Path $docsDest $relativePath
New-Item (Split-Path $destination) -ItemType Directory -Force | Out-Null
Copy-Item $_ -Destination $destination -Force
}
}

Get-ChildItem $DestDir -Force -Recurse -Filter '.DS_Store' | Remove-Item -Force
36 changes: 33 additions & 3 deletions src/scripts/Build-Toolkit.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,44 @@ $templates | ForEach-Object {
# Create target directory
$destDir = "$outdir/$templateName"
Write-Verbose " Creating target directory: $destDir"
Remove-Item $destDir -Recurse -ErrorAction SilentlyContinue
Remove-Item $destDir -Recurse -Force -ErrorAction SilentlyContinue
& "$PSScriptRoot/New-Directory.ps1" $destDir

# Copy required files
function Copy-TemplateItem($source, [string]$destination)
{
$itemName = $source.Name
if ($source.Attributes -band [IO.FileAttributes]::ReparsePoint)
{
$target = $source.Target
if (-not [IO.Path]::IsPathRooted($target))
{
$target = Join-Path (Split-Path -Path $source.FullName -Parent) $target
}

$source = Get-Item (Resolve-Path -LiteralPath $target -ErrorAction Stop)
}

if ($source.PSIsContainer)
{
$destination = Join-Path $destination $itemName
New-Item $destination -ItemType Directory -Force | Out-Null
Get-ChildItem $source -Force | ForEach-Object {
Copy-TemplateItem $_ $destination
}
}
else
{
Copy-Item $source -Destination (Join-Path $destination $itemName) -Force -ErrorAction Stop
}
}

# Copy required files, resolving nested symlinks into the package.
Write-Host " Copying files..."
$sourceFiles = Get-ChildItem $srcDir -Force | Where-Object { $_.Name -notin @(".build.config", ".buildignore", "scaffold.json") }
Write-Verbose " Copying $($sourceFiles.Count) items from source to destination"
$sourceFiles | Copy-Item -Destination $destDir -Recurse
$sourceFiles | ForEach-Object {
Copy-TemplateItem $_ $destDir
}

Get-ChildItem $destDir -Force -Recurse -Filter ".DS_Store" | Remove-Item -Force

Expand Down
Loading
Loading