Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "microsoftdocs-local",
"interface": {
"displayName": "Microsoft Docs Local Plugins Marketplace"
},
"plugins": [
{
"name": "microsoft-docs",
"source": {
"source": "local",
"path": "./"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
31 changes: 31 additions & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "microsoft-docs",
"description": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, and more.",
"version": "0.3.1",
"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",
"interface": {
"displayName": "Microsoft Docs",
"shortDescription": "Search official Microsoft docs and code samples.",
"longDescription": "Access official Microsoft documentation, API references, and code samples for Azure, .NET, Windows, Microsoft 365, and more through the Microsoft Learn MCP server.",
"developerName": "Microsoft",
"category": "Productivity",
"capabilities": ["Research", "Reference"],
"websiteURL": "https://learn.microsoft.com",
"privacyPolicyURL": "https://privacy.microsoft.com/privacystatement",
"termsOfServiceURL": "https://www.microsoft.com/servicesagreement",
"defaultPrompt": [
"Find the official Azure Functions timeout limits.",
"Show official .NET examples for HttpClientFactory.",
"Look up Microsoft docs for Azure OpenAI quotas."
],
"brandColor": "#0078D4"
}
}
42 changes: 42 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 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.

## 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.).

**Shared assets** used across ecosystems:
- `skills/` — agent skill packages (each subfolder has a `SKILL.md`)
- `.mcp.json` — MCP server endpoint config

**Claude** — `.claude-plugin/plugin.json` (source of truth) + `.claude-plugin/marketplace.json`

**GitHub Copilot** — `.github/plugin/plugin.json` — must be an **exact copy** of `.claude-plugin/plugin.json`.

**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 `./`.

## 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.

## CLI

Source is in `cli/src/`, and built output is generated into `cli/dist/` during the build (locally and in CI) rather than checked into the repo. If you change CLI behavior, run `npm run build && npm test` from `cli/`. Targets Node.js 22+. Keep `cli/README.md` aligned with the actual command surface.

## Validation

Run the repo validator after any plugin, skill, layout, or doc changes:

```powershell
pwsh -File scripts/validate-repo.ps1
```

It enforces sync rules, skill structure, file existence, and marketplace wiring. Treat it as the authoritative checklist.

## General principles

- `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.
- Prefer fixing drift immediately over documenting known inconsistency.
183 changes: 182 additions & 1 deletion scripts/validate-repo.ps1
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Validates the repository structure for the Claude plugin, agent skills, MCP config, and CLI.
Validates the repository structure for the Claude plugin, the repo-local Codex plugin, agent skills, MCP config, and CLI.

.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
Expand Down Expand Up @@ -103,6 +107,183 @@ if ((Test-Path $claudePluginJson) -and (Test-Path $githubPluginJson)) {
# .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"
} else {
Write-ValidationError "Invalid JSON: .agents/plugins/marketplace.json"
}
} 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"
}
} else {
Write-ValidationError "Missing: .codex-plugin/plugin.json"
}

# ============================================================================
# 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"

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

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"
}

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."
} else {
Write-ValidationSuccess "Codex marketplace display name is set"
}

if ($null -eq $marketplaceEntry) {
Write-ValidationError "Missing plugin entry '$codexPluginName' in .agents/plugins/marketplace.json"
} else {
Write-ValidationSuccess "Found marketplace entry for '$codexPluginName'"

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"
}

$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"
}

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"
}

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"
}

if ([string]::IsNullOrWhiteSpace($marketplaceEntry.category)) {
Write-ValidationError "Codex marketplace entry '$codexPluginName' is missing category."
} else {
Write-ValidationSuccess "Codex marketplace entry includes category"
}
}
}

# ============================================================================
# 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
}
}

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
}

$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
}

$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/"
}

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"
}

if ($codexObj.name -ne $codexPluginName) {
Write-ValidationError "Codex plugin.json name must be '$codexPluginName'."
$syncOk = $false
}

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
}

# ============================================================================
# Validation 2: Agent Skills Structure
# Each skill folder under /skills must have a SKILL.md describing the skill
Expand Down
Loading