-
Notifications
You must be signed in to change notification settings - Fork 238
feat(plugins): add FinOps Toolkit agent plugins #2167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MSBrett
wants to merge
19
commits into
dev
Choose a base branch
from
features/sre-agent-plugins
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,207
−4,247
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
173aedc
feat(plugins): add FinOps Toolkit agent plugins
347d23f
fix(plugins): remove markdown trailing whitespace
f666bfe
fix(plugins): address Copilot review comments
1f86c8f
fix(plugins): tighten merge review follow-ups
b1acc13
fix(plugins): scrub identifiers, restore versions, add missing Copilo…
11ec9a5
fix(plugins): correct catalog count, fiscal-year examples, and knowle…
MSBrett efd2e9b
fix(plugins): address agent plugin review feedback
e41f083
docs: add GitHub Copilot CLI plugin changelog entry for v15
MSBrett 8caedff
Merge remote-tracking branch 'origin/dev' into features/sre-agent-plu…
MSBrett c106483
feat(plugins): consolidate to single agent-plugin template
MSBrett 0de7dad
fix(plugins): align manifests with shared mcp config
MSBrett c7d65f6
fix(plugins): finalize single-artifact SRE parity
MSBrett 3fcae3e
fix(plugins): unpin azure mcp package version
MSBrett 14ad1b3
fix(docs): update mslearn changelog date
MSBrett 4296ae7
fix(plugins): address final review feedback
MSBrett 0c1ac28
merge: sync dev into features/sre-agent-plugins
MSBrett 6457912
fix(plugins): harden packaging and manifests
MSBrett b20cb97
docs(plugins): clarify deferred skill scope
MSBrett 19963c5
fix(plugins): address latest review feedback
MSBrett File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
|
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" | ||
| }, | ||
| { | ||
|
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" | ||
| } | ||
| ] | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| src/templates/agent-plugin |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../src/templates/agent-plugin |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.