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
3 changes: 2 additions & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
{
"name": "microsoft-finops-toolkit",
"version": "15.0.0",
"source": "./plugins/microsoft-finops-toolkit",
"source": "./src/templates/agent-plugin",
Comment thread
MSBrett marked this conversation as resolved.
"strict": true,
"description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.",
"category": "finops",
"homepage": "https://aka.ms/finops/toolkit"
Expand Down
2 changes: 1 addition & 1 deletion .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{
"name": "microsoft-finops-toolkit",
"version": "15.0.0",
"source": "./plugins/microsoft-finops-toolkit",
"source": "./src/templates/agent-plugin",
"description": "Microsoft FinOps toolkit plugin for AI-powered cloud financial management.",
"category": "finops",
"homepage": "https://aka.ms/finops/toolkit"
Expand Down
46 changes: 34 additions & 12 deletions src/powershell/Tests/Unit/AgentPlugins.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,26 @@ Describe 'Agent plugin manifest' {
Join-Path $script:Plugin ($root.agents -replace '^\./', '') | Should -Exist
}

It 'Loads MCP servers from .mcp.json in the plugin root' {
It 'Lets Claude auto-discover .mcp.json from 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 'Declares resolvable paths for every Claude component' {
$claude = Get-Content (Join-Path $script:Plugin '.claude-plugin/plugin.json') -Raw | ConvertFrom-Json
$paths = @($claude.commands) + @($claude.skills) + @($claude.outputStyles)

$paths.Count | Should -BeGreaterOrEqual 3
$paths | ForEach-Object {
$_ | Should -Not -BeNullOrEmpty
Join-Path $script:Plugin $_ | 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
Expand All @@ -64,15 +74,23 @@ Describe 'Agent plugin manifest' {
}

Describe 'Agent plugin components' {
It 'Ships agent definitions as NAME.agent.md files' {
$agents = Get-ChildItem (Join-Path $script:Plugin 'agents') -Filter '*.agent.md'
It 'Ships agent definitions as Claude-compatible NAME.md files' {
$agents = Get-ChildItem (Join-Path $script:Plugin 'agents') -Filter '*.md'

$agents.Count | Should -BeGreaterThan 0
$agents.Name | ForEach-Object {
$_ | Should -Not -Match '\.agent\.md$'
}
}

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

$content | Should -Match '(?ms)^---\s.*^name:\s*\S.*^description:\s*\S.*^---'
$name.Success | Should -BeTrue
$_.BaseName | Should -Be $name.Groups['name'].Value
}
}

Expand Down Expand Up @@ -164,17 +182,21 @@ Describe 'Plugin discovery and marketplaces' {
Join-Path $pluginRoot 'plugin.json' | Should -Exist
}

It 'Uses repository-root-relative marketplace sources' {
$marketplaces = @{
'.github/plugin/marketplace.json' = './plugins/microsoft-finops-toolkit'
'.claude-plugin/marketplace.json' = './plugins/microsoft-finops-toolkit'
}
It 'Uses a cross-platform source path in both marketplaces' {
$marketplaces = @('.github/plugin/marketplace.json', '.claude-plugin/marketplace.json')

foreach ($marketplace in $marketplaces.Keys)
foreach ($marketplace in $marketplaces)
{
$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 $marketplaces[$marketplace]
$entry.source | Should -Be './src/templates/agent-plugin'
}
}

It 'Enables strict mode explicitly for the Claude plugin' {
$json = Get-Content (Join-Path $script:RepoRoot '.claude-plugin/marketplace.json') -Raw | ConvertFrom-Json
$entry = $json.plugins | Where-Object { $_.name -eq 'microsoft-finops-toolkit' }

$entry.strict | Should -BeTrue
}
}
4 changes: 2 additions & 2 deletions src/templates/agent-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ This plugin is the canonical source for the FinOps Toolkit agent plugins.

## Distribution and runtime conventions

- `plugins/microsoft-finops-toolkit` is the marketplace source used by both marketplace manifests.
- Both marketplace manifests point directly to this directory to avoid relying on repository symlink support.
- `.plugin` is the repository-level discovery pointer for the plugin manifest.
- GitHub Copilot CLI declares agents and `.mcp.json` in `plugin.json`; Claude Code discovers those assets from the plugin root and declares its Claude-specific output style in `.claude-plugin/plugin.json`.
- GitHub Copilot CLI declares agents and `.mcp.json` in `plugin.json`; Claude Code discovers agents and `.mcp.json` from the plugin root and declares its Claude-specific output style in `.claude-plugin/plugin.json`.
- The Azure MCP server uses `@azure/mcp@latest` so the plugin receives compatible Azure MCP updates without a separate plugin release. It is limited to the Kusto namespace and read-only operations.

For setup and usage:
Expand Down
Loading