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
99 changes: 99 additions & 0 deletions .github/workflows/publish-mcp-registry.yml
Original file line number Diff line number Diff line change
@@ -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.<repository owner>/*
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the registry lookup gets a transient 5xx, rate limit, or network error, this conditional treats the version as unpublished and runs the publish path. Distinguish HTTP 404 from lookup errors and retry or fail before publishing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/publish-mcp-registry.yml, line 70:

<comment>When the registry lookup gets a transient 5xx, rate limit, or network error, this conditional treats the version as unpublished and runs the publish path. Distinguish HTTP 404 from lookup errors and retry or fail before publishing.</comment>

<file context>
@@ -0,0 +1,99 @@
+          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"
</file context>

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"
3 changes: 2 additions & 1 deletion .github/workflows/validate-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- ".codex-plugin/**"
- ".cursor-plugin/**"
- "assets/**"
- "server.json"
- "scripts/check-plugin-manifests.sh"
- ".github/workflows/validate-plugin.yml"
push:
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions glama.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://glama.ai/mcp/schemas/server.json",
"maintainers": ["okwasniewski", "szymonrybczak", "lil-bsz"]
}
9 changes: 7 additions & 2 deletions scripts/check-plugin-manifests.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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; }
Expand Down
Loading