diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index b5f6a57b..749d4e3f 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -1,14 +1,14 @@ { - "name": "microsoftdocs-local", + "name": "microsoft-docs-marketplace", "interface": { - "displayName": "Microsoft Docs Local Plugins Marketplace" + "displayName": "Microsoft Docs" }, "plugins": [ { "name": "microsoft-docs", "source": { "source": "local", - "path": "./" + "path": "./plugins/microsoft-docs" }, "policy": { "installation": "AVAILABLE", diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 38c22957..687425e6 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,8 +6,9 @@ "plugins": [ { "name": "microsoft-docs", - "source": "./", - "description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more." + "source": "./plugins/microsoft-docs", + "description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more.", + "version": "0.4.0" } ] } diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json new file mode 100644 index 00000000..47cf3e2e --- /dev/null +++ b/.github/plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "microsoft-docs-marketplace", + "owner": { + "name": "Microsoft" + }, + "metadata": { + "description": "Microsoft Learn documentation plugins for coding agents", + "version": "0.4.0" + }, + "plugins": [ + { + "name": "microsoft-docs", + "description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more.", + "version": "0.4.0", + "source": "./plugins/microsoft-docs" + } + ] +} diff --git a/.github/plugin/plugin.json b/.github/plugin/plugin.json index eece96e2..6b207c57 100644 --- a/.github/plugin/plugin.json +++ b/.github/plugin/plugin.json @@ -1,11 +1,14 @@ { "name": "microsoft-docs", "description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more.", - "version": "0.3.1", + "version": "0.4.0", "author": { "name": "Microsoft" }, "homepage": "https://learn.microsoft.com", "repository": "https://github.com/microsoftdocs/mcp", - "keywords": ["microsoft", "docs", "azure", ".net", "windows", "api", "documentation", "rag", "dynamics", "powerbi", "office"] + "license": "MIT", + "keywords": ["microsoft", "docs", "azure", ".net", "windows", "api", "documentation", "rag", "dynamics", "powerbi", "office"], + "skills": ["plugins/microsoft-docs/skills/"], + "mcpServers": "plugins/microsoft-docs/.mcp.json" } diff --git a/AGENTS.md b/AGENTS.md index 4058b447..05412f24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,24 +1,25 @@ # AGENTS.md -This repo is the community-facing repo for **Microsoft Learn MCP Server** — a remote MCP endpoint (`https://learn.microsoft.com/api/mcp`) that gives AI agents access to official Microsoft documentation. The repo also contains a CLI (`cli/`), agent skills (`skills/`), and plugin manifests for three ecosystems. +This repo is the community-facing repo for **Microsoft Learn MCP Server** — a remote MCP endpoint (`https://learn.microsoft.com/api/mcp`) that gives AI agents access to official Microsoft documentation. The repo also contains a CLI (`cli/`), agent skills, and plugin manifests for three ecosystems. ## Plugin ecosystems -The repo publishes plugin metadata for three ecosystems. `.claude-plugin/plugin.json` is the **source of truth** for shared plugin fields (name, description, version, author, etc.). +The repo publishes plugin metadata for three ecosystems. The shared plugin package lives at `plugins/microsoft-docs/`; root-level files are marketplace catalogs or compatibility shims. **Shared assets** used across ecosystems: -- `skills/` — agent skill packages (each subfolder has a `SKILL.md`) -- `.mcp.json` — MCP server endpoint config +- `plugins/microsoft-docs/skills/` — agent skill packages (each subfolder has a `SKILL.md`) +- `plugins/microsoft-docs/.mcp.json` — MCP server endpoint config for Claude Code and GitHub Copilot CLI +- `plugins/microsoft-docs/.codex.mcp.json` — Codex MCP server endpoint config, using Codex's documented direct server map shape -**Claude** — `.claude-plugin/plugin.json` (source of truth) + `.claude-plugin/marketplace.json` +**Claude** — `.claude-plugin/marketplace.json` points to `plugins/microsoft-docs/`; `plugins/microsoft-docs/.claude-plugin/plugin.json` defines the Claude package. -**GitHub Copilot** — `.github/plugin/plugin.json` — must be an **exact copy** of `.claude-plugin/plugin.json`. +**GitHub Copilot** — `.github/plugin/marketplace.json` points to `plugins/microsoft-docs/`; `plugins/microsoft-docs/plugin.json` defines the marketplace package; `.github/plugin/plugin.json` is a direct-install shim for `/plugin install microsoftdocs/mcp` and points to the shared package assets. -**Codex** — `.codex-plugin/plugin.json` + `.agents/plugins/marketplace.json`. The plugin.json shares fields with Claude but adds Codex-only fields (`skills`, `mcpServers`, `interface`, `license`) that wire it to the shared assets. Keep asset paths relative to repo root (`./skills/`, `./.mcp.json`) — never use `..` paths. The marketplace file must point at `./`. +**Codex** — `.agents/plugins/marketplace.json` points to `plugins/microsoft-docs/`; `plugins/microsoft-docs/.codex-plugin/plugin.json` defines the Codex package. Codex marketplace entries must point to a plugin subfolder such as `./plugins/microsoft-docs`; do not use `./`. ## Sync rules -When editing shared plugin metadata, edit `.claude-plugin/plugin.json` first, then copy it verbatim to `.github/plugin/plugin.json` and update shared fields in `.codex-plugin/plugin.json` to match. +When editing shared plugin metadata, keep identity fields aligned across all plugin manifests: `plugins/microsoft-docs/plugin.json`, `plugins/microsoft-docs/.claude-plugin/plugin.json`, `plugins/microsoft-docs/.codex-plugin/plugin.json`, and `.github/plugin/plugin.json`. The direct-install shim has repo-root-relative asset paths, so it must not be an exact copy of the package manifests. ## CLI @@ -38,5 +39,5 @@ It enforces sync rules, skill structure, file existence, and marketplace wiring. - `README.md` is the primary user-facing document. Update it in the same change whenever install steps, plugin layout, skills, or CLI behavior change. - Make the smallest synchronized set of edits that keeps all three ecosystems coherent. -- Do not reintroduce a nested `plugins/microsoft-docs` copy for Codex packaging. +- Keep plugin runtime assets under `plugins/microsoft-docs/`; root-level plugin files are marketplace catalogs or compatibility shims only. - Prefer fixing drift immediately over documenting known inconsistency. diff --git a/README.md b/README.md index 11bf2afd..15fc2abd 100644 --- a/README.md +++ b/README.md @@ -116,27 +116,44 @@ See [`cli/README.md`](cli/README.md) for the full command reference. | Skill | Purpose | Best For | |-------|---------|----------| -| [`microsoft-docs`](skills/microsoft-docs/SKILL.md) | Understanding concepts, tutorials, architecture, limits | "How does X work?", learning, configuration guides | -| [`microsoft-code-reference`](skills/microsoft-code-reference/SKILL.md) | API lookups, code samples, verification, error fixing | Implementing code, finding correct methods, troubleshooting | -| [`microsoft-skill-creator`](skills/microsoft-skill-creator/SKILL.md) | Meta-skill that generates custom agent skills for any Microsoft technology | Creating a skill to teach agents about a new Azure library, .NET feature, or other Microsoft tech | +| [`microsoft-docs`](plugins/microsoft-docs/skills/microsoft-docs/SKILL.md) | Understanding concepts, tutorials, architecture, limits | "How does X work?", learning, configuration guides | +| [`microsoft-code-reference`](plugins/microsoft-docs/skills/microsoft-code-reference/SKILL.md) | API lookups, code samples, verification, error fixing | Implementing code, finding correct methods, troubleshooting | +| [`microsoft-skill-creator`](plugins/microsoft-docs/skills/microsoft-skill-creator/SKILL.md) | Meta-skill that generates custom agent skills for any Microsoft technology | Creating a skill to teach agents about a new Azure library, .NET feature, or other Microsoft tech | ### Quick Setup -These agent skills are packed in a `microsoft-docs` plugin together with the Learn MCP server itself. If you use Claude Code, run the following command and restart Claude Code: +These agent skills are packed in a `microsoft-docs` plugin together with the Learn MCP server itself. + +If you use GitHub Copilot CLI, run: ``` -/plugin install microsoft-docs@claude-plugins-official +/plugin install microsoftdocs/mcp ``` -Or if you use GitHub Copilot CLI, run this command: +If you use Claude Code, add this repository as a marketplace and install the plugin: ``` -/plugin install microsoftdocs/mcp +/plugin marketplace add microsoftdocs/mcp +/plugin install microsoft-docs@microsoft-docs-marketplace +``` + +If you use Codex CLI, add this repository as a marketplace, then install `microsoft-docs` from the Microsoft Docs marketplace in the plugin directory: +``` +codex plugin marketplace add microsoftdocs/mcp +codex plugin install microsoft-docs@microsoft-docs-marketplace ``` + +The plugin package lives under `plugins/microsoft-docs/` so all three coding agents can share the same skills and MCP configuration. + +Claude Code users can also install from the official Claude marketplace: +``` +/plugin install microsoft-docs@claude-plugins-official +``` + Otherwise: 1. **Install the MCP Server first** — See [Installation](#-installation--getting-started) below 2. **Copy the skill folders** to your project's `.github/skills/` or `.claude/skills/` directory: - - [`microsoft-docs`](skills/microsoft-docs/) — for concepts, tutorials, and factual lookups - - [`microsoft-code-reference`](skills/microsoft-code-reference/) — for API lookups, code samples, and troubleshooting - - [`microsoft-skill-creator`](skills/microsoft-skill-creator/) — meta-skill for generating custom skills about Microsoft technologies + - [`microsoft-docs`](plugins/microsoft-docs/skills/microsoft-docs/) — for concepts, tutorials, and factual lookups + - [`microsoft-code-reference`](plugins/microsoft-docs/skills/microsoft-code-reference/) — for API lookups, code samples, and troubleshooting + - [`microsoft-skill-creator`](plugins/microsoft-docs/skills/microsoft-skill-creator/) — meta-skill for generating custom skills about Microsoft technologies ### Supported Agents @@ -164,10 +181,10 @@ The Microsoft Learn MCP Server supports quick installation across multiple devel | **VS Code** | [![Install in VS Code](https://img.shields.io/badge/Install_in-VS_Code-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=microsoft-learn&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Flearn.microsoft.com%2Fapi%2Fmcp%22%7D)
or search "@mcp learn" in Extensions to show "Microsoft Learn" MCP | [VS Code MCP Official Guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) | | **GitHub Copilot CLI** | `/plugin install microsoftdocs/mcp` | | | **Claude Desktop** | Follow "Add custom connector" instructions in official guide. | [Claude Desktop Remote MCP Guide](https://modelcontextprotocol.io/docs/develop/connect-remote-servers) | -| **Claude Code** | `/plugin install microsoft-docs@claude-plugins-official` (includes MCP server + skills) | [Claude Code Remote MCP Guide](https://code.claude.com/docs/en/mcp) | +| **Claude Code** | `/plugin marketplace add microsoftdocs/mcp` then `/plugin install microsoft-docs@microsoft-docs-marketplace` (includes MCP server + skills) | [Claude Code Remote MCP Guide](https://code.claude.com/docs/en/mcp) | | **Visual Studio** | Upgrade to latest VS 2022 or 2026, "Microsoft Learn" MCP is already built-in | [Visual Studio MCP Official Guide](https://learn.microsoft.com/en-us/visualstudio/ide/mcp-servers?view=vs-2022) | | **Cursor IDE** | [![Install in Cursor](https://img.shields.io/badge/Install_in-Cursor-000000?style=flat-square&logoColor=white)](https://cursor.com/en/install-mcp?name=microsoft-learn&config=eyJuYW1lIjoibWljcm9zb2Z0LWxlYXJuIiwidHlwZSI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2xlYXJuLm1pY3Jvc29mdC5jb20vYXBpL21jcCJ9) | [Cursor MCP Official Guide](https://docs.cursor.com/context/model-context-protocol) | -| **Codex** | `codex mcp add "microsoft-learn" --url "https://learn.microsoft.com/api/mcp"`| [Codex MCP documentation](https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers) | +| **Codex** | `codex plugin marketplace add microsoftdocs/mcp`, then install `microsoft-docs` from the Microsoft Docs marketplace. For MCP-only setup, use `codex mcp add "microsoft-learn" --url "https://learn.microsoft.com/api/mcp"` | [Codex MCP documentation](https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers) | | **Roo Code** | Open [Roo Code Marketplace](https://docs.roocode.com/features/marketplace), search for `Microsoft Learn`, and click `Install` | [Roo Code MCP Official Guide](https://docs.roocode.com/features/mcp/using-mcp-in-roo) | | **Cline** | Manual configuration required
Use `"type": "streamableHttp"` | [Cline MCP Official Guide](https://docs.cline.bot/mcp/connecting-to-a-remote-server) | | **Gemini CLI** | Manual configuration required
View Config**Note**: Add an `mcpServer` object to `.gemini/settings.json` file
{
"Microsoft Learn MCP Server": {
"httpUrl": "https://learn.microsoft.com/api/mcp"
}
}
| [How to set up your MCP server](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#how-to-set-up-your-mcp-server)| diff --git a/plugins/microsoft-docs/.claude-plugin/plugin.json b/plugins/microsoft-docs/.claude-plugin/plugin.json new file mode 100644 index 00000000..d1244ccb --- /dev/null +++ b/plugins/microsoft-docs/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "microsoft-docs", + "description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more.", + "version": "0.4.0", + "author": { + "name": "Microsoft" + }, + "homepage": "https://learn.microsoft.com", + "repository": "https://github.com/microsoftdocs/mcp", + "license": "MIT", + "keywords": ["microsoft", "docs", "azure", ".net", "windows", "api", "documentation", "rag", "dynamics", "powerbi", "office"], + "skills": "./skills/", + "mcpServers": "./.mcp.json" +} diff --git a/.codex-plugin/plugin.json b/plugins/microsoft-docs/.codex-plugin/plugin.json similarity index 95% rename from .codex-plugin/plugin.json rename to plugins/microsoft-docs/.codex-plugin/plugin.json index 34c6461a..127e56fd 100644 --- a/.codex-plugin/plugin.json +++ b/plugins/microsoft-docs/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "microsoft-docs", "description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more.", - "version": "0.3.1", + "version": "0.4.0", "author": { "name": "Microsoft" }, @@ -10,7 +10,7 @@ "license": "MIT", "keywords": ["microsoft", "docs", "azure", ".net", "windows", "api", "documentation", "rag", "dynamics", "powerbi", "office"], "skills": "./skills/", - "mcpServers": "./.mcp.json", + "mcpServers": "./.codex.mcp.json", "interface": { "displayName": "Microsoft Docs", "shortDescription": "Search official Microsoft docs and code samples.", diff --git a/plugins/microsoft-docs/.codex.mcp.json b/plugins/microsoft-docs/.codex.mcp.json new file mode 100644 index 00000000..12c30eee --- /dev/null +++ b/plugins/microsoft-docs/.codex.mcp.json @@ -0,0 +1,6 @@ +{ + "microsoft-learn": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + } +} diff --git a/.mcp.json b/plugins/microsoft-docs/.mcp.json similarity index 100% rename from .mcp.json rename to plugins/microsoft-docs/.mcp.json diff --git a/.claude-plugin/plugin.json b/plugins/microsoft-docs/plugin.json similarity index 69% rename from .claude-plugin/plugin.json rename to plugins/microsoft-docs/plugin.json index eece96e2..0ad233d0 100644 --- a/.claude-plugin/plugin.json +++ b/plugins/microsoft-docs/plugin.json @@ -1,11 +1,14 @@ { "name": "microsoft-docs", "description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more.", - "version": "0.3.1", + "version": "0.4.0", "author": { "name": "Microsoft" }, "homepage": "https://learn.microsoft.com", "repository": "https://github.com/microsoftdocs/mcp", - "keywords": ["microsoft", "docs", "azure", ".net", "windows", "api", "documentation", "rag", "dynamics", "powerbi", "office"] + "license": "MIT", + "keywords": ["microsoft", "docs", "azure", ".net", "windows", "api", "documentation", "rag", "dynamics", "powerbi", "office"], + "skills": ["skills/"], + "mcpServers": ".mcp.json" } diff --git a/skills/microsoft-code-reference/SKILL.md b/plugins/microsoft-docs/skills/microsoft-code-reference/SKILL.md similarity index 100% rename from skills/microsoft-code-reference/SKILL.md rename to plugins/microsoft-docs/skills/microsoft-code-reference/SKILL.md diff --git a/skills/microsoft-docs/SKILL.md b/plugins/microsoft-docs/skills/microsoft-docs/SKILL.md similarity index 100% rename from skills/microsoft-docs/SKILL.md rename to plugins/microsoft-docs/skills/microsoft-docs/SKILL.md diff --git a/skills/microsoft-skill-creator/SKILL.md b/plugins/microsoft-docs/skills/microsoft-skill-creator/SKILL.md similarity index 100% rename from skills/microsoft-skill-creator/SKILL.md rename to plugins/microsoft-docs/skills/microsoft-skill-creator/SKILL.md diff --git a/skills/microsoft-skill-creator/references/skill-templates.md b/plugins/microsoft-docs/skills/microsoft-skill-creator/references/skill-templates.md similarity index 100% rename from skills/microsoft-skill-creator/references/skill-templates.md rename to plugins/microsoft-docs/skills/microsoft-skill-creator/references/skill-templates.md diff --git a/scripts/validate-repo.ps1 b/scripts/validate-repo.ps1 index b201580e..e461c911 100644 --- a/scripts/validate-repo.ps1 +++ b/scripts/validate-repo.ps1 @@ -1,50 +1,45 @@ #!/usr/bin/env pwsh <# .SYNOPSIS - Validates the repository structure for the Claude plugin, the repo-local Codex plugin, agent skills, MCP config, and CLI. + Validates the repository structure for the Microsoft Docs plugin package. .DESCRIPTION - This script validates that all required files and folders exist for: - - 1. Claude Plugin (.claude-plugin/) - - marketplace.json : Plugin metadata for Claude marketplace - - plugin.json : Plugin configuration and capabilities - - 1b. Codex Plugin (repo-local) - - .agents/plugins/marketplace.json : Local marketplace entry that makes the plugin appear in `codex /plugins` - - .codex-plugin/plugin.json : Plugin manifest for the repo-root OpenAI Codex plugin - - 2. Agent Skills (skills/) - - Each subfolder must contain a SKILL.md file describing the skill - - Skills help AI agents use MCP tools more effectively - - 3. MCP Configuration (.mcp.json) - - Root-level MCP server configuration - - 4. CLI (cli/) - - TypeScript source, tests, and package metadata for the in-repo Learn CLI - - Run this script to verify your changes before submitting a PR. - -.EXAMPLE - ./scripts/validate-repo.ps1 + The repo exposes one shared plugin package under plugins/microsoft-docs/ + and root-level marketplace/shim files for Codex, Claude Code, and GitHub + Copilot CLI. #> $ErrorActionPreference = "Stop" -$script:hasErrors = $false +$script:HasErrors = $false $repoRoot = Split-Path -Parent $PSScriptRoot +$pluginName = "microsoft-docs" +$pluginDir = Join-Path (Join-Path $repoRoot "plugins") "microsoft-docs" +$skillsDir = Join-Path $pluginDir "skills" +$mcpJson = Join-Path $pluginDir ".mcp.json" +$codexMcpJson = Join-Path $pluginDir ".codex.mcp.json" + +$codexMarketplaceJson = Join-Path (Join-Path (Join-Path $repoRoot ".agents") "plugins") "marketplace.json" +$claudeMarketplaceJson = Join-Path (Join-Path $repoRoot ".claude-plugin") "marketplace.json" +$copilotMarketplaceJson = Join-Path (Join-Path (Join-Path $repoRoot ".github") "plugin") "marketplace.json" +$copilotShimJson = Join-Path (Join-Path (Join-Path $repoRoot ".github") "plugin") "plugin.json" + +$copilotPackageJson = Join-Path $pluginDir "plugin.json" +$claudePackageJson = Join-Path (Join-Path $pluginDir ".claude-plugin") "plugin.json" +$codexPackageJson = Join-Path (Join-Path $pluginDir ".codex-plugin") "plugin.json" + function Write-ValidationError($message) { - Write-Host "❌ ERROR: $message" -ForegroundColor Red - $script:hasErrors = $true + Write-Host "[ERROR] $message" -ForegroundColor Red + $script:HasErrors = $true } function Write-ValidationSuccess($message) { - Write-Host "✅ $message" -ForegroundColor Green + Write-Host "[OK] $message" -ForegroundColor Green } function Write-ValidationHeader($message) { - Write-Host "`n📋 $message" -ForegroundColor Cyan + Write-Host "" + Write-Host $message -ForegroundColor Cyan Write-Host ("-" * 50) -ForegroundColor Gray } @@ -57,307 +52,198 @@ function Test-ValidJson($path) { } } -# ============================================================================ -# Validation 1: Claude Plugin Files -# The .claude-plugin folder contains configuration for Claude marketplace -# ============================================================================ -Write-ValidationHeader "Validating Claude Plugin (.claude-plugin/)" - -$claudePluginFiles = @( - "marketplace.json", # Plugin metadata (name, description, author, etc.) - "plugin.json" # Plugin capabilities and MCP server reference -) +function Get-Json($path) { + return Get-Content $path -Raw | ConvertFrom-Json +} -foreach ($file in $claudePluginFiles) { - $path = Join-Path $repoRoot ".claude-plugin" $file +function Test-RequiredFile($path, $label) { if (Test-Path $path) { - Write-ValidationSuccess "Found: .claude-plugin/$file" - if (Test-ValidJson $path) { - Write-ValidationSuccess "Valid JSON: .claude-plugin/$file" - } else { - Write-ValidationError "Invalid JSON: .claude-plugin/$file" + Write-ValidationSuccess "Found: $label" + if ($path.EndsWith(".json")) { + if (Test-ValidJson $path) { + Write-ValidationSuccess "Valid JSON: $label" + } else { + Write-ValidationError "Invalid JSON: $label" + } } } else { - Write-ValidationError "Missing: .claude-plugin/$file" + Write-ValidationError "Missing: $label" } } -# ============================================================================ -# Validation 1b: Plugin JSON Sync -# .claude-plugin/plugin.json and .github/plugin/plugin.json must stay in sync. -# .claude-plugin/plugin.json is the source of truth; .github/plugin/plugin.json -# is consumed by GitHub and must mirror it exactly. -# ============================================================================ -Write-ValidationHeader "Validating plugin.json sync" - -$claudePluginJson = Join-Path $repoRoot ".claude-plugin" "plugin.json" -$githubPluginJson = Join-Path $repoRoot ".github" "plugin" "plugin.json" - -if ((Test-Path $claudePluginJson) -and (Test-Path $githubPluginJson)) { - $claudeContent = Get-Content $claudePluginJson -Raw - $githubContent = Get-Content $githubPluginJson -Raw - if ($claudeContent -eq $githubContent) { - Write-ValidationSuccess "plugin.json files are in sync (.claude-plugin/ and .github/plugin/)" +function Test-Equal($actual, $expected, $message) { + if ("$actual" -eq "$expected") { + Write-ValidationSuccess $message } else { - Write-ValidationError "plugin.json drift detected: .claude-plugin/plugin.json and .github/plugin/plugin.json differ. Update both files or copy from the source of truth (.claude-plugin/plugin.json)." + Write-ValidationError "$message Expected '$expected', got '$actual'." } -} elseif (-not (Test-Path $githubPluginJson)) { - Write-ValidationError "Missing: .github/plugin/plugin.json" -} else { - # .claude-plugin/plugin.json missing is already reported in Validation 1 } -# ============================================================================ -# Validation 1c: Codex Plugin Files -# Codex uses a repo-local marketplace entry that points at the repository root, -# where `.codex-plugin/plugin.json` defines the plugin shown in `codex /plugins`. -# ============================================================================ -Write-ValidationHeader "Validating Codex Plugin (repo-local marketplace)" - -$codexPluginName = "microsoft-docs" -$codexMarketplaceJson = Join-Path $repoRoot ".agents" "plugins" "marketplace.json" -$codexPluginDir = $repoRoot -$codexPluginJson = Join-Path $codexPluginDir ".codex-plugin" "plugin.json" - -if (Test-Path $codexMarketplaceJson) { - Write-ValidationSuccess "Found: .agents/plugins/marketplace.json" - if (Test-ValidJson $codexMarketplaceJson) { - Write-ValidationSuccess "Valid JSON: .agents/plugins/marketplace.json" +function Test-StringArrayEqual($actual, $expected, $message) { + $actualJoined = (@($actual) | Sort-Object) -join "," + $expectedJoined = (@($expected) | Sort-Object) -join "," + if ($actualJoined -eq $expectedJoined) { + Write-ValidationSuccess $message } else { - Write-ValidationError "Invalid JSON: .agents/plugins/marketplace.json" + Write-ValidationError "$message Expected '$expectedJoined', got '$actualJoined'." } -} else { - Write-ValidationError "Missing: .agents/plugins/marketplace.json" } -if (Test-Path $codexPluginJson) { - Write-ValidationSuccess "Found: .codex-plugin/plugin.json" - if (Test-ValidJson $codexPluginJson) { - Write-ValidationSuccess "Valid JSON: .codex-plugin/plugin.json" - } else { - Write-ValidationError "Invalid JSON: .codex-plugin/plugin.json" +function Test-SharedIdentity($actualObj, $expectedObj, $label) { + $fields = @("name", "description", "version", "homepage", "repository", "license") + foreach ($field in $fields) { + Test-Equal $actualObj.$field $expectedObj.$field "$label field '$field' matches package manifest" } -} else { - Write-ValidationError "Missing: .codex-plugin/plugin.json" + + Test-Equal $actualObj.author.name $expectedObj.author.name "$label field 'author.name' matches package manifest" + Test-StringArrayEqual $actualObj.keywords $expectedObj.keywords "$label field 'keywords' matches package manifest" } -# ============================================================================ -# Validation 1d: Codex Marketplace Wiring -# The local marketplace entry must point to the repository root plugin and -# include the policy fields required for Codex to show the plugin in /plugins. -# ============================================================================ -Write-ValidationHeader "Validating Codex marketplace wiring" +Write-ValidationHeader "Validating required plugin files" + +$requiredFiles = @( + @{ Path = $codexMarketplaceJson; Label = ".agents/plugins/marketplace.json" }, + @{ Path = $claudeMarketplaceJson; Label = ".claude-plugin/marketplace.json" }, + @{ Path = $copilotMarketplaceJson; Label = ".github/plugin/marketplace.json" }, + @{ Path = $copilotShimJson; Label = ".github/plugin/plugin.json" }, + @{ Path = $copilotPackageJson; Label = "plugins/microsoft-docs/plugin.json" }, + @{ Path = $claudePackageJson; Label = "plugins/microsoft-docs/.claude-plugin/plugin.json" }, + @{ Path = $codexPackageJson; Label = "plugins/microsoft-docs/.codex-plugin/plugin.json" }, + @{ Path = $mcpJson; Label = "plugins/microsoft-docs/.mcp.json" }, + @{ Path = $codexMcpJson; Label = "plugins/microsoft-docs/.codex.mcp.json" } +) -if ((Test-Path $codexMarketplaceJson) -and (Test-ValidJson $codexMarketplaceJson)) { - $marketplaceObj = Get-Content $codexMarketplaceJson -Raw | ConvertFrom-Json - $marketplaceEntry = $marketplaceObj.plugins | Where-Object { $_.name -eq $codexPluginName } | Select-Object -First 1 +foreach ($file in $requiredFiles) { + Test-RequiredFile $file.Path $file.Label +} - if ([string]::IsNullOrWhiteSpace($marketplaceObj.name) -or $marketplaceObj.name.StartsWith("[TODO:")) { - Write-ValidationError "Codex marketplace root 'name' must be set to a real value." - } else { - Write-ValidationSuccess "Codex marketplace root name is set" - } +$unexpectedRootPluginFiles = @( + @{ Path = Join-Path (Join-Path $repoRoot ".claude-plugin") "plugin.json"; Label = ".claude-plugin/plugin.json" }, + @{ Path = Join-Path (Join-Path $repoRoot ".codex-plugin") "plugin.json"; Label = ".codex-plugin/plugin.json" }, + @{ Path = Join-Path $repoRoot ".mcp.json"; Label = ".mcp.json" }, + @{ Path = Join-Path $repoRoot "skills"; Label = "skills/" } +) - if ([string]::IsNullOrWhiteSpace($marketplaceObj.interface.displayName) -or $marketplaceObj.interface.displayName.StartsWith("[TODO:")) { - Write-ValidationError "Codex marketplace interface.displayName must be set to a real value." +foreach ($item in $unexpectedRootPluginFiles) { + if (Test-Path $item.Path) { + Write-ValidationError "Unexpected root plugin artifact remains: $($item.Label)" } else { - Write-ValidationSuccess "Codex marketplace display name is set" + Write-ValidationSuccess "No root plugin artifact: $($item.Label)" } +} - if ($null -eq $marketplaceEntry) { - Write-ValidationError "Missing plugin entry '$codexPluginName' in .agents/plugins/marketplace.json" - } else { - Write-ValidationSuccess "Found marketplace entry for '$codexPluginName'" +Write-ValidationHeader "Validating marketplace wiring" - if ($marketplaceEntry.source.source -ne "local") { - Write-ValidationError "Codex marketplace entry '$codexPluginName' must use source.source = 'local'." - } else { - Write-ValidationSuccess "Codex marketplace entry uses local source" - } +if ((Test-ValidJson $codexMarketplaceJson) -and (Test-ValidJson $claudeMarketplaceJson) -and (Test-ValidJson $copilotMarketplaceJson)) { + $codexMarketplace = Get-Json $codexMarketplaceJson + $claudeMarketplace = Get-Json $claudeMarketplaceJson + $copilotMarketplace = Get-Json $copilotMarketplaceJson - $expectedPluginPath = "./" - if ($marketplaceEntry.source.path -ne $expectedPluginPath) { - Write-ValidationError "Codex marketplace entry '$codexPluginName' must use source.path = '$expectedPluginPath'." - } else { - Write-ValidationSuccess "Codex marketplace entry points to $expectedPluginPath" - } + Test-Equal $codexMarketplace.name "microsoft-docs-marketplace" "Codex marketplace name is aligned" + Test-Equal $codexMarketplace.interface.displayName "Microsoft Docs" "Codex marketplace display name is set" - if ([string]::IsNullOrWhiteSpace($marketplaceEntry.policy.installation)) { - Write-ValidationError "Codex marketplace entry '$codexPluginName' is missing policy.installation." - } else { - Write-ValidationSuccess "Codex marketplace entry includes policy.installation" - } + $codexEntry = $codexMarketplace.plugins | Where-Object { $_.name -eq $pluginName } | Select-Object -First 1 + if ($null -eq $codexEntry) { + Write-ValidationError "Missing Codex marketplace entry '$pluginName'" + } else { + Test-Equal $codexEntry.source.source "local" "Codex marketplace entry uses local source" + Test-Equal $codexEntry.source.path "./plugins/microsoft-docs" "Codex marketplace entry points to plugin subfolder" + Test-Equal $codexEntry.policy.installation "AVAILABLE" "Codex marketplace policy.installation is set" + Test-Equal $codexEntry.policy.authentication "ON_INSTALL" "Codex marketplace policy.authentication is set" + Test-Equal $codexEntry.category "Productivity" "Codex marketplace category is set" + } - if ([string]::IsNullOrWhiteSpace($marketplaceEntry.policy.authentication)) { - Write-ValidationError "Codex marketplace entry '$codexPluginName' is missing policy.authentication." - } else { - Write-ValidationSuccess "Codex marketplace entry includes policy.authentication" - } + $claudeEntry = $claudeMarketplace.plugins | Where-Object { $_.name -eq $pluginName } | Select-Object -First 1 + if ($null -eq $claudeEntry) { + Write-ValidationError "Missing Claude marketplace entry '$pluginName'" + } else { + Test-Equal $claudeEntry.source "./plugins/microsoft-docs" "Claude marketplace entry points to plugin subfolder" + } - if ([string]::IsNullOrWhiteSpace($marketplaceEntry.category)) { - Write-ValidationError "Codex marketplace entry '$codexPluginName' is missing category." - } else { - Write-ValidationSuccess "Codex marketplace entry includes category" - } + $copilotEntry = $copilotMarketplace.plugins | Where-Object { $_.name -eq $pluginName } | Select-Object -First 1 + if ($null -eq $copilotEntry) { + Write-ValidationError "Missing Copilot marketplace entry '$pluginName'" + } else { + Test-Equal $copilotEntry.source "./plugins/microsoft-docs" "Copilot marketplace entry points to plugin subfolder" } } -# ============================================================================ -# Validation 1e: Codex Plugin JSON Sync -# The shared fields in the repo-root Codex plugin.json must match the source of -# truth (.claude-plugin/plugin.json). The Codex file may have additional fields -# (skills, mcpServers, interface) that are not present in the Claude file. -# ============================================================================ -Write-ValidationHeader "Validating Codex plugin.json sync" - -if ((Test-Path $claudePluginJson) -and (Test-ValidJson $claudePluginJson) -and (Test-Path $codexPluginJson) -and (Test-ValidJson $codexPluginJson)) { - $claudeObj = Get-Content $claudePluginJson -Raw | ConvertFrom-Json - $codexObj = Get-Content $codexPluginJson -Raw | ConvertFrom-Json - - $sharedKeys = @("name", "description", "version", "homepage", "repository") - $syncOk = $true - - foreach ($key in $sharedKeys) { - $claudeVal = $claudeObj.$key - $codexVal = $codexObj.$key - if ("$claudeVal" -ne "$codexVal") { - Write-ValidationError "Codex plugin.json field '$key' differs from source of truth (.claude-plugin/plugin.json). Expected '$claudeVal', got '$codexVal'." - $syncOk = $false - } - } +Write-ValidationHeader "Validating plugin manifests" - if ($claudeObj.author.name -ne $codexObj.author.name) { - Write-ValidationError "Codex plugin.json field 'author.name' differs from source of truth. Expected '$($claudeObj.author.name)', got '$($codexObj.author.name)'." - $syncOk = $false - } +if ((Test-ValidJson $copilotPackageJson) -and (Test-ValidJson $claudePackageJson) -and (Test-ValidJson $codexPackageJson) -and (Test-ValidJson $copilotShimJson)) { + $copilotPackage = Get-Json $copilotPackageJson + $claudePackage = Get-Json $claudePackageJson + $codexPackage = Get-Json $codexPackageJson + $copilotShim = Get-Json $copilotShimJson - $claudeKw = ($claudeObj.keywords | Sort-Object) -join "," - $codexKw = ($codexObj.keywords | Sort-Object) -join "," - if ($claudeKw -ne $codexKw) { - Write-ValidationError "Codex plugin.json 'keywords' differ from source of truth (.claude-plugin/plugin.json)." - $syncOk = $false - } + Test-SharedIdentity $claudePackage $copilotPackage "Claude package manifest" + Test-SharedIdentity $codexPackage $copilotPackage "Codex package manifest" + Test-SharedIdentity $copilotShim $copilotPackage "Copilot direct-install shim" - $codexPluginRoot = $codexPluginDir - $skillsPath = ([System.IO.Path]::GetFullPath((Join-Path $codexPluginRoot $codexObj.skills))).TrimEnd('\', '/') - $mcpServersPath = ([System.IO.Path]::GetFullPath((Join-Path $codexPluginRoot $codexObj.mcpServers))).TrimEnd('\', '/') - $repoMcpJsonPath = ([System.IO.Path]::GetFullPath((Join-Path $repoRoot ".mcp.json"))).TrimEnd('\', '/') - $repoSkillsPath = ([System.IO.Path]::GetFullPath((Join-Path $repoRoot "skills"))).TrimEnd('\', '/') - - if ($codexObj.skills -ne "./skills/") { - Write-ValidationError "Codex plugin.json field 'skills' must be './skills/'." - $syncOk = $false - } elseif (-not (Test-Path $skillsPath)) { - Write-ValidationError "Codex plugin.json 'skills' path does not resolve to an existing directory: $skillsPath" - $syncOk = $false - } elseif ($skillsPath -ne $repoSkillsPath) { - Write-ValidationError "Codex plugin.json 'skills' path must resolve to the repo root skills directory: $repoSkillsPath" - $syncOk = $false - } else { - Write-ValidationSuccess "Codex plugin.json skills path resolves to repo root skills/" - } + Test-StringArrayEqual $copilotPackage.skills @("skills/") "Copilot package skills path is plugin-root relative" + Test-Equal $copilotPackage.mcpServers ".mcp.json" "Copilot package MCP path is plugin-root relative" - if ($codexObj.mcpServers -ne "./.mcp.json") { - Write-ValidationError "Codex plugin.json field 'mcpServers' must be './.mcp.json'." - $syncOk = $false - } elseif (-not (Test-Path $mcpServersPath)) { - Write-ValidationError "Codex plugin.json 'mcpServers' path does not resolve to an existing file: $mcpServersPath" - $syncOk = $false - } elseif ($mcpServersPath -ne $repoMcpJsonPath) { - Write-ValidationError "Codex plugin.json 'mcpServers' path must resolve to repo root .mcp.json: $repoMcpJsonPath" - $syncOk = $false - } else { - Write-ValidationSuccess "Codex plugin.json MCP server path resolves to repo root .mcp.json" - } + Test-Equal $claudePackage.skills "./skills/" "Claude package skills path is plugin-root relative" + Test-Equal $claudePackage.mcpServers "./.mcp.json" "Claude package MCP path is plugin-root relative" - if ($codexObj.name -ne $codexPluginName) { - Write-ValidationError "Codex plugin.json name must be '$codexPluginName'." - $syncOk = $false - } + Test-Equal $codexPackage.skills "./skills/" "Codex package skills path is plugin-root relative" + Test-Equal $codexPackage.mcpServers "./.codex.mcp.json" "Codex package MCP path is plugin-root relative" - if ($syncOk) { - Write-ValidationSuccess "Codex plugin.json is in sync with source of truth and wired to repo-root assets" - } -} elseif (-not (Test-Path $codexPluginJson)) { - # Already reported above -} else { - # .claude-plugin/plugin.json missing is already reported in Validation 1 + Test-StringArrayEqual $copilotShim.skills @("plugins/microsoft-docs/skills/") "Copilot shim skills path is repo-root relative" + Test-Equal $copilotShim.mcpServers "plugins/microsoft-docs/.mcp.json" "Copilot shim MCP path is repo-root relative" } -# ============================================================================ -# Validation 2: Agent Skills Structure -# Each skill folder under /skills must have a SKILL.md describing the skill -# ============================================================================ -Write-ValidationHeader "Validating Agent Skills (skills/)" +Write-ValidationHeader "Validating skills and MCP config" -$skillsDir = Join-Path $repoRoot "skills" - -if (-not (Test-Path $skillsDir)) { - Write-ValidationError "Missing: skills/ directory" -} else { +if (Test-Path $skillsDir) { + Write-ValidationSuccess "Found: plugins/microsoft-docs/skills/" $skillFolders = Get-ChildItem -Path $skillsDir -Directory - if ($skillFolders.Count -eq 0) { - Write-ValidationError "No skill folders found in skills/" + Write-ValidationError "No skill folders found in plugins/microsoft-docs/skills/" } else { foreach ($folder in $skillFolders) { $skillMd = Join-Path $folder.FullName "SKILL.md" if (Test-Path $skillMd) { - Write-ValidationSuccess "Found: skills/$($folder.Name)/SKILL.md" + Write-ValidationSuccess "Found: plugins/microsoft-docs/skills/$($folder.Name)/SKILL.md" } else { - Write-ValidationError "Missing: skills/$($folder.Name)/SKILL.md - Each skill folder must have a SKILL.md file" + Write-ValidationError "Missing: plugins/microsoft-docs/skills/$($folder.Name)/SKILL.md" } } } +} else { + Write-ValidationError "Missing: plugins/microsoft-docs/skills/" } -# ============================================================================ -# Validation 3: MCP Configuration -# The .mcp.json file at repo root defines MCP server settings -# ============================================================================ -Write-ValidationHeader "Validating MCP Configuration (.mcp.json)" - -$mcpJsonPath = Join-Path $repoRoot ".mcp.json" -if (Test-Path $mcpJsonPath) { - Write-ValidationSuccess "Found: .mcp.json" - if (Test-ValidJson $mcpJsonPath) { - Write-ValidationSuccess "Valid JSON: .mcp.json" +if (Test-ValidJson $mcpJson) { + $mcpObj = Get-Json $mcpJson + if ($null -eq $mcpObj.mcpServers."microsoft-learn") { + Write-ValidationError "Claude/Copilot .mcp.json is missing mcpServers.microsoft-learn." } else { - Write-ValidationError "Invalid JSON: .mcp.json" + Test-Equal $mcpObj.mcpServers."microsoft-learn".url "https://learn.microsoft.com/api/mcp" "Claude/Copilot MCP server URL is set" } -} else { - Write-ValidationError "Missing: .mcp.json at repository root" } -# ============================================================================ -# Validation 4: CLI Structure -# The cli folder contains the open source CLI implementation -# ============================================================================ -Write-ValidationHeader "Validating CLI (cli/)" +if (Test-ValidJson $codexMcpJson) { + $codexMcpObj = Get-Json $codexMcpJson + if ($null -ne $codexMcpObj.mcpServers) { + Write-ValidationError "Codex .codex.mcp.json should use a direct server map, not a camelCase mcpServers wrapper." + } elseif ($null -eq $codexMcpObj."microsoft-learn") { + Write-ValidationError "Codex .codex.mcp.json is missing the microsoft-learn server entry." + } else { + Test-Equal $codexMcpObj."microsoft-learn".url "https://learn.microsoft.com/api/mcp" "Codex MCP server URL is set" + } +} + +Write-ValidationHeader "Validating CLI" $cliDir = Join-Path $repoRoot "cli" if (-not (Test-Path $cliDir)) { - Write-ValidationError "Missing: cli/ directory" + Write-ValidationError "Missing: cli/" } else { - Write-ValidationSuccess "Found: cli/ directory" - - $cliJsonFiles = @( - "package.json", - "tsconfig.json" - ) - - foreach ($file in $cliJsonFiles) { + Write-ValidationSuccess "Found: cli/" + foreach ($file in @("package.json", "tsconfig.json")) { $path = Join-Path $cliDir $file - if (Test-Path $path) { - Write-ValidationSuccess "Found: cli/$file" - if (Test-ValidJson $path) { - Write-ValidationSuccess "Valid JSON: cli/$file" - } else { - Write-ValidationError "Invalid JSON: cli/$file" - } - } else { - Write-ValidationError "Missing: cli/$file" - } + Test-RequiredFile $path "cli/$file" } $cliRequiredFiles = @( @@ -382,14 +268,12 @@ if (-not (Test-Path $cliDir)) { } } -# ============================================================================ -# Summary -# ============================================================================ -Write-Host "`n" ("-" * 50) -ForegroundColor Gray -if ($script:hasErrors) { - Write-Host "❌ Validation FAILED - Please fix the errors above" -ForegroundColor Red +Write-Host "" +Write-Host ("-" * 50) -ForegroundColor Gray +if ($script:HasErrors) { + Write-Host "Validation FAILED" -ForegroundColor Red exit 1 -} else { - Write-Host "✅ All validations PASSED" -ForegroundColor Green - exit 0 } + +Write-Host "All validations PASSED" -ForegroundColor Green +exit 0