diff --git a/.github/workflows/publish-mcp-registry.yml b/.github/workflows/publish-mcp-registry.yml new file mode 100644 index 0000000..df588df --- /dev/null +++ b/.github/workflows/publish-mcp-registry.yml @@ -0,0 +1,99 @@ +name: Publish to MCP Registry + +# Publishes server.json to the Official MCP Registry (registry.modelcontextprotocol.io) as +# io.github.tester-army/testerarmy, which feeds Glama, PulseMCP and the other aggregators. +# +# Authentication is GitHub Actions OIDC: the registry grants the io.github./* +# namespace to workflows running in this organisation's repositories. A personal +# `mcp-publisher login github` only gets that namespace for org Owners (plain members are refused), +# so this workflow is the route that needs nobody's personal login and no stored secret. +# +# Runs when server.json (or this file) lands on main, and on demand from the Actions tab. Manual +# runs are limited to main because the registry authorises on the repository owner alone and never +# looks at the branch: without the guard, any writer could publish an unreviewed branch under the +# org name. The `mcp-registry-publish` environment exists so an org Owner can add a deployment +# branch rule and a required reviewer in Settings > Environments without touching this file; until +# then the `if:` below is the gate. +# +# The registry refuses a version it already has, so the job first checks whether server.json's +# version is published and skips cleanly if so. To publish again, bump "version" in server.json +# together with the plugin manifests; scripts/check-plugin-manifests.sh fails if they differ. + +on: + push: + branches: [main] + paths: + - "server.json" + - ".github/workflows/publish-mcp-registry.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: mcp-registry-publish + # One publish at a time; a parallel run for the same version would only fail. + concurrency: publish-mcp-registry + permissions: + id-token: write # OIDC token exchanged with the registry + contents: read + env: + # If "login github-oidc" fails with "invalid audience", the registry moved ahead of this + # binary: bump to the latest release at https://github.com/modelcontextprotocol/registry/releases + MCP_PUBLISHER_VERSION: "1.8.1" + REGISTRY_URL: https://registry.modelcontextprotocol.io + steps: + - uses: actions/checkout@v5 + + - name: Install mcp-publisher (download checksum verified) + run: | + base="https://github.com/modelcontextprotocol/registry/releases/download/v${MCP_PUBLISHER_VERSION}" + curl -fsSL --retry 3 "$base/mcp-publisher_linux_amd64.tar.gz" -o mcp-publisher_linux_amd64.tar.gz + curl -fsSL --retry 3 "$base/registry_${MCP_PUBLISHER_VERSION}_checksums.txt" -o checksums.txt + sha256sum --check --ignore-missing checksums.txt + tar xzf mcp-publisher_linux_amd64.tar.gz mcp-publisher + ./mcp-publisher --version + + # The registry's own validator: schema plus the semantic rules the publish endpoint applies. + - name: Validate server.json against the registry + run: ./mcp-publisher validate server.json + + - name: Skip if this version is already published + id: check + run: | + name=$(jq -r .name server.json) + version=$(jq -r .version server.json) + encoded=$(jq -rn --arg n "$name" '$n | @uri') + if curl -fsS -o /dev/null "$REGISTRY_URL/v0/servers/$encoded/versions/$version"; then + echo "::notice::$name@$version is already in the registry; bump version in server.json to publish again" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + + - name: Log in with GitHub Actions OIDC + if: steps.check.outputs.skip != 'true' + run: ./mcp-publisher login github-oidc + + - name: Publish + if: steps.check.outputs.skip != 'true' + run: ./mcp-publisher publish server.json + + # Exact lookup: 404 (and a failed step) if the version is not there. + - name: Confirm the registry entry + run: | + set -euo pipefail + name=$(jq -r .name server.json) + version=$(jq -r .version server.json) + encoded=$(jq -rn --arg n "$name" '$n | @uri') + entry=$(curl -fsS --retry 3 "$REGISTRY_URL/v0/servers/$encoded/versions/$version" \ + | jq '{name: .server.name, version: .server.version, status: ._meta["io.modelcontextprotocol.registry/official"].status, isLatest: ._meta["io.modelcontextprotocol.registry/official"].isLatest, publishedAt: ._meta["io.modelcontextprotocol.registry/official"].publishedAt}') + echo "$entry" + { + echo "### MCP Registry: $name@$version" + echo '```json' + echo "$entry" + echo '```' + echo "Glama connector page (claim it once it appears): https://glama.ai/mcp/connectors/$name" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml index e9b6360..d1ef3ee 100644 --- a/.github/workflows/validate-plugin.yml +++ b/.github/workflows/validate-plugin.yml @@ -10,6 +10,7 @@ on: - ".codex-plugin/**" - ".cursor-plugin/**" - "assets/**" + - "server.json" - "scripts/check-plugin-manifests.sh" - ".github/workflows/validate-plugin.yml" push: @@ -19,7 +20,7 @@ jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Manifests parse and agree with each other run: scripts/check-plugin-manifests.sh - name: Root manifest and mcp.json match the Agent Plugins schemas diff --git a/glama.json b/glama.json new file mode 100644 index 0000000..0cd20e8 --- /dev/null +++ b/glama.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://glama.ai/mcp/schemas/server.json", + "maintainers": ["okwasniewski", "szymonrybczak", "lil-bsz"] +} diff --git a/scripts/check-plugin-manifests.sh b/scripts/check-plugin-manifests.sh index 4503459..debc67e 100755 --- a/scripts/check-plugin-manifests.sh +++ b/scripts/check-plugin-manifests.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# Keeps the four plugin manifests in step. Run locally or in CI. +# Keeps the plugin manifests and the MCP Registry manifest (server.json) in step. Run locally or in CI. set -euo pipefail cd "$(dirname "$0")/.." -files=(.claude-plugin/plugin.json .claude-plugin/marketplace.json .codex-plugin/plugin.json .cursor-plugin/plugin.json plugin.json .mcp.json mcp.json) +files=(.claude-plugin/plugin.json .claude-plugin/marketplace.json .codex-plugin/plugin.json .cursor-plugin/plugin.json plugin.json .mcp.json mcp.json server.json) for f in "${files[@]}"; do jq -e . "$f" >/dev/null || { echo "invalid JSON: $f"; exit 1; }; done # name, version and description must match across every manifest and the marketplace entry @@ -27,6 +27,11 @@ u2=$(jq -r '.mcpServers.testerarmy.url' mcp.json) [ "$(jq -r '.mcpServers.testerarmy.type' .mcp.json)" = "http" ] || { echo ".mcp.json must use type http (Claude Code)"; exit 1; } [ "$(jq -r '.mcpServers.testerarmy.type' mcp.json)" = "streamable-http" ] || { echo "mcp.json must use type streamable-http (Agent Plugins)"; exit 1; } +# the registry manifest is published from main by .github/workflows/publish-mcp-registry.yml: +# same version as the plugins, same server URL as the MCP files +[ "$(jq -r .version server.json)" = "$(jq -r .version plugin.json)" ] || { echo "version differs in server.json (bump it with the plugin manifests)"; exit 1; } +[ "$(jq -r '.remotes[0].url' server.json)" = "$u1" ] || { echo "server.json remotes[0].url differs from the MCP files"; exit 1; } + # referenced logo must exist for p in "$(jq -r .logo .cursor-plugin/plugin.json)" "$(jq -r .interface.logo .codex-plugin/plugin.json)"; do [ -f "${p#./}" ] || { echo "missing logo file: $p"; exit 1; }